From 89e2e0246c79b84d9cce73ac88727c29e37439f3 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Thu, 3 Aug 2017 22:48:40 -0700 Subject: [PATCH 001/492] [ZEPPELIN-2827] Spark's pyspark fails if unsupported version of matplotlib is present ### What is this PR for? %spark.pyspark paragraphs keep running for more than > 5 minutes and then at the end, throw TTransportException. ``` org.apache.thrift.transport.TTransportException at org.apache.thrift.transport.TIOStreamTransport.read(TIOStreamTransport.java:132) at org.apache.thrift.transport.TTransport.readAll(TTransport.java:86) at org.apache.thrift.protocol.TBinaryProtocol.readAll(TBinaryProtocol.java:429) at org.apache.thrift.protocol.TBinaryProtocol.readI32(TBinaryProtocol.java:318) at org.apache.thrift.protocol.TBinaryProtocol.readMessageBegin(TBinaryProtocol.java:219) at org.apache.thrift.TServiceClient.receiveBase(TServiceClient.java:69) ``` This happens because Zeppelin refers to `matplotlib.rcParams['savefig.format']` https://github.com/apache/zeppelin/blob/master/interpreter/lib/python/mpl_config.py#L83 which is only present in matplotlib 1.2.0 (https://github.com/matplotlib/matplotlib/blob/v1.2.0/matplotlibrc.template#L355) and onwards. ### What type of PR is it? [Bug Fix] ### What is the Jira issue? * [ZEPPELIN-2827](https://issues.apache.org/jira/browse/ZEPPELIN-2827) ### How should this be tested? Try running spark.pyspark on a machine with matplotlib==1.1.1 installed, pyspark should work as expected. ### Questions: * Does the licenses files need update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: Prabhjyot Singh Closes #2516 from prabhjyotsingh/ZEPPELIN-2827 and squashes the following commits: 0321fcd3e [Prabhjyot Singh] check if `savefig.format` exists in mpl_config.py da26f5613 [Prabhjyot Singh] add a version check along with check if matplotlib is installed --- interpreter/lib/python/mpl_config.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/interpreter/lib/python/mpl_config.py b/interpreter/lib/python/mpl_config.py index e48678f6359..5c60893b1e4 100644 --- a/interpreter/lib/python/mpl_config.py +++ b/interpreter/lib/python/mpl_config.py @@ -71,7 +71,11 @@ def _on_config_change(): supported_formats = _config['supported_formats'] if fmt not in supported_formats: raise ValueError("Unsupported format %s" %fmt) - matplotlib.rcParams['savefig.format'] = fmt + + if matplotlib.__version__ < '1.2.0': + matplotlib.rcParams.update({'savefig.format': fmt}) + else: + matplotlib.rcParams['savefig.format'] = fmt # Interactive mode interactive = _config['interactive'] @@ -80,6 +84,8 @@ def _on_config_change(): def _init_config(): dpi = matplotlib.rcParams['figure.dpi'] + if matplotlib.__version__ < '1.2.0': + matplotlib.rcParams.update({'savefig.format': 'png'}) fmt = matplotlib.rcParams['savefig.format'] width, height = matplotlib.rcParams['figure.figsize'] fontsize = matplotlib.rcParams['font.size'] From f1c48e87ee32888c2054d64dc50a883e8b4c78f6 Mon Sep 17 00:00:00 2001 From: Shiem Edelbrock Date: Mon, 31 Jul 2017 13:19:30 -0700 Subject: [PATCH 002/492] [ZEPPELIN-2821] Fix Missing import ### What is this PR for? The python interpreter has a bug when trying to render matplotlib images from the z.show() function. Line 139 of `python/src/main/resources/python/zeppelin_python.py` references un-imported package `base64`. `import base64` was added to the file to prevent this error in the future. ### What type of PR is it? Bug Fix ### Todos * [x] - Add missing `base64` module to `python/src/main/resources/python/zeppelin_python.py` ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2821 * Open an issue on Jira https://issues.apache.org/jira/browse/ZEPPELIN/ * Put link here, and add [ZEPPELIN-*Jira number*] in PR title, eg. [ZEPPELIN-533] ### How should this be tested? Display a matplotlib image with zeppelin: ```python import matplotlib.pyplot as plt plt.plot([1,2,3,4]) z.show(plt) ``` ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: Shiem Edelbrock Closes #2511 from Sdedelbrock/patch-1 and squashes the following commits: 2627e70 [Shiem Edelbrock] Fix Missing import --- python/src/main/resources/python/zeppelin_python.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/src/main/resources/python/zeppelin_python.py b/python/src/main/resources/python/zeppelin_python.py index eff88249ee4..446201d223b 100644 --- a/python/src/main/resources/python/zeppelin_python.py +++ b/python/src/main/resources/python/zeppelin_python.py @@ -24,6 +24,7 @@ import traceback import warnings import signal +import base64 from io import BytesIO try: From 6222ce264fd5a923ebcd91f371b77a765e333fd3 Mon Sep 17 00:00:00 2001 From: CloverHearts Date: Wed, 9 Aug 2017 15:20:15 +0900 Subject: [PATCH 003/492] replace pegdown to markdown zeppelin interpreter ### What is this PR for? I was change markdown render librarry for Jupyter note convertor. currently, we can got a same result for markdown. ### What type of PR is it? Improvement ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2824 ### How should this be tested? 1. build jupyter module `mvn clean package -DskipTests -pl 'zeppelin-jupyter' --am` 2. `cd zeppelin-jupyter/target` 3. `java -classpath zeppelin-jupyter-0.8.0-SNAPSHOT.jar org.apache.zeppelin.jupyter.JupyterUtil -i {your ipynb note file path!/getting_started.ipynb` (good sample : [go to sample](https://github.com/SciRuby/sciruby-notebooks/blob/master/getting_started.ipynb) 4. get a `note.json` and import to zeppelin on frontend! 5. enjoy ### Screenshots (if appropriate) #### problem ![28689484-9b13f3d2-72ca-11e7-9bda-02d33b30f036](https://user-images.githubusercontent.com/10525473/28861908-0d05c592-779e-11e7-9a4e-94e3fd2bd176.png) #### after ![image](https://user-images.githubusercontent.com/10525473/28807730-029510e6-76b2-11e7-9111-0e18569b1630.png) ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: CloverHearts Closes #2514 from cloverhearts/ZEPPELIN-2824-2 and squashes the following commits: 7108ab6 [CloverHearts] replace test case for markdown on jupyter note 2e8b332 [CloverHearts] replace pegdown to markdown zeppelin interpreter --- zeppelin-jupyter/pom.xml | 8 ++++---- .../java/org/apache/zeppelin/jupyter/JupyterUtil.java | 9 +++++---- .../zeppelin/jupyter/nbformat/JupyterUtilTest.java | 9 ++++++++- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index ae00db38d75..2390ef1be97 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -35,7 +35,7 @@ Jupyter support for Apache Zeppelin - 1.6.0 + 0.8.0-SNAPSHOT @@ -60,9 +60,9 @@ - org.pegdown - pegdown - ${pegdown.version} + org.apache.zeppelin + zeppelin-markdown + ${zeppelin.version} diff --git a/zeppelin-jupyter/src/main/java/org/apache/zeppelin/jupyter/JupyterUtil.java b/zeppelin-jupyter/src/main/java/org/apache/zeppelin/jupyter/JupyterUtil.java index 9bc5ca49faf..5cdbbbbf109 100644 --- a/zeppelin-jupyter/src/main/java/org/apache/zeppelin/jupyter/JupyterUtil.java +++ b/zeppelin-jupyter/src/main/java/org/apache/zeppelin/jupyter/JupyterUtil.java @@ -53,7 +53,8 @@ import org.apache.zeppelin.jupyter.zformat.Paragraph; import org.apache.zeppelin.jupyter.zformat.Result; import org.apache.zeppelin.jupyter.zformat.TypeData; -import org.pegdown.PegDownProcessor; +import org.apache.zeppelin.markdown.MarkdownParser; +import org.apache.zeppelin.markdown.PegdownParser; /** * @@ -63,7 +64,7 @@ public class JupyterUtil { private final RuntimeTypeAdapterFactory cellTypeFactory; private final RuntimeTypeAdapterFactory outputTypeFactory; - private final PegDownProcessor markdownProcessor; + private final MarkdownParser markdownProcessor; public JupyterUtil() { this.cellTypeFactory = RuntimeTypeAdapterFactory.of(Cell.class, "cell_type") @@ -73,7 +74,7 @@ public JupyterUtil() { .registerSubtype(ExecuteResult.class, "execute_result") .registerSubtype(DisplayData.class, "display_data").registerSubtype(Stream.class, "stream") .registerSubtype(Error.class, "error"); - this.markdownProcessor = new PegDownProcessor(); + this.markdownProcessor = new PegdownParser(); } public Nbformat getNbformat(Reader in) { @@ -141,7 +142,7 @@ public Note getNote(Nbformat nbformat, String codeReplaced, String markdownRepla } } else if (cell instanceof MarkdownCell || cell instanceof HeadingCell) { interpreterName = markdownReplaced; - String markdownContent = markdownProcessor.markdownToHtml(codeText); + String markdownContent = markdownProcessor.render(codeText); typeDataList.add(new TypeData(TypeData.HTML, markdownContent)); paragraph.setUpMarkdownConfig(true); } else { diff --git a/zeppelin-jupyter/src/test/java/org/apache/zeppelin/jupyter/nbformat/JupyterUtilTest.java b/zeppelin-jupyter/src/test/java/org/apache/zeppelin/jupyter/nbformat/JupyterUtilTest.java index a73571cec65..4688a72d4ac 100644 --- a/zeppelin-jupyter/src/test/java/org/apache/zeppelin/jupyter/nbformat/JupyterUtilTest.java +++ b/zeppelin-jupyter/src/test/java/org/apache/zeppelin/jupyter/nbformat/JupyterUtilTest.java @@ -86,7 +86,14 @@ public void getNoteAndVerifyData() throws Exception { assertTrue(((boolean) markdownConfig.get("editorHide")) == true); assertTrue(markdownParagraph.getResults().getCode().equals("SUCCESS")); List results = markdownParagraph.getResults().getMsg(); - assertTrue(results.get(0).getData().equals("\u003cdiv class\u003d\"alert\" style\u003d\"border: 1px solid #aaa; background: radial-gradient(ellipse at center, #ffffff 50%, #eee 100%);\"\u003e\n\u003cdiv class\u003d\"row\"\u003e\n \u003cdiv class\u003d\"col-sm-1\"\u003e\u003cimg src\u003d\"https://knowledgeanyhow.org/static/images/favicon_32x32.png\" style\u003d\"margin-top: -6px\"/\u003e\u003c/div\u003e\n \u003cdiv class\u003d\"col-sm-11\"\u003eThis notebook was created using \u003ca href\u003d\"https://knowledgeanyhow.org\"\u003eIBM Knowledge Anyhow Workbench\u003c/a\u003e. To learn more, visit us at \u003ca href\u003d\"https://knowledgeanyhow.org\"\u003ehttps://knowledgeanyhow.org\u003c/a\u003e.\u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e")); + assertTrue(results.get(0).getData().equals("
\n" + + "
\n" + + "
\n" + + "
\n" + + "
This notebook was created using IBM Knowledge Anyhow Workbench. To learn more, visit us at https://knowledgeanyhow.org.
\n" + + "
\n" + + "
\n" + + "
")); assertTrue(results.get(0).getType().equals("HTML")); } } From 7f2f67bcb795bb9df7545584807c65e58cb9f941 Mon Sep 17 00:00:00 2001 From: Malay Majithia Date: Tue, 1 Aug 2017 18:17:01 -0400 Subject: [PATCH 004/492] ZEPPELIN-2527 Changed editor cursor to thin ### What is this PR for? Previously, it's hard to recognize the cursor since it was too thick. After this PR, it will show the cursor same as other input boxes for consistency. See the attached GIFs for comparison. ### What type of PR is it? [Improvement] ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2527 ### How should this be tested? 1. Build: mvn clean package -Denforcer.skip -DskipTests -Drat.skip 2. Open a paragraph 3. Check the cursor ### Screenshots (if appropriate) Before ![cursor-change-pr-before](https://user-images.githubusercontent.com/1881135/27830832-cd55ec8e-60e5-11e7-8a0e-f75caecaba30.gif) After ![cursor-change-pr](https://user-images.githubusercontent.com/1881135/27830837-d01fba30-60e5-11e7-8722-8056f7abaebc.gif) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Malay Majithia Author: Malay Majithia Closes #2462 from malayhm/ZEP-2527 and squashes the following commits: 628c56c9 [Malay Majithia] Made the width of cursor to match with ace editor e1acaf88 [Malay Majithia] Changed the fix 0d00e3a0 [Malay Majithia] ZEPPELIN-2527 Changed editor cursor to thin --- zeppelin-web/src/app/notebook/paragraph/paragraph.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.css b/zeppelin-web/src/app/notebook/paragraph/paragraph.css index 8ecc7bba752..a0bf299ad24 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.css +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.css @@ -382,8 +382,9 @@ table.table-shortcut { /** set cursor color */ #main .emacs-mode .ace_cursor { - background: #C0C0C0!important; + background: #000 !important; border: none !important; + width: 2px !important; } .ace_text-input, .ace_gutter, .ace_layer, From 71d130521605cb7dcdb80fb18748ffcd87294ed5 Mon Sep 17 00:00:00 2001 From: Shu Jiaming Date: Wed, 9 Aug 2017 15:47:36 +0800 Subject: [PATCH 005/492] [ZEPPELIN-2841] fix a problem in shell interpreter . Working directory '.' can not be found in docker environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What is this PR for? shell interpreter complained that working directory '.' can not be found in docker environment. I add a line of code to set current working directory to USER`s home, and it works. ### What type of PR is it? Bug Fix ### Todos * tests ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2841 ### How should this be tested? run shell interpreter`s test units ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Shu Jiaming Author: 束佳明 Closes #2521 from vistep/master and squashes the following commits: 34a0049 [Shu Jiaming] ZEPPELIN-2841 fix a bug where shell interpreter complained that working directory '.' can not be found while zeppelin was running in docker enviroment. d02104a [束佳明] Merge pull request #1 from apache/master --- .../main/java/org/apache/zeppelin/shell/ShellInterpreter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shell/src/main/java/org/apache/zeppelin/shell/ShellInterpreter.java b/shell/src/main/java/org/apache/zeppelin/shell/ShellInterpreter.java index 07eed5f9ef1..daad0b32886 100644 --- a/shell/src/main/java/org/apache/zeppelin/shell/ShellInterpreter.java +++ b/shell/src/main/java/org/apache/zeppelin/shell/ShellInterpreter.java @@ -20,6 +20,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.File; import java.util.List; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; @@ -98,6 +99,7 @@ public InterpreterResult interpret(String cmd, InterpreterContext contextInterpr contextInterpreter.out, contextInterpreter.out)); executor.setWatchdog(new ExecuteWatchdog(Long.valueOf(getProperty(TIMEOUT_PROPERTY)))); executors.put(contextInterpreter.getParagraphId(), executor); + executor.setWorkingDirectory(new File(System.getProperty("user.home"))); int exitVal = executor.execute(cmdLine); LOGGER.info("Paragraph " + contextInterpreter.getParagraphId() + " return with exit value: " + exitVal); From 340b326d47f3e07b7d98c2760b41c10424bfbb30 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Thu, 10 Aug 2017 15:24:17 -0700 Subject: [PATCH 006/492] [ZEPPELIN-2823] Notebook saved status is wrong if there was a network disconnect or a flaky network. ### What is this PR for? Notebook content doesn't get saved if there is a flaky network, and at times user's paragraph content also gets lost in this process. ### What type of PR is it? [Bug Fix] ### What is the Jira issue? * [ZEPPELIN-2823](https://issues.apache.org/jira/browse/ZEPPELIN-2823) ### How should this be tested? Steps to re-produce: - create a new notebook - in the first paragraph enter text, say "version1" - now disconnect the network (say by removing LAN cable) - update this paragraph again with text "version2" - reconnect network - now observe the on the WebSocket reconnect, the content of this paragraph will go back to "version1" ### Screenshots (if appropriate) Before ![before](https://user-images.githubusercontent.com/674497/28852738-5772029e-76e0-11e7-82ed-8c2a25d3ab47.gif) After ![after](https://user-images.githubusercontent.com/674497/28852739-5774efcc-76e0-11e7-9e48-4bda935c4686.gif) ### Questions: * Does the licenses files need an update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: Prabhjyot Singh Closes #2512 from prabhjyotsingh/ZEPPELIN-2823 and squashes the following commits: 5f693ab93 [Prabhjyot Singh] - replace _.forEach with .map - extract BootstrapDialog.show outside of the for loop db30f479b [Prabhjyot Singh] alter text to `Changes that you have made will not be saved` 947be70b4 [Prabhjyot Singh] check if noteId exists in session or take it from fromMessage 8b8c2f974 [Prabhjyot Singh] check for empty originalText d2a835f77 [Prabhjyot Singh] wait for server confirmation before updating stats of notebook --- .../zeppelin/socket/NotebookServer.java | 3 + .../src/app/notebook/notebook.controller.js | 60 +++++++++++++++++-- .../paragraph/paragraph.controller.js | 36 +++++++++-- .../paragraph/result/result.controller.js | 2 +- .../websocket/websocket-event.factory.js | 2 +- .../websocket/websocket-message.service.js | 5 +- 6 files changed, 95 insertions(+), 13 deletions(-) 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 61bc536c8c4..3ddeec034e2 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 @@ -1186,6 +1186,9 @@ private void updateParagraph(NotebookSocket conn, HashSet userAndRoles, Map params = (Map) fromMessage.get("params"); Map config = (Map) fromMessage.get("config"); String noteId = getOpenNoteId(conn); + if (noteId == null) { + noteId = (String) fromMessage.get("noteId"); + } if (!hasParagraphWriterPermission(conn, notebook, noteId, userAndRoles, fromMessage.principal, "write")) { diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index a51ad4fff82..4b8b23fe024 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -51,6 +51,7 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, $scope.interpreterBindings = [] $scope.isNoteDirty = null $scope.saveTimer = null + $scope.paragraphWarningDialog = {} let connectedOnce = false let isRevisionPath = function (path) { @@ -396,11 +397,6 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, }, 10000) } - angular.element(window).on('beforeunload', function (e) { - $scope.killSaveTimer() - $scope.saveNote() - }) - $scope.setLookAndFeel = function (looknfeel) { $scope.note.config.looknfeel = looknfeel if ($scope.revisionView === true) { @@ -1277,6 +1273,60 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, $scope.note.config.personalizedMode = isPersonalized }) + $scope.$on('$routeChangeStart', function (event, next, current) { + if (!$scope.note || !$scope.note.paragraphs) { + return + } + if ($scope.note && $scope.note.paragraphs) { + $scope.note.paragraphs.map(par => { + if ($scope.allowLeave === true) { + return + } + let thisScope = angular.element( + '#' + par.id + '_paragraphColumn_main').scope() + + if (thisScope.dirtyText === undefined || + thisScope.originalText === undefined || + thisScope.dirtyText === thisScope.originalText) { + return true + } else { + event.preventDefault() + $scope.showParagraphWarning(next) + } + }) + } + }) + + $scope.showParagraphWarning = function (next) { + if ($scope.paragraphWarningDialog.opened !== true) { + $scope.paragraphWarningDialog = BootstrapDialog.show({ + closable: false, + closeByBackdrop: false, + closeByKeyboard: false, + title: 'Do you want to leave this site?', + message: 'Changes that you have made will not be saved.', + buttons: [{ + label: 'Stay', + action: function (dialog) { + dialog.close() + } + }, { + label: 'Leave', + action: function (dialog) { + dialog.close() + let locationToRedirect = next['$$route']['originalPath'] + Object.keys(next.pathParams).map(key => { + locationToRedirect = locationToRedirect.replace(':' + key, + next.pathParams[key]) + }) + $scope.allowLeave = true + $location.path(locationToRedirect) + } + }] + }) + } + } + $scope.$on('$destroy', function () { angular.element(window).off('beforeunload') $scope.killSaveTimer() diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index 141f7b3997e..b4c79dd6dc6 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -382,14 +382,41 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca paragraphText, $scope.paragraph.config, $scope.paragraph.settings.params) } + $scope.bindBeforeUnload = function () { + angular.element(window).off('beforeunload') + + let confirmOnPageExit = function (e) { + // If we haven't been passed the event get the window.event + e = e || window.event + let message = 'Do you want to reload this site?' + + // For IE6-8 and Firefox prior to version 4 + if (e) { + e.returnValue = message + } + // For Chrome, Safari, IE8+ and Opera 12+ + return message + } + angular.element(window).on('beforeunload', confirmOnPageExit) + } + + $scope.unBindBeforeUnload = function () { + angular.element(window).off('beforeunload') + } + $scope.saveParagraph = function (paragraph) { const dirtyText = paragraph.text if (dirtyText === undefined || dirtyText === $scope.originalText) { return } - commitParagraph(paragraph) - $scope.originalText = dirtyText - $scope.dirtyText = undefined + + $scope.bindBeforeUnload() + + commitParagraph(paragraph).then(function () { + $scope.originalText = dirtyText + $scope.dirtyText = undefined + $scope.unBindBeforeUnload() + }) } $scope.toggleEnableDisable = function (paragraph) { @@ -1092,7 +1119,8 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca settings: {params}, } = paragraph - websocketMsgSrv.commitParagraph(id, title, text, config, params) + return websocketMsgSrv.commitParagraph(id, title, text, config, params, + $route.current.pathParams.noteId) } /** Utility function */ diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js index df9ebe96316..be71d9c8492 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -645,7 +645,7 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio }, newParagraphConfig.results[resultIndex], paragraph, resultIndex) renderResult($scope.type, true) } else { - websocketMsgSrv.commitParagraph(paragraph.id, title, text, newParagraphConfig, params) + return websocketMsgSrv.commitParagraph(paragraph.id, title, text, newParagraphConfig, params) } } diff --git a/zeppelin-web/src/components/websocket/websocket-event.factory.js b/zeppelin-web/src/components/websocket/websocket-event.factory.js index db058bbc68b..10cfd9c2190 100644 --- a/zeppelin-web/src/components/websocket/websocket-event.factory.js +++ b/zeppelin-web/src/components/websocket/websocket-event.factory.js @@ -42,7 +42,7 @@ function WebsocketEventFactory ($rootScope, $websocket, $location, baseUrlSrv) { data.roles = '' } console.log('Send >> %o, %o, %o, %o, %o', data.op, data.principal, data.ticket, data.roles, data) - websocketCalls.ws.send(JSON.stringify(data)) + return websocketCalls.ws.send(JSON.stringify(data)) } websocketCalls.isConnected = function () { diff --git a/zeppelin-web/src/components/websocket/websocket-message.service.js b/zeppelin-web/src/components/websocket/websocket-message.service.js index 0dc02c3bfdc..cafc61b1f92 100644 --- a/zeppelin-web/src/components/websocket/websocket-message.service.js +++ b/zeppelin-web/src/components/websocket/websocket-message.service.js @@ -233,11 +233,12 @@ function WebsocketMessageService ($rootScope, websocketEvents) { }) }, - commitParagraph: function (paragraphId, paragraphTitle, paragraphData, paragraphConfig, paragraphParams) { - websocketEvents.sendNewEvent({ + commitParagraph: function (paragraphId, paragraphTitle, paragraphData, paragraphConfig, paragraphParams, noteId) { + return websocketEvents.sendNewEvent({ op: 'COMMIT_PARAGRAPH', data: { id: paragraphId, + noteId: noteId, title: paragraphTitle, paragraph: paragraphData, config: paragraphConfig, From 73426b4ef410db67020ead7e6e6421ec71bed5c2 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Thu, 10 Aug 2017 16:27:28 -0700 Subject: [PATCH 007/492] [ZEPPELIN-2846] Add selenium test case for AnyOfRolesAuthorizationFilter ### What is this PR for? This is to test the new feature that was brought in with ZEPPELIN-2825 (org.apache.zeppelin.utils.AnyOfRolesAuthorizationFilter) ### What type of PR is it? [Improvement] ### What is the Jira issue? * [ZEPPELIN-2846](https://issues.apache.org/jira/browse/ZEPPELIN-2846) ### How should this be tested? CI should be green ### Questions: * Does the licenses files need update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: Prabhjyot Singh Closes #2524 from prabhjyotsingh/ZEPPELIN-2846 and squashes the following commits: e2a7ad548 [Prabhjyot Singh] add selenium test case for AnyOfRolesAuthorizationFilter --- .../apache/zeppelin/AbstractZeppelinIT.java | 1 + .../integration/AuthenticationIT.java | 62 ++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java index 475be50270c..6f537fd80c8 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java @@ -40,6 +40,7 @@ abstract public class AbstractZeppelinIT { protected static WebDriver driver; protected final static Logger LOG = LoggerFactory.getLogger(AbstractZeppelinIT.class); + protected static final long MIN_IMPLICIT_WAIT = 5; protected static final long MAX_IMPLICIT_WAIT = 30; protected static final long MAX_BROWSER_TIMEOUT_SEC = 30; protected static final long MAX_PARAGRAPH_TIMEOUT_SEC = 120; diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java index f87bff2ce5a..38fe5744d9d 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java @@ -23,7 +23,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.List; - import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.zeppelin.AbstractZeppelinIT; @@ -38,6 +37,7 @@ import org.junit.rules.ErrorCollector; import org.openqa.selenium.By; import org.openqa.selenium.Keys; +import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,12 +63,14 @@ public class AuthenticationIT extends AbstractZeppelinIT { "securityManager.sessionManager = $sessionManager\n" + "securityManager.sessionManager.globalSessionTimeout = 86400000\n" + "shiro.loginUrl = /api/login\n" + + "anyofroles = org.apache.zeppelin.utils.AnyOfRolesAuthorizationFilter\n" + "[roles]\n" + "admin = *\n" + "hr = *\n" + "finance = *\n" + "[urls]\n" + "/api/version = anon\n" + + "/api/interpreter/** = authc, anyofroles[admin, finance]\n" + "/** = authc"; static String originalShiro = ""; @@ -182,6 +184,62 @@ public void testSimpleAuthentication() throws Exception { } } + @Test + public void testAnyOfRoles() throws Exception { + if (!endToEndTestEnabled()) { + return; + } + try { + AuthenticationIT authenticationIT = new AuthenticationIT(); + authenticationIT.authenticationUser("admin", "password1"); + + pollingWait(By.xpath("//div/button[contains(@class, 'nav-btn dropdown-toggle ng-scope')]"), + MAX_BROWSER_TIMEOUT_SEC).click(); + clickAndWait(By.xpath("//li/a[contains(@href, '#/interpreter')]")); + + collector.checkThat("Check is user has permission to view this page", true, + CoreMatchers.equalTo(pollingWait(By.xpath( + "//div[@id='main']/div/div[2]"), + MIN_IMPLICIT_WAIT).isDisplayed()) + ); + + authenticationIT.logoutUser("admin"); + + authenticationIT.authenticationUser("finance1", "finance1"); + + pollingWait(By.xpath("//div/button[contains(@class, 'nav-btn dropdown-toggle ng-scope')]"), + MAX_BROWSER_TIMEOUT_SEC).click(); + clickAndWait(By.xpath("//li/a[contains(@href, '#/interpreter')]")); + + collector.checkThat("Check is user has permission to view this page", true, + CoreMatchers.equalTo(pollingWait(By.xpath( + "//div[@id='main']/div/div[2]"), + MIN_IMPLICIT_WAIT).isDisplayed()) + ); + + authenticationIT.logoutUser("finance1"); + + authenticationIT.authenticationUser("hr1", "hr1"); + + pollingWait(By.xpath("//div/button[contains(@class, 'nav-btn dropdown-toggle ng-scope')]"), + MAX_BROWSER_TIMEOUT_SEC).click(); + clickAndWait(By.xpath("//li/a[contains(@href, '#/interpreter')]")); + + try { + collector.checkThat("Check is user has permission to view this page", + true, CoreMatchers.equalTo( + pollingWait(By.xpath("//li[contains(@class, 'ng-toast__message')]//span/span"), + MIN_IMPLICIT_WAIT).isDisplayed())); + } catch (TimeoutException e) { + throw new Exception("Expected ngToast not found", e); + } + authenticationIT.logoutUser("hr1"); + + } catch (Exception e) { + handleException("Exception in AuthenticationIT while testAnyOfRoles ", e); + } + } + @Test public void testGroupPermission() throws Exception { if (!endToEndTestEnabled()) { @@ -254,7 +312,7 @@ public void testGroupPermission() throws Exception { } catch (Exception e) { - handleException("Exception in ParagraphActionsIT while testGroupPermission ", e); + handleException("Exception in AuthenticationIT while testGroupPermission ", e); } } From 724cc48ed7ffe4e2e961f1dfd9a38bec1a74d113 Mon Sep 17 00:00:00 2001 From: Brent Kim Date: Tue, 15 Aug 2017 16:10:43 +0900 Subject: [PATCH 008/492] [ZEPPELIN-2859] Use es6 promise in interpreter.controller ### What is this PR for? Replace old angular callbacks with ES6 Promise callbacks. ### What type of PR is it? Improvement ### Todos More of the same works in the rest of the codebase needed. ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2859 ### How should this be tested? Open the page `/interpreter` and see if it lists interpreters and editing them works correctly. ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? N * Is there breaking changes for older versions? N * Does this needs documentation? N Author: Brent Kim Closes #2534 from Devgrapher/feature/pycon_sprint and squashes the following commits: a5f3a8c [Brent Kim] Use es6 promise in interpreter.controller --- .../app/interpreter/interpreter.controller.js | 88 ++++++++++--------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/zeppelin-web/src/app/interpreter/interpreter.controller.js b/zeppelin-web/src/app/interpreter/interpreter.controller.js index dc3619eb183..ef8840240e7 100644 --- a/zeppelin-web/src/app/interpreter/interpreter.controller.js +++ b/zeppelin-web/src/app/interpreter/interpreter.controller.js @@ -110,11 +110,11 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou let getInterpreterSettings = function () { $http.get(baseUrlSrv.getRestApiBase() + '/interpreter/setting') - .success(function (data, status, headers, config) { - $scope.interpreterSettings = data.body + .then(function (res) { + $scope.interpreterSettings = res.data.body checkDownloadingDependencies() - }).error(function (data, status, headers, config) { - if (status === 401) { + }).catch(function (res) { + if (res.status === 401) { ngToast.danger({ content: 'You don\'t have permission on this page', verticalPosition: 'bottom', @@ -124,7 +124,7 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou window.location = baseUrlSrv.getBase() }, 3000) } - console.log('Error %o %o', status, data.message) + console.log('Error %o %o', res.status, res.data ? res.data.message : '') }) } @@ -155,19 +155,19 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou } let getAvailableInterpreters = function () { - $http.get(baseUrlSrv.getRestApiBase() + '/interpreter').success(function (data, status, headers, config) { - $scope.availableInterpreters = data.body - }).error(function (data, status, headers, config) { - console.log('Error %o %o', status, data.message) + $http.get(baseUrlSrv.getRestApiBase() + '/interpreter').then(function (res) { + $scope.availableInterpreters = res.data.body + }).catch(function (res) { + console.log('Error %o %o', res.status, res.data ? res.data.message : '') }) } let getAvailableInterpreterPropertyWidgets = function () { $http.get(baseUrlSrv.getRestApiBase() + '/interpreter/property/types') - .success(function (data, status, headers, config) { - $scope.interpreterPropertyTypes = data.body - }).error(function (data, status, headers, config) { - console.log('Error %o %o', status, data.message) + .then(function (res) { + $scope.interpreterPropertyTypes = res.data.body + }).catch(function (res) { + console.log('Error %o %o', res.status, res.data ? res.data.message : '') }) } @@ -391,15 +391,16 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou .html(' Saving Setting') $http.put(baseUrlSrv.getRestApiBase() + '/interpreter/setting/' + settingId, request) - .success(function (data, status, headers, config) { - $scope.interpreterSettings[index] = data.body + .then(function (res) { + $scope.interpreterSettings[index] = res.data.body removeTMPSettings(index) checkDownloadingDependencies() thisConfirm.close() }) - .error(function (data, status, headers, config) { - console.log('Error %o %o', status, data.message) - ngToast.danger({content: data.message, verticalPosition: 'bottom'}) + .catch(function (res) { + const message = res.data ? res.data.message : 'Could not connect to server.' + console.log('Error %o %o', res.status, message) + ngToast.danger({content: message, verticalPosition: 'bottom'}) form.$show() thisConfirm.close() }) @@ -427,11 +428,11 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou callback: function (result) { if (result) { $http.delete(baseUrlSrv.getRestApiBase() + '/interpreter/setting/' + settingId) - .success(function (data, status, headers, config) { + .then(function (res) { let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) $scope.interpreterSettings.splice(index, 1) - }).error(function (data, status, headers, config) { - console.log('Error %o %o', status, data.message) + }).catch(function (res) { + console.log('Error %o %o', res.status, res.data ? res.data.message : '') }) } } @@ -463,13 +464,13 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou callback: function (result) { if (result) { $http.put(baseUrlSrv.getRestApiBase() + '/interpreter/setting/restart/' + settingId) - .success(function (data, status, headers, config) { + .then(function (res) { let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - $scope.interpreterSettings[index] = data.body + $scope.interpreterSettings[index] = res.data.body ngToast.info('Interpreter stopped. Will be lazily started on next run.') - }).error(function (data, status, headers, config) { - let errorMsg = (data !== null) ? data.message : 'Could not connect to server.' - console.log('Error %o %o', status, errorMsg) + }).catch(function (res) { + let errorMsg = (res.data !== null) ? res.data.message : 'Could not connect to server.' + console.log('Error %o %o', res.status, errorMsg) ngToast.danger(errorMsg) }) } @@ -535,14 +536,15 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou request.properties = newProperties $http.post(baseUrlSrv.getRestApiBase() + '/interpreter/setting', request) - .success(function (data, status, headers, config) { + .then(function (res) { $scope.resetNewInterpreterSetting() getInterpreterSettings() $scope.showAddNewSetting = false checkDownloadingDependencies() - }).error(function (data, status, headers, config) { - console.log('Error %o %o', status, data.message) - ngToast.danger({content: data.message, verticalPosition: 'bottom'}) + }).catch(function (res) { + const errorMsg = res.data ? res.data.message : 'Could not connect to server.' + console.log('Error %o %o', res.status, errorMsg) + ngToast.danger({content: errorMsg, verticalPosition: 'bottom'}) }) } @@ -699,12 +701,12 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou let request = angular.copy($scope.newRepoSetting) $http.post(baseUrlSrv.getRestApiBase() + '/interpreter/repository', request) - .success(function (data, status, headers, config) { + .then(function (res) { getRepositories() $scope.resetNewRepositorySetting() angular.element('#repoModal').modal('hide') - }).error(function (data, status, headers, config) { - console.log('Error %o %o', headers, config) + }).catch(function (res) { + console.log('Error %o %o', res.headers, res.config) }) } @@ -716,11 +718,11 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou callback: function (result) { if (result) { $http.delete(baseUrlSrv.getRestApiBase() + '/interpreter/repository/' + repoId) - .success(function (data, status, headers, config) { + .then(function (res) { let index = _.findIndex($scope.repositories, {'id': repoId}) $scope.repositories.splice(index, 1) - }).error(function (data, status, headers, config) { - console.log('Error %o %o', status, data.message) + }).catch(function (res) { + console.log('Error %o %o', res.status, res.data ? res.data.message : '') }) } } @@ -755,22 +757,22 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou $scope.showSparkUI = function (settingId) { $http.get(baseUrlSrv.getRestApiBase() + '/interpreter/metadata/' + settingId) - .success(function (data, status, headers, config) { - if (data.body === undefined) { + .then(function (res) { + if (res.data.body === undefined) { BootstrapDialog.alert({ message: 'No spark application running' }) return } - if (data.body.url) { - window.open(data.body.url, '_blank') + if (res.data.body.url) { + window.open(res.data.body.url, '_blank') } else { BootstrapDialog.alert({ - message: data.body.message + message: res.data.body.message }) } - }).error(function (data, status, headers, config) { - console.log('Error %o %o', status, data.message) + }).catch(function (res) { + console.log('Error %o %o', res.status, res.data ? res.data.message : '') }) } From ab037dbd2dbf6b57eb67a0c8d95b9fbfb7f8cbc0 Mon Sep 17 00:00:00 2001 From: Loun Lee Date: Tue, 15 Aug 2017 14:52:30 +0900 Subject: [PATCH 009/492] [ZEPPELIN-2853] Change the order of contents in index document. ### What is this PR for? To change the order of content in index documentation. Now the last two contents is "External Resources" and "Available Interpreters". This PR change the order to "Available Interpreters" and "External Resources". ### What type of PR is it? [Documentation] ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2853 ### How should this be tested? 1. Read the doc ### Screenshots (if appropriate) Before screen shot 2017-08-15 at 3 08 14 pm After screen shot 2017-08-15 at 3 04 44 pm ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Loun Lee Closes #2530 from beneficial02/feature/change_doc_order and squashes the following commits: 10a7172 [Loun Lee] feature: change the order of doc content --- docs/index.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/index.md b/docs/index.md index 102af4cdbea..5e991f1903f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -123,11 +123,6 @@ limitations under the License. * [Useful Developer Tools](./development/contribution/useful_developer_tools.html) * [How to Contribute (code)](./development/contribution/how_to_contribute_code.html) * [How to Contribute (website)](./development/contribution/how_to_contribute_website.html) - -#### External Resources - * [Mailing List](https://zeppelin.apache.org/community.html) - * [Apache Zeppelin Wiki](https://cwiki.apache.org/confluence/display/ZEPPELIN/Zeppelin+Home) - * [Stackoverflow Questions about Zeppelin (tag: `apache-zeppelin`)](http://stackoverflow.com/questions/tagged/apache-zeppelin) #### Available Interpreters * [Alluxio](./interpreter/alluxio.html) @@ -156,3 +151,7 @@ limitations under the License. * [Shell](./interpreter/Shell.html) * [Spark](./interpreter/spark.html) +#### External Resources + * [Mailing List](https://zeppelin.apache.org/community.html) + * [Apache Zeppelin Wiki](https://cwiki.apache.org/confluence/display/ZEPPELIN/Zeppelin+Home) + * [Stackoverflow Questions about Zeppelin (tag: `apache-zeppelin`)](http://stackoverflow.com/questions/tagged/apache-zeppelin) From 478b842c42ac18553d48d2e6b180324c61de8bc7 Mon Sep 17 00:00:00 2001 From: geonhee lee Date: Tue, 15 Aug 2017 15:30:33 +0900 Subject: [PATCH 010/492] [ZEPPELIN-2854] provide tooltip in notebook title ### What is this PR for? when title's length is too long it is hard to read full name. in case i think that it is good for tooltip ### What type of PR is it? Improvement ### Todos ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2854 ### How should this be tested? 1. Build: mvn clean package -Denforcer.skip -DskipTests -Drat.skip 2. create new notebook with long title 3. hover mouse on the title. 4. check tooltip providing full title ### Screenshots (if appropriate) before 2017-08-15 3 38 26 after 2017-08-15 3 36 06 ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: geonhee lee Closes #2531 from csk746/feature/ZEPPELIN-2854 and squashes the following commits: 06bbe2a [geonhee lee] feature: provide tooltip in title --- zeppelin-web/src/app/notebook/notebook-actionBar.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index 47ec8d7abf0..0e04d4934fc 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -19,7 +19,11 @@

-

{{noteName(note)}}

+

{{noteName(note)}}

From 25a9ecd96e95b30100a32750c92d224d5d57b567 Mon Sep 17 00:00:00 2001 From: EomJeongyeon Date: Tue, 15 Aug 2017 16:57:36 +0900 Subject: [PATCH 011/492] update screenshot images with new about_menu.png and settings_menu.png ### What is this PR for? Current screenshot images for "Settings" and "About Zeppelin" is outdated since UI is updated. So it'll be better update them with latest one. ### What type of PR is it? Documentation ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2856 ### How should this be tested? Just seeing file changes will be enough :) ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: EomJeongyeon Closes #2537 from EomJeongyeon/ZEPPELIN-2856 and squashes the following commits: 153a891 [EomJeongyeon] update screenshot images with new about_menu.png and settings_menu.png --- .../themes/zeppelin/img/ui-img/about_menu.png | Bin 73818 -> 91056 bytes .../zeppelin/img/ui-img/settings_menu.png | Bin 29508 -> 36521 bytes 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 docs/assets/themes/zeppelin/img/ui-img/about_menu.png mode change 100644 => 100755 docs/assets/themes/zeppelin/img/ui-img/settings_menu.png diff --git a/docs/assets/themes/zeppelin/img/ui-img/about_menu.png b/docs/assets/themes/zeppelin/img/ui-img/about_menu.png old mode 100644 new mode 100755 index 18ed125c9b0ba63b23b9228c2b0ecd44d2197738..1668678f31e2b2901e5240f129e807ce80681a4c GIT binary patch literal 91056 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 r)VVfu<}N_cF=M5lFuib< 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 diff --git a/docs/assets/themes/zeppelin/img/ui-img/settings_menu.png b/docs/assets/themes/zeppelin/img/ui-img/settings_menu.png old mode 100644 new mode 100755 index f4e8154637146c590c52db3860b9b9a6977aed21..9f19f5c4976fdaae99350bd08165784080e9aecb GIT binary patch literal 36521 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

meqV11;*9t<8 From cbdaf22a92d6ddded017aae2bc4ee5409d308f01 Mon Sep 17 00:00:00 2001 From: Jinkyu Yi Date: Tue, 15 Aug 2017 16:52:42 +0900 Subject: [PATCH 012/492] [ZEPPELIN-2861] Use OpenJDK in docker image. ### What is this PR for? Using OpenJDK at distributing docker image will reduce legal threats. ### What type of PR is it? Bug Fix ### Todos ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2861 ### How should this be tested? `docker build scripts/docker/zeppelin/bin` ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? **NO** * Is there breaking changes for older versions? **NO** * Does this needs documentation? **NO** Author: Jinkyu Yi Closes #2536 from jincreator/ZEPPELIN-2861 and squashes the following commits: 3b4fbcb [Jinkyu Yi] [ZEPPELIN-2861] Use OpenJDK in docker image. --- scripts/docker/zeppelin/bin/Dockerfile | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/scripts/docker/zeppelin/bin/Dockerfile b/scripts/docker/zeppelin/bin/Dockerfile index 9fb1aff70f4..9c8cfa3b3de 100644 --- a/scripts/docker/zeppelin/bin/Dockerfile +++ b/scripts/docker/zeppelin/bin/Dockerfile @@ -39,14 +39,11 @@ RUN echo "$LOG_TAG install tini related packages" && \ dpkg -i tini.deb && \ rm tini.deb -ENV JAVA_HOME=/usr/lib/jvm/java-8-oracle +ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 RUN echo "$LOG_TAG Install java8" && \ - echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections && \ - add-apt-repository -y ppa:webupd8team/java && \ apt-get -y update && \ - apt-get install -y oracle-java8-installer && \ - rm -rf /var/lib/apt/lists/* && \ - rm -rf /var/cache/oracle-jdk8-installer + apt-get install -y openjdk-8-jdk && \ + rm -rf /var/lib/apt/lists/* # should install conda first before numpy, matploylib since pip and python will be installed by conda RUN echo "$LOG_TAG Install miniconda2 related packages" && \ From 7b5db0492d8396892a0fd01c4187298abd111e68 Mon Sep 17 00:00:00 2001 From: Sachin Date: Thu, 17 Aug 2017 10:14:50 +0530 Subject: [PATCH 013/492] Fixing zeppelin-2281 by clearing output when data is available ### What is this PR for? Fixing ZEPPELIN-2281 by clearing previous when new data is available ### What type of PR is it? [Bug Fix] ### Todos * [x] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-2281 ### How should this be tested? * Execute a paragraph and see the output * Execute the same paragraph again but abort it before completion and see the output.Paragraph is clear and previous output is lost ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? NA * Is there breaking changes for older versions? NA * Does this needs documentation? NA Author: Sachin Closes #2518 from SachinJanani/ZEPPELIN-2281 and squashes the following commits: 5e276b9 [Sachin] Merge branch 'master' into ZEPPELIN-2281 c5ace52 [Sachin] Fixing zeppelin-2281 by clearing output when data is available --- .../interpreter/remote/RemoteInterpreterEventPoller.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java index 6927b3b6687..26c9d7994ed 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java @@ -193,11 +193,11 @@ public void run() { String paragraphId = (String) outputUpdate.get("paragraphId"); // clear the output - listener.onOutputClear(noteId, paragraphId); List> messages = (List>) outputUpdate.get("messages"); if (messages != null) { + listener.onOutputClear(noteId, paragraphId); for (int i = 0; i < messages.size(); i++) { Map m = messages.get(i); InterpreterResult.Type type = From b87bcf5a996dbf40b2d4d9d2804e422235e47d6a Mon Sep 17 00:00:00 2001 From: mingmxu Date: Wed, 16 Aug 2017 09:47:25 -0700 Subject: [PATCH 014/492] [ZEPPELIN-2865] upgrade Beam interpreter to latest version ### What is this PR for? upgrade Beam interpreter to use the latest version of Apache Beam. ### What type of PR is it? [Improvement] ### Todos * ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-2865 ### How should this be tested? * Start the Zeppelin server * The prefix of interpreter is %beam and then write your code with required imports and the runner Refer to `docs/interpreter/beam.md` for an example; ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? yes, updated `docs/interpreter/beam.md` and `README.md` Author: mingmxu Closes #2541 from XuMingmin/ZEPPELIN-2865 and squashes the following commits: 520f0fd7 [mingmxu] restore the notice message of scala-2.10 93b3e24d [mingmxu] upgrade to Apache Beam 2.0.0 --- beam/README.md | 2 +- beam/pom.xml | 10 +++++++++- docs/interpreter/beam.md | 14 +++----------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/beam/README.md b/beam/README.md index 57150a0208a..948c95cfc0f 100644 --- a/beam/README.md +++ b/beam/README.md @@ -8,7 +8,7 @@ Current interpreter implementation supports the static repl. It compiles the cod You have to first build the Beam interpreter by enable the **beam** profile as follows: ``` -mvn clean package -Pbeam -DskipTests +mvn clean package -Pbeam -DskipTests -Pscala-2.10 ``` ### Notice diff --git a/beam/pom.xml b/beam/pom.xml index c02695c460d..166652793fe 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -35,7 +35,7 @@ 2.3.0 1.6.2 - 0.2.0-incubating + 2.0.0 4.1.1.Final @@ -211,6 +211,14 @@ ${beam.beam.version} jar + + + org.apache.beam + beam-runners-flink_${scala.binary.version} + ${beam.beam.version} + + + ${project.groupId} diff --git a/docs/interpreter/beam.md b/docs/interpreter/beam.md index cbcd5e37d51..d992b8ee5b5 100644 --- a/docs/interpreter/beam.md +++ b/docs/interpreter/beam.md @@ -44,18 +44,10 @@ import java.io.Serializable; import java.util.Arrays; import java.util.List; import java.util.ArrayList; -import org.apache.spark.api.java.*; -import org.apache.spark.api.java.function.Function; -import org.apache.spark.SparkConf; -import org.apache.spark.streaming.*; -import org.apache.spark.SparkContext; import org.apache.beam.runners.direct.*; import org.apache.beam.sdk.runners.*; import org.apache.beam.sdk.options.*; -import org.apache.beam.runners.spark.*; -import org.apache.beam.runners.spark.io.ConsoleIO; import org.apache.beam.runners.flink.*; -import org.apache.beam.runners.flink.examples.WordCount.Options; import org.apache.beam.sdk.Pipeline; import org.apache.beam.sdk.io.TextIO; import org.apache.beam.sdk.options.PipelineOptionsFactory; @@ -89,12 +81,12 @@ public class MinimalWordCount { }; static final List SENTENCES = Arrays.asList(SENTENCES_ARRAY); public static void main(String[] args) { - Options options = PipelineOptionsFactory.create().as(Options.class); + PipelineOptions options = PipelineOptionsFactory.create().as(PipelineOptions.class); options.setRunner(FlinkRunner.class); Pipeline p = Pipeline.create(options); p.apply(Create.of(SENTENCES).withCoder(StringUtf8Coder.of())) .apply("ExtractWords", ParDo.of(new DoFn() { - @Override + @ProcessElement public void processElement(ProcessContext c) { for (String word : c.element().split("[^a-zA-Z']+")) { if (!word.isEmpty()) { @@ -105,7 +97,7 @@ public class MinimalWordCount { })) .apply(Count. perElement()) .apply("FormatResults", ParDo.of(new DoFn, String>() { - @Override + @ProcessElement public void processElement(DoFn, String>.ProcessContext arg0) throws Exception { s.add("\n" + arg0.element().getKey() + "\t" + arg0.element().getValue()); From 0d390ecaec857854c5931f1f785c60425fee9b86 Mon Sep 17 00:00:00 2001 From: Shinhong Park Date: Tue, 15 Aug 2017 17:11:29 +0900 Subject: [PATCH 015/492] [ZEPPELIN-2862] misalign text and icon in notebook dropdown on navbar ### What is this PR for? Currently, there is that misaligned text and icon in notebook dropdown on navbar ### What type of PR is it? [ Improvement ] ### What is the Jira issue? [ZEPPELIN-2862](https://issues.apache.org/jira/browse/ZEPPELIN-2862) ### How should this be tested? 1. Run web development mode in local. 2. Click Notebook Button on navbar. ### Screenshots (if appropriate) - Before 2017-08-15 5 07 41 - After 2017-08-15 5 20 11 ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Shinhong Park Closes #2538 from wishwisdom/ZEPPELIN-2862 and squashes the following commits: 2a1a0155 [Shinhong Park] fix: misalign text and icon in navbar --- .../src/components/navbar/navbar-note-list-elem.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zeppelin-web/src/components/navbar/navbar-note-list-elem.html b/zeppelin-web/src/components/navbar/navbar-note-list-elem.html index cb36cfaca9d..ad1f771f2b2 100644 --- a/zeppelin-web/src/components/navbar/navbar-note-list-elem.html +++ b/zeppelin-web/src/components/navbar/navbar-note-list-elem.html @@ -13,7 +13,7 @@ --> - + {{noteName(node)}} @@ -22,7 +22,7 @@

- + {{noteName(node)}}
- + Trash
From f942e6cccbf86049682ae717aee814e751429c69 Mon Sep 17 00:00:00 2001 From: yammathe Date: Wed, 16 Aug 2017 12:38:46 +0300 Subject: [PATCH 016/492] [ZEPPELIN-2770] Zeppelin is not working in IE-11 ### What is this PR for? There is a javascript error while loading zeppelin homepage in IE-11, causing a blank page to appear. ### What type of PR is it? [Bug Fix] ### What is the Jira issue? * [ZEPPELIN-2770](https://issues.apache.org/jira/browse/ZEPPELIN-2770) ### How should this be tested? Try opening Zeppelin homepage in IE-11. ### Questions: * Does the licenses files need update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: yammathe Closes #2527 from byamthev/IE_COMPATIBILITY and squashes the following commits: 2d7230e9 [yammathe] [ZEPPELIN-2770] Zeppelin is not working in IE-11 81cfde88 [yammathe] fix IE bug: startsWith do not exists for string --- zeppelin-web/src/app/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zeppelin-web/src/app/app.js b/zeppelin-web/src/app/app.js index 034de2a6ca6..d46d026858a 100644 --- a/zeppelin-web/src/app/app.js +++ b/zeppelin-web/src/app/app.js @@ -185,7 +185,7 @@ function auth () { $rootScope.ticket = angular.fromJson(response.data).body $rootScope.ticket.screenUsername = $rootScope.ticket.principal - if ($rootScope.ticket.principal.startsWith('#Pac4j')) { + if ($rootScope.ticket.principal.indexOf('#Pac4j') === 0) { let re = ', name=(.*?),' $rootScope.ticket.screenUsername = $rootScope.ticket.principal.match(re)[1] } From af189913677dd18816cc8aea8727adce9d7fe29a Mon Sep 17 00:00:00 2001 From: agura Date: Tue, 27 Jun 2017 16:45:27 +0300 Subject: [PATCH 017/492] ZEPPELIN-2674 Ignite version upgraded up to 2.0 ### What is this PR for? Apache Ignite 2.0 brings many improvements and new SQL related functionality. This PR upgrades Ignite version for Ignite interpreter. ### What type of PR is it? [Improvement] ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2674 ### How should this be tested? Unit-tests are enough. Author: agura Closes #2445 from agura/zeppelin-2674 and squashes the following commits: 85145c3d [agura] ZEPPELIN-2674 Ignite version upgraded up to 2.0 --- ignite/pom.xml | 2 +- .../org/apache/zeppelin/ignite/IgniteSqlInterpreterTest.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ignite/pom.xml b/ignite/pom.xml index aadb9dd329c..4af25ceb3f5 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -32,7 +32,7 @@ Zeppelin: Apache Ignite interpreter - 1.9.0 + 2.0.0 diff --git a/ignite/src/test/java/org/apache/zeppelin/ignite/IgniteSqlInterpreterTest.java b/ignite/src/test/java/org/apache/zeppelin/ignite/IgniteSqlInterpreterTest.java index 08146e198a8..b06d457c38d 100644 --- a/ignite/src/test/java/org/apache/zeppelin/ignite/IgniteSqlInterpreterTest.java +++ b/ignite/src/test/java/org/apache/zeppelin/ignite/IgniteSqlInterpreterTest.java @@ -24,7 +24,6 @@ import org.apache.ignite.Ignition; import org.apache.ignite.configuration.CacheConfiguration; import org.apache.ignite.configuration.IgniteConfiguration; -import org.apache.ignite.marshaller.optimized.OptimizedMarshaller; import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder; import org.apache.zeppelin.interpreter.InterpreterContext; From f6b58ee5a06d32af37903cc768e106079d267b2d Mon Sep 17 00:00:00 2001 From: tinkoff-dwh Date: Mon, 31 Jul 2017 15:56:34 +0500 Subject: [PATCH 018/492] [HOTFIX] JDBC completions. fix cursor position ### What is this PR for? If text of paragraph contains interpreter name then parser works incorrect (cursor position incorrect). ### What type of PR is it? Bug Fix ### Screenshots (if appropriate) before ![before](https://user-images.githubusercontent.com/25951039/28326867-55195f50-6bfb-11e7-8f93-c381658d598a.png) after ![after](https://user-images.githubusercontent.com/25951039/28326904-6f053fa6-6bfb-11e7-8586-00da4fa95b21.png) ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: tinkoff-dwh Author: Tinkoff DWH Closes #2495 from tinkoff-dwh/completion_hotfix and squashes the following commits: bac1ac2 [tinkoff-dwh] Merge remote-tracking branch 'upstream/master' into completion_hotfix 80c8e2a [Tinkoff DWH] small refactoring 5e9aa73 [tinkoff-dwh] add more data to dataset of test 9f3dbc7 [tinkoff-dwh] added test 43f8ffd [tinkoff-dwh] another solution to fix cursor position + small fix to return table names 4e6347a [Tinkoff DWH] fix cursor position for completions --- .../apache/zeppelin/jdbc/SqlCompleter.java | 3 +- .../apache/zeppelin/notebook/Paragraph.java | 85 +++++++++++++------ .../zeppelin/notebook/ParagraphTest.java | 43 ++++++++-- 3 files changed, 97 insertions(+), 34 deletions(-) diff --git a/jdbc/src/main/java/org/apache/zeppelin/jdbc/SqlCompleter.java b/jdbc/src/main/java/org/apache/zeppelin/jdbc/SqlCompleter.java index 46cc4bd0c26..6103fc7bc16 100644 --- a/jdbc/src/main/java/org/apache/zeppelin/jdbc/SqlCompleter.java +++ b/jdbc/src/main/java/org/apache/zeppelin/jdbc/SqlCompleter.java @@ -179,7 +179,8 @@ private static Set getCatalogNames(DatabaseMetaData meta, List s private static void fillTableNames(String schema, DatabaseMetaData meta, Set tables) { - try (ResultSet tbls = meta.getTables(schema, schema, "%", null)) { + try (ResultSet tbls = meta.getTables(schema, schema, "%", + new String[]{"TABLE", "VIEW", "ALIAS", "SYNONYM", "GLOBAL TEMPORARY", "LOCAL TEMPORARY"})) { while (tbls.next()) { String table = tbls.getString("TABLE_NAME"); tables.add(table); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java index ac3d19f4cfc..37138e63c42 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java @@ -17,35 +17,58 @@ package org.apache.zeppelin.notebook; -import com.google.common.collect.Maps; -import com.google.common.base.Strings; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import org.apache.commons.lang.StringUtils; import org.apache.zeppelin.common.JsonSerializable; import org.apache.zeppelin.completer.CompletionType; import org.apache.zeppelin.display.AngularObject; import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.helium.HeliumPackage; -import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; -import org.apache.zeppelin.user.AuthenticationInfo; -import org.apache.zeppelin.user.Credentials; -import org.apache.zeppelin.user.UserCredentials; import org.apache.zeppelin.display.GUI; import org.apache.zeppelin.display.Input; -import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.helium.HeliumPackage; +import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.Interpreter.FormType; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterContextRunner; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterFactory; +import org.apache.zeppelin.interpreter.InterpreterInfo; +import org.apache.zeppelin.interpreter.InterpreterOption; +import org.apache.zeppelin.interpreter.InterpreterOutput; +import org.apache.zeppelin.interpreter.InterpreterOutputListener; +import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResult.Code; +import org.apache.zeppelin.interpreter.InterpreterResultMessage; +import org.apache.zeppelin.interpreter.InterpreterResultMessageOutput; +import org.apache.zeppelin.interpreter.InterpreterSetting; +import org.apache.zeppelin.interpreter.InterpreterSettingManager; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.resource.ResourcePool; import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.scheduler.JobListener; import org.apache.zeppelin.scheduler.Scheduler; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.apache.zeppelin.user.Credentials; +import org.apache.zeppelin.user.UserCredentials; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.io.Serializable; -import java.util.*; - import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.Maps; /** * Paragraph is a representation of an execution unit. @@ -204,7 +227,7 @@ public boolean isEnabled() { } public String getRequiredReplName() { - return getRequiredReplName(text); + return getRequiredReplName(text != null ? text.trim() : text); } public static String getRequiredReplName(String text) { @@ -212,15 +235,14 @@ public static String getRequiredReplName(String text) { return null; } - String trimmed = text.trim(); - if (!trimmed.startsWith("%")) { + if (!text.startsWith("%")) { return null; } // get script head int scriptHeadIndex = 0; - for (int i = 0; i < trimmed.length(); i++) { - char ch = trimmed.charAt(i); + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); if (Character.isWhitespace(ch) || ch == '(' || ch == '\n') { break; } @@ -247,11 +269,10 @@ public static String getScriptBody(String text) { return text; } - String trimmed = text.trim(); - if (magic.length() + 1 >= trimmed.length()) { + if (magic.length() + 1 >= text.length()) { return ""; } - return trimmed.substring(magic.length() + 1).trim(); + return text.substring(magic.length() + 1).trim(); } public Interpreter getRepl(String name) { @@ -288,13 +309,12 @@ public List completion(String buffer, int cursor) { return getInterpreterCompletion(); } } + String trimmedBuffer = buffer != null ? buffer.trim() : null; + cursor = calculateCursorPosition(buffer, trimmedBuffer, cursor); - String replName = getRequiredReplName(buffer); - if (replName != null && cursor > replName.length()) { - cursor -= replName.length() + 1; - } + String replName = getRequiredReplName(trimmedBuffer); - String body = getScriptBody(buffer); + String body = getScriptBody(trimmedBuffer); Interpreter repl = getRepl(replName); if (repl == null) { return null; @@ -306,6 +326,21 @@ public List completion(String buffer, int cursor) { return completion; } + public int calculateCursorPosition(String buffer, String trimmedBuffer, int cursor) { + int countWhitespacesAtStart = buffer.indexOf(trimmedBuffer); + if (countWhitespacesAtStart > 0) { + cursor -= countWhitespacesAtStart; + } + + String replName = getRequiredReplName(trimmedBuffer); + if (replName != null && cursor > replName.length()) { + String body = trimmedBuffer.substring(replName.length() + 1); + cursor -= replName.length() + 1 + body.indexOf(body.trim()); + } + + return cursor; + } + public void setInterpreterFactory(InterpreterFactory factory) { this.factory = factory; } diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java index 0e778463753..7bd6819c39a 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java @@ -21,10 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -33,7 +30,11 @@ import static org.mockito.Mockito.when; import com.google.common.collect.Lists; + +import java.util.Arrays; import java.util.List; + +import org.apache.commons.lang3.tuple.Triple; import org.apache.zeppelin.display.AngularObject; import org.apache.zeppelin.display.AngularObjectBuilder; import org.apache.zeppelin.display.AngularObjectRegistry; @@ -41,7 +42,6 @@ import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.Interpreter.FormType; import org.apache.zeppelin.interpreter.InterpreterContext; -import org.apache.zeppelin.interpreter.InterpreterFactory; import org.apache.zeppelin.interpreter.InterpreterGroup; import org.apache.zeppelin.interpreter.InterpreterOption; import org.apache.zeppelin.interpreter.InterpreterResult; @@ -52,14 +52,12 @@ import org.apache.zeppelin.interpreter.InterpreterSetting.Status; import org.apache.zeppelin.interpreter.InterpreterSettingManager; import org.apache.zeppelin.resource.ResourcePool; -import org.apache.zeppelin.scheduler.JobListener; import org.apache.zeppelin.user.AuthenticationInfo; import org.apache.zeppelin.user.Credentials; import org.junit.Test; import java.util.HashMap; import java.util.Map; -import org.mockito.ArgumentCaptor; import org.mockito.Mockito; public class ParagraphTest { @@ -228,8 +226,37 @@ public void returnUnchangedResultsWithDifferentUser() throws Throwable { assertNotEquals(p1.getReturn().toString(), p2.getReturn().toString()); assertEquals(p1, spyParagraph.getUserParagraph(user1.getUser())); + } - - + @Test + public void testCursorPosition() { + Paragraph paragraph = spy(new Paragraph()); + doReturn(null).when(paragraph).getRepl(anyString()); + // left = buffer, middle = cursor position into source code, right = cursor position after parse + List> dataSet = Arrays.asList( + Triple.of("%jdbc schema.", 13, 7), + Triple.of(" %jdbc schema.", 16, 7), + Triple.of(" \n%jdbc schema.", 15, 7), + Triple.of("%jdbc schema.table. ", 19, 13), + Triple.of("%jdbc schema.\n\n", 13, 7), + Triple.of(" %jdbc schema.tab\n\n", 18, 10), + Triple.of(" \n%jdbc schema.\n \n", 16, 7), + Triple.of(" \n%jdbc schema.\n \n", 16, 7), + Triple.of(" \n%jdbc\n\n schema\n \n", 17, 6), + Triple.of("%another\n\n schema.", 18, 7), + Triple.of("\n\n schema.", 10, 7), + Triple.of("schema.", 7, 7), + Triple.of("schema. \n", 7, 7), + Triple.of(" \n %jdbc", 11, 0), + Triple.of("\n %jdbc", 9, 0), + Triple.of("%jdbc \n schema", 16, 6), + Triple.of("%jdbc \n \n schema", 20, 6) + ); + + for (Triple data : dataSet) { + Integer actual = paragraph.calculateCursorPosition(data.getLeft(), data.getLeft().trim(), data.getMiddle()); + assertEquals(data.getRight(), actual); + } } + } From b21f89f6fe8dfe209ee6e827b6092c2ae9fe7314 Mon Sep 17 00:00:00 2001 From: imnotkind Date: Thu, 17 Aug 2017 18:56:31 +0900 Subject: [PATCH 019/492] [ZEPPELIN-2850] URI Data scheme -> Blob creation ###What is this PR for? in save-as.service.js, if we use URI Data scheme, we could only contain 2MB data in chrome. using the createObjectURL and File API's blob feature, i managed to upgrade the capacity to about 900MB. plus this update is better in debugging too. if we exceed the 2MB limit in URI data scheme, the download just failed with no accurate console log originally, so it was kinda hard to know why this happens. But using this technique, if it exceeds the 900MB limit, the console log points directly about what the problem is. like this : Uncaught RangeError: Failed to construct 'Blob': Array length exceeds supported limit. https://github.com/apache/zeppelin/blob/master/zeppelin-web/src/app/notebook/save-as/save-as.service.js ###What type of PR is it? Improvement ###Todos nothing more i guess ###What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2850 ###How should this be tested? open zeppelin using chrome. make a table by select, then download it by csv or tsv. the table should be BIG, like really big, (but not that big for companies, which is my case) to test. in the original version if the whole data exceeds 2MB, you could see that the download fails. but using my script, it doesn't fail until it reaches about 900MB~1GB, which is a tremendous improvement. ###Screenshots (if appropriate) i'll post it later if you really need it. but i'm pretty sure you guys know what i'm talking about :) ###Questions: Does the licenses files need update? no (i guess) Is there breaking changes for older versions? no Does this needs documentation? maybe? Author: imnotkind Closes #2532 from imnotkind/master and squashes the following commits: 075c4ec [imnotkind] Update save-as.service.js db778b1 [imnotkind] Merge pull request #1 from imnotkind/imnotkind-patch-1 e9ad52e [imnotkind] Update save-as.service.js --- zeppelin-web/src/app/notebook/save-as/save-as.service.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/zeppelin-web/src/app/notebook/save-as/save-as.service.js b/zeppelin-web/src/app/notebook/save-as/save-as.service.js index c71c0f7f72a..ff463c85289 100644 --- a/zeppelin-web/src/app/notebook/save-as/save-as.service.js +++ b/zeppelin-web/src/app/notebook/save-as/save-as.service.js @@ -38,7 +38,11 @@ function SaveAsService (browserDetectService) { } angular.element('body > iframe#SaveAsId').remove() } else { - content = 'data:image/svg;charset=utf-8,' + BOM + encodeURIComponent(content) + let binaryData = [] + binaryData.push(BOM) + binaryData.push(content) + content = window.URL.createObjectURL(new Blob(binaryData)) + angular.element('body').append('') let saveAsElement = angular.element('body > a#SaveAsId') saveAsElement.attr('href', content) @@ -46,6 +50,7 @@ function SaveAsService (browserDetectService) { saveAsElement.attr('target', '_blank') saveAsElement[0].click() saveAsElement.remove() + window.URL.revokeObjectURL(content) } } } From dfc62f5f4c6d7821e566650cc05587a4ea3793b8 Mon Sep 17 00:00:00 2001 From: Andrey Gura Date: Tue, 22 Aug 2017 15:32:33 +0300 Subject: [PATCH 020/492] ZEPPELIN-2872 Ignite version upgraded up to 2.1 ### What is this PR for? This PR upgrades Ignite version for Ignite interpreter up to 2.1. ### What type of PR is it? [Improvement] ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2872 ### How should this be tested? Unit-tests are enough. Author: Andrey Gura Closes #2549 from agura/zeppelin-2872 and squashes the following commits: c2e7a30 [Andrey Gura] ZEPPELIN-2872 Ignite version upgraded up to 2.1 --- ignite/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ignite/pom.xml b/ignite/pom.xml index 4af25ceb3f5..dfaae090f43 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -32,7 +32,7 @@ Zeppelin: Apache Ignite interpreter - 2.0.0 + 2.1.0 From 30bfcae0c0c9650aff3ed1f8fe41eee9c4e93cb1 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Fri, 30 Jun 2017 14:48:22 +0800 Subject: [PATCH 021/492] ZEPPELIN-1515. Notebook: HDFS as a backend storage (Use hadoop client jar) ### What is this PR for? This PR is trying to add hdfs as another implementation for `NotebookRepo`. There's another PR about using webhdfs to implement that. Actually hdfs client library is compatibility cross major versions. See http://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-common/Compatibility.html#Wire_compatibility, if using webhdfs, the code become more complicated and may lose some features of hdfs. This PR is also required for HA of zeppelin, so that multiple zeppelin instances can share notes via hdfs. I add hadoop-client in pom file. So zeppelin will package hadoop client jar into its binary distribution. This is because zeppelin may be installed in a gateway machine where no hadoop is installed (only hadoop configuration file is existed in this machine) And since the hadoop client will work with multiple versions of hadoop, so it is fine to package into binary distribution. Spark also package hadoop client jar in its binary distribution. ### What type of PR is it? [Feature] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-1515 ### How should this be tested? Unit test is added. Also manually verify it in a single node cluster. ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2455 from zjffdu/ZEPPELIN-1515 and squashes the following commits: b3e83ab [Jeff Zhang] ZEPPELIN-1515. Notebook: HDFS as a backend storage (Read & Write Mode) --- bin/zeppelin-daemon.sh | 4 + bin/zeppelin.sh | 4 + conf/zeppelin-site.xml.template | 20 ++ docs/setup/storage/storage.md | 17 ++ zeppelin-server/pom.xml | 89 -------- zeppelin-zengine/pom.xml | 66 ++++++ .../zeppelin/conf/ZeppelinConfiguration.java | 5 +- .../org/apache/zeppelin/notebook/Note.java | 1 + .../notebook/repo/HdfsNotebookRepo.java | 200 ++++++++++++++++++ .../notebook/repo/NotebookRepoSync.java | 1 + .../notebook/repo/HdfsNotebookRepoTest.java | 101 +++++++++ 11 files changed, 418 insertions(+), 90 deletions(-) create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/HdfsNotebookRepo.java create mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/HdfsNotebookRepoTest.java diff --git a/bin/zeppelin-daemon.sh b/bin/zeppelin-daemon.sh index e88c26fc43c..5982aee2e0f 100755 --- a/bin/zeppelin-daemon.sh +++ b/bin/zeppelin-daemon.sh @@ -67,6 +67,10 @@ if [[ -d "${ZEPPELIN_HOME}/zeppelin-server/target/classes" ]]; then ZEPPELIN_CLASSPATH+=":${ZEPPELIN_HOME}/zeppelin-server/target/classes" fi +if [[ -n "${HADOOP_CONF_DIR}" ]] && [[ -d "${HADOOP_CONF_DIR}" ]]; then + ZEPPELIN_CLASSPATH+=":${HADOOP_CONF_DIR}" +fi + # Add jdbc connector jar # ZEPPELIN_CLASSPATH+=":${ZEPPELIN_HOME}/jdbc/jars/jdbc-connector-jar" diff --git a/bin/zeppelin.sh b/bin/zeppelin.sh index 44fc2cfe89a..a13f9db977d 100755 --- a/bin/zeppelin.sh +++ b/bin/zeppelin.sh @@ -73,6 +73,10 @@ addJarInDir "${ZEPPELIN_HOME}/zeppelin-web/target/lib" ZEPPELIN_CLASSPATH="$CLASSPATH:$ZEPPELIN_CLASSPATH" +if [[ -n "${HADOOP_CONF_DIR}" ]] && [[ -d "${HADOOP_CONF_DIR}" ]]; then + ZEPPELIN_CLASSPATH+=":${HADOOP_CONF_DIR}" +fi + if [[ ! -d "${ZEPPELIN_LOG_DIR}" ]]; then echo "Log dir doesn't exist, create ${ZEPPELIN_LOG_DIR}" $(mkdir -p "${ZEPPELIN_LOG_DIR}") diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template index adf58102cab..3ec6e27c246 100755 --- a/conf/zeppelin-site.xml.template +++ b/conf/zeppelin-site.xml.template @@ -173,6 +173,26 @@ --> + + com.google.code.gson gson - - org.apache.hadoop - hadoop-common - ${hadoop-common.version} - - - com.sun.jersey - jersey-core - - - com.sun.jersey - jersey-json - - - com.sun.jersey - jersey-server - - - - javax.servlet - servlet-api - - - org.apache.avro - avro - - - org.apache.jackrabbit - jackrabbit-webdav - - - io.netty - netty - - - commons-httpclient - commons-httpclient - - - org.apache.zookeeper - zookeeper - - - org.eclipse.jgit - org.eclipse.jgit - - - com.jcraft - jsch - - - org.apache.commons - commons-compress - - - - org.quartz-scheduler quartz diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index 38b1e830068..337b71045ef 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -36,6 +36,7 @@ + 2.6.0 3.4 2.0 1.10.62 @@ -301,6 +302,71 @@ 1.5 + + org.apache.hadoop + hadoop-client + ${hadoop.version} + + + + com.sun.jersey + jersey-core + + + com.sun.jersey + jersey-json + + + com.sun.jersey + jersey-server + + + + javax.servlet + servlet-api + + + org.apache.avro + avro + + + org.apache.jackrabbit + jackrabbit-webdav + + + io.netty + netty + + + commons-httpclient + commons-httpclient + + + org.apache.zookeeper + zookeeper + + + org.eclipse.jgit + org.eclipse.jgit + + + com.jcraft + jsch + + + org.apache.commons + commons-compress + + + xml-apis + xml-apis + + + xerces + xercesImpl + + + 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 9822ecf2c8f..f00fe9346f3 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 @@ -681,7 +681,10 @@ public static enum ConfVars { ZEPPELIN_SERVER_XFRAME_OPTIONS("zeppelin.server.xframe.options", "SAMEORIGIN"), ZEPPELIN_SERVER_JETTY_NAME("zeppelin.server.jetty.name", null), ZEPPELIN_SERVER_STRICT_TRANSPORT("zeppelin.server.strict.transport", "max-age=631138519"), - ZEPPELIN_SERVER_X_XSS_PROTECTION("zeppelin.server.xxss.protection", "1"); + ZEPPELIN_SERVER_X_XSS_PROTECTION("zeppelin.server.xxss.protection", "1"), + + ZEPPELIN_HDFS_KEYTAB("zeppelin.hdfs.keytab", ""), + ZEPPELIN_HDFS_PRINCIPAL("zeppelin.hdfs.principal", ""); private String varName; @SuppressWarnings("rawtypes") diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java index ff5931c4e63..198e278e04b 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java @@ -106,6 +106,7 @@ public class Note implements ParagraphJobListener, JsonSerializable { public Note() { + generateId(); } public Note(NotebookRepo repo, InterpreterFactory factory, diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/HdfsNotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/HdfsNotebookRepo.java new file mode 100644 index 00000000000..fdaaf04310b --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/HdfsNotebookRepo.java @@ -0,0 +1,200 @@ +package org.apache.zeppelin.notebook.repo; + +import org.apache.commons.lang.StringUtils; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.io.IOUtils; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.notebook.Note; +import org.apache.zeppelin.notebook.NoteInfo; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.PrivilegedAction; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * NotebookRepos for hdfs. + * + * Assume the notebook directory structure is as following + * - notebookdir + * - noteId/note.json + * - noteId/note.json + * - noteId/note.json + */ +public class HdfsNotebookRepo implements NotebookRepo { + private static final Logger LOGGER = LoggerFactory.getLogger(HdfsNotebookRepo.class); + + + private Configuration hadoopConf; + private ZeppelinConfiguration zConf; + private boolean isSecurityEnabled = false; + private FileSystem fs; + private Path notebookDir; + + public HdfsNotebookRepo(ZeppelinConfiguration zConf) throws IOException { + this.zConf = zConf; + this.hadoopConf = new Configuration(); + this.notebookDir = new Path(zConf.getNotebookDir()); + LOGGER.info("Use hdfs directory {} to store notebook", notebookDir); + this.isSecurityEnabled = UserGroupInformation.isSecurityEnabled(); + if (isSecurityEnabled) { + String keytab = zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_HDFS_KEYTAB); + String principal = zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_HDFS_PRINCIPAL); + if (StringUtils.isBlank(keytab) || StringUtils.isBlank(principal)) { + throw new IOException("keytab and principal can not be empty, keytab: " + keytab + + ", principal: " + principal); + } + UserGroupInformation.loginUserFromKeytab(principal, keytab); + } + + this.fs = FileSystem.get(new Configuration()); + if (!fs.exists(notebookDir)) { + fs.mkdirs(notebookDir); + LOGGER.info("Create notebook dir {} in hdfs", notebookDir.toString()); + } + if (fs.isFile(notebookDir)) { + throw new IOException("notebookDir {} is file instead of directory, please remove it or " + + "specify another directory"); + } + + } + + @Override + public List list(AuthenticationInfo subject) throws IOException { + return callHdfsOperation(new HdfsOperation>() { + @Override + public List call() throws IOException { + List noteInfos = new ArrayList<>(); + for (FileStatus status : fs.globStatus(new Path(notebookDir, "*/note.json"))) { + NoteInfo noteInfo = new NoteInfo(status.getPath().getParent().getName(), "", null); + noteInfos.add(noteInfo); + } + return noteInfos; + } + }); + } + + @Override + public Note get(final String noteId, AuthenticationInfo subject) throws IOException { + return callHdfsOperation(new HdfsOperation() { + @Override + public Note call() throws IOException { + Path notePath = new Path(notebookDir.toString() + "/" + noteId + "/note.json"); + LOGGER.debug("Read note from file: " + notePath); + ByteArrayOutputStream noteBytes = new ByteArrayOutputStream(); + IOUtils.copyBytes(fs.open(notePath), noteBytes, hadoopConf); + return Note.fromJson(new String(noteBytes.toString( + zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_ENCODING)))); + } + }); + } + + @Override + public void save(final Note note, AuthenticationInfo subject) throws IOException { + callHdfsOperation(new HdfsOperation() { + @Override + public Void call() throws IOException { + Path notePath = new Path(notebookDir.toString() + "/" + note.getId() + "/note.json"); + Path tmpNotePath = new Path(notebookDir.toString() + "/" + note.getId() + "/.note.json"); + LOGGER.debug("Saving note to file: " + notePath); + if (fs.exists(tmpNotePath)) { + fs.delete(tmpNotePath, true); + } + InputStream in = new ByteArrayInputStream(note.toJson().getBytes( + zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_ENCODING))); + IOUtils.copyBytes(in, fs.create(tmpNotePath), hadoopConf); + fs.delete(notePath, true); + fs.rename(tmpNotePath, notePath); + return null; + } + }); + } + + @Override + public void remove(final String noteId, AuthenticationInfo subject) throws IOException { + callHdfsOperation(new HdfsOperation() { + @Override + public Void call() throws IOException { + Path noteFolder = new Path(notebookDir.toString() + "/" + noteId); + fs.delete(noteFolder, true); + return null; + } + }); + } + + @Override + public void close() { + LOGGER.warn("close is not implemented for HdfsNotebookRepo"); + } + + @Override + public Revision checkpoint(String noteId, String checkpointMsg, AuthenticationInfo subject) + throws IOException { + LOGGER.warn("checkpoint is not implemented for HdfsNotebookRepo"); + return null; + } + + @Override + public Note get(String noteId, String revId, AuthenticationInfo subject) throws IOException { + LOGGER.warn("get revId is not implemented for HdfsNotebookRepo"); + return null; + } + + @Override + public List revisionHistory(String noteId, AuthenticationInfo subject) { + LOGGER.warn("revisionHistory is not implemented for HdfsNotebookRepo"); + return null; + } + + @Override + public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subject) + throws IOException { + LOGGER.warn("setNoteRevision is not implemented for HdfsNotebookRepo"); + return null; + } + + @Override + public List getSettings(AuthenticationInfo subject) { + LOGGER.warn("getSettings is not implemented for HdfsNotebookRepo"); + return null; + } + + @Override + public void updateSettings(Map settings, AuthenticationInfo subject) { + LOGGER.warn("updateSettings is not implemented for HdfsNotebookRepo"); + } + + private interface HdfsOperation { + T call() throws IOException; + } + + public T callHdfsOperation(final HdfsOperation func) throws IOException { + if (isSecurityEnabled) { + UserGroupInformation.getLoginUser().reloginFromKeytab(); + try { + return UserGroupInformation.getCurrentUser().doAs(new PrivilegedExceptionAction() { + @Override + public T run() throws Exception { + return func.call(); + } + }); + } catch (InterruptedException e) { + throw new IOException(e); + } + } else { + return func.call(); + } + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java index 6bbd5bca99d..28de7c8189a 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java @@ -81,6 +81,7 @@ public NotebookRepoSync(ZeppelinConfiguration conf) { Constructor constructor = notebookStorageClass.getConstructor( ZeppelinConfiguration.class); repos.add((NotebookRepo) constructor.newInstance(conf)); + LOG.info("Instantiate NotebookRepo: " + storageClassNames[i]); } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/HdfsNotebookRepoTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/HdfsNotebookRepoTest.java new file mode 100644 index 00000000000..952d7442553 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/HdfsNotebookRepoTest.java @@ -0,0 +1,101 @@ +package org.apache.zeppelin.notebook.repo; + + +import org.apache.commons.io.FileUtils; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.notebook.Note; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class HdfsNotebookRepoTest { + + private ZeppelinConfiguration zConf; + private Configuration hadoopConf; + private FileSystem fs; + private HdfsNotebookRepo hdfsNotebookRepo; + private String notebookDir; + private AuthenticationInfo authInfo = AuthenticationInfo.ANONYMOUS; + + @Before + public void setUp() throws IOException { + notebookDir = Files.createTempDirectory("HdfsNotebookRepoTest").toFile().getAbsolutePath(); + zConf = new ZeppelinConfiguration(); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), notebookDir); + hadoopConf = new Configuration(); + fs = FileSystem.get(hadoopConf); + hdfsNotebookRepo = new HdfsNotebookRepo(zConf); + } + + @After + public void tearDown() throws IOException { + FileUtils.deleteDirectory(new File(notebookDir)); + } + + @Test + public void testBasics() throws IOException { + assertEquals(0, hdfsNotebookRepo.list(authInfo).size()); + + // create a new note + Note note = new Note(); + note.setName("title_1"); + + Map config = new HashMap<>(); + config.put("config_1", "value_1"); + note.setConfig(config); + hdfsNotebookRepo.save(note, authInfo); + assertEquals(1, hdfsNotebookRepo.list(authInfo).size()); + + // read this note from hdfs + Note note_copy = hdfsNotebookRepo.get(note.getId(), authInfo); + assertEquals(note.getName(), note_copy.getName()); + assertEquals(note.getConfig(), note_copy.getConfig()); + + // update this note + note.setName("title_2"); + hdfsNotebookRepo.save(note, authInfo); + assertEquals(1, hdfsNotebookRepo.list(authInfo).size()); + note_copy = hdfsNotebookRepo.get(note.getId(), authInfo); + assertEquals(note.getName(), note_copy.getName()); + assertEquals(note.getConfig(), note_copy.getConfig()); + + // delete this note + hdfsNotebookRepo.remove(note.getId(), authInfo); + assertEquals(0, hdfsNotebookRepo.list(authInfo).size()); + } + + @Test + public void testComplicatedScenarios() throws IOException { + // scenario_1: notebook_dir is not clean. There're some unrecognized dir and file under notebook_dir + fs.mkdirs(new Path(notebookDir, "1/2")); + OutputStream out = fs.create(new Path(notebookDir, "1/a.json")); + out.close(); + + assertEquals(0, hdfsNotebookRepo.list(authInfo).size()); + + // scenario_2: note_folder is existed. + // create a new note + Note note = new Note(); + note.setName("title_1"); + Map config = new HashMap<>(); + config.put("config_1", "value_1"); + note.setConfig(config); + + fs.mkdirs(new Path(notebookDir, note.getId())); + hdfsNotebookRepo.save(note, authInfo); + assertEquals(1, hdfsNotebookRepo.list(authInfo).size()); + } +} From 79d139a494a969d766e812976c49841b3d21fe71 Mon Sep 17 00:00:00 2001 From: Vipin Rathor Date: Tue, 22 Aug 2017 13:20:47 -0700 Subject: [PATCH 022/492] ZEPPELIN-2873 - Add documentation on secure cookie in Shiro ### What is this PR for? Adding a section in Shiro Authentication about how to enable secure cookie via Shiro. Shiro do support configuring 'HttpOnly' flag in response cookie. A Zeppelin user, who is security conscious, should know how to enable this in Zeppelin's Shiro configuration. ### What type of PR is it? Documentation ### What is the Jira issue? ZEPPELIN-2873 ### How should this be tested? Doc changes. CI test should pass. ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Vipin Rathor Closes #2545 from VipinRathor/ZEPPELIN-2873 and squashes the following commits: c7b7995 [Vipin Rathor] Updated doc as per the review comments. fec8d7e [Vipin Rathor] ZEPPELIN-2873 - Add documentation on Zeppelin Shiro's abliity to configure secure cookie --- docs/setup/security/shiro_authentication.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/setup/security/shiro_authentication.md b/docs/setup/security/shiro_authentication.md index 0dcb722e316..33b67d0ca8a 100644 --- a/docs/setup/security/shiro_authentication.md +++ b/docs/setup/security/shiro_authentication.md @@ -210,6 +210,21 @@ securityManager.realms = $zeppelinHubRealm > Note: ZeppelinHub is not releated to Apache Zeppelin project. +## Secure Cookie for Zeppelin Sessions (optional) +Zeppelin can be configured to set `HttpOnly` flag in the session cookie. With this configuration, Zeppelin cookies can +not be accessed via client side scripts thus preventing majority of Cross-site scripting (XSS) attacks. + +To enable secure cookie support via Shiro, add the following lines in `conf/shiro.ini` under `[main]` section, after +defining a `sessionManager`. + +``` +cookie = org.apache.shiro.web.servlet.SimpleCookie +cookie.name = JSESSIONID +cookie.secure = true +cookie.httpOnly = true +sessionManager.sessionIdCookie = $cookie +``` + ## Secure your Zeppelin information (optional) By default, anyone who defined in `[users]` can share **Interpreter Setting**, **Credential** and **Configuration** information in Apache Zeppelin. Sometimes you might want to hide these information for your use case. From 4a369f10082d6d81af92d94048353afbcfc1819f Mon Sep 17 00:00:00 2001 From: byung-u Date: Thu, 17 Aug 2017 17:09:01 +0900 Subject: [PATCH 023/492] [ZEPPELIN-2860] Invalid last job execution time ### What is this PR for? Fixed invalid last job execution time ### What type of PR is it? [Bug Fix] ### What is the Jira issue? * [ZEPPELIN-2860](https://issues.apache.org/jira/browse/ZEPPELIN-2860) ### How should this be tested? Run Notebook and check job page execution time ### Screenshots (if appropriate) ![image](https://user-images.githubusercontent.com/16890077/29394147-74a35a42-8343-11e7-973f-0aee5acb81bc.png) ### Questions: * Does the licenses files need update? NO * Is there breaking changes for older versions? NO * Does this needs documentation? NO Author: byung-u Closes #2543 from byung-u/feature/fix-last-job-execution-time and squashes the following commits: 5d970a91 [byung-u] Fix last job execution time 278eaaa2 [byung-u] Revert "feature: fix last job execution time" 180b951a [byung-u] feature: fix last job execution time --- .../src/main/java/org/apache/zeppelin/notebook/Notebook.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java index 07febf17a1f..a0c1dff8059 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java @@ -829,7 +829,7 @@ public List> getJobListByUnixTime(boolean needsReload, // get data for the job manager. Map paragraphItem = getParagraphForJobManagerItem(paragraph); - lastRunningUnixTime = getUnixTimeLastRunParagraph(paragraph); + lastRunningUnixTime = Math.max(getUnixTimeLastRunParagraph(paragraph), lastRunningUnixTime); // is update note for last server update time. if (lastRunningUnixTime > lastUpdateServerUnixTime) { From f3e659f46ae376806f01f13e65cc42957404afc0 Mon Sep 17 00:00:00 2001 From: byung-u Date: Tue, 22 Aug 2017 11:21:37 +0900 Subject: [PATCH 024/492] Fix trivial typo ### What is this PR for? Fixes a trivial typo in travis_check.py. The semicolon does nothing in travis_check.py. Python use semicolon when several statements on the same line, here is [python doc.](https://docs.python.org/3/reference/compound_stmts.html) ### Questions: * Does the licenses files need update? NO * Is there breaking changes for older versions? NO * Does this needs documentation? NO Author: byung-u Closes #2547 from byung-u/feature/fix-trivial-typo and squashes the following commits: fef77e51 [byung-u] feature: remove semicolon end of statement --- travis_check.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/travis_check.py b/travis_check.py index cbf9623dba5..ea5e37b3805 100644 --- a/travis_check.py +++ b/travis_check.py @@ -57,7 +57,7 @@ def getBuildStatus(author, commit): build = None if len(data) == 0: - return build; + return build for b in data: if b["commit"][:len(commit)] == commit: @@ -102,7 +102,7 @@ def printBuildStatus(build): for sleep in check: info("--------------------------------") - time.sleep(sleep); + time.sleep(sleep) info("Get build status ...") build = getBuildStatus(author, commit) if build == None: From 26a39df08567c46aebbcab6581fcad4daaa89079 Mon Sep 17 00:00:00 2001 From: Nelson Costa Date: Tue, 8 Aug 2017 08:11:17 +0100 Subject: [PATCH 025/492] [ZEPPELIN-2815] Improve interpreter dependencies logging ### What is this PR for? As a developer, I want to be able to debug issues on dependency resolution "easier". To achieve this, making available (in debug mode), existing listeners. Also, exposing exceptions on dependency resolver. ### What type of PR is it? Improvement ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-2815 ### How should this be tested? Enable DEBUG logging mode. Import dependencies. Check detailed info. from dependency loading being logged. ### Questions: * Does the licenses files need update? N * Is there breaking changes for older versions? N * Does this needs documentation? N Author: Nelson Costa Closes #2507 from necosta/zeppelin2815 and squashes the following commits: ea353fa [Nelson Costa] [ZEPPELIN-2815] Improve interpreter dependencies logging d381f3b [Nelson Costa] [ZEPPELIN-2815] Improve interpreter dependencies logging b773d8a [Nelson Costa] [ZEPPELIN-2815] Improve interpreter dependencies logging --- .../main/java/org/apache/zeppelin/dep/Booter.java | 11 ++++++++--- .../apache/zeppelin/dep/DependencyResolver.java | 12 +++++------- .../org/apache/zeppelin/dep/TransferListener.java | 14 ++++++-------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/dep/Booter.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/dep/Booter.java index 5bc58edb5b3..6339a4f02b8 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/dep/Booter.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/dep/Booter.java @@ -19,6 +19,8 @@ import org.apache.commons.lang.Validate; import org.apache.maven.repository.internal.MavenRepositorySystemSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.sonatype.aether.RepositorySystem; import org.sonatype.aether.RepositorySystemSession; import org.sonatype.aether.repository.LocalRepository; @@ -30,6 +32,8 @@ * Manage mvn repository. */ public class Booter { + private static Logger logger = LoggerFactory.getLogger(Booter.class); + public static RepositorySystem newRepositorySystem() { return RepositorySystemFactory.newRepositorySystem(); } @@ -43,9 +47,10 @@ public static RepositorySystemSession newRepositorySystemSession( LocalRepository localRepo = new LocalRepository(resolveLocalRepoPath(localRepoPath)); session.setLocalRepositoryManager(system.newLocalRepositoryManager(localRepo)); - // session.setTransferListener(new ConsoleTransferListener()); - // session.setRepositoryListener(new ConsoleRepositoryListener()); - + if (logger.isDebugEnabled()) { + session.setTransferListener(new TransferListener()); + session.setRepositoryListener(new RepositoryListener()); + } // uncomment to generate dirty trees // session.setDependencyGraphTransformer( null ); diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/dep/DependencyResolver.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/dep/DependencyResolver.java index 889101fb6db..c3ecdeedc13 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/dep/DependencyResolver.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/dep/DependencyResolver.java @@ -19,7 +19,6 @@ import java.io.File; import java.io.IOException; -import java.net.URL; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; @@ -33,25 +32,23 @@ import org.sonatype.aether.RepositoryException; import org.sonatype.aether.artifact.Artifact; import org.sonatype.aether.collection.CollectRequest; -import org.sonatype.aether.collection.DependencyCollectionException; import org.sonatype.aether.graph.Dependency; import org.sonatype.aether.graph.DependencyFilter; import org.sonatype.aether.repository.RemoteRepository; import org.sonatype.aether.resolution.ArtifactResult; import org.sonatype.aether.resolution.DependencyRequest; +import org.sonatype.aether.resolution.DependencyResolutionException; import org.sonatype.aether.util.artifact.DefaultArtifact; import org.sonatype.aether.util.artifact.JavaScopes; import org.sonatype.aether.util.filter.DependencyFilterUtils; import org.sonatype.aether.util.filter.PatternExclusionsDependencyFilter; -import org.sonatype.aether.util.graph.DefaultDependencyNode; - /** * Deps resolver. * Add new dependencies from mvn repo (at runtime) to Zeppelin. */ public class DependencyResolver extends AbstractDependencyResolver { - Logger logger = LoggerFactory.getLogger(DependencyResolver.class); + private Logger logger = LoggerFactory.getLogger(DependencyResolver.class); private final String[] exclusions = new String[] {"org.apache.zeppelin:zeppelin-zengine", "org.apache.zeppelin:zeppelin-interpreter", @@ -177,8 +174,9 @@ public List getArtifactsWithDep(String dependency, DependencyFilterUtils.andFilter(exclusionFilter, classpathFilter)); try { return system.resolveDependencies(session, dependencyRequest).getArtifactResults(); - } catch (NullPointerException ex) { - throw new RepositoryException(String.format("Cannot fetch dependencies for %s", dependency)); + } catch (NullPointerException | DependencyResolutionException ex) { + throw new RepositoryException( + String.format("Cannot fetch dependencies for %s", dependency), ex); } } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/dep/TransferListener.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/dep/TransferListener.java index fd9029fa569..7f25e3bafae 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/dep/TransferListener.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/dep/TransferListener.java @@ -17,7 +17,6 @@ package org.apache.zeppelin.dep; -import java.io.PrintStream; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.Locale; @@ -34,8 +33,7 @@ * Simple listener that show deps downloading progress. */ public class TransferListener extends AbstractTransferListener { - Logger logger = LoggerFactory.getLogger(TransferListener.class); - private PrintStream out; + private Logger logger = LoggerFactory.getLogger(TransferListener.class); private Map downloads = new ConcurrentHashMap<>(); @@ -55,13 +53,13 @@ public void transferInitiated(TransferEvent event) { @Override public void transferProgressed(TransferEvent event) { TransferResource resource = event.getResource(); - downloads.put(resource, Long.valueOf(event.getTransferredBytes())); + downloads.put(resource, event.getTransferredBytes()); StringBuilder buffer = new StringBuilder(64); for (Map.Entry entry : downloads.entrySet()) { long total = entry.getKey().getContentLength(); - long complete = entry.getValue().longValue(); + long complete = entry.getValue(); buffer.append(getStatus(complete, total)).append(" "); } @@ -122,7 +120,7 @@ public void transferSucceeded(TransferEvent event) { @Override public void transferFailed(TransferEvent event) { transferCompleted(event); - event.getException().printStackTrace(out); + logger.warn("Unsuccessful transfer", event.getException()); } private void transferCompleted(TransferEvent event) { @@ -135,10 +133,10 @@ private void transferCompleted(TransferEvent event) { @Override public void transferCorrupted(TransferEvent event) { - event.getException().printStackTrace(out); + logger.error("Corrupted transfer", event.getException()); } - protected long toKB(long bytes) { + private long toKB(long bytes) { return (bytes + 1023) / 1024; } From 32517c9d9fbdc2235560388a47f9e3eff4ec4854 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Sat, 26 Aug 2017 11:59:43 +0800 Subject: [PATCH 026/492] [ZEPPELIN-2753] Basic Implementation of IPython Interpreter ### What is this PR for? This is the first step for implement IPython Interpreter in Zeppelin. I just use the jupyter_client to create and manage the ipython kernel. We don't need to care about python compilation and execution, all the things are delegated to ipython kernel. Ideally all the features of ipython should be available in Zeppelin as well. For now, user can use %python.ipython for IPython Interpreter. And if ipython is available, the default python interpreter will use ipython. But user can still set `zeppelin.python.useIPython` as false to enforce to use the old implementation of python interpreter. Main features: * IPython interpreter support ** All the ipython features are available, including visualization, ipython magics. * ZeppelinContext support * Streaming output support * Support Ipython in PySpark Regarding the visualization, ideally all the visualization libraries work in jupyter should also work here. In unit test, I only verify the following 3 popular visualization library. could add more later. * matplotlib * bokeh * ggplot ### What type of PR is it? [Feature ] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-2753 ### How should this be tested? Unit test is added. ### Screenshots (if appropriate) Verify bokeh in IPython Interpreter ![image](https://user-images.githubusercontent.com/164491/27999716-756d749e-6552-11e7-90bb-4c6b08f4ab5c.png) Verify matplotlib ![image](https://user-images.githubusercontent.com/164491/28046960-e881b28e-6619-11e7-9e1f-7f4662f842f3.png) Verify ZeppelinContext ![image](https://user-images.githubusercontent.com/164491/28119378-4212620c-6747-11e7-89d5-3b5e609593ce.png) Verify Streaming ![streaming](https://user-images.githubusercontent.com/164491/28950974-8f92fe1e-78fa-11e7-841f-3174da198bb7.gif) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2474 from zjffdu/ZEPPELIN-2753 and squashes the following commits: e869f31 [Jeff Zhang] address comments b0b5c95 [Jeff Zhang] [ZEPPELIN-2753] Basic Implementation of IPython Interpreter --- .travis.yml | 24 +- alluxio/pom.xml | 1 + docs/interpreter/python.md | 64 ++ docs/interpreter/spark.md | 6 + pom.xml | 13 +- python/README.md | 19 + python/pom.xml | 65 ++ .../apache/zeppelin/python/IPythonClient.java | 211 +++++ .../zeppelin/python/IPythonInterpreter.java | 359 +++++++++ .../zeppelin/python/PythonInterpreter.java | 66 ++ .../python/PythonZeppelinContext.java | 49 ++ python/src/main/proto/ipython.proto | 102 +++ .../src/main/resources/grpc/generate_rpc.sh | 18 + .../resources/grpc/python/ipython_client.py | 36 + .../main/resources/grpc/python/ipython_pb2.py | 751 ++++++++++++++++++ .../resources/grpc/python/ipython_pb2_grpc.py | 129 +++ .../resources/grpc/python/ipython_server.py | 155 ++++ .../resources/grpc/python/zeppelin_python.py | 107 +++ .../main/resources/interpreter-setting.json | 23 + .../python/IPythonInterpreterTest.java | 402 ++++++++++ .../PythonInterpreterMatplotlibTest.java | 1 + .../PythonInterpreterPandasSqlTest.java | 38 +- .../python/PythonInterpreterTest.java | 2 + python/src/test/resources/log4j.properties | 31 + spark/pom.xml | 51 +- .../zeppelin/spark/IPySparkInterpreter.java | 110 +++ .../zeppelin/spark/PySparkInterpreter.java | 92 ++- .../main/resources/interpreter-setting.json | 17 + .../resources/python/zeppelin_ipyspark.py | 53 ++ .../sparkr-resources/interpreter-setting.json | 11 + .../spark/IPySparkInterpreterTest.java | 203 +++++ .../PySparkInterpreterMatplotlibTest.java | 18 +- .../spark/PySparkInterpreterTest.java | 36 +- spark/src/test/resources/log4j.properties | 2 + testing/install_external_dependencies.sh | 3 +- zeppelin-interpreter/pom.xml | 5 - .../interpreter/BaseZeppelinContext.java | 4 + .../interpreter/InterpreterOutput.java | 11 + .../remote/RemoteInterpreterServer.java | 7 + .../util/InterpreterOutputStream.java | 2 +- zeppelin-jupyter/pom.xml | 1 + zeppelin-server/pom.xml | 6 +- .../org/apache/zeppelin/WebDriverManager.java | 1 + .../integration/InterpreterModeActionsIT.java | 23 +- .../integration/SparkParagraphIT.java | 2 +- .../zeppelin/rest/AbstractTestRestApi.java | 3 + .../rest/ZeppelinSparkClusterTest.java | 6 +- zeppelin-zengine/pom.xml | 13 +- 48 files changed, 3257 insertions(+), 95 deletions(-) create mode 100644 python/src/main/java/org/apache/zeppelin/python/IPythonClient.java create mode 100644 python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java create mode 100644 python/src/main/java/org/apache/zeppelin/python/PythonZeppelinContext.java create mode 100644 python/src/main/proto/ipython.proto create mode 100755 python/src/main/resources/grpc/generate_rpc.sh create mode 100644 python/src/main/resources/grpc/python/ipython_client.py create mode 100644 python/src/main/resources/grpc/python/ipython_pb2.py create mode 100644 python/src/main/resources/grpc/python/ipython_pb2_grpc.py create mode 100644 python/src/main/resources/grpc/python/ipython_server.py create mode 100644 python/src/main/resources/grpc/python/zeppelin_python.py create mode 100644 python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java create mode 100644 python/src/test/resources/log4j.properties create mode 100644 spark/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java create mode 100644 spark/src/main/resources/python/zeppelin_ipyspark.py create mode 100644 spark/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java diff --git a/.travis.yml b/.travis.yml index 099fb385d60..97ca60a853e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ addons: env: global: # Interpreters does not required by zeppelin-server integration tests - - INTERPRETERS='!hbase,!pig,!jdbc,!file,!flink,!ignite,!kylin,!python,!lens,!cassandra,!elasticsearch,!bigquery,!alluxio,!scio,!livy,!groovy' + - INTERPRETERS='!hbase,!pig,!jdbc,!file,!flink,!ignite,!kylin,!lens,!cassandra,!elasticsearch,!bigquery,!alluxio,!scio,!livy,!groovy' matrix: include: @@ -53,7 +53,7 @@ matrix: sudo: false dist: trusty jdk: "oraclejdk8" - env: WEB_E2E="true" SCALA_VER="2.11" SPARK_VER="2.1.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pscala-2.11" BUILD_FLAG="package -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl ${INTERPRETERS}" TEST_MODULES="-pl zeppelin-web" TEST_PROJECTS="-Pweb-e2e" + env: PYTHON="2" WEB_E2E="true" SCALA_VER="2.11" SPARK_VER="2.1.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pscala-2.11" BUILD_FLAG="package -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl ${INTERPRETERS}" TEST_MODULES="-pl zeppelin-web" TEST_PROJECTS="-Pweb-e2e" addons: apt: sources: @@ -68,54 +68,54 @@ matrix: # After issues are fixed these tests need to be included back by removing them from the "-Dtests.to.exclude" property - jdk: "oraclejdk8" dist: precise - env: SCALA_VER="2.11" SPARK_VER="2.2.0" HADOOP_VER="2.6" PROFILE="-Pspark-2.2 -Pweb-ci -Pscalding -Phelium-dev -Pexamples -Pscala-2.11" BUILD_FLAG="package -Pbuild-distr -DskipRat" TEST_FLAG="verify -Pusing-packaged-distr -DskipRat" MODULES="-pl ${INTERPRETERS}" TEST_PROJECTS="-Dtests.to.exclude=**/ZeppelinSparkClusterTest.java,**/org.apache.zeppelin.spark.*,**/HeliumApplicationFactoryTest.java -DfailIfNoTests=false" + env: PYTHON="3" SCALA_VER="2.11" SPARK_VER="2.2.0" HADOOP_VER="2.6" PROFILE="-Pspark-2.2 -Pweb-ci -Pscalding -Phelium-dev -Pexamples -Pscala-2.11" BUILD_FLAG="package -Pbuild-distr -DskipRat" TEST_FLAG="verify -Pusing-packaged-distr -DskipRat" MODULES="-pl ${INTERPRETERS}" TEST_PROJECTS="-Dtests.to.exclude=**/ZeppelinSparkClusterTest.java,**/org.apache.zeppelin.spark.*,**/HeliumApplicationFactoryTest.java -DfailIfNoTests=false" # Test selenium with spark module for 1.6.3 - jdk: "oraclejdk7" dist: precise - env: TEST_SELENIUM="true" SCALA_VER="2.10" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop-2.6 -Phelium-dev -Pexamples" BUILD_FLAG="package -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" TEST_PROJECTS="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark -Dtest=org.apache.zeppelin.AbstractFunctionalSuite -DfailIfNoTests=false" + env: PYTHON="2" TEST_SELENIUM="true" SCALA_VER="2.10" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop-2.6 -Phelium-dev -Pexamples" BUILD_FLAG="package -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" TEST_PROJECTS="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark,python -Dtest=org.apache.zeppelin.AbstractFunctionalSuite -DfailIfNoTests=false" # Test interpreter modules - jdk: "oraclejdk7" dist: precise - env: SCALA_VER="2.10" PROFILE="-Pscalding" BUILD_FLAG="package -DskipTests -DskipRat -Pr" TEST_FLAG="test -DskipRat" MODULES="-pl $(echo .,zeppelin-interpreter,${INTERPRETERS} | sed 's/!//g')" TEST_PROJECTS="" + env: PYTHON="3" SCALA_VER="2.10" PROFILE="-Pscalding" BUILD_FLAG="install -DskipTests -DskipRat -Pr" TEST_FLAG="test -DskipRat" MODULES="-pl $(echo .,zeppelin-interpreter,${INTERPRETERS} | sed 's/!//g')" TEST_PROJECTS="" # Test spark module for 2.2.0 with scala 2.11, livy - jdk: "oraclejdk8" dist: precise - env: SCALA_VER="2.11" SPARK_VER="2.2.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.2 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="package -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark,livy" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.livy.* -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.2.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.2 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark,python,livy" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.livy.* -DfailIfNoTests=false" # Test spark module for 2.1.0 with scala 2.11, livy - jdk: "oraclejdk7" dist: precise - env: SCALA_VER="2.11" SPARK_VER="2.1.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.1 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="package -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark,livy" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.livy.* -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.1.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.1 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark,python,livy" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.livy.* -DfailIfNoTests=false" # Test spark module for 2.0.2 with scala 2.11 - jdk: "oraclejdk7" dist: precise - env: SCALA_VER="2.11" SPARK_VER="2.0.2" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.0 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="package -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.0.2" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.0 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" # Test spark module for 1.6.3 with scala 2.10 - jdk: "oraclejdk7" dist: precise - env: SCALA_VER="2.10" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop-2.6 -Pscala-2.10" SPARKR="true" BUILD_FLAG="package -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + env: PYTHON="3" SCALA_VER="2.10" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop-2.6 -Pscala-2.10" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.spark.* -DfailIfNoTests=false" # Test spark module for 1.6.3 with scala 2.11 - jdk: "oraclejdk7" dist: precise - env: SCALA_VER="2.11" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="package -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" # Test python/pyspark with python 2, livy 0.2 - sudo: required dist: precise jdk: "oraclejdk7" - env: PYTHON="2" SCALA_VER="2.10" SPARK_VER="1.6.1" HADOOP_VER="2.6" LIVY_VER="0.2.0" PROFILE="-Pspark-1.6 -Phadoop-2.6 -Plivy-0.2" BUILD_FLAG="package -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark-dependencies,spark,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.10" SPARK_VER="1.6.1" HADOOP_VER="2.6" LIVY_VER="0.2.0" PROFILE="-Pspark-1.6 -Phadoop-2.6 -Plivy-0.2 -Pscala-2.10" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark-dependencies,spark,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" # Test python/pyspark with python 3, livy 0.3 - sudo: required dist: precise jdk: "oraclejdk7" - env: PYTHON="3" SCALA_VER="2.11" SPARK_VER="2.0.0" HADOOP_VER="2.6" LIVY_VER="0.3.0" PROFILE="-Pspark-2.0 -Phadoop-2.6 -Pscala-2.11 -Plivy-0.3" BUILD_FLAG="package -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark-dependencies,spark,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" + env: PYTHON="3" SCALA_VER="2.11" SPARK_VER="2.0.0" HADOOP_VER="2.6" LIVY_VER="0.3.0" PROFILE="-Pspark-2.0 -Phadoop-2.6 -Pscala-2.11 -Plivy-0.3" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark-dependencies,spark,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" before_install: # check files included in commit range, clear bower_components if a bower.json file has changed. diff --git a/alluxio/pom.xml b/alluxio/pom.xml index 38135b81793..ed6b9812865 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -47,6 +47,7 @@ com.google.guava guava + 15.0 diff --git a/docs/interpreter/python.md b/docs/interpreter/python.md index b4b5ca86514..1965fc95697 100644 --- a/docs/interpreter/python.md +++ b/docs/interpreter/python.md @@ -232,6 +232,70 @@ SELECT * FROM rates WHERE age < 40 Otherwise it can be referred to as `%python.sql` +## IPython Support + +IPython is more powerful than the default python interpreter with extra functionality. You can use IPython with Python2 or Python3 which depends on which python you set `zeppelin.python`. + + **Pre-requests** + + - Jupyter `pip install jupyter` + - grpcio `pip install grpcio` + +If you already install anaconda, then you just need to install `grpcio` as Jupyter is already included in anaconda. + +In addition to all basic functions of the python interpreter, you can use all the IPython advanced features as you use it in Jupyter Notebook. + +e.g. + +Use IPython magic + +``` +%python.ipython + +#python help +range? + +#timeit +%timeit range(100) +``` + +Use matplotlib + +``` +%python.ipython + + +%matplotlib inline +import matplotlib.pyplot as plt + +print("hello world") +data=[1,2,3,4] +plt.figure() +plt.plot(data) +``` + +We also make `ZeppelinContext` available in IPython Interpreter. You can use `ZeppelinContext` to create dynamic forms and display pandas DataFrame. + +e.g. + +Create dynamic form + +``` +z.input(name='my_name', defaultValue='hello') +``` + +Show pandas dataframe + +``` +import pandas as pd +df = pd.DataFrame({'id':[1,2,3], 'name':['a','b','c']}) +z.show(df) + +``` + +By default, we would use IPython in `%python.python` if IPython is available. Otherwise it would fall back to the original Python implementation. +If you don't want to use IPython, then you can set `zeppelin.python.useIPython` as `false` in interpreter setting. + ## Technical description For in-depth technical details on current implementation please refer to [python/README.md](https://github.com/apache/zeppelin/blob/master/python/README.md). diff --git a/docs/interpreter/spark.md b/docs/interpreter/spark.md index 122c8db3b84..8ba9247f6cc 100644 --- a/docs/interpreter/spark.md +++ b/docs/interpreter/spark.md @@ -414,6 +414,12 @@ You can choose one of `shared`, `scoped` and `isolated` options wheh you configu Spark interpreter creates separated Scala compiler per each notebook but share a single SparkContext in `scoped` mode (experimental). It creates separated SparkContext per each notebook in `isolated` mode. +## IPython support + +By default, zeppelin would use IPython in `pyspark` when IPython is available, Otherwise it would fall back to the original PySpark implementation. +If you don't want to use IPython, then you can set `zeppelin.spark.useIPython` as `false` in interpreter setting. For the IPython features, you can refer doc +[Python Interpreter](python.html) + ## Setting up Zeppelin with Kerberos Logical setup with Zeppelin, Kerberos Key Distribution Center (KDC), and Spark on YARN: diff --git a/pom.xml b/pom.xml index b85645417fa..acfcd0572c0 100644 --- a/pom.xml +++ b/pom.xml @@ -101,7 +101,6 @@ 0.9.2 2.2 0.2.1 - 15.0 9.2.15.v20160210 4.4.1 4.5.1 @@ -246,12 +245,6 @@ ${commons.cli.version} - - com.google.guava - guava - ${guava.version} - - org.apache.shiro @@ -404,7 +397,7 @@ true - org/apache/zeppelin/interpreter/thrift/*,org/apache/zeppelin/scio/avro/* + org/apache/zeppelin/interpreter/thrift/*,org/apache/zeppelin/scio/avro/*,org/apache/zeppelin/python/proto/* @@ -414,7 +407,7 @@ checkstyle-aggregate - org/apache/zeppelin/interpreter/thrift/*,org/apache/zeppelin/scio/avro/* + org/apache/zeppelin/interpreter/thrift/*,org/apache/zeppelin/scio/avro/*,org/apache/zeppelin/python/proto/* @@ -1082,6 +1075,8 @@ **/R/rzeppelin/DESCRIPTION **/R/rzeppelin/NAMESPACE + + python/src/main/resources/grpc/**/* diff --git a/python/README.md b/python/README.md index cd8a0ca1c72..7a20e8d0fe7 100644 --- a/python/README.md +++ b/python/README.md @@ -50,3 +50,22 @@ mvn -Dpython.test.exclude='' test -pl python -am * Matplotlib figures are displayed inline with the notebook automatically using a built-in backend for zeppelin in conjunction with a post-execute hook. * `%python.sql` support for Pandas DataFrames is optional and provided using https://github.com/yhat/pandasql if user have one installed + + +# IPython Overview +IPython interpreter for Apache Zeppelin + +# IPython Requirements +You need to install the following python packages to make the IPython interpreter work. + * jupyter 5.x + * IPython + * ipykernel + * grpcio + +If you have installed anaconda, then you just need to install grpc. + +# IPython Architecture +Current interpreter delegate the whole work to ipython kernel via `jupyter_client`. Zeppelin would launch a python process which host the ipython kernel. +Zeppelin interpreter process will communicate with the python process via `grpc`. Ideally every feature works in IPython should work in Zeppelin as well. + + diff --git a/python/pom.xml b/python/pom.xml index 380a874317e..d46cd10c2d7 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -41,6 +41,7 @@ https://pypi.python.org/packages /64/5c/01e13b68e8caafece40d549f232c9b5677ad1016071a48d04cc3895acaa3 + 1.4.0 @@ -73,6 +74,28 @@ slf4j-log4j12 + + io.grpc + grpc-netty + ${grpc.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + + com.google.guava + guava + 18.0 + + junit @@ -88,7 +111,36 @@ + + + + kr.motd.maven + os-maven-plugin + 1.4.1.Final + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.5.0 + + com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier} + + + + + compile + compile-custom + + + + + maven-enforcer-plugin 1.3.1 @@ -136,6 +188,19 @@ + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + + test-jar + + + + org.apache.maven.plugins diff --git a/python/src/main/java/org/apache/zeppelin/python/IPythonClient.java b/python/src/main/java/org/apache/zeppelin/python/IPythonClient.java new file mode 100644 index 00000000000..40b9afdac43 --- /dev/null +++ b/python/src/main/java/org/apache/zeppelin/python/IPythonClient.java @@ -0,0 +1,211 @@ +/* +* 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.python; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.stub.StreamObserver; +import org.apache.zeppelin.interpreter.util.InterpreterOutputStream; +import org.apache.zeppelin.python.proto.CancelRequest; +import org.apache.zeppelin.python.proto.CancelResponse; +import org.apache.zeppelin.python.proto.CompletionRequest; +import org.apache.zeppelin.python.proto.CompletionResponse; +import org.apache.zeppelin.python.proto.ExecuteRequest; +import org.apache.zeppelin.python.proto.ExecuteResponse; +import org.apache.zeppelin.python.proto.ExecuteStatus; +import org.apache.zeppelin.python.proto.IPythonGrpc; +import org.apache.zeppelin.python.proto.OutputType; +import org.apache.zeppelin.python.proto.StatusRequest; +import org.apache.zeppelin.python.proto.StatusResponse; +import org.apache.zeppelin.python.proto.StopRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Grpc client for IPython kernel + */ +public class IPythonClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(IPythonClient.class.getName()); + + private final ManagedChannel channel; + private final IPythonGrpc.IPythonBlockingStub blockingStub; + private final IPythonGrpc.IPythonStub asyncStub; + + private Random random = new Random(); + + /** + * Construct client for accessing RouteGuide server at {@code host:port}. + */ + public IPythonClient(String host, int port) { + this(ManagedChannelBuilder.forAddress(host, port).usePlaintext(true)); + } + + /** + * Construct client for accessing RouteGuide server using the existing channel. + */ + public IPythonClient(ManagedChannelBuilder channelBuilder) { + channel = channelBuilder.build(); + blockingStub = IPythonGrpc.newBlockingStub(channel); + asyncStub = IPythonGrpc.newStub(channel); + } + + public void shutdown() throws InterruptedException { + channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); + } + + // execute the code and make the output as streaming by writing it to InterpreterOutputStream + // one by one. + public ExecuteResponse stream_execute(ExecuteRequest request, + final InterpreterOutputStream interpreterOutput) { + final ExecuteResponse.Builder finalResponseBuilder = ExecuteResponse.newBuilder() + .setStatus(ExecuteStatus.SUCCESS); + final AtomicBoolean completedFlag = new AtomicBoolean(false); + LOGGER.debug("stream_execute code:\n" + request.getCode()); + asyncStub.execute(request, new StreamObserver() { + int index = 0; + boolean isPreviousOutputImage = false; + + @Override + public void onNext(ExecuteResponse executeResponse) { + if (executeResponse.getType() == OutputType.TEXT) { + try { + LOGGER.debug("Interpreter Streaming Output: " + executeResponse.getOutput()); + if (isPreviousOutputImage) { + // add '\n' when switch from image to text + interpreterOutput.write("\n".getBytes()); + } + isPreviousOutputImage = false; + interpreterOutput.write(executeResponse.getOutput().getBytes()); + interpreterOutput.getInterpreterOutput().flush(); + } catch (IOException e) { + LOGGER.error("Unexpected IOException", e); + } + } + if (executeResponse.getType() == OutputType.IMAGE) { + try { + LOGGER.debug("Interpreter Streaming Output: IMAGE_DATA"); + if (index != 0) { + // add '\n' if this is the not the first element. otherwise it would mix the image + // with the text + interpreterOutput.write("\n".getBytes()); + } + interpreterOutput.write(("%img " + executeResponse.getOutput()).getBytes()); + interpreterOutput.getInterpreterOutput().flush(); + isPreviousOutputImage = true; + } catch (IOException e) { + LOGGER.error("Unexpected IOException", e); + } + } + if (executeResponse.getStatus() == ExecuteStatus.ERROR) { + // set the finalResponse to ERROR if any ERROR happens, otherwise the finalResponse would + // be SUCCESS. + finalResponseBuilder.setStatus(ExecuteStatus.ERROR); + } + index++; + } + + @Override + public void onError(Throwable throwable) { + try { + interpreterOutput.getInterpreterOutput().flush(); + } catch (IOException e) { + LOGGER.error("Unexpected IOException", e); + } + LOGGER.error("Fail to call IPython grpc", throwable); + } + + @Override + public void onCompleted() { + synchronized (completedFlag) { + try { + LOGGER.debug("stream_execute is completed"); + interpreterOutput.getInterpreterOutput().flush(); + } catch (IOException e) { + LOGGER.error("Unexpected IOException", e); + } + completedFlag.set(true); + completedFlag.notify(); + } + } + }); + + synchronized (completedFlag) { + if (!completedFlag.get()) { + try { + completedFlag.wait(); + } catch (InterruptedException e) { + LOGGER.error("Unexpected Interruption", e); + } + } + } + return finalResponseBuilder.build(); + } + + // blocking execute the code + public ExecuteResponse block_execute(ExecuteRequest request) { + ExecuteResponse.Builder responseBuilder = ExecuteResponse.newBuilder(); + responseBuilder.setStatus(ExecuteStatus.SUCCESS); + Iterator iter = blockingStub.execute(request); + StringBuilder outputBuilder = new StringBuilder(); + while (iter.hasNext()) { + ExecuteResponse nextResponse = iter.next(); + if (nextResponse.getStatus() == ExecuteStatus.ERROR) { + responseBuilder.setStatus(ExecuteStatus.ERROR); + } + outputBuilder.append(nextResponse.getOutput()); + } + responseBuilder.setOutput(outputBuilder.toString()); + return responseBuilder.build(); + } + + public CancelResponse cancel(CancelRequest request) { + return blockingStub.cancel(request); + } + + public CompletionResponse complete(CompletionRequest request) { + return blockingStub.complete(request); + } + + public StatusResponse status(StatusRequest request) { + return blockingStub.status(request); + } + + public void stop(StopRequest request) { + asyncStub.stop(request, null); + } + + + public static void main(String[] args) { + IPythonClient client = new IPythonClient("localhost", 50053); + client.status(StatusRequest.newBuilder().build()); + + ExecuteResponse response = client.block_execute(ExecuteRequest.newBuilder(). + setCode("abcd=2").build()); + System.out.println(response.getOutput()); + + } +} diff --git a/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java b/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java new file mode 100644 index 00000000000..9b6f730a0ef --- /dev/null +++ b/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java @@ -0,0 +1,359 @@ +/* +* 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.python; + +import org.apache.commons.exec.CommandLine; +import org.apache.commons.exec.DefaultExecutor; +import org.apache.commons.exec.ExecuteException; +import org.apache.commons.exec.ExecuteResultHandler; +import org.apache.commons.exec.ExecuteWatchdog; +import org.apache.commons.exec.LogOutputStream; +import org.apache.commons.exec.PumpStreamHandler; +import org.apache.commons.exec.environment.EnvironmentUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterUtils; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.apache.zeppelin.interpreter.util.InterpreterOutputStream; +import org.apache.zeppelin.python.proto.CancelRequest; +import org.apache.zeppelin.python.proto.CompletionRequest; +import org.apache.zeppelin.python.proto.CompletionResponse; +import org.apache.zeppelin.python.proto.ExecuteRequest; +import org.apache.zeppelin.python.proto.ExecuteResponse; +import org.apache.zeppelin.python.proto.ExecuteStatus; +import org.apache.zeppelin.python.proto.IPythonStatus; +import org.apache.zeppelin.python.proto.StatusRequest; +import org.apache.zeppelin.python.proto.StatusResponse; +import org.apache.zeppelin.python.proto.StopRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import py4j.GatewayServer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * IPython Interpreter for Zeppelin + */ +public class IPythonInterpreter extends Interpreter implements ExecuteResultHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(IPythonInterpreter.class); + + private ExecuteWatchdog watchDog; + private IPythonClient ipythonClient; + private GatewayServer gatewayServer; + + private PythonZeppelinContext zeppelinContext; + private String pythonExecutable; + private long ipythonLaunchTimeout; + private String additionalPythonPath; + private String additionalPythonInitFile; + + private InterpreterOutputStream interpreterOutput = new InterpreterOutputStream(LOGGER); + + public IPythonInterpreter(Properties properties) { + super(properties); + } + + /** + * Sub class can customize the interpreter by adding more python packages under PYTHONPATH. + * e.g. PySparkInterpreter + * + * @param additionalPythonPath + */ + public void setAdditionalPythonPath(String additionalPythonPath) { + this.additionalPythonPath = additionalPythonPath; + } + + /** + * Sub class can customize the interpreter by running additional python init code. + * e.g. PySparkInterpreter + * + * @param additionalPythonInitFile + */ + public void setAdditionalPythonInitFile(String additionalPythonInitFile) { + this.additionalPythonInitFile = additionalPythonInitFile; + } + + @Override + public void open() { + try { + if (ipythonClient != null) { + // IPythonInterpreter might already been opened by PythonInterpreter + return; + } + pythonExecutable = getProperty().getProperty("zeppelin.python", "python"); + ipythonLaunchTimeout = Long.parseLong( + getProperty().getProperty("zeppelin.ipython.launch.timeout", "30000")); + this.zeppelinContext = new PythonZeppelinContext( + getInterpreterGroup().getInterpreterHookRegistry(), + Integer.parseInt(getProperty().getProperty("zeppelin.python.maxResult", "1000"))); + int ipythonPort = RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces(); + int jvmGatewayPort = RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces(); + LOGGER.info("Launching IPython Kernel at port: " + ipythonPort); + LOGGER.info("Launching JVM Gateway at port: " + jvmGatewayPort); + ipythonClient = new IPythonClient("127.0.0.1", ipythonPort); + launchIPythonKernel(ipythonPort); + setupJVMGateway(jvmGatewayPort); + } catch (Exception e) { + throw new RuntimeException("Fail to open IPythonInterpreter", e); + } + } + + public boolean checkIPythonPrerequisite() { + ProcessBuilder processBuilder = new ProcessBuilder("pip", "freeze"); + try { + File stderrFile = File.createTempFile("zeppelin", ".txt"); + processBuilder.redirectError(stderrFile); + File stdoutFile = File.createTempFile("zeppelin", ".txt"); + processBuilder.redirectOutput(stdoutFile); + + Process proc = processBuilder.start(); + int ret = proc.waitFor(); + if (ret != 0) { + LOGGER.warn("Fail to run pip freeze.\n" + + IOUtils.toString(new FileInputStream(stderrFile))); + return false; + } + String freezeOutput = IOUtils.toString(new FileInputStream(stdoutFile)); + if (!freezeOutput.contains("jupyter-client=")) { + InterpreterContext.get().out.write("jupyter-client is not installed\n".getBytes()); + return false; + } + if (!freezeOutput.contains("ipykernel=")) { + InterpreterContext.get().out.write("ipkernel is not installed\n".getBytes()); + return false; + } + if (!freezeOutput.contains("ipython=")) { + InterpreterContext.get().out.write("ipython is not installed\n".getBytes()); + return false; + } + if (!freezeOutput.contains("grpcio=")) { + InterpreterContext.get().out.write("grpcio is not installed\n".getBytes()); + return false; + } + LOGGER.info("IPython prerequisite is meet"); + return true; + } catch (Exception e) { + LOGGER.warn("Fail to checkIPythonPrerequisite", e); + return false; + } + } + + private void setupJVMGateway(int jvmGatewayPort) throws IOException { + gatewayServer = new GatewayServer(this, jvmGatewayPort); + gatewayServer.start(); + + InputStream input = + getClass().getClassLoader().getResourceAsStream("grpc/python/zeppelin_python.py"); + List lines = IOUtils.readLines(input); + ExecuteResponse response = ipythonClient.block_execute(ExecuteRequest.newBuilder() + .setCode(StringUtils.join(lines, System.lineSeparator()) + .replace("${JVM_GATEWAY_PORT}", jvmGatewayPort + "")).build()); + if (response.getStatus() == ExecuteStatus.ERROR) { + throw new IOException("Fail to setup JVMGateway\n" + response.getOutput()); + } + + if (additionalPythonInitFile != null) { + input = getClass().getClassLoader().getResourceAsStream(additionalPythonInitFile); + lines = IOUtils.readLines(input); + response = ipythonClient.block_execute(ExecuteRequest.newBuilder() + .setCode(StringUtils.join(lines, System.lineSeparator()) + .replace("${JVM_GATEWAY_PORT}", jvmGatewayPort + "")).build()); + if (response.getStatus() == ExecuteStatus.ERROR) { + throw new IOException("Fail to run additional Python init file: " + + additionalPythonInitFile + "\n" + response.getOutput()); + } + } + } + + + private void launchIPythonKernel(int ipythonPort) + throws IOException, URISyntaxException { + // copy the python scripts to a temp directory, then launch ipython kernel in that folder + File tmpPythonScriptFolder = Files.createTempDirectory("zeppelin_ipython").toFile(); + String[] ipythonScripts = {"ipython_server.py", "ipython_pb2.py", "ipython_pb2_grpc.py"}; + for (String ipythonScript : ipythonScripts) { + URL url = getClass().getClassLoader().getResource("grpc/python" + + "/" + ipythonScript); + FileUtils.copyURLToFile(url, new File(tmpPythonScriptFolder, ipythonScript)); + } + + CommandLine cmd = CommandLine.parse(pythonExecutable); + cmd.addArgument(tmpPythonScriptFolder.getAbsolutePath() + "/ipython_server.py"); + cmd.addArgument(ipythonPort + ""); + DefaultExecutor executor = new DefaultExecutor(); + ProcessLogOutputStream processOutput = new ProcessLogOutputStream(LOGGER); + executor.setStreamHandler(new PumpStreamHandler(processOutput)); + watchDog = new ExecuteWatchdog(ExecuteWatchdog.INFINITE_TIMEOUT); + executor.setWatchdog(watchDog); + + String py4jLibPath = null; + if (System.getenv("ZEPPELIN_HOME") != null) { + py4jLibPath = System.getenv("ZEPPELIN_HOME") + File.separator + + PythonInterpreter.ZEPPELIN_PY4JPATH; + } else { + Path workingPath = Paths.get("..").toAbsolutePath(); + py4jLibPath = workingPath + File.separator + PythonInterpreter.ZEPPELIN_PY4JPATH; + } + if (additionalPythonPath != null) { + // put the py4j at the end, because additionalPythonPath may already contain py4j. + // e.g. PySparkInterpreter + additionalPythonPath = additionalPythonPath + ":" + py4jLibPath; + } else { + additionalPythonPath = py4jLibPath; + } + Map envs = EnvironmentUtils.getProcEnvironment(); + if (envs.containsKey("PYTHONPATH")) { + envs.put("PYTHONPATH", additionalPythonPath + ":" + envs.get("PYTHONPATH")); + } else { + envs.put("PYTHONPATH", additionalPythonPath); + } + + LOGGER.debug("PYTHONPATH: " + envs.get("PYTHONPATH")); + executor.execute(cmd, envs, this); + + // wait until IPython kernel is started or timeout + long startTime = System.currentTimeMillis(); + while (true) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + LOGGER.error("Interrupted by something", e); + } + + try { + StatusResponse response = ipythonClient.status(StatusRequest.newBuilder().build()); + if (response.getStatus() == IPythonStatus.RUNNING) { + LOGGER.info("IPython Kernel is Running"); + break; + } else { + LOGGER.info("Wait for IPython Kernel to be started"); + } + } catch (Exception e) { + // ignore the exception, because is may happen when grpc server has not started yet. + LOGGER.info("Wait for IPython Kernel to be started"); + } + + if ((System.currentTimeMillis() - startTime) > ipythonLaunchTimeout) { + throw new IOException("Fail to launch IPython Kernel in " + ipythonLaunchTimeout / 1000 + + " seconds"); + } + } + } + + @Override + public void close() { + if (watchDog != null) { + LOGGER.debug("Kill IPython Process"); + ipythonClient.stop(StopRequest.newBuilder().build()); + watchDog.destroyProcess(); + gatewayServer.shutdown(); + } + } + + @Override + public InterpreterResult interpret(String st, InterpreterContext context) { + zeppelinContext.setGui(context.getGui()); + interpreterOutput.setInterpreterOutput(context.out); + ExecuteResponse response = + ipythonClient.stream_execute(ExecuteRequest.newBuilder().setCode(st).build(), + interpreterOutput); + try { + interpreterOutput.getInterpreterOutput().flush(); + } catch (IOException e) { + throw new RuntimeException("Fail to write output", e); + } + InterpreterResult result = new InterpreterResult( + InterpreterResult.Code.valueOf(response.getStatus().name())); + return result; + } + + @Override + public void cancel(InterpreterContext context) { + ipythonClient.cancel(CancelRequest.newBuilder().build()); + } + + @Override + public FormType getFormType() { + return FormType.SIMPLE; + } + + @Override + public int getProgress(InterpreterContext context) { + return 0; + } + + @Override + public List completion(String buf, int cursor, + InterpreterContext interpreterContext) { + List completions = new ArrayList<>(); + CompletionResponse response = + ipythonClient.complete( + CompletionRequest.getDefaultInstance().newBuilder().setCode(buf) + .setCursor(cursor).build()); + for (int i = 0; i < response.getMatchesCount(); i++) { + completions.add(new InterpreterCompletion( + response.getMatches(i), response.getMatches(i), "")); + } + return completions; + } + + public PythonZeppelinContext getZeppelinContext() { + return zeppelinContext; + } + + @Override + public void onProcessComplete(int exitValue) { + LOGGER.warn("Python Process is completed with exitValue: " + exitValue); + } + + @Override + public void onProcessFailed(ExecuteException e) { + LOGGER.warn("Exception happens in Python Process", e); + } + + private static class ProcessLogOutputStream extends LogOutputStream { + + private Logger logger; + + public ProcessLogOutputStream(Logger logger) { + this.logger = logger; + } + + @Override + protected void processLine(String s, int i) { + this.logger.debug("Process Output: " + s); + } + } +} diff --git a/python/src/main/java/org/apache/zeppelin/python/PythonInterpreter.java b/python/src/main/java/org/apache/zeppelin/python/PythonInterpreter.java index 0bfcae0d3e6..50f6a8b9c63 100644 --- a/python/src/main/java/org/apache/zeppelin/python/PythonInterpreter.java +++ b/python/src/main/java/org/apache/zeppelin/python/PythonInterpreter.java @@ -91,6 +91,7 @@ public class PythonInterpreter extends Interpreter implements ExecuteResultHandl private static final int MAX_TIMEOUT_SEC = 10; private long pythonPid = 0; + private IPythonInterpreter iPythonInterpreter; Integer statementSetNotifier = new Integer(0); @@ -219,6 +220,37 @@ private void createGatewayServerAndStartScript() throws UnknownHostException { @Override public void open() { + // try IPythonInterpreter first. If it is not available, we will fallback to the original + // python interpreter implementation. + iPythonInterpreter = getIPythonInterpreter(); + if (getProperty().getProperty("zeppelin.python.useIPython", "true").equals("true") && + iPythonInterpreter.checkIPythonPrerequisite()) { + try { + iPythonInterpreter.open(); + if (InterpreterContext.get() != null) { + InterpreterContext.get().out.write(("IPython is available, " + + "use IPython for PythonInterpreter\n") + .getBytes()); + } + LOG.info("Use IPythonInterpreter to replace PythonInterpreter"); + return; + } catch (Exception e) { + iPythonInterpreter = null; + } + } + // reset iPythonInterpreter to null + iPythonInterpreter = null; + + try { + if (InterpreterContext.get() != null) { + InterpreterContext.get().out.write(("IPython is not available, " + + "use the native PythonInterpreter\n") + .getBytes()); + } + } catch (IOException e) { + LOG.warn("Fail to write InterpreterOutput", e.getMessage()); + } + // Add matplotlib display hook InterpreterGroup intpGroup = getInterpreterGroup(); if (intpGroup != null && intpGroup.getInterpreterHookRegistry() != null) { @@ -232,8 +264,27 @@ public void open() { } } + private IPythonInterpreter getIPythonInterpreter() { + LazyOpenInterpreter lazy = null; + IPythonInterpreter ipython = null; + Interpreter p = getInterpreterInTheSameSessionByClassName(IPythonInterpreter.class.getName()); + + while (p instanceof WrappedInterpreter) { + if (p instanceof LazyOpenInterpreter) { + lazy = (LazyOpenInterpreter) p; + } + p = ((WrappedInterpreter) p).getInnerInterpreter(); + } + ipython = (IPythonInterpreter) p; + return ipython; + } + @Override public void close() { + if (iPythonInterpreter != null) { + iPythonInterpreter.close(); + return; + } pythonscriptRunning = false; pythonScriptInitialized = false; @@ -319,6 +370,9 @@ public void appendOutput(String message) throws IOException { @Override public InterpreterResult interpret(String cmd, InterpreterContext contextInterpreter) { + if (iPythonInterpreter != null) { + return iPythonInterpreter.interpret(cmd, contextInterpreter); + } if (cmd == null || cmd.isEmpty()) { return new InterpreterResult(Code.SUCCESS, ""); } @@ -411,6 +465,9 @@ public void interrupt() throws IOException { @Override public void cancel(InterpreterContext context) { + if (iPythonInterpreter != null) { + iPythonInterpreter.cancel(context); + } try { interrupt(); } catch (IOException e) { @@ -425,11 +482,17 @@ public FormType getFormType() { @Override public int getProgress(InterpreterContext context) { + if (iPythonInterpreter != null) { + return iPythonInterpreter.getProgress(context); + } return 0; } @Override public Scheduler getScheduler() { + if (iPythonInterpreter != null) { + return iPythonInterpreter.getScheduler(); + } return SchedulerFactory.singleton().createOrGetFIFOScheduler( PythonInterpreter.class.getName() + this.hashCode()); } @@ -437,6 +500,9 @@ public Scheduler getScheduler() { @Override public List completion(String buf, int cursor, InterpreterContext interpreterContext) { + if (iPythonInterpreter != null) { + return iPythonInterpreter.completion(buf, cursor, interpreterContext); + } return null; } diff --git a/python/src/main/java/org/apache/zeppelin/python/PythonZeppelinContext.java b/python/src/main/java/org/apache/zeppelin/python/PythonZeppelinContext.java new file mode 100644 index 00000000000..3d476e069fb --- /dev/null +++ b/python/src/main/java/org/apache/zeppelin/python/PythonZeppelinContext.java @@ -0,0 +1,49 @@ +/* +* 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.python; + +import org.apache.zeppelin.interpreter.BaseZeppelinContext; +import org.apache.zeppelin.interpreter.InterpreterHookRegistry; + +import java.util.List; +import java.util.Map; + +/** + * ZeppelinContext for Python + */ +public class PythonZeppelinContext extends BaseZeppelinContext { + + public PythonZeppelinContext(InterpreterHookRegistry hooks, int maxResult) { + super(hooks, maxResult); + } + + @Override + public Map getInterpreterClassMap() { + return null; + } + + @Override + public List getSupportedClasses() { + return null; + } + + @Override + protected String showData(Object obj) { + return null; + } +} diff --git a/python/src/main/proto/ipython.proto b/python/src/main/proto/ipython.proto new file mode 100644 index 00000000000..a54f36d49e5 --- /dev/null +++ b/python/src/main/proto/ipython.proto @@ -0,0 +1,102 @@ +/* + * 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. + */ + +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "org.apache.zeppelin.python.proto"; +option java_outer_classname = "IPythonProto"; +option objc_class_prefix = "IPython"; + +package ipython; + +// The IPython service definition. +service IPython { + // Sends code + rpc execute (ExecuteRequest) returns (stream ExecuteResponse) {} + + // Get completion + rpc complete (CompletionRequest) returns (CompletionResponse) {} + + // Cancel the running statement + rpc cancel (CancelRequest) returns (CancelResponse) {} + + // Get ipython kernel status + rpc status (StatusRequest) returns (StatusResponse) {} + + rpc stop(StopRequest) returns (StopResponse) {} +} + +enum ExecuteStatus { + SUCCESS = 0; + ERROR = 1; +} + +enum IPythonStatus { + STARTING = 0; + RUNNING = 1; +} + +enum OutputType { + TEXT = 0; + IMAGE = 1; +} + +// The request message containing the code +message ExecuteRequest { + string code = 1; +} + +// The response message containing the execution result. +message ExecuteResponse { + ExecuteStatus status = 1; + OutputType type = 2; + string output = 3; +} + +message CancelRequest { + +} + +message CancelResponse { + +} + +message CompletionRequest { + string code = 1; + int32 cursor = 2; +} + +message CompletionResponse { + repeated string matches = 1; +} + +message StatusRequest { + +} + +message StatusResponse { + IPythonStatus status = 1; +} + +message StopRequest { + +} + +message StopResponse { + +} \ No newline at end of file diff --git a/python/src/main/resources/grpc/generate_rpc.sh b/python/src/main/resources/grpc/generate_rpc.sh new file mode 100755 index 00000000000..efa5fbe4ccc --- /dev/null +++ b/python/src/main/resources/grpc/generate_rpc.sh @@ -0,0 +1,18 @@ +# 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. + +#!/usr/bin/env bash + +python -m grpc_tools.protoc -I../../proto --python_out=python --grpc_python_out=python ../../proto/ipython.proto diff --git a/python/src/main/resources/grpc/python/ipython_client.py b/python/src/main/resources/grpc/python/ipython_client.py new file mode 100644 index 00000000000..b8d1ee0983d --- /dev/null +++ b/python/src/main/resources/grpc/python/ipython_client.py @@ -0,0 +1,36 @@ +# 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 grpc + +import ipython_pb2 +import ipython_pb2_grpc + + +def run(): + channel = grpc.insecure_channel('localhost:50053') + stub = ipython_pb2_grpc.IPythonStub(channel) + response = stub.execute(ipython_pb2.ExecuteRequest(code="import time\nfor i in range(1,4):\n\ttime.sleep(1)\n\tprint(i)\n" + + "%matplotlib inline\nimport matplotlib.pyplot as plt\ndata=[1,1,2,3,4]\nplt.figure()\nplt.plot(data)")) + for r in response: + print("output:" + r.output) + + response = stub.execute(ipython_pb2.ExecuteRequest(code="range?")) + for r in response: + print(r) + +if __name__ == '__main__': + run() diff --git a/python/src/main/resources/grpc/python/ipython_pb2.py b/python/src/main/resources/grpc/python/ipython_pb2.py new file mode 100644 index 00000000000..eca3dfe3b08 --- /dev/null +++ b/python/src/main/resources/grpc/python/ipython_pb2.py @@ -0,0 +1,751 @@ +# 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. + +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: ipython.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='ipython.proto', + package='ipython', + syntax='proto3', + serialized_pb=_b('\n\ripython.proto\x12\x07ipython\"\x1e\n\x0e\x45xecuteRequest\x12\x0c\n\x04\x63ode\x18\x01 \x01(\t\"l\n\x0f\x45xecuteResponse\x12&\n\x06status\x18\x01 \x01(\x0e\x32\x16.ipython.ExecuteStatus\x12!\n\x04type\x18\x02 \x01(\x0e\x32\x13.ipython.OutputType\x12\x0e\n\x06output\x18\x03 \x01(\t\"\x0f\n\rCancelRequest\"\x10\n\x0e\x43\x61ncelResponse\"1\n\x11\x43ompletionRequest\x12\x0c\n\x04\x63ode\x18\x01 \x01(\t\x12\x0e\n\x06\x63ursor\x18\x02 \x01(\x05\"%\n\x12\x43ompletionResponse\x12\x0f\n\x07matches\x18\x01 \x03(\t\"\x0f\n\rStatusRequest\"8\n\x0eStatusResponse\x12&\n\x06status\x18\x01 \x01(\x0e\x32\x16.ipython.IPythonStatus\"\r\n\x0bStopRequest\"\x0e\n\x0cStopResponse*\'\n\rExecuteStatus\x12\x0b\n\x07SUCCESS\x10\x00\x12\t\n\x05\x45RROR\x10\x01**\n\rIPythonStatus\x12\x0c\n\x08STARTING\x10\x00\x12\x0b\n\x07RUNNING\x10\x01*!\n\nOutputType\x12\x08\n\x04TEXT\x10\x00\x12\t\n\x05IMAGE\x10\x01\x32\xc3\x02\n\x07IPython\x12@\n\x07\x65xecute\x12\x17.ipython.ExecuteRequest\x1a\x18.ipython.ExecuteResponse\"\x00\x30\x01\x12\x45\n\x08\x63omplete\x12\x1a.ipython.CompletionRequest\x1a\x1b.ipython.CompletionResponse\"\x00\x12;\n\x06\x63\x61ncel\x12\x16.ipython.CancelRequest\x1a\x17.ipython.CancelResponse\"\x00\x12;\n\x06status\x12\x16.ipython.StatusRequest\x1a\x17.ipython.StatusResponse\"\x00\x12\x35\n\x04stop\x12\x14.ipython.StopRequest\x1a\x15.ipython.StopResponse\"\x00\x42<\n org.apache.zeppelin.python.protoB\x0cIPythonProtoP\x01\xa2\x02\x07IPythonb\x06proto3') +) + +_EXECUTESTATUS = _descriptor.EnumDescriptor( + name='ExecuteStatus', + full_name='ipython.ExecuteStatus', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='SUCCESS', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='ERROR', index=1, number=1, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=399, + serialized_end=438, +) +_sym_db.RegisterEnumDescriptor(_EXECUTESTATUS) + +ExecuteStatus = enum_type_wrapper.EnumTypeWrapper(_EXECUTESTATUS) +_IPYTHONSTATUS = _descriptor.EnumDescriptor( + name='IPythonStatus', + full_name='ipython.IPythonStatus', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='STARTING', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='RUNNING', index=1, number=1, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=440, + serialized_end=482, +) +_sym_db.RegisterEnumDescriptor(_IPYTHONSTATUS) + +IPythonStatus = enum_type_wrapper.EnumTypeWrapper(_IPYTHONSTATUS) +_OUTPUTTYPE = _descriptor.EnumDescriptor( + name='OutputType', + full_name='ipython.OutputType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='TEXT', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='IMAGE', index=1, number=1, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=484, + serialized_end=517, +) +_sym_db.RegisterEnumDescriptor(_OUTPUTTYPE) + +OutputType = enum_type_wrapper.EnumTypeWrapper(_OUTPUTTYPE) +SUCCESS = 0 +ERROR = 1 +STARTING = 0 +RUNNING = 1 +TEXT = 0 +IMAGE = 1 + + + +_EXECUTEREQUEST = _descriptor.Descriptor( + name='ExecuteRequest', + full_name='ipython.ExecuteRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='code', full_name='ipython.ExecuteRequest.code', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=26, + serialized_end=56, +) + + +_EXECUTERESPONSE = _descriptor.Descriptor( + name='ExecuteResponse', + full_name='ipython.ExecuteResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='ipython.ExecuteResponse.status', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='type', full_name='ipython.ExecuteResponse.type', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='output', full_name='ipython.ExecuteResponse.output', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=58, + serialized_end=166, +) + + +_CANCELREQUEST = _descriptor.Descriptor( + name='CancelRequest', + full_name='ipython.CancelRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=168, + serialized_end=183, +) + + +_CANCELRESPONSE = _descriptor.Descriptor( + name='CancelResponse', + full_name='ipython.CancelResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=185, + serialized_end=201, +) + + +_COMPLETIONREQUEST = _descriptor.Descriptor( + name='CompletionRequest', + full_name='ipython.CompletionRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='code', full_name='ipython.CompletionRequest.code', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='cursor', full_name='ipython.CompletionRequest.cursor', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=203, + serialized_end=252, +) + + +_COMPLETIONRESPONSE = _descriptor.Descriptor( + name='CompletionResponse', + full_name='ipython.CompletionResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='matches', full_name='ipython.CompletionResponse.matches', index=0, + number=1, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=254, + serialized_end=291, +) + + +_STATUSREQUEST = _descriptor.Descriptor( + name='StatusRequest', + full_name='ipython.StatusRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=293, + serialized_end=308, +) + + +_STATUSRESPONSE = _descriptor.Descriptor( + name='StatusResponse', + full_name='ipython.StatusResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='ipython.StatusResponse.status', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=310, + serialized_end=366, +) + + +_STOPREQUEST = _descriptor.Descriptor( + name='StopRequest', + full_name='ipython.StopRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=368, + serialized_end=381, +) + + +_STOPRESPONSE = _descriptor.Descriptor( + name='StopResponse', + full_name='ipython.StopResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=383, + serialized_end=397, +) + +_EXECUTERESPONSE.fields_by_name['status'].enum_type = _EXECUTESTATUS +_EXECUTERESPONSE.fields_by_name['type'].enum_type = _OUTPUTTYPE +_STATUSRESPONSE.fields_by_name['status'].enum_type = _IPYTHONSTATUS +DESCRIPTOR.message_types_by_name['ExecuteRequest'] = _EXECUTEREQUEST +DESCRIPTOR.message_types_by_name['ExecuteResponse'] = _EXECUTERESPONSE +DESCRIPTOR.message_types_by_name['CancelRequest'] = _CANCELREQUEST +DESCRIPTOR.message_types_by_name['CancelResponse'] = _CANCELRESPONSE +DESCRIPTOR.message_types_by_name['CompletionRequest'] = _COMPLETIONREQUEST +DESCRIPTOR.message_types_by_name['CompletionResponse'] = _COMPLETIONRESPONSE +DESCRIPTOR.message_types_by_name['StatusRequest'] = _STATUSREQUEST +DESCRIPTOR.message_types_by_name['StatusResponse'] = _STATUSRESPONSE +DESCRIPTOR.message_types_by_name['StopRequest'] = _STOPREQUEST +DESCRIPTOR.message_types_by_name['StopResponse'] = _STOPRESPONSE +DESCRIPTOR.enum_types_by_name['ExecuteStatus'] = _EXECUTESTATUS +DESCRIPTOR.enum_types_by_name['IPythonStatus'] = _IPYTHONSTATUS +DESCRIPTOR.enum_types_by_name['OutputType'] = _OUTPUTTYPE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +ExecuteRequest = _reflection.GeneratedProtocolMessageType('ExecuteRequest', (_message.Message,), dict( + DESCRIPTOR = _EXECUTEREQUEST, + __module__ = 'ipython_pb2' + # @@protoc_insertion_point(class_scope:ipython.ExecuteRequest) + )) +_sym_db.RegisterMessage(ExecuteRequest) + +ExecuteResponse = _reflection.GeneratedProtocolMessageType('ExecuteResponse', (_message.Message,), dict( + DESCRIPTOR = _EXECUTERESPONSE, + __module__ = 'ipython_pb2' + # @@protoc_insertion_point(class_scope:ipython.ExecuteResponse) + )) +_sym_db.RegisterMessage(ExecuteResponse) + +CancelRequest = _reflection.GeneratedProtocolMessageType('CancelRequest', (_message.Message,), dict( + DESCRIPTOR = _CANCELREQUEST, + __module__ = 'ipython_pb2' + # @@protoc_insertion_point(class_scope:ipython.CancelRequest) + )) +_sym_db.RegisterMessage(CancelRequest) + +CancelResponse = _reflection.GeneratedProtocolMessageType('CancelResponse', (_message.Message,), dict( + DESCRIPTOR = _CANCELRESPONSE, + __module__ = 'ipython_pb2' + # @@protoc_insertion_point(class_scope:ipython.CancelResponse) + )) +_sym_db.RegisterMessage(CancelResponse) + +CompletionRequest = _reflection.GeneratedProtocolMessageType('CompletionRequest', (_message.Message,), dict( + DESCRIPTOR = _COMPLETIONREQUEST, + __module__ = 'ipython_pb2' + # @@protoc_insertion_point(class_scope:ipython.CompletionRequest) + )) +_sym_db.RegisterMessage(CompletionRequest) + +CompletionResponse = _reflection.GeneratedProtocolMessageType('CompletionResponse', (_message.Message,), dict( + DESCRIPTOR = _COMPLETIONRESPONSE, + __module__ = 'ipython_pb2' + # @@protoc_insertion_point(class_scope:ipython.CompletionResponse) + )) +_sym_db.RegisterMessage(CompletionResponse) + +StatusRequest = _reflection.GeneratedProtocolMessageType('StatusRequest', (_message.Message,), dict( + DESCRIPTOR = _STATUSREQUEST, + __module__ = 'ipython_pb2' + # @@protoc_insertion_point(class_scope:ipython.StatusRequest) + )) +_sym_db.RegisterMessage(StatusRequest) + +StatusResponse = _reflection.GeneratedProtocolMessageType('StatusResponse', (_message.Message,), dict( + DESCRIPTOR = _STATUSRESPONSE, + __module__ = 'ipython_pb2' + # @@protoc_insertion_point(class_scope:ipython.StatusResponse) + )) +_sym_db.RegisterMessage(StatusResponse) + +StopRequest = _reflection.GeneratedProtocolMessageType('StopRequest', (_message.Message,), dict( + DESCRIPTOR = _STOPREQUEST, + __module__ = 'ipython_pb2' + # @@protoc_insertion_point(class_scope:ipython.StopRequest) + )) +_sym_db.RegisterMessage(StopRequest) + +StopResponse = _reflection.GeneratedProtocolMessageType('StopResponse', (_message.Message,), dict( + DESCRIPTOR = _STOPRESPONSE, + __module__ = 'ipython_pb2' + # @@protoc_insertion_point(class_scope:ipython.StopResponse) + )) +_sym_db.RegisterMessage(StopResponse) + + +DESCRIPTOR.has_options = True +DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n org.apache.zeppelin.python.protoB\014IPythonProtoP\001\242\002\007IPython')) +try: + # THESE ELEMENTS WILL BE DEPRECATED. + # Please use the generated *_pb2_grpc.py files instead. + import grpc + from grpc.beta import implementations as beta_implementations + from grpc.beta import interfaces as beta_interfaces + from grpc.framework.common import cardinality + from grpc.framework.interfaces.face import utilities as face_utilities + + + class IPythonStub(object): + """The IPython service definition. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.execute = channel.unary_stream( + '/ipython.IPython/execute', + request_serializer=ExecuteRequest.SerializeToString, + response_deserializer=ExecuteResponse.FromString, + ) + self.complete = channel.unary_unary( + '/ipython.IPython/complete', + request_serializer=CompletionRequest.SerializeToString, + response_deserializer=CompletionResponse.FromString, + ) + self.cancel = channel.unary_unary( + '/ipython.IPython/cancel', + request_serializer=CancelRequest.SerializeToString, + response_deserializer=CancelResponse.FromString, + ) + self.status = channel.unary_unary( + '/ipython.IPython/status', + request_serializer=StatusRequest.SerializeToString, + response_deserializer=StatusResponse.FromString, + ) + self.stop = channel.unary_unary( + '/ipython.IPython/stop', + request_serializer=StopRequest.SerializeToString, + response_deserializer=StopResponse.FromString, + ) + + + class IPythonServicer(object): + """The IPython service definition. + """ + + def execute(self, request, context): + """Sends code + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def complete(self, request, context): + """Get completion + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def cancel(self, request, context): + """Cancel the running statement + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def status(self, request, context): + """Get ipython kernel status + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def stop(self, request, context): + # missing associated documentation comment in .proto file + pass + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + + def add_IPythonServicer_to_server(servicer, server): + rpc_method_handlers = { + 'execute': grpc.unary_stream_rpc_method_handler( + servicer.execute, + request_deserializer=ExecuteRequest.FromString, + response_serializer=ExecuteResponse.SerializeToString, + ), + 'complete': grpc.unary_unary_rpc_method_handler( + servicer.complete, + request_deserializer=CompletionRequest.FromString, + response_serializer=CompletionResponse.SerializeToString, + ), + 'cancel': grpc.unary_unary_rpc_method_handler( + servicer.cancel, + request_deserializer=CancelRequest.FromString, + response_serializer=CancelResponse.SerializeToString, + ), + 'status': grpc.unary_unary_rpc_method_handler( + servicer.status, + request_deserializer=StatusRequest.FromString, + response_serializer=StatusResponse.SerializeToString, + ), + 'stop': grpc.unary_unary_rpc_method_handler( + servicer.stop, + request_deserializer=StopRequest.FromString, + response_serializer=StopResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'ipython.IPython', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + class BetaIPythonServicer(object): + """The Beta API is deprecated for 0.15.0 and later. + + It is recommended to use the GA API (classes and functions in this + file not marked beta) for all further purposes. This class was generated + only to ease transition from grpcio<0.15.0 to grpcio>=0.15.0.""" + """The IPython service definition. + """ + def execute(self, request, context): + """Sends code + """ + context.code(beta_interfaces.StatusCode.UNIMPLEMENTED) + def complete(self, request, context): + """Get completion + """ + context.code(beta_interfaces.StatusCode.UNIMPLEMENTED) + def cancel(self, request, context): + """Cancel the running statement + """ + context.code(beta_interfaces.StatusCode.UNIMPLEMENTED) + def status(self, request, context): + """Get ipython kernel status + """ + context.code(beta_interfaces.StatusCode.UNIMPLEMENTED) + def stop(self, request, context): + # missing associated documentation comment in .proto file + pass + context.code(beta_interfaces.StatusCode.UNIMPLEMENTED) + + + class BetaIPythonStub(object): + """The Beta API is deprecated for 0.15.0 and later. + + It is recommended to use the GA API (classes and functions in this + file not marked beta) for all further purposes. This class was generated + only to ease transition from grpcio<0.15.0 to grpcio>=0.15.0.""" + """The IPython service definition. + """ + def execute(self, request, timeout, metadata=None, with_call=False, protocol_options=None): + """Sends code + """ + raise NotImplementedError() + def complete(self, request, timeout, metadata=None, with_call=False, protocol_options=None): + """Get completion + """ + raise NotImplementedError() + complete.future = None + def cancel(self, request, timeout, metadata=None, with_call=False, protocol_options=None): + """Cancel the running statement + """ + raise NotImplementedError() + cancel.future = None + def status(self, request, timeout, metadata=None, with_call=False, protocol_options=None): + """Get ipython kernel status + """ + raise NotImplementedError() + status.future = None + def stop(self, request, timeout, metadata=None, with_call=False, protocol_options=None): + # missing associated documentation comment in .proto file + pass + raise NotImplementedError() + stop.future = None + + + def beta_create_IPython_server(servicer, pool=None, pool_size=None, default_timeout=None, maximum_timeout=None): + """The Beta API is deprecated for 0.15.0 and later. + + It is recommended to use the GA API (classes and functions in this + file not marked beta) for all further purposes. This function was + generated only to ease transition from grpcio<0.15.0 to grpcio>=0.15.0""" + request_deserializers = { + ('ipython.IPython', 'cancel'): CancelRequest.FromString, + ('ipython.IPython', 'complete'): CompletionRequest.FromString, + ('ipython.IPython', 'execute'): ExecuteRequest.FromString, + ('ipython.IPython', 'status'): StatusRequest.FromString, + ('ipython.IPython', 'stop'): StopRequest.FromString, + } + response_serializers = { + ('ipython.IPython', 'cancel'): CancelResponse.SerializeToString, + ('ipython.IPython', 'complete'): CompletionResponse.SerializeToString, + ('ipython.IPython', 'execute'): ExecuteResponse.SerializeToString, + ('ipython.IPython', 'status'): StatusResponse.SerializeToString, + ('ipython.IPython', 'stop'): StopResponse.SerializeToString, + } + method_implementations = { + ('ipython.IPython', 'cancel'): face_utilities.unary_unary_inline(servicer.cancel), + ('ipython.IPython', 'complete'): face_utilities.unary_unary_inline(servicer.complete), + ('ipython.IPython', 'execute'): face_utilities.unary_stream_inline(servicer.execute), + ('ipython.IPython', 'status'): face_utilities.unary_unary_inline(servicer.status), + ('ipython.IPython', 'stop'): face_utilities.unary_unary_inline(servicer.stop), + } + server_options = beta_implementations.server_options(request_deserializers=request_deserializers, response_serializers=response_serializers, thread_pool=pool, thread_pool_size=pool_size, default_timeout=default_timeout, maximum_timeout=maximum_timeout) + return beta_implementations.server(method_implementations, options=server_options) + + + def beta_create_IPython_stub(channel, host=None, metadata_transformer=None, pool=None, pool_size=None): + """The Beta API is deprecated for 0.15.0 and later. + + It is recommended to use the GA API (classes and functions in this + file not marked beta) for all further purposes. This function was + generated only to ease transition from grpcio<0.15.0 to grpcio>=0.15.0""" + request_serializers = { + ('ipython.IPython', 'cancel'): CancelRequest.SerializeToString, + ('ipython.IPython', 'complete'): CompletionRequest.SerializeToString, + ('ipython.IPython', 'execute'): ExecuteRequest.SerializeToString, + ('ipython.IPython', 'status'): StatusRequest.SerializeToString, + ('ipython.IPython', 'stop'): StopRequest.SerializeToString, + } + response_deserializers = { + ('ipython.IPython', 'cancel'): CancelResponse.FromString, + ('ipython.IPython', 'complete'): CompletionResponse.FromString, + ('ipython.IPython', 'execute'): ExecuteResponse.FromString, + ('ipython.IPython', 'status'): StatusResponse.FromString, + ('ipython.IPython', 'stop'): StopResponse.FromString, + } + cardinalities = { + 'cancel': cardinality.Cardinality.UNARY_UNARY, + 'complete': cardinality.Cardinality.UNARY_UNARY, + 'execute': cardinality.Cardinality.UNARY_STREAM, + 'status': cardinality.Cardinality.UNARY_UNARY, + 'stop': cardinality.Cardinality.UNARY_UNARY, + } + stub_options = beta_implementations.stub_options(host=host, metadata_transformer=metadata_transformer, request_serializers=request_serializers, response_deserializers=response_deserializers, thread_pool=pool, thread_pool_size=pool_size) + return beta_implementations.dynamic_stub(channel, 'ipython.IPython', cardinalities, options=stub_options) +except ImportError: + pass +# @@protoc_insertion_point(module_scope) diff --git a/python/src/main/resources/grpc/python/ipython_pb2_grpc.py b/python/src/main/resources/grpc/python/ipython_pb2_grpc.py new file mode 100644 index 00000000000..a590319dd9a --- /dev/null +++ b/python/src/main/resources/grpc/python/ipython_pb2_grpc.py @@ -0,0 +1,129 @@ +# 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. + +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +import grpc + +import ipython_pb2 as ipython__pb2 + + +class IPythonStub(object): + """The IPython service definition. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.execute = channel.unary_stream( + '/ipython.IPython/execute', + request_serializer=ipython__pb2.ExecuteRequest.SerializeToString, + response_deserializer=ipython__pb2.ExecuteResponse.FromString, + ) + self.complete = channel.unary_unary( + '/ipython.IPython/complete', + request_serializer=ipython__pb2.CompletionRequest.SerializeToString, + response_deserializer=ipython__pb2.CompletionResponse.FromString, + ) + self.cancel = channel.unary_unary( + '/ipython.IPython/cancel', + request_serializer=ipython__pb2.CancelRequest.SerializeToString, + response_deserializer=ipython__pb2.CancelResponse.FromString, + ) + self.status = channel.unary_unary( + '/ipython.IPython/status', + request_serializer=ipython__pb2.StatusRequest.SerializeToString, + response_deserializer=ipython__pb2.StatusResponse.FromString, + ) + self.stop = channel.unary_unary( + '/ipython.IPython/stop', + request_serializer=ipython__pb2.StopRequest.SerializeToString, + response_deserializer=ipython__pb2.StopResponse.FromString, + ) + + +class IPythonServicer(object): + """The IPython service definition. + """ + + def execute(self, request, context): + """Sends code + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def complete(self, request, context): + """Get completion + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def cancel(self, request, context): + """Cancel the running statement + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def status(self, request, context): + """Get ipython kernel status + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def stop(self, request, context): + # missing associated documentation comment in .proto file + pass + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_IPythonServicer_to_server(servicer, server): + rpc_method_handlers = { + 'execute': grpc.unary_stream_rpc_method_handler( + servicer.execute, + request_deserializer=ipython__pb2.ExecuteRequest.FromString, + response_serializer=ipython__pb2.ExecuteResponse.SerializeToString, + ), + 'complete': grpc.unary_unary_rpc_method_handler( + servicer.complete, + request_deserializer=ipython__pb2.CompletionRequest.FromString, + response_serializer=ipython__pb2.CompletionResponse.SerializeToString, + ), + 'cancel': grpc.unary_unary_rpc_method_handler( + servicer.cancel, + request_deserializer=ipython__pb2.CancelRequest.FromString, + response_serializer=ipython__pb2.CancelResponse.SerializeToString, + ), + 'status': grpc.unary_unary_rpc_method_handler( + servicer.status, + request_deserializer=ipython__pb2.StatusRequest.FromString, + response_serializer=ipython__pb2.StatusResponse.SerializeToString, + ), + 'stop': grpc.unary_unary_rpc_method_handler( + servicer.stop, + request_deserializer=ipython__pb2.StopRequest.FromString, + response_serializer=ipython__pb2.StopResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'ipython.IPython', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) diff --git a/python/src/main/resources/grpc/python/ipython_server.py b/python/src/main/resources/grpc/python/ipython_server.py new file mode 100644 index 00000000000..1d927664f4b --- /dev/null +++ b/python/src/main/resources/grpc/python/ipython_server.py @@ -0,0 +1,155 @@ +# 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. + +from __future__ import print_function + +import jupyter_client +import sys +import threading +import time +from concurrent import futures + +import grpc +import ipython_pb2 +import ipython_pb2_grpc + +_ONE_DAY_IN_SECONDS = 60 * 60 * 24 + + +is_py2 = sys.version[0] == '2' +if is_py2: + import Queue as queue +else: + import queue as queue + + +TIMEOUT = 30 + +class IPython(ipython_pb2_grpc.IPythonServicer): + + def __init__(self, server): + self._status = ipython_pb2.STARTING + self._server = server + + def start(self): + print("starting...") + sys.stdout.flush() + self._km, self._kc = jupyter_client.manager.start_new_kernel(kernel_name='python') + self._status = ipython_pb2.RUNNING + + def execute(self, request, context): + print("execute code: " + request.code) + sys.stdout.flush() + stdout_queue = queue.Queue(maxsize = 10) + stderr_queue = queue.Queue(maxsize = 10) + image_queue = queue.Queue(maxsize = 5) + + def _output_hook(msg): + msg_type = msg['header']['msg_type'] + content = msg['content'] + if msg_type == 'stream': + stdout_queue.put(content['text']) + elif msg_type in ('display_data', 'execute_result'): + stdout_queue.put(content['data'].get('text/plain', '')) + if 'image/png' in content['data']: + image_queue.put(content['data']['image/png']) + elif msg_type == 'error': + stderr_queue.put('\n'.join(content['traceback'])) + + + payload_reply = [] + def execute_worker(): + reply = self._kc.execute_interactive(request.code, + output_hook=_output_hook, + timeout=TIMEOUT) + payload_reply.append(reply) + + t = threading.Thread(name="ConsumerThread", target=execute_worker) + t.start() + + while t.is_alive(): + while not stdout_queue.empty(): + output = stdout_queue.get() + yield ipython_pb2.ExecuteResponse(status=ipython_pb2.SUCCESS, + type=ipython_pb2.TEXT, + output=output) + while not stderr_queue.empty(): + output = stderr_queue.get() + yield ipython_pb2.ExecuteResponse(status=ipython_pb2.ERROR, + type=ipython_pb2.TEXT, + output=output) + while not image_queue.empty(): + output = image_queue.get() + yield ipython_pb2.ExecuteResponse(status=ipython_pb2.SUCCESS, + type=ipython_pb2.IMAGE, + output=output) + + while not stdout_queue.empty(): + output = stdout_queue.get() + yield ipython_pb2.ExecuteResponse(status=ipython_pb2.SUCCESS, + type=ipython_pb2.TEXT, + output=output) + while not stderr_queue.empty(): + output = stderr_queue.get() + yield ipython_pb2.ExecuteResponse(status=ipython_pb2.ERROR, + type=ipython_pb2.TEXT, + output=output) + while not image_queue.empty(): + output = image_queue.get() + yield ipython_pb2.ExecuteResponse(status=ipython_pb2.SUCCESS, + type=ipython_pb2.IMAGE, + output=output) + + if payload_reply: + result = [] + for payload in payload_reply[0]['content']['payload']: + if payload['data']['text/plain']: + result.append(payload['data']['text/plain']) + if result: + yield ipython_pb2.ExecuteResponse(status=ipython_pb2.SUCCESS, + type=ipython_pb2.TEXT, + output='\n'.join(result)) + + def cancel(self, request, context): + self._km.interrupt_kernel() + return ipython_pb2.CancelResponse() + + def complete(self, request, context): + reply = self._kc.complete(request.code, request.cursor, reply=True, timeout=TIMEOUT) + return ipython_pb2.CompletionResponse(matches=reply['content']['matches']) + + def status(self, request, context): + return ipython_pb2.StatusResponse(status = self._status) + + def stop(self, request, context): + self._server.stop(0) + sys.exit(0) + + +def serve(port): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + ipython = IPython(server) + ipython_pb2_grpc.add_IPythonServicer_to_server(ipython, server) + server.add_insecure_port('[::]:' + port) + server.start() + ipython.start() + try: + while True: + time.sleep(_ONE_DAY_IN_SECONDS) + except KeyboardInterrupt: + server.stop(0) + +if __name__ == '__main__': + serve(sys.argv[1]) diff --git a/python/src/main/resources/grpc/python/zeppelin_python.py b/python/src/main/resources/grpc/python/zeppelin_python.py new file mode 100644 index 00000000000..0f5638fd478 --- /dev/null +++ b/python/src/main/resources/grpc/python/zeppelin_python.py @@ -0,0 +1,107 @@ +# +# 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. +# + +from py4j.java_gateway import java_import, JavaGateway, GatewayClient + +from io import BytesIO +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +class PyZeppelinContext(object): + """ A context impl that uses Py4j to communicate to JVM + """ + + def __init__(self, z): + self.z = z + self.paramOption = gateway.jvm.org.apache.zeppelin.display.ui.OptionInput.ParamOption + self.javaList = gateway.jvm.java.util.ArrayList + self.max_result = z.getMaxResult() + + def input(self, name, defaultValue=""): + return self.z.getGui().input(name, defaultValue) + + def select(self, name, options, defaultValue=""): + javaOptions = gateway.new_array(self.paramOption, len(options)) + i = 0 + for tuple in options: + javaOptions[i] = self.paramOption(tuple[0], tuple[1]) + i += 1 + return self.z.getGui().select(name, defaultValue, javaOptions) + + def checkbox(self, name, options, defaultChecked=[]): + javaOptions = gateway.new_array(self.paramOption, len(options)) + i = 0 + for tuple in options: + javaOptions[i] = self.paramOption(tuple[0], tuple[1]) + i += 1 + javaDefaultCheck = self.javaList() + for check in defaultChecked: + javaDefaultCheck.append(check) + return self.z.getGui().checkbox(name, javaDefaultCheck, javaOptions) + + def show(self, p, **kwargs): + if type(p).__name__ == "DataFrame": # does not play well with sub-classes + # `isinstance(p, DataFrame)` would req `import pandas.core.frame.DataFrame` + # and so a dependency on pandas + self.show_dataframe(p, **kwargs) + elif hasattr(p, '__call__'): + p() #error reporting + + def show_dataframe(self, df, show_index=False, **kwargs): + """Pretty prints DF using Table Display System + """ + limit = len(df) > self.max_result + header_buf = StringIO("") + if show_index: + idx_name = str(df.index.name) if df.index.name is not None else "" + header_buf.write(idx_name + "\t") + header_buf.write(str(df.columns[0])) + for col in df.columns[1:]: + header_buf.write("\t") + header_buf.write(str(col)) + header_buf.write("\n") + + body_buf = StringIO("") + rows = df.head(self.max_result).values if limit else df.values + index = df.index.values + for idx, row in zip(index, rows): + if show_index: + body_buf.write("%html {}".format(idx)) + body_buf.write("\t") + body_buf.write(str(row[0])) + for cell in row[1:]: + body_buf.write("\t") + body_buf.write(str(cell)) + body_buf.write("\n") + body_buf.seek(0); header_buf.seek(0) + #TODO(bzz): fix it, so it shows red notice, as in Spark + print("%table " + header_buf.read() + body_buf.read()) # + + # ("\nResults are limited by {}." \ + # .format(self.max_result) if limit else "") + #) + body_buf.close(); header_buf.close() + + +# start JVM gateway +client = GatewayClient(address='127.0.0.1', port=${JVM_GATEWAY_PORT}) +gateway = JavaGateway(client) +java_import(gateway.jvm, "org.apache.zeppelin.display.Input") +intp = gateway.entry_point +z = __zeppelin__ = PyZeppelinContext(intp.getZeppelinContext()) + diff --git a/python/src/main/resources/interpreter-setting.json b/python/src/main/resources/interpreter-setting.json index 3bc42b8d110..bc1a746efd4 100644 --- a/python/src/main/resources/interpreter-setting.json +++ b/python/src/main/resources/interpreter-setting.json @@ -17,6 +17,29 @@ "defaultValue": "1000", "description": "Max number of dataframe rows to display.", "type": "number" + }, + "zeppelin.python.useIPython": { + "propertyName": "zeppelin.python.useIPython", + "defaultValue": true, + "description": "whether use IPython when it is available", + "type": "checkbox" + } + }, + "editor": { + "language": "python", + "editOnDblClick": false + } + }, + { + "group": "python", + "name": "ipython", + "className": "org.apache.zeppelin.python.IPythonInterpreter", + "properties": { + "zeppelin.ipython.launch.timeout": { + "propertyName": "zeppelin.ipython.launch.timeout", + "defaultValue": "30000", + "description": "time out for ipython launch", + "type": "number" } }, "editor": { diff --git a/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java b/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java new file mode 100644 index 00000000000..137d622bf85 --- /dev/null +++ b/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java @@ -0,0 +1,402 @@ +/* + * 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.python; + +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.display.ui.CheckBox; +import org.apache.zeppelin.display.ui.Select; +import org.apache.zeppelin.display.ui.TextBox; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterOutput; +import org.apache.zeppelin.interpreter.InterpreterOutputListener; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResultMessage; +import org.apache.zeppelin.interpreter.InterpreterResultMessageOutput; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.CopyOnWriteArrayList; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; + + +public class IPythonInterpreterTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(IPythonInterpreterTest.class); + private IPythonInterpreter interpreter; + + @Before + public void setUp() { + Properties properties = new Properties(); + interpreter = new IPythonInterpreter(properties); + InterpreterGroup mockInterpreterGroup = mock(InterpreterGroup.class); + interpreter.setInterpreterGroup(mockInterpreterGroup); + interpreter.open(); + } + + @After + public void close() { + interpreter.close(); + } + + + @Test + public void testIPython() throws IOException, InterruptedException { + testInterpreter(interpreter); + } + + public static void testInterpreter(final Interpreter interpreter) throws IOException, InterruptedException { + // to make this test can run under both python2 and python3 + InterpreterResult result = interpreter.interpret("from __future__ import print_function", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + // single output without print + InterpreterContext context = getInterpreterContext(); + result = interpreter.interpret("'hello world'", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + List interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertEquals("'hello world'", interpreterResultMessages.get(0).getData()); + + // only the last statement is printed + context = getInterpreterContext(); + result = interpreter.interpret("'hello world'\n'hello world2'", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertEquals("'hello world2'", interpreterResultMessages.get(0).getData()); + + // single output + context = getInterpreterContext(); + result = interpreter.interpret("print('hello world')", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertEquals("hello world\n", interpreterResultMessages.get(0).getData()); + + // multiple output + context = getInterpreterContext(); + result = interpreter.interpret("print('hello world')\nprint('hello world2')", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertEquals("hello world\nhello world2\n", interpreterResultMessages.get(0).getData()); + + // assignment + context = getInterpreterContext(); + result = interpreter.interpret("abc=1",context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(0, interpreterResultMessages.size()); + + // if block + context = getInterpreterContext(); + result = interpreter.interpret("if abc > 0:\n\tprint('True')\nelse:\n\tprint('False')", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertEquals("True\n", interpreterResultMessages.get(0).getData()); + + // for loop + context = getInterpreterContext(); + result = interpreter.interpret("for i in range(3):\n\tprint(i)", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertEquals("0\n1\n2\n", interpreterResultMessages.get(0).getData()); + + // syntax error + context = getInterpreterContext(); + result = interpreter.interpret("print(unknown)", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.ERROR, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertTrue(interpreterResultMessages.get(0).getData().contains("name 'unknown' is not defined")); + + // raise runtime exception + context = getInterpreterContext(); + result = interpreter.interpret("1/0", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.ERROR, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertTrue(interpreterResultMessages.get(0).getData().contains("ZeroDivisionError")); + + // ZEPPELIN-1133 + context = getInterpreterContext(); + result = interpreter.interpret("def greet(name):\n" + + " print('Hello', name)\n" + + "greet('Jack')", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertEquals("Hello Jack\n",interpreterResultMessages.get(0).getData()); + + // ZEPPELIN-1114 + context = getInterpreterContext(); + result = interpreter.interpret("print('there is no Error: ok')", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertEquals("there is no Error: ok\n", interpreterResultMessages.get(0).getData()); + + // completion + context = getInterpreterContext(); + List completions = interpreter.completion("ab", 2, context); + assertEquals(2, completions.size()); + assertEquals("abc", completions.get(0).getValue()); + assertEquals("abs", completions.get(1).getValue()); + + context = getInterpreterContext(); + interpreter.interpret("import sys", context); + completions = interpreter.completion("sys.", 4, context); + assertFalse(completions.isEmpty()); + + context = getInterpreterContext(); + completions = interpreter.completion("sys.std", 7, context); + assertEquals(3, completions.size()); + assertEquals("sys.stderr", completions.get(0).getValue()); + assertEquals("sys.stdin", completions.get(1).getValue()); + assertEquals("sys.stdout", completions.get(2).getValue()); + + // there's no completion for 'a.' because it is not recognized by compiler for now. + context = getInterpreterContext(); + String st = "a='hello'\na."; + completions = interpreter.completion(st, st.length(), context); + assertEquals(0, completions.size()); + + // define `a` first + context = getInterpreterContext(); + st = "a='hello'"; + result = interpreter.interpret(st, context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(0, interpreterResultMessages.size()); + + // now we can get the completion for `a.` + context = getInterpreterContext(); + st = "a."; + completions = interpreter.completion(st, st.length(), context); + // it is different for python2 and python3 and may even different for different minor version + // so only verify it is larger than 20 + assertTrue(completions.size() > 20); + + context = getInterpreterContext(); + st = "a.co"; + completions = interpreter.completion(st, st.length(), context); + assertEquals(1, completions.size()); + assertEquals("a.count", completions.get(0).getValue()); + + // cursor is in the middle of code + context = getInterpreterContext(); + st = "a.co\b='hello"; + completions = interpreter.completion(st, 4, context); + assertEquals(1, completions.size()); + assertEquals("a.count", completions.get(0).getValue()); + + // ipython help + context = getInterpreterContext(); + result = interpreter.interpret("range?", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertTrue(interpreterResultMessages.get(0).getData().contains("range(stop)")); + + // timeit + context = getInterpreterContext(); + result = interpreter.interpret("%timeit range(100)", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertTrue(interpreterResultMessages.get(0).getData().contains("loops")); + + // cancel + final InterpreterContext context2 = getInterpreterContext(); + new Thread() { + @Override + public void run() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + interpreter.cancel(context2); + } + }.start(); + result = interpreter.interpret("import time\ntime.sleep(10)", context2); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.ERROR, result.code()); + interpreterResultMessages = context2.out.getInterpreterResultMessages(); + assertTrue(interpreterResultMessages.get(0).getData().contains("KeyboardInterrupt")); + + // matplotlib + context = getInterpreterContext(); + result = interpreter.interpret("%matplotlib inline\nimport matplotlib.pyplot as plt\ndata=[1,1,2,3,4]\nplt.figure()\nplt.plot(data)", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + // the order of IMAGE and TEXT is not determined + // check there must be one IMAGE output + boolean hasImageOutput = false; + for (InterpreterResultMessage msg : interpreterResultMessages) { + if (msg.getType() == InterpreterResult.Type.IMG) { + hasImageOutput = true; + } + } + assertTrue("No Image Output", hasImageOutput); + + // bokeh + // bokeh initialization + context = getInterpreterContext(); + result = interpreter.interpret("from bokeh.io import output_notebook, show\n" + + "from bokeh.plotting import figure\n" + + "output_notebook(notebook_type='zeppelin')", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(2, interpreterResultMessages.size()); + assertEquals(InterpreterResult.Type.HTML, interpreterResultMessages.get(0).getType()); + assertTrue(interpreterResultMessages.get(0).getData().contains("Loading BokehJS")); + assertEquals(InterpreterResult.Type.HTML, interpreterResultMessages.get(1).getType()); + assertTrue(interpreterResultMessages.get(1).getData().contains("BokehJS is being loaded")); + + // bokeh plotting + context = getInterpreterContext(); + result = interpreter.interpret("from bokeh.plotting import figure, output_file, show\n" + + "x = [1, 2, 3, 4, 5]\n" + + "y = [6, 7, 2, 4, 5]\n" + + "p = figure(title=\"simple line example\", x_axis_label='x', y_axis_label='y')\n" + + "p.line(x, y, legend=\"Temp.\", line_width=2)\n" + + "show(p)", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertEquals(InterpreterResult.Type.HTML, interpreterResultMessages.get(0).getType()); + // docs_json is the source data of plotting which bokeh would use to render the plotting. + assertTrue(interpreterResultMessages.get(0).getData().contains("docs_json")); + + // ggplot + context = getInterpreterContext(); + result = interpreter.interpret("from ggplot import *\n" + + "ggplot(diamonds, aes(x='price', fill='cut')) +\\\n" + + " geom_density(alpha=0.25) +\\\n" + + " facet_wrap(\"clarity\")", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + // the order of IMAGE and TEXT is not determined + // check there must be one IMAGE output + hasImageOutput = false; + for (InterpreterResultMessage msg : interpreterResultMessages) { + if (msg.getType() == InterpreterResult.Type.IMG) { + hasImageOutput = true; + } + } + assertTrue("No Image Output", hasImageOutput); + + // ZeppelinContext + + // TextBox + context = getInterpreterContext(); + result = interpreter.interpret("z.input(name='text_1', defaultValue='value_1')", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertTrue(interpreterResultMessages.get(0).getData().contains("'value_1'")); + assertEquals(1, context.getGui().getForms().size()); + assertTrue(context.getGui().getForms().get("text_1") instanceof TextBox); + TextBox textbox = (TextBox) context.getGui().getForms().get("text_1"); + assertEquals("text_1", textbox.getName()); + assertEquals("value_1", textbox.getDefaultValue()); + + // Select + context = getInterpreterContext(); + result = interpreter.interpret("z.select(name='select_1', options=[('value_1', 'name_1'), ('value_2', 'name_2')])", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, context.getGui().getForms().size()); + assertTrue(context.getGui().getForms().get("select_1") instanceof Select); + Select select = (Select) context.getGui().getForms().get("select_1"); + assertEquals("select_1", select.getName()); + assertEquals(2, select.getOptions().length); + assertEquals("name_1", select.getOptions()[0].getDisplayName()); + assertEquals("value_1", select.getOptions()[0].getValue()); + + // CheckBox + context = getInterpreterContext(); + result = interpreter.interpret("z.checkbox(name='checkbox_1', options=[('value_1', 'name_1'), ('value_2', 'name_2')])", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, context.getGui().getForms().size()); + assertTrue(context.getGui().getForms().get("checkbox_1") instanceof CheckBox); + CheckBox checkbox = (CheckBox) context.getGui().getForms().get("checkbox_1"); + assertEquals("checkbox_1", checkbox.getName()); + assertEquals(2, checkbox.getOptions().length); + assertEquals("name_1", checkbox.getOptions()[0].getDisplayName()); + assertEquals("value_1", checkbox.getOptions()[0].getValue()); + + // Pandas DataFrame + context = getInterpreterContext(); + result = interpreter.interpret("import pandas as pd\ndf = pd.DataFrame({'id':[1,2,3], 'name':['a','b','c']})\nz.show(df)", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(InterpreterResult.Type.TABLE, interpreterResultMessages.get(0).getType()); + assertEquals("id\tname\n1\ta\n2\tb\n3\tc\n", interpreterResultMessages.get(0).getData()); + } + + private static InterpreterContext getInterpreterContext() { + return new InterpreterContext( + "noteId", + "paragraphId", + "replName", + "paragraphTitle", + "paragraphText", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + null, + null, + null, + new InterpreterOutput(null)); + } +} diff --git a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterMatplotlibTest.java b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterMatplotlibTest.java index 8b48b24439d..d649e8979b5 100644 --- a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterMatplotlibTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterMatplotlibTest.java @@ -53,6 +53,7 @@ public void setUp() throws Exception { Properties p = new Properties(); p.setProperty("zeppelin.python", "python"); p.setProperty("zeppelin.python.maxResult", "100"); + p.setProperty("zeppelin.python.useIPython", "false"); intpGroup = new InterpreterGroup(); diff --git a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterPandasSqlTest.java b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterPandasSqlTest.java index f200a0a9429..9e918c082f6 100644 --- a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterPandasSqlTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterPandasSqlTest.java @@ -73,9 +73,21 @@ public void setUp() throws Exception { Properties p = new Properties(); p.setProperty("zeppelin.python", "python"); p.setProperty("zeppelin.python.maxResult", "100"); + p.setProperty("zeppelin.python.useIPython", "false"); intpGroup = new InterpreterGroup(); + out = new InterpreterOutput(this); + context = new InterpreterContext("note", "id", null, "title", "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("id"), + new LinkedList(), + out); + InterpreterContext.set(context); + python = new PythonInterpreter(p); python.setInterpreterGroup(intpGroup); python.open(); @@ -85,16 +97,7 @@ public void setUp() throws Exception { intpGroup.put("note", Arrays.asList(python, sql)); - out = new InterpreterOutput(this); - context = new InterpreterContext("note", "id", null, "title", "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("id"), - new LinkedList(), - out); // to make sure python is running. InterpreterResult ret = python.interpret("\n", context); @@ -140,10 +143,10 @@ public void sqlOverTestDataPrintsTable() throws IOException { ret = sql.interpret("select name, age from df2 where age < 40", context); //then - assertEquals(new String(out.getOutputAt(0).toByteArray()), InterpreterResult.Code.SUCCESS, ret.code()); - assertEquals(new String(out.getOutputAt(0).toByteArray()), Type.TABLE, out.getOutputAt(0).getType()); - assertTrue(new String(out.getOutputAt(0).toByteArray()).indexOf("moon\t33") > 0); - assertTrue(new String(out.getOutputAt(0).toByteArray()).indexOf("park\t34") > 0); + assertEquals(new String(out.getOutputAt(1).toByteArray()), InterpreterResult.Code.SUCCESS, ret.code()); + assertEquals(new String(out.getOutputAt(1).toByteArray()), Type.TABLE, out.getOutputAt(1).getType()); + assertTrue(new String(out.getOutputAt(1).toByteArray()).indexOf("moon\t33") > 0); + assertTrue(new String(out.getOutputAt(1).toByteArray()).indexOf("park\t34") > 0); assertEquals(InterpreterResult.Code.SUCCESS, sql.interpret("select case when name==\"aa\" then name else name end from df2", context).code()); } @@ -156,7 +159,6 @@ public void badSqlSyntaxFails() throws IOException { //then assertNotNull("Interpreter returned 'null'", ret); assertEquals(ret.toString(), InterpreterResult.Code.ERROR, ret.code()); - assertTrue(out.toInterpreterResultMessage().size() == 0); } @Test @@ -176,10 +178,10 @@ public void showDataFrame() throws IOException { // then assertEquals(new String(out.getOutputAt(0).toByteArray()), InterpreterResult.Code.SUCCESS, ret.code()); - assertEquals(new String(out.getOutputAt(0).toByteArray()), Type.TABLE, out.getOutputAt(0).getType()); - assertTrue(new String(out.getOutputAt(0).toByteArray()).contains("index_name")); - assertTrue(new String(out.getOutputAt(0).toByteArray()).contains("nan")); - assertTrue(new String(out.getOutputAt(0).toByteArray()).contains("6.7")); + assertEquals(new String(out.getOutputAt(1).toByteArray()), Type.TABLE, out.getOutputAt(1).getType()); + assertTrue(new String(out.getOutputAt(1).toByteArray()).contains("index_name")); + assertTrue(new String(out.getOutputAt(1).toByteArray()).contains("nan")); + assertTrue(new String(out.getOutputAt(1).toByteArray()).contains("6.7")); } @Override diff --git a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java index 837626c1ba3..195935d2135 100644 --- a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java @@ -59,6 +59,7 @@ public static Properties getPythonTestProperties() { Properties p = new Properties(); p.setProperty(ZEPPELIN_PYTHON, DEFAULT_ZEPPELIN_PYTHON); p.setProperty(MAX_RESULT, "1000"); + p.setProperty("zeppelin.python.useIPython", "false"); return p; } @@ -85,6 +86,7 @@ public void beforeTest() throws IOException { new LocalResourcePool("id"), new LinkedList(), out); + InterpreterContext.set(context); pythonInterpreter.open(); } diff --git a/python/src/test/resources/log4j.properties b/python/src/test/resources/log4j.properties new file mode 100644 index 00000000000..a8e2c44e6c0 --- /dev/null +++ b/python/src/test/resources/log4j.properties @@ -0,0 +1,31 @@ +# +# 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. +# + +# Direct log messages to stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c:%L - %m%n +#log4j.appender.stdout.layout.ConversionPattern= +#%5p [%t] (%F:%L) - %m%n +#%-4r [%t] %-5p %c %x - %m%n +# + +# Root logger option +log4j.rootLogger=INFO, stdout +log4j.logger.org.apache.zeppelin.python.IPythonInterpreter=DEBUG +log4j.logger.org.apache.zeppelin.python.IPythonClient=DEBUG diff --git a/spark/pom.xml b/spark/pom.xml index d35f9739f0d..36e4a6cbf4b 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -71,6 +71,32 @@ ${project.version} + + ${project.groupId} + zeppelin-python + ${project.version} + + + net.sf.py4j + py4j + + + + + + ${project.groupId} + zeppelin-python + ${project.version} + tests + test + + + net.sf.py4j + py4j + + + + org.slf4j slf4j-api @@ -95,12 +121,6 @@ provided - - com.google.guava - guava - ${guava.version} - - org.apache.maven @@ -355,6 +375,12 @@ **/SparkRInterpreterTest.java ${pyspark.test.exclude} + + + ../interpreter/spark/pyspark/pyspark.zip:../interpreter/spark/pyspark/py4j-${spark.py4j.version}-src.zip:../interpreter/lib/python + @@ -379,6 +405,19 @@ reference.conf + + + + + com.google + org.apache.zeppelin.com.google + + + + io.netty + org.apache.zeppelin.io.netty + + diff --git a/spark/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java b/spark/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java new file mode 100644 index 00000000000..f1b143541ea --- /dev/null +++ b/spark/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java @@ -0,0 +1,110 @@ +/* + * 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.spark; + +import org.apache.spark.SparkConf; +import org.apache.spark.api.java.JavaSparkContext; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.LazyOpenInterpreter; +import org.apache.zeppelin.interpreter.WrappedInterpreter; +import org.apache.zeppelin.python.IPythonInterpreter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Properties; + +/** + * PySparkInterpreter which use IPython underlying. + */ +public class IPySparkInterpreter extends IPythonInterpreter { + + private static final Logger LOGGER = LoggerFactory.getLogger(IPySparkInterpreter.class); + + private SparkInterpreter sparkInterpreter; + + public IPySparkInterpreter(Properties property) { + super(property); + } + + @Override + public void open() { + getProperty().setProperty("zeppelin.python", PySparkInterpreter.getPythonExec(property)); + sparkInterpreter = getSparkInterpreter(); + SparkConf conf = sparkInterpreter.getSparkContext().getConf(); + String additionalPythonPath = conf.get("spark.submit.pyFiles").replaceAll(",", ":") + + ":../interpreter/lib/python"; + setAdditionalPythonPath(additionalPythonPath); + setAdditionalPythonInitFile("python/zeppelin_ipyspark.py"); + super.open(); + } + + private SparkInterpreter getSparkInterpreter() { + LazyOpenInterpreter lazy = null; + SparkInterpreter spark = null; + Interpreter p = getInterpreterInTheSameSessionByClassName(SparkInterpreter.class.getName()); + + while (p instanceof WrappedInterpreter) { + if (p instanceof LazyOpenInterpreter) { + lazy = (LazyOpenInterpreter) p; + } + p = ((WrappedInterpreter) p).getInnerInterpreter(); + } + spark = (SparkInterpreter) p; + + if (lazy != null) { + lazy.open(); + } + return spark; + } + + @Override + public void cancel(InterpreterContext context) { + super.cancel(context); + sparkInterpreter.cancel(context); + } + + @Override + public void close() { + super.close(); + if (sparkInterpreter != null) { + sparkInterpreter.close(); + } + } + + @Override + public int getProgress(InterpreterContext context) { + return sparkInterpreter.getProgress(context); + } + + public boolean isSpark2() { + return sparkInterpreter.getSparkVersion().newerThanEquals(SparkVersion.SPARK_2_0_0); + } + + public JavaSparkContext getJavaSparkContext() { + return sparkInterpreter.getJavaSparkContext(); + } + + public Object getSQLContext() { + return sparkInterpreter.getSQLContext(); + } + + public Object getSparkSession() { + return sparkInterpreter.getSparkSession(); + } +} diff --git a/spark/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java b/spark/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java index 28910b2546b..e65df22beb1 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java +++ b/spark/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java @@ -76,6 +76,8 @@ public class PySparkInterpreter extends Interpreter implements ExecuteResultHand private static final int MAX_TIMEOUT_SEC = 10; private long pythonPid; + private IPySparkInterpreter iPySparkInterpreter; + public PySparkInterpreter(Properties property) { super(property); @@ -111,6 +113,37 @@ private void createPythonScript() { @Override public void open() { + // try IPySparkInterpreter first + iPySparkInterpreter = getIPySparkInterpreter(); + if (property.getProperty("zeppelin.spark.useIPython", "true").equals("true") && + iPySparkInterpreter.checkIPythonPrerequisite()) { + try { + iPySparkInterpreter.open(); + if (InterpreterContext.get() != null) { + // don't print it when it is in testing, just for easy output check in test. + InterpreterContext.get().out.write(("IPython is available, " + + "use IPython for PySparkInterpreter\n") + .getBytes()); + } + LOGGER.info("Use IPySparkInterpreter to replace PySparkInterpreter"); + return; + } catch (Exception e) { + LOGGER.warn("Fail to open IPySparkInterpreter", e); + } + } + iPySparkInterpreter = null; + + if (property.getProperty("zeppelin.spark.useIPython", "true").equals("true")) { + // don't print it when it is in testing, just for easy output check in test. + try { + InterpreterContext.get().out.write(("IPython is not available, " + + "use the native PySparkInterpreter\n") + .getBytes()); + } catch (IOException e) { + LOGGER.warn("Fail to write InterpreterOutput", e); + } + } + // Add matplotlib display hook InterpreterGroup intpGroup = getInterpreterGroup(); if (intpGroup != null && intpGroup.getInterpreterHookRegistry() != null) { @@ -190,9 +223,24 @@ private Map setupPySparkEnv() throws IOException{ } } + LOGGER.debug("PYTHONPATH: " + env.get("PYTHONPATH")); return env; } + // Run python shell + // Choose python in the order of + // PYSPARK_DRIVER_PYTHON > PYSPARK_PYTHON > zeppelin.pyspark.python + public static String getPythonExec(Properties properties) { + String pythonExec = properties.getProperty("zeppelin.pyspark.python", "python"); + if (System.getenv("PYSPARK_PYTHON") != null) { + pythonExec = System.getenv("PYSPARK_PYTHON"); + } + if (System.getenv("PYSPARK_DRIVER_PYTHON") != null) { + pythonExec = System.getenv("PYSPARK_DRIVER_PYTHON"); + } + return pythonExec; + } + private void createGatewayServerAndStartScript() { // create python script createPythonScript(); @@ -202,16 +250,7 @@ private void createGatewayServerAndStartScript() { gatewayServer = new GatewayServer(this, port); gatewayServer.start(); - // Run python shell - // Choose python in the order of - // PYSPARK_DRIVER_PYTHON > PYSPARK_PYTHON > zeppelin.pyspark.python - String pythonExec = getProperty("zeppelin.pyspark.python"); - if (System.getenv("PYSPARK_PYTHON") != null) { - pythonExec = System.getenv("PYSPARK_PYTHON"); - } - if (System.getenv("PYSPARK_DRIVER_PYTHON") != null) { - pythonExec = System.getenv("PYSPARK_DRIVER_PYTHON"); - } + String pythonExec = getPythonExec(property); CommandLine cmd = CommandLine.parse(pythonExec); cmd.addArgument(scriptPath, false); cmd.addArgument(Integer.toString(port), false); @@ -263,6 +302,10 @@ private int findRandomOpenPortOnAllLocalInterfaces() { @Override public void close() { + if (iPySparkInterpreter != null) { + iPySparkInterpreter.close(); + return; + } executor.getWatchdog().destroyProcess(); new File(scriptPath).delete(); gatewayServer.shutdown(); @@ -353,6 +396,10 @@ public InterpreterResult interpret(String st, InterpreterContext context) { + sparkInterpreter.getSparkVersion().toString() + " is not supported"); } + if (iPySparkInterpreter != null) { + return iPySparkInterpreter.interpret(st, context); + } + if (!pythonscriptRunning) { return new InterpreterResult(Code.ERROR, "python process not running" + outputStream.toString()); @@ -448,6 +495,10 @@ public void interrupt() throws IOException { @Override public void cancel(InterpreterContext context) { + if (iPySparkInterpreter != null) { + iPySparkInterpreter.cancel(context); + return; + } SparkInterpreter sparkInterpreter = getSparkInterpreter(); sparkInterpreter.cancel(context); try { @@ -464,6 +515,9 @@ public FormType getFormType() { @Override public int getProgress(InterpreterContext context) { + if (iPySparkInterpreter != null) { + return iPySparkInterpreter.getProgress(context); + } SparkInterpreter sparkInterpreter = getSparkInterpreter(); return sparkInterpreter.getProgress(context); } @@ -472,6 +526,9 @@ public int getProgress(InterpreterContext context) { @Override public List completion(String buf, int cursor, InterpreterContext interpreterContext) { + if (iPySparkInterpreter != null) { + return iPySparkInterpreter.completion(buf, cursor, interpreterContext); + } if (buf.length() < cursor) { cursor = buf.length(); } @@ -588,6 +645,21 @@ private SparkInterpreter getSparkInterpreter() { return spark; } + private IPySparkInterpreter getIPySparkInterpreter() { + LazyOpenInterpreter lazy = null; + IPySparkInterpreter iPySpark = null; + Interpreter p = getInterpreterInTheSameSessionByClassName(IPySparkInterpreter.class.getName()); + + while (p instanceof WrappedInterpreter) { + if (p instanceof LazyOpenInterpreter) { + lazy = (LazyOpenInterpreter) p; + } + p = ((WrappedInterpreter) p).getInnerInterpreter(); + } + iPySpark = (IPySparkInterpreter) p; + return iPySpark; + } + public SparkZeppelinContext getZeppelinContext() { SparkInterpreter sparkIntp = getSparkInterpreter(); if (sparkIntp != null) { diff --git a/spark/src/main/resources/interpreter-setting.json b/spark/src/main/resources/interpreter-setting.json index e96265f00a5..d646805da19 100644 --- a/spark/src/main/resources/interpreter-setting.json +++ b/spark/src/main/resources/interpreter-setting.json @@ -149,11 +149,28 @@ "defaultValue": "python", "description": "Python command to run pyspark with", "type": "string" + }, + "zeppelin.spark.useIPython": { + "envName": null, + "propertyName": "zeppelin.spark.useIPython", + "defaultValue": true, + "description": "whether use IPython when it is available", + "type": "checkbox" } }, "editor": { "language": "python", "editOnDblClick": false } + }, + { + "group": "spark", + "name": "ipyspark", + "className": "org.apache.zeppelin.spark.IPySparkInterpreter", + "properties": {}, + "editor": { + "language": "python", + "editOnDblClick": false + } } ] diff --git a/spark/src/main/resources/python/zeppelin_ipyspark.py b/spark/src/main/resources/python/zeppelin_ipyspark.py new file mode 100644 index 00000000000..324f48155ec --- /dev/null +++ b/spark/src/main/resources/python/zeppelin_ipyspark.py @@ -0,0 +1,53 @@ +# +# 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. +# + + +from py4j.java_gateway import java_import, JavaGateway, GatewayClient +from pyspark.conf import SparkConf +from pyspark.context import SparkContext + +# for back compatibility +from pyspark.sql import SQLContext + +# start JVM gateway +client = GatewayClient(port=${JVM_GATEWAY_PORT}) +gateway = JavaGateway(client, auto_convert=True) + +java_import(gateway.jvm, "org.apache.spark.SparkEnv") +java_import(gateway.jvm, "org.apache.spark.SparkConf") +java_import(gateway.jvm, "org.apache.spark.api.java.*") +java_import(gateway.jvm, "org.apache.spark.api.python.*") +java_import(gateway.jvm, "org.apache.spark.mllib.api.python.*") + +intp = gateway.entry_point +jsc = intp.getJavaSparkContext() + +java_import(gateway.jvm, "org.apache.spark.sql.*") +java_import(gateway.jvm, "org.apache.spark.sql.hive.*") +java_import(gateway.jvm, "scala.Tuple2") + +jconf = jsc.getConf() +conf = SparkConf(_jvm=gateway.jvm, _jconf=jconf) +sc = _zsc_ = SparkContext(jsc=jsc, gateway=gateway, conf=conf) + +if intp.isSpark2(): + from pyspark.sql import SparkSession + + spark = __zSpark__ = SparkSession(sc, intp.getSparkSession()) + sqlContext = sqlc = __zSqlc__ = __zSpark__._wrapped +else: + sqlContext = sqlc = __zSqlc__ = SQLContext(sparkContext=sc, sqlContext=intp.getSQLContext()) diff --git a/spark/src/main/sparkr-resources/interpreter-setting.json b/spark/src/main/sparkr-resources/interpreter-setting.json index d0fbd3ec2e2..300aff03150 100644 --- a/spark/src/main/sparkr-resources/interpreter-setting.json +++ b/spark/src/main/sparkr-resources/interpreter-setting.json @@ -189,5 +189,16 @@ "editor": { "language": "r" } + }, + + { + "group": "spark", + "name": "ipyspark", + "className": "org.apache.zeppelin.spark.IPySparkInterpreter", + "properties": {}, + "editor": { + "language": "python", + "editOnDblClick": false + } } ] diff --git a/spark/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java b/spark/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java new file mode 100644 index 00000000000..5a2e8849e78 --- /dev/null +++ b/spark/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java @@ -0,0 +1,203 @@ +/* + * 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.spark; + + +import com.google.common.io.Files; +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterContextRunner; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterOutput; +import org.apache.zeppelin.interpreter.InterpreterOutputListener; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResultMessage; +import org.apache.zeppelin.interpreter.InterpreterResultMessageOutput; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.apache.zeppelin.python.IPythonInterpreterTest; +import org.apache.zeppelin.resource.LocalResourcePool; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class IPySparkInterpreterTest { + + private IPySparkInterpreter iPySparkInterpreter; + private InterpreterGroup intpGroup; + + @Before + public void setup() { + Properties p = new Properties(); + p.setProperty("spark.master", "local[4]"); + p.setProperty("master", "local[4]"); + p.setProperty("spark.app.name", "Zeppelin Test"); + p.setProperty("zeppelin.spark.useHiveContext", "true"); + p.setProperty("zeppelin.spark.maxResult", "1000"); + p.setProperty("zeppelin.spark.importImplicit", "true"); + p.setProperty("zeppelin.pyspark.python", "python"); + p.setProperty("zeppelin.dep.localrepo", Files.createTempDir().getAbsolutePath()); + + intpGroup = new InterpreterGroup(); + intpGroup.put("session_1", new LinkedList()); + + SparkInterpreter sparkInterpreter = new SparkInterpreter(p); + intpGroup.get("session_1").add(sparkInterpreter); + sparkInterpreter.setInterpreterGroup(intpGroup); + sparkInterpreter.open(); + + iPySparkInterpreter = new IPySparkInterpreter(p); + intpGroup.get("session_1").add(iPySparkInterpreter); + iPySparkInterpreter.setInterpreterGroup(intpGroup); + iPySparkInterpreter.open(); + } + + + @After + public void tearDown() { + if (iPySparkInterpreter != null) { + iPySparkInterpreter.close(); + } + } + + @Test + public void testBasics() throws InterruptedException, IOException { + // all the ipython test should pass too. + IPythonInterpreterTest.testInterpreter(iPySparkInterpreter); + + // rdd + InterpreterContext context = getInterpreterContext(); + InterpreterResult result = iPySparkInterpreter.interpret("sc.range(1,10).sum()", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + List interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals("45", interpreterResultMessages.get(0).getData()); + + context = getInterpreterContext(); + result = iPySparkInterpreter.interpret("sc.version", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + // spark sql + context = getInterpreterContext(); + if (interpreterResultMessages.get(0).getData().startsWith("'1.") || + interpreterResultMessages.get(0).getData().startsWith("u'1.")) { + result = iPySparkInterpreter.interpret("df = sqlContext.createDataFrame([(1,'a'),(2,'b')])\ndf.show()", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals( + "+---+---+\n" + + "| _1| _2|\n" + + "+---+---+\n" + + "| 1| a|\n" + + "| 2| b|\n" + + "+---+---+\n\n", interpreterResultMessages.get(0).getData()); + } else { + result = iPySparkInterpreter.interpret("df = spark.createDataFrame([(1,'a'),(2,'b')])\ndf.show()", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals( + "+---+---+\n" + + "| _1| _2|\n" + + "+---+---+\n" + + "| 1| a|\n" + + "| 2| b|\n" + + "+---+---+\n\n", interpreterResultMessages.get(0).getData()); + } + + // cancel + final InterpreterContext context2 = getInterpreterContext(); + + Thread thread = new Thread(){ + @Override + public void run() { + InterpreterResult result = iPySparkInterpreter.interpret("import time\nsc.range(1,10).foreach(lambda x: time.sleep(1))", context2); + assertEquals(InterpreterResult.Code.ERROR, result.code()); + List interpreterResultMessages = null; + try { + interpreterResultMessages = context2.out.getInterpreterResultMessages(); + assertTrue(interpreterResultMessages.get(0).getData().contains("cancelled")); + } catch (IOException e) { + e.printStackTrace(); + } + } + }; + thread.start(); + + // sleep 1 second to wait for the spark job starts + Thread.sleep(1000); + iPySparkInterpreter.cancel(context); + thread.join(); + + // completions + List completions = iPySparkInterpreter.completion("sc.ran", 6, getInterpreterContext()); + assertEquals(1, completions.size()); + assertEquals("sc.range", completions.get(0).getValue()); + + // pyspark streaming + context = getInterpreterContext(); + result = iPySparkInterpreter.interpret( + "from pyspark.streaming import StreamingContext\n" + + "import time\n" + + "ssc = StreamingContext(sc, 1)\n" + + "rddQueue = []\n" + + "for i in range(5):\n" + + " rddQueue += [ssc.sparkContext.parallelize([j for j in range(1, 1001)], 10)]\n" + + "inputStream = ssc.queueStream(rddQueue)\n" + + "mappedStream = inputStream.map(lambda x: (x % 10, 1))\n" + + "reducedStream = mappedStream.reduceByKey(lambda a, b: a + b)\n" + + "reducedStream.pprint()\n" + + "ssc.start()\n" + + "time.sleep(6)\n" + + "ssc.stop(stopSparkContext=False, stopGraceFully=True)", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertTrue(interpreterResultMessages.get(0).getData().contains("(0, 100)")); + } + + private InterpreterContext getInterpreterContext() { + return new InterpreterContext( + "noteId", + "paragraphId", + "replName", + "paragraphTitle", + "paragraphText", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + null, + null, + null, + new InterpreterOutput(null)); + } +} diff --git a/spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterMatplotlibTest.java b/spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterMatplotlibTest.java index 7fe8b5e3a8e..c6eb1d401e0 100644 --- a/spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterMatplotlibTest.java +++ b/spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterMatplotlibTest.java @@ -89,6 +89,7 @@ private static Properties getPySparkTestProperties() throws IOException { p.setProperty("zeppelin.spark.importImplicit", "true"); p.setProperty("zeppelin.pyspark.python", "python"); p.setProperty("zeppelin.dep.localrepo", tmpDir.newFolder().getAbsolutePath()); + p.setProperty("zeppelin.spark.useIPython", "false"); return p; } @@ -110,6 +111,15 @@ public static int getSparkVersionNumber() { public static void setUp() throws Exception { intpGroup = new InterpreterGroup(); intpGroup.put("note", new LinkedList()); + context = new InterpreterContext("note", "id", null, "title", "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("id"), + new LinkedList(), + new InterpreterOutput(null)); + InterpreterContext.set(context); sparkInterpreter = new SparkInterpreter(getPySparkTestProperties()); intpGroup.get("note").add(sparkInterpreter); @@ -121,14 +131,6 @@ public static void setUp() throws Exception { pyspark.setInterpreterGroup(intpGroup); pyspark.open(); - context = new InterpreterContext("note", "id", null, "title", "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("id"), - new LinkedList(), - new InterpreterOutput(null)); } @AfterClass diff --git a/spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterTest.java b/spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterTest.java index ce0c86cf00f..ffdb4e8816e 100644 --- a/spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterTest.java +++ b/spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterTest.java @@ -59,6 +59,7 @@ private static Properties getPySparkTestProperties() throws IOException { p.setProperty("zeppelin.spark.importImplicit", "true"); p.setProperty("zeppelin.pyspark.python", "python"); p.setProperty("zeppelin.dep.localrepo", tmpDir.newFolder().getAbsolutePath()); + p.setProperty("zeppelin.spark.useIPython", "false"); return p; } @@ -81,6 +82,16 @@ public static void setUp() throws Exception { intpGroup = new InterpreterGroup(); intpGroup.put("note", new LinkedList()); + context = new InterpreterContext("note", "id", null, "title", "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("id"), + new LinkedList(), + new InterpreterOutput(null)); + InterpreterContext.set(context); + sparkInterpreter = new SparkInterpreter(getPySparkTestProperties()); intpGroup.get("note").add(sparkInterpreter); sparkInterpreter.setInterpreterGroup(intpGroup); @@ -91,14 +102,7 @@ public static void setUp() throws Exception { pySparkInterpreter.setInterpreterGroup(intpGroup); pySparkInterpreter.open(); - context = new InterpreterContext("note", "id", null, "title", "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("id"), - new LinkedList(), - new InterpreterOutput(null)); + } @AfterClass @@ -113,6 +117,22 @@ public void testBasicIntp() { assertEquals(InterpreterResult.Code.SUCCESS, pySparkInterpreter.interpret("a = 1\n", context).code()); } + + InterpreterResult result = pySparkInterpreter.interpret( + "from pyspark.streaming import StreamingContext\n" + + "import time\n" + + "ssc = StreamingContext(sc, 1)\n" + + "rddQueue = []\n" + + "for i in range(5):\n" + + " rddQueue += [ssc.sparkContext.parallelize([j for j in range(1, 1001)], 10)]\n" + + "inputStream = ssc.queueStream(rddQueue)\n" + + "mappedStream = inputStream.map(lambda x: (x % 10, 1))\n" + + "reducedStream = mappedStream.reduceByKey(lambda a, b: a + b)\n" + + "reducedStream.pprint()\n" + + "ssc.start()\n" + + "time.sleep(6)\n" + + "ssc.stop(stopSparkContext=False, stopGraceFully=True)", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); } @Test diff --git a/spark/src/test/resources/log4j.properties b/spark/src/test/resources/log4j.properties index b0d1067bc46..3ee61ab864b 100644 --- a/spark/src/test/resources/log4j.properties +++ b/spark/src/test/resources/log4j.properties @@ -45,3 +45,5 @@ log4j.logger.org.hibernate.type=ALL log4j.logger.org.apache.zeppelin.interpreter=DEBUG log4j.logger.org.apache.zeppelin.spark=DEBUG +log4j.logger.org.apache.zeppelin.python.IPythonInterpreter=DEBUG +log4j.logger.org.apache.zeppelin.python.IPythonClient=DEBUG diff --git a/testing/install_external_dependencies.sh b/testing/install_external_dependencies.sh index e88f63bc285..c5c06762dbc 100755 --- a/testing/install_external_dependencies.sh +++ b/testing/install_external_dependencies.sh @@ -44,5 +44,6 @@ if [[ -n "$PYTHON" ]] ; then conda update -q conda conda info -a conda config --add channels conda-forge - conda install -q matplotlib pandasql + conda install -q matplotlib pandasql ipython jupyter_client ipykernel matplotlib bokeh + pip install grpcio ggplot fi diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index 109099cfc7b..384b9d1c59d 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -214,11 +214,6 @@ ${jline.version} - - com.google.guava - guava - - junit junit diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/BaseZeppelinContext.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/BaseZeppelinContext.java index 6774531bb32..12376f0dd75 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/BaseZeppelinContext.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/BaseZeppelinContext.java @@ -123,6 +123,10 @@ public void setGui(GUI o) { this.gui = o; } + public GUI getGui() { + return gui; + } + private void restartInterpreter() { } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterOutput.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterOutput.java index c3d25c91b2c..d89dad0cde4 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterOutput.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterOutput.java @@ -23,6 +23,7 @@ import java.io.*; import java.net.URISyntaxException; import java.net.URL; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -115,6 +116,16 @@ public void onUpdate(InterpreterResultMessageOutput out) { }; } + public List getInterpreterResultMessages() throws IOException { + synchronized (resultMessageOutputs) { + List resultMessages = new ArrayList<>(); + for (InterpreterResultMessageOutput output : this.resultMessageOutputs) { + resultMessages.add(output.toInterpreterResultMessage()); + } + return resultMessages; + } + } + public InterpreterResultMessageOutput getCurrentOutput() { synchronized (resultMessageOutputs) { return currentOut; diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java index f501014bf23..38534682b34 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java @@ -513,6 +513,13 @@ protected Object jobRun() throws Throwable { List resultMessages = context.out.toInterpreterResultMessage(); resultMessages.addAll(result.message()); + for (InterpreterResultMessage msg : resultMessages) { + if (msg.getType() == InterpreterResult.Type.IMG) { + logger.debug("InterpreterResultMessage: IMAGE_DATA"); + } else { + logger.debug("InterpreterResultMessage: " + msg.toString()); + } + } // put result into resource pool if (resultMessages.size() > 0) { int lastMessageIndex = resultMessages.size() - 1; diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/util/InterpreterOutputStream.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/util/InterpreterOutputStream.java index 6f2a0b4059a..258a65d0da1 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/util/InterpreterOutputStream.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/util/InterpreterOutputStream.java @@ -29,7 +29,7 @@ */ public class InterpreterOutputStream extends LogOutputStream { private Logger logger; - InterpreterOutput interpreterOutput; + volatile InterpreterOutput interpreterOutput; boolean ignoreLeadingNewLinesFromScalaReporter = false; public InterpreterOutputStream(Logger logger) { diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index 2390ef1be97..914ec51abc7 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -52,6 +52,7 @@ com.google.guava guava + 15.0 diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index d73f7bf5907..e69fba4dadd 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -87,7 +87,7 @@ ${project.groupId} zeppelin-zengine - ${project.version} + 0.8.0-SNAPSHOT com.fasterxml.jackson.core @@ -275,6 +275,10 @@ org.apache.commons commons-lang3 + + com.google.guava + guava + diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/WebDriverManager.java b/zeppelin-server/src/test/java/org/apache/zeppelin/WebDriverManager.java index da34e7299a6..1405cb29044 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/WebDriverManager.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/WebDriverManager.java @@ -141,6 +141,7 @@ public Boolean apply(WebDriver d) { fail(); } + driver.manage().window().maximize(); return driver; } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/InterpreterModeActionsIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/InterpreterModeActionsIT.java index 9bfeae01846..7f8765f00d8 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/InterpreterModeActionsIT.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/InterpreterModeActionsIT.java @@ -72,7 +72,7 @@ public class InterpreterModeActionsIT extends AbstractZeppelinIT { static String interpreterOptionPath = ""; static String originalInterpreterOption = ""; - static String cmdPsPython = "ps aux | grep 'zeppelin_python-' | grep -v 'grep' | wc -l"; + static String cmdPsPython = "ps aux | grep 'zeppelin_ipython' | grep -v 'grep' | wc -l"; static String cmdPsInterpreter = "ps aux | grep 'zeppelin/interpreter/python/*' |" + " sed -E '/grep|local-repo/d' | wc -l"; @@ -145,19 +145,19 @@ private void authenticationUser(String userName, String password) { } private void logoutUser(String userName) throws URISyntaxException { - pollingWait(By.xpath("//div[contains(@class, 'navbar-collapse')]//li[contains(.,'" + - userName + "')]"), MAX_BROWSER_TIMEOUT_SEC).click(); - pollingWait(By.xpath("//div[contains(@class, 'navbar-collapse')]//li[contains(.,'" + - userName + "')]//a[@ng-click='navbar.logout()']"), MAX_BROWSER_TIMEOUT_SEC).click(); - - By locator = By.xpath("//*[@id='loginModal']//div[contains(@class, 'modal-header')]/button"); - WebElement element = (new WebDriverWait(driver, MAX_BROWSER_TIMEOUT_SEC)) - .until(ExpectedConditions.visibilityOfElementLocated(locator)); - if (element.isDisplayed()) { + ZeppelinITUtils.sleep(500, false); + driver.findElement(By.xpath("//div[contains(@class, 'navbar-collapse')]//li[contains(.,'" + + userName + "')]")).click(); + ZeppelinITUtils.sleep(500, false); + driver.findElement(By.xpath("//div[contains(@class, 'navbar-collapse')]//li[contains(.,'" + + userName + "')]//a[@ng-click='navbar.logout()']")).click(); + ZeppelinITUtils.sleep(2000, false); + if (driver.findElement(By.xpath("//*[@id='loginModal']//div[contains(@class, 'modal-header')]/button")) + .isDisplayed()) { driver.findElement(By.xpath("//*[@id='loginModal']//div[contains(@class, 'modal-header')]/button")).click(); } driver.get(new URI(driver.getCurrentUrl()).resolve("/#/").toString()); - ZeppelinITUtils.sleep(1000, false); + ZeppelinITUtils.sleep(500, false); } private void setPythonParagraph(int num, String text) { @@ -199,7 +199,6 @@ public void testGloballyAction() throws Exception { "//div[@class='modal-dialog']//div[@class='bootstrap-dialog-footer-buttons']//button[contains(., 'OK')]")); clickAndWait(By.xpath("//a[@class='navbar-brand navbar-title'][contains(@href, '#/')]")); interpreterModeActionsIT.logoutUser("admin"); - //step 2: (user1) login, create a new note, run two paragraph with 'python', check result, check process, logout //paragraph: Check if the result is 'user1' in the second paragraph //System: Check if the number of python interpreter process is '1' 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 9b651c1f16f..8afdb9bff9f 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 @@ -138,7 +138,7 @@ public void testPySpark() throws Exception { WebElement paragraph1Result = driver.findElement(By.xpath( getParagraphXPath(1) + "//div[contains(@id,\"_text\")]")); collector.checkThat("Paragraph from SparkParagraphIT of testPySpark result: ", - paragraph1Result.getText().toString(), CoreMatchers.equalTo("test loop 0\ntest loop 1\ntest loop 2") + paragraph1Result.getText().toString(), CoreMatchers.containsString("test loop 0\ntest loop 1\ntest loop 2") ); // the last statement's evaluation result is printed 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 ae0911cc424..a7907db286b 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 @@ -211,6 +211,7 @@ private static void start(boolean withAuth) throws Exception { // set spark home for pyspark sparkProperties.put("spark.home", new InterpreterProperty("spark.home", getSparkHome(), InterpreterPropertyType.TEXTAREA.getValue())); + sparkProperties.put("zeppelin.spark.useIPython", new InterpreterProperty("zeppelin.spark.useIPython", "false", InterpreterPropertyType.TEXTAREA.getValue())); sparkIntpSetting.setProperties(sparkProperties); pySpark = true; @@ -233,6 +234,8 @@ private static void start(boolean withAuth) throws Exception { new InterpreterProperty("spark.home", sparkHome, InterpreterPropertyType.TEXTAREA.getValue())); sparkProperties.put("zeppelin.spark.useHiveContext", new InterpreterProperty("zeppelin.spark.useHiveContext", false, InterpreterPropertyType.CHECKBOX.getValue())); + sparkProperties.put("zeppelin.spark.useIPython", new InterpreterProperty("zeppelin.spark.useIPython", "false", InterpreterPropertyType.TEXTAREA.getValue())); + pySpark = true; sparkR = true; } 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 e1700b2fcd4..3e464498ad0 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 @@ -271,7 +271,8 @@ public void pySparkTest() throws IOException { note.run(p.getId()); waitForFinish(p); assertEquals(Status.FINISHED, p.getStatus()); - assertEquals("[Row(len=u'3')]\n", p.getResult().message().get(0).getData()); + assertTrue("[Row(len=u'3')]\n".equals(p.getResult().message().get(0).getData()) || + "[Row(len='3')]\n".equals(p.getResult().message().get(0).getData())); // test exception p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -321,7 +322,8 @@ public void pySparkTest() throws IOException { note.run(p.getId()); waitForFinish(p); assertEquals(Status.FINISHED, p.getStatus()); - assertEquals("[Row(len=u'3')]\n", p.getResult().message().get(0).getData()); + assertTrue("[Row(len=u'3')]\n".equals(p.getResult().message().get(0).getData()) || + "[Row(len='3')]\n".equals(p.getResult().message().get(0).getData())); } } ZeppelinServer.notebook.removeNote(note.getId(), anonymous); diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index 337b71045ef..b3d5c63b4fc 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -57,7 +57,7 @@ ${project.groupId} zeppelin-interpreter - ${project.version} + 0.8.0-SNAPSHOT @@ -171,6 +171,7 @@ com.google.guava guava + 15.0 @@ -262,6 +263,12 @@ truth ${google.truth.version} test + + + com.google.guava + guava + + @@ -365,6 +372,10 @@ xerces xercesImpl + + com.google.guava + guava + From 8d4902e717ba2932ddb0f7110f2a669811f5d1af Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 23 May 2017 15:52:15 +0800 Subject: [PATCH 027/492] [ZEPPELIN-2627] Interpreter refactor ### What is this PR for? I didn't intended to make such large change at the beginning, but found many things are coupled together that I have to make such large change. Several suggestions for you how to review and read it. * I move the interpreter package from zeppelin-zengine to zeppelin-interpreter, this is needed for this refactoring. * The overall change is the same as I described in the design doc. I would suggest you to read the unit test first. These unit test is very readable and easy to understand what the code is doing now. `InterpreterFactoryTest`, `InterpreterGroupTest`, `InterpreterSettingTest`, `InterpreterSettingManagerTest`, `RemoteInterpreterTest`. * Remove the referent counting logic. Now I will kill the interpreter process as long as all the sessions in the same interpreter group is closed. (I plan to add another kind of policy for the interpreter process lifecycle control, ZEPPELIN-2197) * The `RemoteFunction` I introduced is for reducing code duplicates when we use RPC. * The changes in Job.java and RemoteScheduler is for fixing the race issue bug. This bug cause the flaky test we see often in `ZeppelinSparkClusterTest.pySparkTest` ### What type of PR is it? [Bug Fix | Refactoring] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-2627 ### How should this be tested? Unit test is added ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2422 from zjffdu/interpreter_refactor and squashes the following commits: 4724c98 [Jeff Zhang] [ZEPPELIN-2627] Interpreter Component Refactoring --- .travis.yml | 1 + .../zeppelin/helium/ZeppelinDevServer.java | 8 +- zeppelin-interpreter/pom.xml | 22 + .../zeppelin/conf/ZeppelinConfiguration.java | 20 +- .../zeppelin/interpreter/Interpreter.java | 1 - .../interpreter/InterpreterFactory.java | 115 ++ .../interpreter/InterpreterGroup.java | 272 ++-- .../zeppelin/interpreter/InterpreterInfo.java | 0 .../interpreter/InterpreterInfoSaving.java | 101 ++ .../interpreter/InterpreterProperty.java | 1 + .../interpreter/InterpreterRunner.java | 11 + .../interpreter/InterpreterSetting.java | 911 +++++++++++++ .../InterpreterSettingManager.java | 886 +++++++++++++ .../install/InstallInterpreter.java | 4 +- .../remote/RemoteAngularObjectRegistry.java | 76 +- .../interpreter/remote/RemoteInterpreter.java | 371 ++++++ .../remote/RemoteInterpreterEventPoller.java | 279 ++-- .../RemoteInterpreterManagedProcess.java | 13 + .../remote/RemoteInterpreterProcess.java | 154 +-- .../RemoteInterpreterRunningProcess.java | 0 .../remote/RemoteInterpreterServer.java | 58 +- .../zeppelin/resource/ResourcePoolUtils.java | 138 -- .../org/apache/zeppelin/scheduler/Job.java | 11 +- .../zeppelin/scheduler/RemoteScheduler.java | 134 +- .../zeppelin/scheduler/SchedulerFactory.java | 49 +- .../zeppelin/tabledata/TableDataProxy.java | 1 - .../org/apache/zeppelin/util/IdHashes.java | 76 ++ .../java/org/apache/zeppelin/util/Util.java | 2 +- .../interpreter/AbstractInterpreterTest.java | 74 ++ .../interpreter/DoubleEchoInterpreter.java | 40 +- .../interpreter/DummyInterpreter.java | 43 - .../zeppelin/interpreter/EchoInterpreter.java | 53 +- .../interpreter/InterpreterFactoryTest.java | 66 + .../interpreter/InterpreterGroupTest.java | 90 ++ .../InterpreterOutputChangeWatcherTest.java | 11 +- .../InterpreterSettingManagerTest.java | 270 ++++ .../interpreter/InterpreterSettingTest.java | 411 ++++++ .../zeppelin/interpreter/InterpreterTest.java | 7 +- .../interpreter/SleepInterpreter.java | 60 + .../install/InstallInterpreterTest.java | 0 .../interpreter/mock/MockInterpreter1.java | 15 +- .../interpreter/mock/MockInterpreter11.java | 20 +- .../interpreter/mock/MockInterpreter2.java | 10 +- .../remote/AppendOutputRunnerTest.java | 30 +- .../remote/RemoteAngularObjectTest.java | 89 +- .../RemoteInterpreterEventPollerTest.java | 0 .../RemoteInterpreterOutputTestStream.java | 67 +- .../remote/RemoteInterpreterTest.java | 520 ++++++++ .../remote/RemoteInterpreterUtilsTest.java | 5 +- .../mock/GetEnvPropertyInterpreter.java | 12 +- .../remote/mock/MockInterpreterA.java | 7 +- .../remote/mock/MockInterpreterAngular.java | 9 +- .../remote/mock/MockInterpreterB.java | 14 +- .../mock/MockInterpreterOutputStream.java | 5 +- .../mock/MockInterpreterResourcePool.java | 11 +- .../scheduler/RemoteSchedulerTest.java | 129 +- .../src/test/resources/conf/interpreter.json | 115 ++ .../interpreter/test/interpreter-setting.json | 42 + .../src/test/resources/log4j.properties | 4 +- .../zeppelin/rest/InterpreterRestApi.java | 4 +- .../zeppelin/server/ZeppelinServer.java | 35 +- .../zeppelin/socket/NotebookServer.java | 32 +- .../zeppelin/rest/AbstractTestRestApi.java | 7 +- .../zeppelin/rest/InterpreterRestApiTest.java | 6 +- .../zeppelin/socket/NotebookServerTest.java | 13 +- .../src/test/resources/log4j.properties | 1 - zeppelin-zengine/pom.xml | 13 +- .../org/apache/zeppelin/helium/Helium.java | 51 +- .../helium/HeliumApplicationFactory.java | 120 +- .../interpreter/InterpreterFactory.java | 423 ------ .../interpreter/InterpreterGroupFactory.java | 26 - .../interpreter/InterpreterSetting.java | 459 ------- .../InterpreterSettingManager.java | 1136 ----------------- .../interpreter/remote/RemoteInterpreter.java | 597 --------- .../zeppelin/notebook/ApplicationState.java | 1 - .../org/apache/zeppelin/notebook/Note.java | 12 +- .../apache/zeppelin/notebook/Notebook.java | 23 +- .../apache/zeppelin/notebook/Paragraph.java | 21 +- .../helium/HeliumApplicationFactoryTest.java | 79 +- .../apache/zeppelin/helium/HeliumTest.java | 8 +- .../interpreter/InterpreterFactoryTest.java | 497 -------- .../interpreter/InterpreterSettingTest.java | 327 ----- .../remote/RemoteInterpreterProcessTest.java | 131 -- .../remote/RemoteInterpreterTest.java | 975 -------------- .../notebook/NoteInterpreterLoaderTest.java | 243 ---- .../zeppelin/notebook/NotebookTest.java | 181 ++- .../zeppelin/notebook/ParagraphTest.java | 9 +- .../notebook/repo/NotebookRepoSyncTest.java | 74 +- .../notebook/repo/VFSNotebookRepoTest.java | 55 +- .../resource/DistributedResourcePoolTest.java | 96 +- .../src/test/resources/conf/interpreter.json | 1 + .../interpreter/mock/interpreter-setting.json | 12 - .../mock1/interpreter-setting.json | 19 + .../mock2/interpreter-setting.json | 19 + .../interpreter-setting.json | 19 + .../src/test/resources/log4j.properties | 3 +- 96 files changed, 5252 insertions(+), 6420 deletions(-) rename {zeppelin-zengine => zeppelin-interpreter}/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java (99%) create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java rename {zeppelin-zengine => zeppelin-interpreter}/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfo.java (100%) create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java rename {zeppelin-zengine => zeppelin-interpreter}/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java (98%) rename {zeppelin-zengine => zeppelin-interpreter}/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java (71%) create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java rename {zeppelin-zengine => zeppelin-interpreter}/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java (93%) rename {zeppelin-zengine => zeppelin-interpreter}/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java (100%) delete mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/ResourcePoolUtils.java create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/IdHashes.java rename {zeppelin-zengine => zeppelin-interpreter}/src/main/java/org/apache/zeppelin/util/Util.java (98%) create mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java rename zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java => zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DoubleEchoInterpreter.java (58%) delete mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DummyInterpreter.java rename zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java => zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/EchoInterpreter.java (50%) create mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java create mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterGroupTest.java create mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java create mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java create mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/SleepInterpreter.java rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/install/InstallInterpreterTest.java (100%) rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java (96%) rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java (91%) rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java (100%) rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java (97%) rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java (78%) rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPollerTest.java (100%) rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java (79%) create mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java (94%) rename zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterEnv.java => zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/GetEnvPropertyInterpreter.java (83%) rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java (97%) rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java (98%) rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java (89%) rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java (91%) rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java (95%) rename {zeppelin-zengine => zeppelin-interpreter}/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java (77%) create mode 100644 zeppelin-interpreter/src/test/resources/conf/interpreter.json create mode 100644 zeppelin-interpreter/src/test/resources/interpreter/test/interpreter-setting.json delete mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java delete mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroupFactory.java delete mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java delete mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java delete mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java delete mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java delete mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java delete mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessTest.java delete mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java delete mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteInterpreterLoaderTest.java create mode 100644 zeppelin-zengine/src/test/resources/conf/interpreter.json delete mode 100644 zeppelin-zengine/src/test/resources/interpreter/mock/interpreter-setting.json create mode 100644 zeppelin-zengine/src/test/resources/interpreter/mock1/interpreter-setting.json create mode 100644 zeppelin-zengine/src/test/resources/interpreter/mock2/interpreter-setting.json create mode 100644 zeppelin-zengine/src/test/resources/interpreter/mock_resource_pool/interpreter-setting.json diff --git a/.travis.yml b/.travis.yml index 97ca60a853e..64ea5596803 100644 --- a/.travis.yml +++ b/.travis.yml @@ -162,6 +162,7 @@ after_success: after_failure: - echo "Travis exited with ${TRAVIS_TEST_RESULT}" - find . -name rat.txt | xargs cat + - cat logs/* - cat zeppelin-distribution/target/zeppelin-*-SNAPSHOT/zeppelin-*-SNAPSHOT/logs/zeppelin*.log - cat zeppelin-distribution/target/zeppelin-*-SNAPSHOT/zeppelin-*-SNAPSHOT/logs/zeppelin*.out - cat zeppelin-web/npm-debug.log diff --git a/helium-dev/src/main/java/org/apache/zeppelin/helium/ZeppelinDevServer.java b/helium-dev/src/main/java/org/apache/zeppelin/helium/ZeppelinDevServer.java index 21ce283abba..24844693c5f 100644 --- a/helium-dev/src/main/java/org/apache/zeppelin/helium/ZeppelinDevServer.java +++ b/helium-dev/src/main/java/org/apache/zeppelin/helium/ZeppelinDevServer.java @@ -43,13 +43,13 @@ public ZeppelinDevServer(int port) throws TException { } @Override - protected Interpreter getInterpreter(String sessionKey, String className) throws TException { + protected Interpreter getInterpreter(String sessionId, String className) throws TException { synchronized (this) { InterpreterGroup interpreterGroup = getInterpreterGroup(); if (interpreterGroup == null || interpreterGroup.isEmpty()) { createInterpreter( "dev", - sessionKey, + sessionId, DevInterpreter.class.getName(), new HashMap(), "anonymous"); @@ -57,11 +57,11 @@ protected Interpreter getInterpreter(String sessionKey, String className) throws } } - Interpreter intp = super.getInterpreter(sessionKey, className); + Interpreter intp = super.getInterpreter(sessionId, className); interpreter = (DevInterpreter) ( ((LazyOpenInterpreter) intp).getInnerInterpreter()); interpreter.setInterpreterEvent(this); - return super.getInterpreter(sessionKey, className); + return super.getInterpreter(sessionId, className); } @Override diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index 384b9d1c59d..d8f9c43931d 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -51,6 +51,11 @@ + + commons-configuration + commons-configuration + + org.apache.thrift libthrift @@ -226,4 +231,21 @@ test + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + + test-jar + + + + + + diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java similarity index 99% rename from zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java rename to zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java index f00fe9346f3..03cc0699552 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java @@ -17,22 +17,21 @@ package org.apache.zeppelin.conf; -import java.io.File; -import java.net.URL; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.XMLConfiguration; import org.apache.commons.configuration.tree.ConfigurationNode; import org.apache.commons.lang.StringUtils; -import org.apache.zeppelin.notebook.repo.GitNotebookRepo; import org.apache.zeppelin.util.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; +import java.net.URL; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * Zeppelin configuration. * @@ -528,7 +527,7 @@ public Map dumpConfigurations(ZeppelinConfiguration conf, ConfigurationKeyPredicate predicate) { Map configurations = new HashMap<>(); - for (ZeppelinConfiguration.ConfVars v : ZeppelinConfiguration.ConfVars.values()) { + for (ConfVars v : ConfVars.values()) { String key = v.getVarName(); if (!predicate.apply(key)) { @@ -653,7 +652,8 @@ public static enum ConfVars { ZEPPELIN_NOTEBOOK_MONGO_COLLECTION("zeppelin.notebook.mongo.collection", "notes"), ZEPPELIN_NOTEBOOK_MONGO_URI("zeppelin.notebook.mongo.uri", "mongodb://localhost"), ZEPPELIN_NOTEBOOK_MONGO_AUTOIMPORT("zeppelin.notebook.mongo.autoimport", false), - ZEPPELIN_NOTEBOOK_STORAGE("zeppelin.notebook.storage", GitNotebookRepo.class.getName()), + ZEPPELIN_NOTEBOOK_STORAGE("zeppelin.notebook.storage", + "org.apache.zeppelin.notebook.repo.GitNotebookRepo"), ZEPPELIN_NOTEBOOK_ONE_WAY_SYNC("zeppelin.notebook.one.way.sync", false), // whether by default note is public or private ZEPPELIN_NOTEBOOK_PUBLIC("zeppelin.notebook.public", true), diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java index 74506dd402d..05599a01a35 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java @@ -149,7 +149,6 @@ public Scheduler getScheduler() { @ZeppelinApi public Interpreter(Properties property) { - logger.debug("Properties: {}", property); this.property = property; } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java new file mode 100644 index 00000000000..a2fdb045b9a --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java @@ -0,0 +1,115 @@ +/* + * 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.interpreter; + +import com.google.common.base.Preconditions; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonatype.aether.RepositoryException; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * //TODO(zjffdu) considering to move to InterpreterSettingManager + * + * Manage interpreters. + */ +public class InterpreterFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterFactory.class); + + private final InterpreterSettingManager interpreterSettingManager; + + public InterpreterFactory(InterpreterSettingManager interpreterSettingManager) { + this.interpreterSettingManager = interpreterSettingManager; + } + + private InterpreterSetting getInterpreterSettingByGroup(List settings, + String group) { + + Preconditions.checkNotNull(group, "group should be not null"); + for (InterpreterSetting setting : settings) { + if (group.equals(setting.getName())) { + return setting; + } + } + return null; + } + + public Interpreter getInterpreter(String user, String noteId, String replName) { + List settings = interpreterSettingManager.getInterpreterSettings(noteId); + InterpreterSetting setting; + Interpreter interpreter; + + if (settings == null || settings.size() == 0) { + LOGGER.error("No interpreter is binded to this note: " + noteId); + return null; + } + + if (StringUtils.isBlank(replName)) { + // Get the default interpreter of the first interpreter binding + InterpreterSetting defaultSetting = settings.get(0); + return defaultSetting.getDefaultInterpreter(user, noteId); + } + + String[] replNameSplit = replName.split("\\."); + if (replNameSplit.length == 2) { + String group = replNameSplit[0]; + String name = replNameSplit[1]; + setting = getInterpreterSettingByGroup(settings, group); + if (null != setting) { + interpreter = setting.getInterpreter(user, noteId, name); + if (null != interpreter) { + return interpreter; + } + } + throw new InterpreterException(replName + " interpreter not found"); + + } else { + // first assume replName is 'name' of interpreter. ('groupName' is ommitted) + // search 'name' from first (default) interpreter group + // TODO(jl): Handle with noteId to support defaultInterpreter per note. + setting = settings.get(0); + interpreter = setting.getInterpreter(user, noteId, replName); + + if (null != interpreter) { + return interpreter; + } + + // next, assume replName is 'group' of interpreter ('name' is ommitted) + // search interpreter group and return first interpreter. + setting = getInterpreterSettingByGroup(settings, replName); + + if (null != setting) { + return setting.getDefaultInterpreter(user, noteId); + } + + // Support the legacy way to use it + for (InterpreterSetting s : settings) { + if (s.getGroup().equals(replName)) { + return setting.getDefaultInterpreter(user, noteId); + } + } + } + //TODO(zjffdu) throw InterpreterException instead of return null + return null; + } +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroup.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroup.java index 5cbab6bdd9a..ae7ad3a9e47 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroup.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroup.java @@ -17,14 +17,7 @@ package org.apache.zeppelin.interpreter; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Random; -import java.util.concurrent.ConcurrentHashMap; - +import com.google.common.annotations.VisibleForTesting; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; import org.apache.zeppelin.resource.ResourcePool; @@ -33,91 +26,93 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + /** - * InterpreterGroup is list of interpreters in the same interpreter group. - * For example spark, pyspark, sql interpreters are in the same 'spark' group - * and InterpreterGroup will have reference to these all interpreters. + * InterpreterGroup is collections of interpreter sessions. + * One session could include multiple interpreters. + * For example spark, pyspark, sql interpreters are in the same 'spark' interpreter session. * * Remember, list of interpreters are dedicated to a session. Session could be shared across user * or notes, so the sessionId could be user or noteId or their combination. - * So InterpreterGroup internally manages map of [interpreterSessionKey(noteId, user, or + * So InterpreterGroup internally manages map of [sessionId(noteId, user, or * their combination), list of interpreters] * - * A InterpreterGroup runs on interpreter process. - * And unit of interpreter instantiate, restart, bind, unbind. + * A InterpreterGroup runs both in zeppelin server process and interpreter process. */ -public class InterpreterGroup extends ConcurrentHashMap> { - String id; +public class InterpreterGroup { private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterGroup.class); - AngularObjectRegistry angularObjectRegistry; - InterpreterHookRegistry hookRegistry; - RemoteInterpreterProcess remoteInterpreterProcess; // attached remote interpreter process - ResourcePool resourcePool; - boolean angularRegistryPushed = false; - - // map [notebook session, Interpreters in the group], to support per note session interpreters - //Map> interpreters = new ConcurrentHashMap>(); - - private static final Map allInterpreterGroups = - new ConcurrentHashMap<>(); - - public static InterpreterGroup getByInterpreterGroupId(String id) { - return allInterpreterGroups.get(id); - } + private String id; + // sessionId --> interpreters + private Map> sessions = new ConcurrentHashMap(); + private InterpreterSetting interpreterSetting; + private AngularObjectRegistry angularObjectRegistry; + private InterpreterHookRegistry hookRegistry; + private RemoteInterpreterProcess remoteInterpreterProcess; // attached remote interpreter process + private ResourcePool resourcePool; + private boolean angularRegistryPushed = false; - public static Collection getAll() { - return new LinkedList(allInterpreterGroups.values()); + /** + * Create InterpreterGroup with given id, used in InterpreterProcess + * @param id + */ + public InterpreterGroup(String id) { + this.id = id; } /** - * Create InterpreterGroup with given id + * Create InterpreterGroup with given id and interpreterSetting, used in ZeppelinServer * @param id + * @param interpreterSetting */ - public InterpreterGroup(String id) { + InterpreterGroup(String id, InterpreterSetting interpreterSetting) { this.id = id; - allInterpreterGroups.put(id, this); + this.interpreterSetting = interpreterSetting; } /** * Create InterpreterGroup with autogenerated id */ + @VisibleForTesting public InterpreterGroup() { - getId(); - allInterpreterGroups.put(id, this); + this.id = generateId(); } private static String generateId() { - return "InterpreterGroup_" + System.currentTimeMillis() + "_" - + new Random().nextInt(); + return "InterpreterGroup_" + System.currentTimeMillis() + "_" + new Random().nextInt(); } public String getId() { - synchronized (this) { - if (id == null) { - id = generateId(); - } - return id; - } + return this.id; } - /** - * Get combined property of all interpreters in this group - * @return - */ - public Properties getProperty() { - Properties p = new Properties(); + //TODO(zjffdu) change it to getSession. For now just keep this method to reduce code change + public synchronized List get(String sessionId) { + return sessions.get(sessionId); + } - for (List intpGroupForASession : this.values()) { - for (Interpreter intp : intpGroupForASession) { - p.putAll(intp.getProperty()); - } - // it's okay to break here while every List will have the same property set - break; + //TODO(zjffdu) change it to addSession. For now just keep this method to reduce code change + public synchronized void put(String sessionId, List interpreters) { + this.sessions.put(sessionId, interpreters); + } + + public synchronized void addInterpreterToSession(Interpreter interpreter, String sessionId) { + LOGGER.debug("Add Interpreter {} to session {}", interpreter.getClassName(), sessionId); + List interpreters = get(sessionId); + if (interpreters == null) { + interpreters = new ArrayList<>(); } - return p; + interpreters.add(interpreter); + put(sessionId, interpreters); + } + + //TODO(zjffdu) rename it to a more proper name. + //For now just keep this method to reduce code change + public Collection> values() { + return sessions.values(); } public AngularObjectRegistry getAngularObjectRegistry() { @@ -136,128 +131,69 @@ public void setInterpreterHookRegistry(InterpreterHookRegistry hookRegistry) { this.hookRegistry = hookRegistry; } - public RemoteInterpreterProcess getRemoteInterpreterProcess() { + public InterpreterSetting getInterpreterSetting() { + return interpreterSetting; + } + + public synchronized RemoteInterpreterProcess getOrCreateInterpreterProcess() { + if (remoteInterpreterProcess == null) { + LOGGER.info("Create InterperterProcess for InterpreterGroup: " + getId()); + remoteInterpreterProcess = interpreterSetting.createInterpreterProcess(); + } return remoteInterpreterProcess; } - public void setRemoteInterpreterProcess(RemoteInterpreterProcess remoteInterpreterProcess) { - this.remoteInterpreterProcess = remoteInterpreterProcess; + public RemoteInterpreterProcess getRemoteInterpreterProcess() { + return remoteInterpreterProcess; } + /** * Close all interpreter instances in this group */ - public void close() { - LOGGER.info("Close interpreter group " + getId()); - List intpToClose = new LinkedList<>(); - for (List intpGroupForSession : this.values()) { - intpToClose.addAll(intpGroupForSession); + public synchronized void close() { + LOGGER.info("Close InterpreterGroup: " + id); + for (String sessionId : sessions.keySet()) { + close(sessionId); } - close(intpToClose); - - // make sure remote interpreter process terminates - if (remoteInterpreterProcess != null) { - while (remoteInterpreterProcess.referenceCount() > 0) { - remoteInterpreterProcess.dereference(); - } - remoteInterpreterProcess = null; - } - allInterpreterGroups.remove(id); } /** - * Close all interpreter instances in this group for the session + * Close all interpreter instances in this session * @param sessionId */ - public void close(String sessionId) { - LOGGER.info("Close interpreter group " + getId() + " for session: " + sessionId); - final List intpForSession = this.get(sessionId); - - close(intpForSession); - } - - private void close(final Collection intpToClose) { - close(null, null, null, intpToClose); + public synchronized void close(String sessionId) { + LOGGER.info("Close Session: " + sessionId); + close(sessions.remove(sessionId)); + //TODO(zjffdu) whether close InterpreterGroup if there's no session left in Zeppelin Server + if (sessions.isEmpty() && interpreterSetting != null) { + LOGGER.info("Remove this InterpreterGroup {} as all the sessions are closed", id); + interpreterSetting.removeInterpreterGroup(id); + if (remoteInterpreterProcess != null) { + LOGGER.info("Kill RemoteInterpreterProcess"); + remoteInterpreterProcess.stop(); + remoteInterpreterProcess = null; + } + } } - public void close(final Map interpreterGroupRef, - final String processKey, final String sessionKey) { - LOGGER.info("Close interpreter group " + getId() + " for session: " + sessionKey); - close(interpreterGroupRef, processKey, sessionKey, this.get(sessionKey)); + public int getSessionNum() { + return sessions.size(); } - private void close(final Map interpreterGroupRef, - final String processKey, final String sessionKey, final Collection intpToClose) { - if (intpToClose == null) { + private void close(Collection interpreters) { + if (interpreters == null) { return; } - Thread t = new Thread() { - public void run() { - for (Interpreter interpreter : intpToClose) { - Scheduler scheduler = interpreter.getScheduler(); - interpreter.close(); - - if (null != scheduler) { - SchedulerFactory.singleton().removeScheduler(scheduler.getName()); - } - } - - if (remoteInterpreterProcess != null) { - //TODO(jl): Because interpreter.close() runs as a seprate thread, we cannot guarantee - // refernceCount is a proper value. And as the same reason, we must not call - // remoteInterpreterProcess.dereference twice - this method also be called by - // interpreter.close(). - - // remoteInterpreterProcess.dereference(); - if (remoteInterpreterProcess.referenceCount() <= 0) { - remoteInterpreterProcess = null; - allInterpreterGroups.remove(id); - } - } - - // TODO(jl): While closing interpreters in a same session, we should remove after all - // interpreters are removed. OMG. It's too dirty!! - if (null != interpreterGroupRef && null != processKey && null != sessionKey) { - InterpreterGroup interpreterGroup = interpreterGroupRef.get(processKey); - if (1 == interpreterGroup.size() && interpreterGroup.containsKey(sessionKey)) { - interpreterGroupRef.remove(processKey); - } else { - interpreterGroup.remove(sessionKey); - } - } - } - }; - - t.start(); - try { - t.join(); - } catch (InterruptedException e) { - LOGGER.error("Can't close interpreter: {}", getId(), e); - } - - } - - /** - * Close all interpreter instances in this group - */ - public void shutdown() { - LOGGER.info("Close interpreter group " + getId()); - - // make sure remote interpreter process terminates - if (remoteInterpreterProcess != null) { - while (remoteInterpreterProcess.referenceCount() > 0) { - remoteInterpreterProcess.dereference(); + for (Interpreter interpreter : interpreters) { + Scheduler scheduler = interpreter.getScheduler(); + interpreter.close(); + //TODO(zjffdu) move the close of schedule to Interpreter + if (null != scheduler) { + SchedulerFactory.singleton().removeScheduler(scheduler.getName()); } - remoteInterpreterProcess = null; } - allInterpreterGroups.remove(id); - - List intpToClose = new LinkedList<>(); - for (List intpGroupForSession : this.values()) { - intpToClose.addAll(intpGroupForSession); - } - close(intpToClose); } public void setResourcePool(ResourcePool resourcePool) { @@ -275,4 +211,22 @@ public boolean isAngularRegistryPushed() { public void setAngularRegistryPushed(boolean angularRegistryPushed) { this.angularRegistryPushed = angularRegistryPushed; } + + public synchronized List getOrCreateSession(String user, String sessionId) { + if (sessions.containsKey(sessionId)) { + return sessions.get(sessionId); + } else { + List interpreters = interpreterSetting.createInterpreters(user, sessionId); + for (Interpreter interpreter : interpreters) { + interpreter.setInterpreterGroup(this); + } + LOGGER.info("Create Session {} in InterpreterGroup {} for user {}", sessionId, id, user); + sessions.put(sessionId, interpreters); + return interpreters; + } + } + + public boolean isEmpty() { + return sessions.isEmpty(); + } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfo.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfo.java similarity index 100% rename from zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfo.java rename to zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfo.java diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java new file mode 100644 index 00000000000..4ccc262699a --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java @@ -0,0 +1,101 @@ +/* + * 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.interpreter; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.internal.StringMap; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonatype.aether.repository.RemoteRepository; + +import java.io.BufferedReader; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.util.*; + +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; + +/** + * + */ +public class InterpreterInfoSaving { + + private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterInfoSaving.class); + private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + public Map interpreterSettings = new HashMap<>(); + public Map> interpreterBindings = new HashMap<>(); + public List interpreterRepositories = new ArrayList<>(); + + public static InterpreterInfoSaving loadFromFile(Path file) throws IOException { + LOGGER.info("Load interpreter setting from file: " + file); + InterpreterInfoSaving infoSaving = null; + try (BufferedReader json = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + JsonParser jsonParser = new JsonParser(); + JsonObject jsonObject = jsonParser.parse(json).getAsJsonObject(); + infoSaving = InterpreterInfoSaving.fromJson(jsonObject.toString()); + + if (infoSaving != null && infoSaving.interpreterSettings != null) { + for (InterpreterSetting interpreterSetting : infoSaving.interpreterSettings.values()) { + // Always use separate interpreter process + // While we decided to turn this feature on always (without providing + // enable/disable option on GUI). + // previously created setting should turn this feature on here. + interpreterSetting.getOption().setRemote(true); + interpreterSetting.convertPermissionsFromUsersToOwners( + jsonObject.getAsJsonObject("interpreterSettings") + .getAsJsonObject(interpreterSetting.getId())); + } + } + } + return infoSaving == null ? new InterpreterInfoSaving() : infoSaving; + } + + public void saveToFile(Path file) throws IOException { + if (!Files.exists(file)) { + Files.createFile(file); + try { + Set permissions = EnumSet.of(OWNER_READ, OWNER_WRITE); + Files.setPosixFilePermissions(file, permissions); + } catch (UnsupportedOperationException e) { + // File system does not support Posix file permissions (likely windows) - continue anyway. + LOGGER.warn("unable to setPosixFilePermissions on '{}'.", file); + }; + } + LOGGER.info("Save Interpreter Settings to " + file); + IOUtils.write(this.toJson(), new FileOutputStream(file.toFile())); + } + + public String toJson() { + return gson.toJson(this); + } + + public static InterpreterInfoSaving fromJson(String json) { + return gson.fromJson(json, InterpreterInfoSaving.class); + } +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterProperty.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterProperty.java index 0bb3d42dd19..ac40de36a04 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterProperty.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterProperty.java @@ -34,6 +34,7 @@ public InterpreterProperty(String name, Object value, String type) { public InterpreterProperty(String name, Object value) { this.name = name; this.value = value; + this.type = "textarea"; } public String getName() { diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterRunner.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterRunner.java index 020564b5fb3..ae7bb884ec7 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterRunner.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterRunner.java @@ -1,5 +1,6 @@ package org.apache.zeppelin.interpreter; +import com.google.common.annotations.VisibleForTesting; import com.google.gson.annotations.SerializedName; /** @@ -12,6 +13,16 @@ public class InterpreterRunner { @SerializedName("win") private String winPath; + public InterpreterRunner() { + + } + + @VisibleForTesting + public InterpreterRunner(String linuxPath, String winPath) { + this.linuxPath = linuxPath; + this.winPath = winPath; + } + public String getPath() { return System.getProperty("os.name").startsWith("Windows") ? winPath : linuxPath; } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java new file mode 100644 index 00000000000..3f84cd00178 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java @@ -0,0 +1,911 @@ +/* + * 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.interpreter; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; +import com.google.gson.internal.StringMap; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.dep.Dependency; +import org.apache.zeppelin.dep.DependencyResolver; +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.display.AngularObjectRegistryListener; +import org.apache.zeppelin.helium.ApplicationEventListener; +import org.apache.zeppelin.interpreter.remote.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileNotFoundException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import static org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_MAX_POOL_SIZE; +import static org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_OUTPUT_LIMIT; +import static org.apache.zeppelin.util.IdHashes.generateId; + +/** + * Represent one InterpreterSetting in the interpreter setting page + */ +public class InterpreterSetting { + + private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterSetting.class); + private static final String SHARED_PROCESS = "shared_process"; + private static final String SHARED_SESSION = "shared_session"; + private static final Map DEFAULT_EDITOR = ImmutableMap.of( + "language", (Object) "text", + "editOnDblClick", false); + + private String id; + private String name; + // the original interpreter setting template name where it is created from + private String group; + + //TODO(zjffdu) make the interpreter.json consistent with interpreter-setting.json + /** + * properties can be either Properties or Map + * properties should be: + * - Properties when Interpreter instances are saved to `conf/interpreter.json` file + * - Map when Interpreters are registered + * : this is needed after https://github.com/apache/zeppelin/pull/1145 + * which changed the way of getting default interpreter setting AKA interpreterSettingsRef + * Note(mina): In order to simplify the implementation, I chose to change properties + * from Properties to Object instead of creating new classes. + */ + private Object properties = new Properties(); + + private Status status; + private String errorReason; + + @SerializedName("interpreterGroup") + private List interpreterInfos; + + private List dependencies = new ArrayList<>(); + private InterpreterOption option = new InterpreterOption(true); + + @SerializedName("runner") + private InterpreterRunner interpreterRunner; + + /////////////////////////////////////////////////////////////////////////////////////////// + private transient InterpreterSettingManager interpreterSettingManager; + private transient String interpreterDir; + private final transient Map interpreterGroups = + new ConcurrentHashMap<>(); + + private final transient ReentrantReadWriteLock.ReadLock interpreterGroupReadLock; + private final transient ReentrantReadWriteLock.WriteLock interpreterGroupWriteLock; + + private transient AngularObjectRegistryListener angularObjectRegistryListener; + private transient RemoteInterpreterProcessListener remoteInterpreterProcessListener; + private transient ApplicationEventListener appEventListener; + private transient DependencyResolver dependencyResolver; + + private transient Map infos; + + // Map of the note and paragraphs which has runtime infos generated by this interpreter setting. + // This map is used to clear the infos in paragraph when the interpretersetting is restarted + private transient Map> runtimeInfosToBeCleared; + + private transient ZeppelinConfiguration conf = new ZeppelinConfiguration(); + + private transient Map cleanCl = + Collections.synchronizedMap(new HashMap()); + /////////////////////////////////////////////////////////////////////////////////////////// + + + /** + * Builder class for InterpreterSetting + */ + public static class Builder { + private InterpreterSetting interpreterSetting; + + public Builder() { + this.interpreterSetting = new InterpreterSetting(); + } + + public Builder setId(String id) { + interpreterSetting.id = id; + return this; + } + + public Builder setName(String name) { + interpreterSetting.name = name; + return this; + } + + public Builder setGroup(String group) { + interpreterSetting.group = group; + return this; + } + + public Builder setInterpreterInfos(List interpreterInfos) { + interpreterSetting.interpreterInfos = interpreterInfos; + return this; + } + + public Builder setProperties(Object properties) { + interpreterSetting.properties = properties; + return this; + } + + public Builder setOption(InterpreterOption option) { + interpreterSetting.option = option; + return this; + } + + public Builder setInterpreterDir(String interpreterDir) { + interpreterSetting.interpreterDir = interpreterDir; + return this; + } + + public Builder setRunner(InterpreterRunner runner) { + interpreterSetting.interpreterRunner = runner; + return this; + } + + public Builder setDependencies(List dependencies) { + interpreterSetting.dependencies = dependencies; + return this; + } + + public Builder setConf(ZeppelinConfiguration conf) { + interpreterSetting.conf = conf; + return this; + } + + public Builder setDependencyResolver(DependencyResolver dependencyResolver) { + interpreterSetting.dependencyResolver = dependencyResolver; + return this; + } + +// public Builder setInterpreterRunner(InterpreterRunner runner) { +// interpreterSetting.interpreterRunner = runner; +// return this; +// } + + public Builder setIntepreterSettingManager( + InterpreterSettingManager interpreterSettingManager) { + interpreterSetting.interpreterSettingManager = interpreterSettingManager; + return this; + } + + public Builder setRemoteInterpreterProcessListener(RemoteInterpreterProcessListener + remoteInterpreterProcessListener) { + interpreterSetting.remoteInterpreterProcessListener = remoteInterpreterProcessListener; + return this; + } + + public Builder setAngularObjectRegistryListener( + AngularObjectRegistryListener angularObjectRegistryListener) { + interpreterSetting.angularObjectRegistryListener = angularObjectRegistryListener; + return this; + } + + public Builder setApplicationEventListener(ApplicationEventListener applicationEventListener) { + interpreterSetting.appEventListener = applicationEventListener; + return this; + } + + public InterpreterSetting create() { + // post processing + interpreterSetting.postProcessing(); + return interpreterSetting; + } + } + + public InterpreterSetting() { + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + this.id = generateId(); + interpreterGroupReadLock = lock.readLock(); + interpreterGroupWriteLock = lock.writeLock(); + } + + void postProcessing() { + this.status = Status.READY; + } + + /** + * Create interpreter from InterpreterSettingTemplate + * + * @param o interpreterSetting from InterpreterSettingTemplate + */ + public InterpreterSetting(InterpreterSetting o) { + this(); + this.id = generateId(); + this.name = o.name; + this.group = o.group; + this.properties = convertInterpreterProperties( + (Map) o.getProperties()); + this.interpreterInfos = new ArrayList<>(o.getInterpreterInfos()); + this.option = InterpreterOption.fromInterpreterOption(o.getOption()); + this.dependencies = new ArrayList<>(o.getDependencies()); + this.interpreterDir = o.getInterpreterDir(); + this.interpreterRunner = o.getInterpreterRunner(); + this.conf = o.getConf(); + } + + public AngularObjectRegistryListener getAngularObjectRegistryListener() { + return angularObjectRegistryListener; + } + + public RemoteInterpreterProcessListener getRemoteInterpreterProcessListener() { + return remoteInterpreterProcessListener; + } + + public ApplicationEventListener getAppEventListener() { + return appEventListener; + } + + public DependencyResolver getDependencyResolver() { + return dependencyResolver; + } + + public InterpreterSettingManager getInterpreterSettingManager() { + return interpreterSettingManager; + } + + public void setAngularObjectRegistryListener(AngularObjectRegistryListener + angularObjectRegistryListener) { + this.angularObjectRegistryListener = angularObjectRegistryListener; + } + + public void setAppEventListener(ApplicationEventListener appEventListener) { + this.appEventListener = appEventListener; + } + + public void setRemoteInterpreterProcessListener(RemoteInterpreterProcessListener + remoteInterpreterProcessListener) { + this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; + } + + public void setDependencyResolver(DependencyResolver dependencyResolver) { + this.dependencyResolver = dependencyResolver; + } + + public void setInterpreterSettingManager(InterpreterSettingManager interpreterSettingManager) { + this.interpreterSettingManager = interpreterSettingManager; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getGroup() { + return group; + } + + private String getInterpreterGroupId(String user, String noteId) { + String key; + if (option.isExistingProcess) { + key = Constants.EXISTING_PROCESS; + } else if (getOption().isProcess()) { + key = (option.perUserIsolated() ? user : "") + ":" + (option.perNoteIsolated() ? noteId : ""); + } else { + key = SHARED_PROCESS; + } + + //TODO(zjffdu) we encode interpreter setting id into groupId, this is not a good design + return id + ":" + key; + } + + private String getInterpreterSessionId(String user, String noteId) { + String key; + if (option.isExistingProcess()) { + key = Constants.EXISTING_PROCESS; + } else if (option.perNoteScoped() && option.perUserScoped()) { + key = user + ":" + noteId; + } else if (option.perUserScoped()) { + key = user; + } else if (option.perNoteScoped()) { + key = noteId; + } else { + key = SHARED_SESSION; + } + + return key; + } + + public InterpreterGroup getOrCreateInterpreterGroup(String user, String noteId) { + String groupId = getInterpreterGroupId(user, noteId); + try { + interpreterGroupWriteLock.lock(); + if (!interpreterGroups.containsKey(groupId)) { + LOGGER.info("Create InterpreterGroup with groupId {} for user {} and note {}", + groupId, user, noteId); + InterpreterGroup intpGroup = createInterpreterGroup(groupId); + interpreterGroups.put(groupId, intpGroup); + } + return interpreterGroups.get(groupId); + } finally { + interpreterGroupWriteLock.unlock();; + } + } + + void removeInterpreterGroup(String groupId) { + this.interpreterGroups.remove(groupId); + } + + InterpreterGroup getInterpreterGroup(String user, String noteId) { + String groupId = getInterpreterGroupId(user, noteId); + try { + interpreterGroupReadLock.lock(); + return interpreterGroups.get(groupId); + } finally { + interpreterGroupReadLock.unlock();; + } + } + + InterpreterGroup getInterpreterGroup(String groupId) { + return interpreterGroups.get(groupId); + } + + @VisibleForTesting + public ArrayList getAllInterpreterGroups() { + try { + interpreterGroupReadLock.lock(); + return new ArrayList(interpreterGroups.values()); + } finally { + interpreterGroupReadLock.unlock(); + } + } + + Map getEditorFromSettingByClassName(String className) { + for (InterpreterInfo intpInfo : interpreterInfos) { + if (className.equals(intpInfo.getClassName())) { + if (intpInfo.getEditor() == null) { + break; + } + return intpInfo.getEditor(); + } + } + return DEFAULT_EDITOR; + } + + void closeInterpreters(String user, String noteId) { + InterpreterGroup interpreterGroup = getInterpreterGroup(user, noteId); + if (interpreterGroup != null) { + String sessionId = getInterpreterSessionId(user, noteId); + interpreterGroup.close(sessionId); + } + } + + public void close() { + LOGGER.info("Close InterpreterSetting: " + name); + for (InterpreterGroup intpGroup : interpreterGroups.values()) { + intpGroup.close(); + } + interpreterGroups.clear(); + this.runtimeInfosToBeCleared = null; + this.infos = null; + } + + public void setProperties(Object object) { + if (object instanceof StringMap) { + StringMap map = (StringMap) properties; + Properties newProperties = new Properties(); + for (String key : map.keySet()) { + newProperties.put(key, map.get(key)); + } + this.properties = newProperties; + } else { + this.properties = object; + } + } + + + public Object getProperties() { + return properties; + } + + @VisibleForTesting + public void setProperty(String name, String value) { + ((Map) properties).put(name, new InterpreterProperty(name, value)); + } + + // This method is supposed to be only called by InterpreterSetting + // but not InterpreterSetting Template + public Properties getJavaProperties() { + Properties jProperties = new Properties(); + Map iProperties = (Map) properties; + for (Map.Entry entry : iProperties.entrySet()) { + jProperties.setProperty(entry.getKey(), entry.getValue().getValue().toString()); + } + + if (!jProperties.containsKey("zeppelin.interpreter.output.limit")) { + jProperties.setProperty("zeppelin.interpreter.output.limit", + conf.getInt(ZEPPELIN_INTERPRETER_OUTPUT_LIMIT) + ""); + } + + if (!jProperties.containsKey("zeppelin.interpreter.max.poolsize")) { + jProperties.setProperty("zeppelin.interpreter.max.poolsize", + conf.getInt(ZEPPELIN_INTERPRETER_MAX_POOL_SIZE) + ""); + } + + String interpreterLocalRepoPath = conf.getInterpreterLocalRepoPath(); + //TODO(zjffdu) change it to interpreterDir/{interpreter_name} + jProperties.setProperty("zeppelin.interpreter.localRepo", + interpreterLocalRepoPath + "/" + id); + return jProperties; + } + + public ZeppelinConfiguration getConf() { + return conf; + } + + public void setConf(ZeppelinConfiguration conf) { + this.conf = conf; + } + + public List getDependencies() { + return dependencies; + } + + public void setDependencies(List dependencies) { + this.dependencies = dependencies; + loadInterpreterDependencies(); + } + + public InterpreterOption getOption() { + return option; + } + + public void setOption(InterpreterOption option) { + this.option = option; + } + + public String getInterpreterDir() { + return interpreterDir; + } + + public void setInterpreterDir(String interpreterDir) { + this.interpreterDir = interpreterDir; + } + + public List getInterpreterInfos() { + return interpreterInfos; + } + + void appendDependencies(List dependencies) { + for (Dependency dependency : dependencies) { + if (!this.dependencies.contains(dependency)) { + this.dependencies.add(dependency); + } + } + loadInterpreterDependencies(); + } + + void setInterpreterOption(InterpreterOption interpreterOption) { + this.option = interpreterOption; + } + + public void setProperties(Properties p) { + this.properties = p; + } + + void setGroup(String group) { + this.group = group; + } + + void setName(String name) { + this.name = name; + } + + /*** + * Interpreter status + */ + public enum Status { + DOWNLOADING_DEPENDENCIES, + ERROR, + READY + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getErrorReason() { + return errorReason; + } + + public void setErrorReason(String errorReason) { + this.errorReason = errorReason; + } + + public void setInterpreterInfos(List interpreterInfos) { + this.interpreterInfos = interpreterInfos; + } + + public void setInfos(Map infos) { + this.infos = infos; + } + + public Map getInfos() { + return infos; + } + + public InterpreterRunner getInterpreterRunner() { + return interpreterRunner; + } + + public void setInterpreterRunner(InterpreterRunner interpreterRunner) { + this.interpreterRunner = interpreterRunner; + } + + public void addNoteToPara(String noteId, String paraId) { + if (runtimeInfosToBeCleared == null) { + runtimeInfosToBeCleared = new HashMap<>(); + } + Set paraIdSet = runtimeInfosToBeCleared.get(noteId); + if (paraIdSet == null) { + paraIdSet = new HashSet<>(); + runtimeInfosToBeCleared.put(noteId, paraIdSet); + } + paraIdSet.add(paraId); + } + + public Map> getNoteIdAndParaMap() { + return runtimeInfosToBeCleared; + } + + public void clearNoteIdAndParaMap() { + runtimeInfosToBeCleared = null; + } + + + //////////////////////////// IMPORTANT //////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////// + // This is the only place to create interpreters. For now we always create multiple interpreter + // together (one session). We don't support to create single interpreter yet. + List createInterpreters(String user, String sessionId) { + List interpreters = new ArrayList<>(); + List interpreterInfos = getInterpreterInfos(); + for (InterpreterInfo info : interpreterInfos) { + Interpreter interpreter = null; + if (option.isRemote()) { + interpreter = new RemoteInterpreter(getJavaProperties(), sessionId, + info.getClassName(), user); + } else { + interpreter = createLocalInterpreter(info.getClassName()); + } + + if (info.isDefaultInterpreter()) { + interpreters.add(0, interpreter); + } else { + interpreters.add(interpreter); + } + LOGGER.info("Interpreter {} created for user: {}, sessionId: {}", + interpreter.getClassName(), user, sessionId); + } + return interpreters; + } + + // Create Interpreter in ZeppelinServer for non-remote mode + private Interpreter createLocalInterpreter(String className) + throws InterpreterException { + LOGGER.info("Create Local Interpreter {} from {}", className, interpreterDir); + + ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); + try { + + URLClassLoader ccl = cleanCl.get(interpreterDir); + if (ccl == null) { + // classloader fallback + ccl = URLClassLoader.newInstance(new URL[]{}, oldcl); + } + + boolean separateCL = true; + try { // check if server's classloader has driver already. + Class cls = this.getClass().forName(className); + if (cls != null) { + separateCL = false; + } + } catch (Exception e) { + LOGGER.error("exception checking server classloader driver", e); + } + + URLClassLoader cl; + + if (separateCL == true) { + cl = URLClassLoader.newInstance(new URL[]{}, ccl); + } else { + cl = ccl; + } + Thread.currentThread().setContextClassLoader(cl); + + Class replClass = (Class) cl.loadClass(className); + Constructor constructor = + replClass.getConstructor(new Class[]{Properties.class}); + Interpreter repl = constructor.newInstance(getJavaProperties()); + repl.setClassloaderUrls(ccl.getURLs()); + LazyOpenInterpreter intp = new LazyOpenInterpreter(new ClassloaderInterpreter(repl, cl)); + return intp; + } catch (SecurityException e) { + throw new InterpreterException(e); + } catch (NoSuchMethodException e) { + throw new InterpreterException(e); + } catch (IllegalArgumentException e) { + throw new InterpreterException(e); + } catch (InstantiationException e) { + throw new InterpreterException(e); + } catch (IllegalAccessException e) { + throw new InterpreterException(e); + } catch (InvocationTargetException e) { + throw new InterpreterException(e); + } catch (ClassNotFoundException e) { + throw new InterpreterException(e); + } finally { + Thread.currentThread().setContextClassLoader(oldcl); + } + } + + RemoteInterpreterProcess createInterpreterProcess() { + RemoteInterpreterProcess remoteInterpreterProcess = null; + int connectTimeout = + conf.getInt(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_CONNECT_TIMEOUT); + String localRepoPath = conf.getInterpreterLocalRepoPath() + "/" + id; + if (option.isExistingProcess()) { + // TODO(zjffdu) remove the existing process approach seems no one is using this. + // use the existing process + remoteInterpreterProcess = new RemoteInterpreterRunningProcess( + connectTimeout, + remoteInterpreterProcessListener, + appEventListener, + option.getHost(), + option.getPort()); + } else { + // create new remote process + remoteInterpreterProcess = new RemoteInterpreterManagedProcess( + interpreterRunner != null ? interpreterRunner.getPath() : + conf.getInterpreterRemoteRunnerPath(), interpreterDir, localRepoPath, + getEnvFromInterpreterProperty(getJavaProperties()), connectTimeout, + remoteInterpreterProcessListener, appEventListener, group); + } + return remoteInterpreterProcess; + } + + private Map getEnvFromInterpreterProperty(Properties property) { + Map env = new HashMap(); + StringBuilder sparkConfBuilder = new StringBuilder(); + for (String key : property.stringPropertyNames()) { + if (RemoteInterpreterUtils.isEnvString(key)) { + env.put(key, property.getProperty(key)); + } + if (key.equals("master")) { + sparkConfBuilder.append(" --master " + property.getProperty("master")); + } + if (isSparkConf(key, property.getProperty(key))) { + sparkConfBuilder.append(" --conf " + key + "=" + + toShellFormat(property.getProperty(key))); + } + } + env.put("ZEPPELIN_SPARK_CONF", sparkConfBuilder.toString()); + return env; + } + + private String toShellFormat(String value) { + if (value.contains("\'") && value.contains("\"")) { + throw new RuntimeException("Spark property value could not contain both \" and '"); + } else if (value.contains("\'")) { + return "\"" + value + "\""; + } else { + return "\'" + value + "\'"; + } + } + + static boolean isSparkConf(String key, String value) { + return !StringUtils.isEmpty(key) && key.startsWith("spark.") && !StringUtils.isEmpty(value); + } + + private List getOrCreateSession(String user, String noteId) { + InterpreterGroup interpreterGroup = getOrCreateInterpreterGroup(user, noteId); + Preconditions.checkNotNull(interpreterGroup, "No InterpreterGroup existed for user {}, " + + "noteId {}", user, noteId); + String sessionId = getInterpreterSessionId(user, noteId); + return interpreterGroup.getOrCreateSession(user, sessionId); + } + + public Interpreter getDefaultInterpreter(String user, String noteId) { + return getOrCreateSession(user, noteId).get(0); + } + + public Interpreter getInterpreter(String user, String noteId, String replName) { + Preconditions.checkNotNull(noteId, "noteId should be not null"); + Preconditions.checkNotNull(replName, "replName should be not null"); + + String className = getInterpreterClassFromInterpreterSetting(replName); + if (className == null) { + return null; + } + List interpreters = getOrCreateSession(user, noteId); + for (Interpreter interpreter : interpreters) { + if (className.equals(interpreter.getClassName())) { + return interpreter; + } + } + return null; + } + + private String getInterpreterClassFromInterpreterSetting(String replName) { + Preconditions.checkNotNull(replName, "replName should be not null"); + + for (InterpreterInfo info : interpreterInfos) { + String infoName = info.getName(); + if (null != info.getName() && replName.equals(infoName)) { + return info.getClassName(); + } + } + return null; + } + + private InterpreterGroup createInterpreterGroup(String groupId) throws InterpreterException { + AngularObjectRegistry angularObjectRegistry; + InterpreterGroup interpreterGroup = new InterpreterGroup(groupId, this); + if (option.isRemote()) { + angularObjectRegistry = + new RemoteAngularObjectRegistry(groupId, angularObjectRegistryListener, interpreterGroup); + } else { + angularObjectRegistry = new AngularObjectRegistry(id, angularObjectRegistryListener); + // TODO(moon) : create distributed resource pool for local interpreters and set + } + + interpreterGroup.setAngularObjectRegistry(angularObjectRegistry); + return interpreterGroup; + } + + private void loadInterpreterDependencies() { + setStatus(Status.DOWNLOADING_DEPENDENCIES); + setErrorReason(null); + Thread t = new Thread() { + public void run() { + try { + // dependencies to prevent library conflict + File localRepoDir = new File(conf.getInterpreterLocalRepoPath() + "/" + getId()); + if (localRepoDir.exists()) { + try { + FileUtils.forceDelete(localRepoDir); + } catch (FileNotFoundException e) { + LOGGER.info("A file that does not exist cannot be deleted, nothing to worry", e); + } + } + + // load dependencies + List deps = getDependencies(); + if (deps != null) { + for (Dependency d : deps) { + File destDir = new File( + conf.getRelativeDir(ZeppelinConfiguration.ConfVars.ZEPPELIN_DEP_LOCALREPO)); + + if (d.getExclusions() != null) { + dependencyResolver.load(d.getGroupArtifactVersion(), d.getExclusions(), + new File(destDir, id)); + } else { + dependencyResolver + .load(d.getGroupArtifactVersion(), new File(destDir, id)); + } + } + } + + setStatus(Status.READY); + setErrorReason(null); + } catch (Exception e) { + LOGGER.error(String.format("Error while downloading repos for interpreter group : %s," + + " go to interpreter setting page click on edit and save it again to make " + + "this interpreter work properly. : %s", + getGroup(), e.getLocalizedMessage()), e); + setErrorReason(e.getLocalizedMessage()); + setStatus(Status.ERROR); + } + } + }; + + t.start(); + } + + //TODO(zjffdu) ugly code, should not use JsonObject as parameter. not readable + public void convertPermissionsFromUsersToOwners(JsonObject jsonObject) { + if (jsonObject != null) { + JsonObject option = jsonObject.getAsJsonObject("option"); + if (option != null) { + JsonArray users = option.getAsJsonArray("users"); + if (users != null) { + if (this.option.getOwners() == null) { + this.option.owners = new LinkedList<>(); + } + for (JsonElement user : users) { + this.option.getOwners().add(user.getAsString()); + } + } + } + } + } + + // For backward compatibility of interpreter.json format after ZEPPELIN-2403 + static Map convertInterpreterProperties(Object properties) { + if (properties != null && properties instanceof StringMap) { + Map newProperties = new HashMap<>(); + StringMap p = (StringMap) properties; + for (Object o : p.entrySet()) { + Map.Entry entry = (Map.Entry) o; + if (!(entry.getValue() instanceof StringMap)) { + InterpreterProperty newProperty = new InterpreterProperty( + entry.getKey().toString(), + entry.getValue(), + InterpreterPropertyType.STRING.getValue()); + newProperties.put(entry.getKey().toString(), newProperty); + } else { + // already converted + return (Map) properties; + } + } + return newProperties; + + } else if (properties instanceof Map) { + Map dProperties = + (Map) properties; + Map newProperties = new HashMap<>(); + for (String key : dProperties.keySet()) { + Object value = dProperties.get(key); + if (value instanceof InterpreterProperty) { + return (Map) properties; + } else if (value instanceof StringMap) { + StringMap stringMap = (StringMap) value; + InterpreterProperty newProperty = new InterpreterProperty( + key, + stringMap.get("value"), + stringMap.get("type").toString()); + + newProperties.put(newProperty.getName(), newProperty); + } else if (value instanceof DefaultInterpreterProperty){ + DefaultInterpreterProperty dProperty = (DefaultInterpreterProperty) value; + InterpreterProperty property = new InterpreterProperty( + key, + dProperty.getValue(), + dProperty.getType() != null ? dProperty.getType() : "string" + // in case user forget to specify type in interpreter-setting.json + ); + newProperties.put(key, property); + } else { + throw new RuntimeException("Can not convert this type of property: " + value.getClass()); + } + } + return newProperties; + } + throw new RuntimeException("Can not convert this type: " + properties.getClass()); + } +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java new file mode 100644 index 00000000000..ed3ebd890d1 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java @@ -0,0 +1,886 @@ +/* + * 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.interpreter; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; +import org.apache.zeppelin.dep.Dependency; +import org.apache.zeppelin.dep.DependencyResolver; +import org.apache.zeppelin.display.AngularObjectRegistryListener; +import org.apache.zeppelin.helium.ApplicationEventListener; +import org.apache.zeppelin.interpreter.Interpreter.RegisteredInterpreter; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; +import org.apache.zeppelin.resource.Resource; +import org.apache.zeppelin.resource.ResourcePool; +import org.apache.zeppelin.resource.ResourceSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonatype.aether.repository.Authentication; +import org.sonatype.aether.repository.Proxy; +import org.sonatype.aether.repository.RemoteRepository; + +import java.io.*; +import java.lang.reflect.Type; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.DirectoryStream.Filter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +/** + * InterpreterSettingManager is the component which manage all the interpreter settings. + * (load/create/update/remove/get) + * Besides that InterpreterSettingManager also manage the interpreter setting binding. + * TODO(zjffdu) We could move it into another separated component. + */ +public class InterpreterSettingManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterSettingManager.class); + private static final Map DEFAULT_EDITOR = ImmutableMap.of( + "language", (Object) "text", + "editOnDblClick", false); + + private final ZeppelinConfiguration conf; + private final Path interpreterDirPath; + private final Path interpreterSettingPath; + + /** + * This is only InterpreterSetting templates with default name and properties + * name --> InterpreterSetting + */ + private final Map interpreterSettingTemplates = + Maps.newConcurrentMap(); + /** + * This is used by creating and running Interpreters + * id --> InterpreterSetting + * TODO(zjffdu) change it to name --> InterpreterSetting + */ + private final Map interpreterSettings = + Maps.newConcurrentMap(); + + /** + * noteId --> list of InterpreterSettingId + */ + private final Map> interpreterBindings = + Maps.newConcurrentMap(); + + private final List interpreterRepositories; + private InterpreterOption defaultOption; + private List interpreterGroupOrderList; + private final Gson gson; + + private AngularObjectRegistryListener angularObjectRegistryListener; + private RemoteInterpreterProcessListener remoteInterpreterProcessListener; + private ApplicationEventListener appEventListener; + private DependencyResolver dependencyResolver; + + + public InterpreterSettingManager(ZeppelinConfiguration zeppelinConfiguration, + AngularObjectRegistryListener angularObjectRegistryListener, + RemoteInterpreterProcessListener + remoteInterpreterProcessListener, + ApplicationEventListener appEventListener) + throws IOException { + this(zeppelinConfiguration, new InterpreterOption(true), + angularObjectRegistryListener, + remoteInterpreterProcessListener, + appEventListener); + } + + @VisibleForTesting + public InterpreterSettingManager(ZeppelinConfiguration conf, + InterpreterOption defaultOption, + AngularObjectRegistryListener angularObjectRegistryListener, + RemoteInterpreterProcessListener + remoteInterpreterProcessListener, + ApplicationEventListener appEventListener) throws IOException { + this.conf = conf; + this.defaultOption = defaultOption; + this.interpreterDirPath = Paths.get(conf.getInterpreterDir()); + LOGGER.debug("InterpreterRootPath: {}", interpreterDirPath); + this.interpreterSettingPath = Paths.get(conf.getInterpreterSettingPath()); + LOGGER.debug("InterpreterBindingPath: {}", interpreterSettingPath); + this.dependencyResolver = new DependencyResolver( + conf.getString(ConfVars.ZEPPELIN_INTERPRETER_LOCALREPO)); + this.interpreterRepositories = dependencyResolver.getRepos(); + this.interpreterGroupOrderList = Arrays.asList(conf.getString( + ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER).split(",")); + this.gson = new GsonBuilder().setPrettyPrinting().create(); + + this.angularObjectRegistryListener = angularObjectRegistryListener; + this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; + this.appEventListener = appEventListener; + init(); + } + + /** + * Load interpreter setting from interpreter-setting.json + */ + private void loadFromFile() { + if (!Files.exists(interpreterSettingPath)) { + // nothing to read + LOGGER.warn("Interpreter Setting file {} doesn't exist", interpreterSettingPath); + return; + } + + try { + InterpreterInfoSaving infoSaving = InterpreterInfoSaving.loadFromFile(interpreterSettingPath); + //TODO(zjffdu) still ugly (should move all to InterpreterInfoSaving) + for (InterpreterSetting savedInterpreterSetting : infoSaving.interpreterSettings.values()) { + savedInterpreterSetting.setConf(conf); + savedInterpreterSetting.setInterpreterSettingManager(this); + savedInterpreterSetting.setAngularObjectRegistryListener(angularObjectRegistryListener); + savedInterpreterSetting.setRemoteInterpreterProcessListener( + remoteInterpreterProcessListener); + savedInterpreterSetting.setAppEventListener(appEventListener); + savedInterpreterSetting.setDependencyResolver(dependencyResolver); + savedInterpreterSetting.setProperties(InterpreterSetting.convertInterpreterProperties( + savedInterpreterSetting.getProperties() + )); + + InterpreterSetting interpreterSettingTemplate = + interpreterSettingTemplates.get(savedInterpreterSetting.getGroup()); + // InterpreterSettingTemplate is from interpreter-setting.json which represent the latest + // InterpreterSetting, while InterpreterSetting is from interpreter.json which represent + // the user saved interpreter setting + if (interpreterSettingTemplate != null) { + savedInterpreterSetting.setInterpreterDir(interpreterSettingTemplate.getInterpreterDir()); + // merge properties from interpreter-setting.json and interpreter.json + Map mergedProperties = + new HashMap<>(InterpreterSetting.convertInterpreterProperties( + interpreterSettingTemplate.getProperties())); + mergedProperties.putAll(InterpreterSetting.convertInterpreterProperties( + savedInterpreterSetting.getProperties())); + savedInterpreterSetting.setProperties(mergedProperties); + // merge InterpreterInfo + savedInterpreterSetting.setInterpreterInfos( + interpreterSettingTemplate.getInterpreterInfos()); + } else { + LOGGER.warn("No InterpreterSetting Template found for InterpreterSetting: " + + savedInterpreterSetting.getGroup()); + } + + // Overwrite the default InterpreterSetting we registered from InterpreterSetting Templates + // remove it first + for (InterpreterSetting setting : interpreterSettings.values()) { + if (setting.getName().equals(savedInterpreterSetting.getName())) { + interpreterSettings.remove(setting.getId()); + } + } + savedInterpreterSetting.postProcessing(); + LOGGER.info("Create Interpreter Setting {} from interpreter.json", + savedInterpreterSetting.getName()); + interpreterSettings.put(savedInterpreterSetting.getId(), savedInterpreterSetting); + } + + interpreterBindings.putAll(infoSaving.interpreterBindings); + + if (infoSaving.interpreterRepositories != null) { + for (RemoteRepository repo : infoSaving.interpreterRepositories) { + if (!dependencyResolver.getRepos().contains(repo)) { + this.interpreterRepositories.add(repo); + } + } + } + } catch (IOException e) { + LOGGER.error("Fail to load interpreter setting configuration file: " + + interpreterSettingPath, e); + } + } + + public void saveToFile() throws IOException { + synchronized (interpreterSettings) { + InterpreterInfoSaving info = new InterpreterInfoSaving(); + info.interpreterBindings = interpreterBindings; + info.interpreterSettings = interpreterSettings; + info.interpreterRepositories = interpreterRepositories; + info.saveToFile(interpreterSettingPath); + } + } + + private void init() throws IOException { + + // 1. detect interpreter setting via interpreter-setting.json in each interpreter folder + // 2. detect interpreter setting in interpreter.json that is saved before + String interpreterJson = conf.getInterpreterJson(); + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (Files.exists(interpreterDirPath)) { + for (Path interpreterDir : Files + .newDirectoryStream(interpreterDirPath, new Filter() { + @Override + public boolean accept(Path entry) throws IOException { + return Files.exists(entry) && Files.isDirectory(entry); + } + })) { + String interpreterDirString = interpreterDir.toString(); + + /** + * Register interpreter by the following ordering + * 1. Register it from path {ZEPPELIN_HOME}/interpreter/{interpreter_name}/ + * interpreter-setting.json + * 2. Register it from interpreter-setting.json in classpath + * {ZEPPELIN_HOME}/interpreter/{interpreter_name} + */ + if (!registerInterpreterFromPath(interpreterDirString, interpreterJson)) { + if (!registerInterpreterFromResource(cl, interpreterDirString, interpreterJson)) { + LOGGER.warn("No interpreter-setting.json found in " + interpreterDirPath); + } + } + } + } else { + LOGGER.warn("InterpreterDir {} doesn't exist", interpreterDirPath); + } + + loadFromFile(); + saveToFile(); + } + + private boolean registerInterpreterFromResource(ClassLoader cl, String interpreterDir, + String interpreterJson) throws IOException { + URL[] urls = recursiveBuildLibList(new File(interpreterDir)); + ClassLoader tempClassLoader = new URLClassLoader(urls, cl); + + URL url = tempClassLoader.getResource(interpreterJson); + if (url == null) { + return false; + } + + LOGGER.debug("Reading interpreter-setting.json from {} as Resource", url); + List registeredInterpreterList = + getInterpreterListFromJson(url.openStream()); + registerInterpreterSetting(registeredInterpreterList, interpreterDir); + return true; + } + + private boolean registerInterpreterFromPath(String interpreterDir, String interpreterJson) + throws IOException { + + Path interpreterJsonPath = Paths.get(interpreterDir, interpreterJson); + if (Files.exists(interpreterJsonPath)) { + LOGGER.debug("Reading interpreter-setting.json from file {}", interpreterJsonPath); + List registeredInterpreterList = + getInterpreterListFromJson(new FileInputStream(interpreterJsonPath.toFile())); + registerInterpreterSetting(registeredInterpreterList, interpreterDir); + return true; + } + return false; + } + + private List getInterpreterListFromJson(InputStream stream) { + Type registeredInterpreterListType = new TypeToken>() { + }.getType(); + return gson.fromJson(new InputStreamReader(stream), registeredInterpreterListType); + } + + private void registerInterpreterSetting(List registeredInterpreters, + String interpreterDir) throws IOException { + + Map properties = new HashMap<>(); + List interpreterInfos = new ArrayList<>(); + InterpreterOption option = defaultOption; + String group = null; + InterpreterRunner runner = null; + for (RegisteredInterpreter registeredInterpreter : registeredInterpreters) { + //TODO(zjffdu) merge RegisteredInterpreter & InterpreterInfo + InterpreterInfo interpreterInfo = + new InterpreterInfo(registeredInterpreter.getClassName(), registeredInterpreter.getName(), + registeredInterpreter.isDefaultInterpreter(), registeredInterpreter.getEditor()); + group = registeredInterpreter.getGroup(); + runner = registeredInterpreter.getRunner(); + // use defaultOption if it is not specified in interpreter-setting.json + if (registeredInterpreter.getOption() != null) { + option = registeredInterpreter.getOption(); + } + properties.putAll(registeredInterpreter.getProperties()); + interpreterInfos.add(interpreterInfo); + } + + InterpreterSetting interpreterSettingTemplate = new InterpreterSetting.Builder() + .setGroup(group) + .setName(group) + .setInterpreterInfos(interpreterInfos) + .setProperties(properties) + .setDependencies(new ArrayList()) + .setOption(option) + .setRunner(runner) + .setInterpreterDir(interpreterDir) + .setRunner(runner) + .setConf(conf) + .setIntepreterSettingManager(this) + .create(); + + LOGGER.info("Register InterpreterSettingTemplate & InterpreterSetting: {}", + interpreterSettingTemplate.getName()); + interpreterSettingTemplates.put(interpreterSettingTemplate.getName(), + interpreterSettingTemplate); + + InterpreterSetting interpreterSetting = new InterpreterSetting(interpreterSettingTemplate); + interpreterSetting.setAngularObjectRegistryListener(angularObjectRegistryListener); + interpreterSetting.setRemoteInterpreterProcessListener(remoteInterpreterProcessListener); + interpreterSetting.setAppEventListener(appEventListener); + interpreterSetting.setDependencyResolver(dependencyResolver); + interpreterSetting.setInterpreterSettingManager(this); + interpreterSetting.postProcessing(); + interpreterSettings.put(interpreterSetting.getId(), interpreterSetting); + } + + @VisibleForTesting + public InterpreterSetting getDefaultInterpreterSetting(String noteId) { + return getInterpreterSettings(noteId).get(0); + } + + public List getInterpreterSettings(String noteId) { + List settings = new ArrayList<>(); + synchronized (interpreterSettings) { + List interpreterSettingIds = interpreterBindings.get(noteId); + if (interpreterSettingIds != null) { + for (String settingId : interpreterSettingIds) { + if (interpreterSettings.containsKey(settingId)) { + settings.add(interpreterSettings.get(settingId)); + } else { + LOGGER.warn("InterpreterSetting {} has been removed, but note {} still bind to it.", + settingId, noteId); + } + } + } + } + return settings; + } + + public InterpreterGroup getInterpreterGroupById(String groupId) { + for (InterpreterSetting setting : interpreterSettings.values()) { + InterpreterGroup interpreterGroup = setting.getInterpreterGroup(groupId); + if (interpreterGroup != null) { + return interpreterGroup; + } + } + return null; + } + + //TODO(zjffdu) logic here is a little ugly + public Map getEditorSetting(Interpreter interpreter, String user, String noteId, + String replName) { + Map editor = DEFAULT_EDITOR; + String group = StringUtils.EMPTY; + try { + String defaultSettingName = getDefaultInterpreterSetting(noteId).getName(); + List intpSettings = getInterpreterSettings(noteId); + for (InterpreterSetting intpSetting : intpSettings) { + String[] replNameSplit = replName.split("\\."); + if (replNameSplit.length == 2) { + group = replNameSplit[0]; + } + // when replName is 'name' of interpreter + if (defaultSettingName.equals(intpSetting.getName())) { + editor = intpSetting.getEditorFromSettingByClassName(interpreter.getClassName()); + } + // when replName is 'alias name' of interpreter or 'group' of interpreter + if (replName.equals(intpSetting.getName()) || group.equals(intpSetting.getName())) { + editor = intpSetting.getEditorFromSettingByClassName(interpreter.getClassName()); + break; + } + } + } catch (NullPointerException e) { + // Use `debug` level because this log occurs frequently + LOGGER.debug("Couldn't get interpreter editor setting"); + } + return editor; + } + + public List getAllInterpreterGroup() { + List interpreterGroups = new ArrayList<>(); + for (InterpreterSetting interpreterSetting : interpreterSettings.values()) { + interpreterGroups.addAll(interpreterSetting.getAllInterpreterGroups()); + } + return interpreterGroups; + } + + //TODO(zjffdu) move Resource related api to ResourceManager + public ResourceSet getAllResources() { + return getAllResourcesExcept(null); + } + + private ResourceSet getAllResourcesExcept(String interpreterGroupExcludsion) { + ResourceSet resourceSet = new ResourceSet(); + for (InterpreterGroup intpGroup : getAllInterpreterGroup()) { + if (interpreterGroupExcludsion != null && + intpGroup.getId().equals(interpreterGroupExcludsion)) { + continue; + } + + RemoteInterpreterProcess remoteInterpreterProcess = intpGroup.getRemoteInterpreterProcess(); + if (remoteInterpreterProcess == null) { + ResourcePool localPool = intpGroup.getResourcePool(); + if (localPool != null) { + resourceSet.addAll(localPool.getAll()); + } + } else if (remoteInterpreterProcess.isRunning()) { + List resourceList = remoteInterpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction>() { + @Override + public List call(RemoteInterpreterService.Client client) throws Exception { + return client.resourcePoolGetAll(); + } + }); + for (String res : resourceList) { + resourceSet.add(Resource.fromJson(res)); + } + } + } + return resourceSet; + } + + public void removeResourcesBelongsToParagraph(String noteId, String paragraphId) { + for (InterpreterGroup intpGroup : getAllInterpreterGroup()) { + ResourceSet resourceSet = new ResourceSet(); + RemoteInterpreterProcess remoteInterpreterProcess = intpGroup.getRemoteInterpreterProcess(); + if (remoteInterpreterProcess == null) { + ResourcePool localPool = intpGroup.getResourcePool(); + if (localPool != null) { + resourceSet.addAll(localPool.getAll()); + } + if (noteId != null) { + resourceSet = resourceSet.filterByNoteId(noteId); + } + if (paragraphId != null) { + resourceSet = resourceSet.filterByParagraphId(paragraphId); + } + + for (Resource r : resourceSet) { + localPool.remove( + r.getResourceId().getNoteId(), + r.getResourceId().getParagraphId(), + r.getResourceId().getName()); + } + } else if (remoteInterpreterProcess.isRunning()) { + List resourceList = remoteInterpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction>() { + @Override + public List call(RemoteInterpreterService.Client client) throws Exception { + return client.resourcePoolGetAll(); + } + }); + for (String res : resourceList) { + resourceSet.add(Resource.fromJson(res)); + } + + if (noteId != null) { + resourceSet = resourceSet.filterByNoteId(noteId); + } + if (paragraphId != null) { + resourceSet = resourceSet.filterByParagraphId(paragraphId); + } + + for (final Resource r : resourceSet) { + remoteInterpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + + @Override + public Void call(RemoteInterpreterService.Client client) throws Exception { + client.resourceRemove( + r.getResourceId().getNoteId(), + r.getResourceId().getParagraphId(), + r.getResourceId().getName()); + return null; + } + }); + } + } + } + } + + public void removeResourcesBelongsToNote(String noteId) { + removeResourcesBelongsToParagraph(noteId, null); + } + + /** + * Overwrite dependency jar under local-repo/{interpreterId} + * if jar file in original path is changed + */ + private void copyDependenciesFromLocalPath(final InterpreterSetting setting) { + setting.setStatus(InterpreterSetting.Status.DOWNLOADING_DEPENDENCIES); + synchronized (interpreterSettings) { + final Thread t = new Thread() { + public void run() { + try { + List deps = setting.getDependencies(); + if (deps != null) { + for (Dependency d : deps) { + File destDir = new File( + conf.getRelativeDir(ConfVars.ZEPPELIN_DEP_LOCALREPO)); + + int numSplits = d.getGroupArtifactVersion().split(":").length; + if (!(numSplits >= 3 && numSplits <= 6)) { + dependencyResolver.copyLocalDependency(d.getGroupArtifactVersion(), + new File(destDir, setting.getId())); + } + } + } + setting.setStatus(InterpreterSetting.Status.READY); + } catch (Exception e) { + LOGGER.error(String.format("Error while copying deps for interpreter group : %s," + + " go to interpreter setting page click on edit and save it again to make " + + "this interpreter work properly.", + setting.getGroup()), e); + setting.setErrorReason(e.getLocalizedMessage()); + setting.setStatus(InterpreterSetting.Status.ERROR); + } finally { + + } + } + }; + t.start(); + } + } + + /** + * Return ordered interpreter setting list. + * The list does not contain more than one setting from the same interpreter class. + * Order by InterpreterClass (order defined by ZEPPELIN_INTERPRETERS), Interpreter setting name + */ + public List getInterpreterSettingIds() { + List settingIdList = new ArrayList<>(); + for (InterpreterSetting interpreterSetting : get()) { + settingIdList.add(interpreterSetting.getId()); + } + return settingIdList; + } + + public InterpreterSetting createNewSetting(String name, String group, + List dependencies, InterpreterOption option, Map p) + throws IOException { + + if (name.indexOf(".") >= 0) { + throw new IOException("'.' is invalid for InterpreterSetting name."); + } + // check if name is existed + for (InterpreterSetting interpreterSetting : interpreterSettings.values()) { + if (interpreterSetting.getName().equals(name)) { + throw new IOException("Interpreter " + name + " already existed"); + } + } + InterpreterSetting setting = new InterpreterSetting(interpreterSettingTemplates.get(group)); + setting.setName(name); + setting.setGroup(group); + //TODO(zjffdu) Should use setDependencies + setting.appendDependencies(dependencies); + setting.setInterpreterOption(option); + setting.setProperties(p); + setting.setAppEventListener(appEventListener); + setting.setRemoteInterpreterProcessListener(remoteInterpreterProcessListener); + setting.setDependencyResolver(dependencyResolver); + setting.setAngularObjectRegistryListener(angularObjectRegistryListener); + setting.setInterpreterSettingManager(this); + setting.postProcessing(); + interpreterSettings.put(setting.getId(), setting); + saveToFile(); + return setting; + } + + @VisibleForTesting + public void addInterpreterSetting(InterpreterSetting interpreterSetting) { + interpreterSettingTemplates.put(interpreterSetting.getName(), interpreterSetting); + interpreterSetting.setAppEventListener(appEventListener); + interpreterSetting.setDependencyResolver(dependencyResolver); + interpreterSetting.setAngularObjectRegistryListener(angularObjectRegistryListener); + interpreterSetting.setRemoteInterpreterProcessListener(remoteInterpreterProcessListener); + interpreterSetting.setInterpreterSettingManager(this); + interpreterSettings.put(interpreterSetting.getId(), interpreterSetting); + } + + /** + * map interpreter ids into noteId + * + * @param user user name + * @param noteId note id + * @param settingIdList InterpreterSetting id list + */ + public void setInterpreterBinding(String user, String noteId, List settingIdList) + throws IOException { + List unBindedSettingIdList = new LinkedList<>(); + + synchronized (interpreterSettings) { + List oldSettingIdList = interpreterBindings.get(noteId); + if (oldSettingIdList != null) { + for (String oldSettingId : oldSettingIdList) { + if (!settingIdList.contains(oldSettingId)) { + unBindedSettingIdList.add(oldSettingId); + } + } + } + interpreterBindings.put(noteId, settingIdList); + saveToFile(); + + for (String settingId : unBindedSettingIdList) { + InterpreterSetting interpreterSetting = interpreterSettings.get(settingId); + //TODO(zjffdu) Add test for this scenario + //only close Interpreters when it is note scoped + if (interpreterSetting.getOption().perNoteIsolated() || + interpreterSetting.getOption().perNoteScoped()) { + interpreterSetting.closeInterpreters(user, noteId); + } + } + } + } + + public List getInterpreterBinding(String noteId) { + return interpreterBindings.get(noteId); + } + + @VisibleForTesting + public void closeNote(String user, String noteId) { + // close interpreters in this note session + LOGGER.info("Close Note: {}", noteId); + List settings = getInterpreterSettings(noteId); + for (InterpreterSetting setting : settings) { + setting.closeInterpreters(user, noteId); + } + } + + public Map getInterpreterSettingTemplates() { + return interpreterSettingTemplates; + } + + private URL[] recursiveBuildLibList(File path) throws MalformedURLException { + URL[] urls = new URL[0]; + if (path == null || !path.exists()) { + return urls; + } else if (path.getName().startsWith(".")) { + return urls; + } else if (path.isDirectory()) { + File[] files = path.listFiles(); + if (files != null) { + for (File f : files) { + urls = (URL[]) ArrayUtils.addAll(urls, recursiveBuildLibList(f)); + } + } + return urls; + } else { + return new URL[]{path.toURI().toURL()}; + } + } + + public List getRepositories() { + return this.interpreterRepositories; + } + + public void addRepository(String id, String url, boolean snapshot, Authentication auth, + Proxy proxy) throws IOException { + dependencyResolver.addRepo(id, url, snapshot, auth, proxy); + saveToFile(); + } + + public void removeRepository(String id) throws IOException { + dependencyResolver.delRepo(id); + saveToFile(); + } + + public void removeNoteInterpreterSettingBinding(String user, String noteId) throws IOException { + setInterpreterBinding(user, noteId, new ArrayList()); + interpreterBindings.remove(noteId); + } + + /** + * Change interpreter property and restart + */ + public void setPropertyAndRestart(String id, InterpreterOption option, + Map properties, + List dependencies) throws IOException { + synchronized (interpreterSettings) { + InterpreterSetting intpSetting = interpreterSettings.get(id); + if (intpSetting != null) { + try { + intpSetting.close(); + intpSetting.setOption(option); + intpSetting.setProperties(properties); + intpSetting.setDependencies(dependencies); + intpSetting.postProcessing(); + saveToFile(); + } catch (Exception e) { + loadFromFile(); + throw e; + } + } else { + throw new InterpreterException("Interpreter setting id " + id + " not found"); + } + } + } + + // restart in note page + public void restart(String settingId, String noteId, String user) { + InterpreterSetting intpSetting = interpreterSettings.get(settingId); + Preconditions.checkNotNull(intpSetting); + synchronized (interpreterSettings) { + intpSetting = interpreterSettings.get(settingId); + // Check if dependency in specified path is changed + // If it did, overwrite old dependency jar with new one + if (intpSetting != null) { + //clean up metaInfos + intpSetting.setInfos(null); + copyDependenciesFromLocalPath(intpSetting); + + if (user.equals("anonymous")) { + intpSetting.close(); + } else { + intpSetting.closeInterpreters(user, noteId); + } + + } else { + throw new InterpreterException("Interpreter setting id " + settingId + " not found"); + } + } + } + + public void restart(String id) { + restart(id, "", "anonymous"); + } + + public InterpreterSetting get(String id) { + synchronized (interpreterSettings) { + return interpreterSettings.get(id); + } + } + + @VisibleForTesting + public InterpreterSetting getByName(String name) { + for (InterpreterSetting interpreterSetting : interpreterSettings.values()) { + if (interpreterSetting.getName().equals(name)) { + return interpreterSetting; + } + } + throw new RuntimeException("No InterpreterSetting: " + name); + } + + public void remove(String id) throws IOException { + // 1. close interpreter groups of this interpreter setting + // 2. remove this interpreter setting + // 3. remove this interpreter setting from note binding + // 4. clean local repo directory + LOGGER.info("Remove interpreter setting: " + id); + synchronized (interpreterSettings) { + if (interpreterSettings.containsKey(id)) { + + InterpreterSetting intp = interpreterSettings.get(id); + intp.close(); + interpreterSettings.remove(id); + for (List settings : interpreterBindings.values()) { + Iterator it = settings.iterator(); + while (it.hasNext()) { + String settingId = it.next(); + if (settingId.equals(id)) { + it.remove(); + } + } + } + saveToFile(); + } + } + + File localRepoDir = new File(conf.getInterpreterLocalRepoPath() + "/" + id); + FileUtils.deleteDirectory(localRepoDir); + } + + /** + * Get interpreter settings + */ + public List get() { + synchronized (interpreterSettings) { + List orderedSettings = new ArrayList<>(interpreterSettings.values()); + Collections.sort(orderedSettings, new Comparator() { + @Override + public int compare(InterpreterSetting o1, InterpreterSetting o2) { + int i = interpreterGroupOrderList.indexOf(o1.getGroup()); + int j = interpreterGroupOrderList.indexOf(o2.getGroup()); + if (i < 0) { + LOGGER.warn("InterpreterGroup " + o1.getGroup() + + " is not specified in " + ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER.getVarName()); + // move the unknown interpreter to last + i = Integer.MAX_VALUE; + } + if (j < 0) { + LOGGER.warn("InterpreterGroup " + o2.getGroup() + + " is not specified in " + ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER.getVarName()); + // move the unknown interpreter to last + j = Integer.MAX_VALUE; + } + if (i < j) { + return -1; + } else if (i > j) { + return 1; + } else { + return 0; + } + } + }); + return orderedSettings; + } + } + + @VisibleForTesting + public List getSettingIds() { + List settingIds = new ArrayList<>(); + for (InterpreterSetting interpreterSetting : get()) { + settingIds.add(interpreterSetting.getId()); + } + return settingIds; + } + + public void close(String settingId) { + get(settingId).close(); + } + + public void close() { + List closeThreads = new LinkedList<>(); + synchronized (interpreterSettings) { + Collection intpSettings = interpreterSettings.values(); + for (final InterpreterSetting intpSetting : intpSettings) { + Thread t = new Thread() { + public void run() { + intpSetting.close(); + } + }; + t.start(); + closeThreads.add(t); + } + } + + for (Thread t : closeThreads) { + try { + t.join(); + } catch (InterruptedException e) { + LOGGER.error("Can't close interpreterGroup", e); + } + } + } + +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java similarity index 98% rename from zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java rename to zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java index 3838f63b20d..08175959b85 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java @@ -17,19 +17,17 @@ package org.apache.zeppelin.interpreter.install; import org.apache.commons.io.FileUtils; -import org.apache.log4j.ConsoleAppender; -import org.apache.log4j.Logger; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.dep.DependencyResolver; import org.apache.zeppelin.util.Util; import org.sonatype.aether.RepositoryException; + import java.io.File; import java.io.IOException; import java.net.URL; import java.util.LinkedList; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java similarity index 71% rename from zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java rename to zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java index 0ac71165348..74a2da2aad3 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java @@ -17,18 +17,18 @@ package org.apache.zeppelin.interpreter.remote; -import java.util.List; - +import com.google.gson.Gson; import org.apache.thrift.TException; import org.apache.zeppelin.display.AngularObject; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.AngularObjectRegistryListener; import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.Gson; +import java.util.List; /** * Proxy for AngularObjectRegistry that exists in remote interpreter process @@ -56,31 +56,29 @@ private RemoteInterpreterProcess getRemoteInterpreterProcess() { * @param noteId * @return */ - public AngularObject addAndNotifyRemoteProcess(String name, Object o, String noteId, String - paragraphId) { - Gson gson = new Gson(); + public AngularObject addAndNotifyRemoteProcess(final String name, + final Object o, + final String noteId, + final String paragraphId) { + RemoteInterpreterProcess remoteInterpreterProcess = getRemoteInterpreterProcess(); if (!remoteInterpreterProcess.isRunning()) { return super.add(name, o, noteId, paragraphId, true); } - Client client = null; - boolean broken = false; - try { - client = remoteInterpreterProcess.getClient(); - client.angularObjectAdd(name, noteId, paragraphId, gson.toJson(o)); - return super.add(name, o, noteId, paragraphId, true); - } catch (TException e) { - broken = true; - logger.error("Error", e); - } catch (Exception e) { - logger.error("Error", e); - } finally { - if (client != null) { - remoteInterpreterProcess.releaseClient(client, broken); - } - } - return null; + remoteInterpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public Void call(Client client) throws Exception { + Gson gson = new Gson(); + client.angularObjectAdd(name, noteId, paragraphId, gson.toJson(o)); + return null; + } + } + ); + + return super.add(name, o, noteId, paragraphId, true); + } /** @@ -91,30 +89,24 @@ public AngularObject addAndNotifyRemoteProcess(String name, Object o, String not * @param paragraphId * @return */ - public AngularObject removeAndNotifyRemoteProcess(String name, String noteId, String - paragraphId) { + public AngularObject removeAndNotifyRemoteProcess(final String name, + final String noteId, + final String paragraphId) { RemoteInterpreterProcess remoteInterpreterProcess = getRemoteInterpreterProcess(); if (remoteInterpreterProcess == null || !remoteInterpreterProcess.isRunning()) { return super.remove(name, noteId, paragraphId); } - - Client client = null; - boolean broken = false; - try { - client = remoteInterpreterProcess.getClient(); - client.angularObjectRemove(name, noteId, paragraphId); - return super.remove(name, noteId, paragraphId); - } catch (TException e) { - broken = true; - logger.error("Error", e); - } catch (Exception e) { - logger.error("Error", e); - } finally { - if (client != null) { - remoteInterpreterProcess.releaseClient(client, broken); + remoteInterpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public Void call(Client client) throws Exception { + client.angularObjectRemove(name, noteId, paragraphId); + return null; + } } - } - return null; + ); + + return super.remove(name, noteId, paragraphId); } public void removeAllAndNotifyRemoteProcess(String noteId, String paragraphId) { diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java new file mode 100644 index 00000000000..bb90dd89fb3 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java @@ -0,0 +1,371 @@ +/* + * 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.interpreter.remote; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.apache.thrift.TException; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.display.AngularObject; +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.display.Input; +import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterContext; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterResult; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterResultMessage; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; +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 java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * Proxy for Interpreter instance that runs on separate process + */ +public class RemoteInterpreter extends Interpreter { + private static final Logger LOGGER = LoggerFactory.getLogger(RemoteInterpreter.class); + private static final Gson gson = new Gson(); + + + private String className; + private String sessionId; + private String userName; + private FormType formType; + + private RemoteInterpreterProcess interpreterProcess; + private volatile boolean isOpened = false; + private volatile boolean isCreated = false; + + /** + * Remote interpreter and manage interpreter process + */ + public RemoteInterpreter(Properties properties, + String sessionId, + String className, + String userName) { + super(properties); + this.sessionId = sessionId; + this.className = className; + this.userName = userName; + } + + public boolean isOpened() { + return isOpened; + } + + @Override + public String getClassName() { + return className; + } + + public String getSessionId() { + return this.sessionId; + } + + public synchronized RemoteInterpreterProcess getOrCreateInterpreterProcess() { + if (this.interpreterProcess != null) { + return this.interpreterProcess; + } + InterpreterGroup intpGroup = getInterpreterGroup(); + this.interpreterProcess = intpGroup.getOrCreateInterpreterProcess(); + synchronized (interpreterProcess) { + if (!interpreterProcess.isRunning()) { + interpreterProcess.start(userName, false); + interpreterProcess.getRemoteInterpreterEventPoller() + .setInterpreterProcess(interpreterProcess); + interpreterProcess.getRemoteInterpreterEventPoller().setInterpreterGroup(intpGroup); + interpreterProcess.getRemoteInterpreterEventPoller().start(); + } + } + return interpreterProcess; + } + + @Override + public void open() { + synchronized (this) { + if (!isOpened) { + // create all the interpreters of the same session first, then Open the internal interpreter + // of this RemoteInterpreter. + // The why we we create all the interpreter of the session is because some interpreter + // depends on other interpreter. e.g. PySparkInterpreter depends on SparkInterpreter. + // also see method Interpreter.getInterpreterInTheSameSessionByClassName + for (Interpreter interpreter : getInterpreterGroup().getOrCreateSession( + userName, sessionId)) { + ((RemoteInterpreter) interpreter).internal_create(); + } + + interpreterProcess.callRemoteFunction(new RemoteInterpreterProcess.RemoteFunction() { + @Override + public Void call(Client client) throws Exception { + LOGGER.info("Open RemoteInterpreter {}", getClassName()); + client.open(sessionId, className); + // Push angular object loaded from JSON file to remote interpreter + synchronized (getInterpreterGroup()) { + if (!getInterpreterGroup().isAngularRegistryPushed()) { + pushAngularObjectRegistryToRemote(client); + getInterpreterGroup().setAngularRegistryPushed(true); + } + } + return null; + } + }); + isOpened = true; + } + } + } + + private void internal_create() { + synchronized (this) { + if (!isCreated) { + RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); + interpreterProcess.callRemoteFunction(new RemoteInterpreterProcess.RemoteFunction() { + @Override + public Void call(Client client) throws Exception { + LOGGER.info("Create RemoteInterpreter {}", getClassName()); + client.createInterpreter(getInterpreterGroup().getId(), sessionId, + className, (Map) property, userName); + return null; + } + }); + isCreated = true; + } + } + } + + + @Override + public void close() { + if (isOpened) { + RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); + interpreterProcess.callRemoteFunction(new RemoteInterpreterProcess.RemoteFunction() { + @Override + public Void call(Client client) throws Exception { + client.close(sessionId, className); + return null; + } + }); + } else { + LOGGER.warn("close is called when RemoterInterpreter is not opened for " + className); + } + } + + @Override + public InterpreterResult interpret(final String st, final InterpreterContext context) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("st:\n{}", st); + } + + final FormType form = getFormType(); + RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); + InterpreterContextRunnerPool interpreterContextRunnerPool = interpreterProcess + .getInterpreterContextRunnerPool(); + List runners = context.getRunners(); + if (runners != null && runners.size() != 0) { + // assume all runners in this InterpreterContext have the same note id + String noteId = runners.get(0).getNoteId(); + + interpreterContextRunnerPool.clear(noteId); + interpreterContextRunnerPool.addAll(noteId, runners); + } + return interpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public InterpreterResult call(Client client) throws Exception { + + RemoteInterpreterResult remoteResult = client.interpret( + sessionId, className, st, convert(context)); + Map remoteConfig = (Map) gson.fromJson( + remoteResult.getConfig(), new TypeToken>() { + }.getType()); + context.getConfig().clear(); + context.getConfig().putAll(remoteConfig); + GUI currentGUI = context.getGui(); + if (form == FormType.NATIVE) { + GUI remoteGui = GUI.fromJson(remoteResult.getGui()); + currentGUI.clear(); + currentGUI.setParams(remoteGui.getParams()); + currentGUI.setForms(remoteGui.getForms()); + } else if (form == FormType.SIMPLE) { + final Map currentForms = currentGUI.getForms(); + final Map currentParams = currentGUI.getParams(); + final GUI remoteGUI = GUI.fromJson(remoteResult.getGui()); + final Map remoteForms = remoteGUI.getForms(); + final Map remoteParams = remoteGUI.getParams(); + currentForms.putAll(remoteForms); + currentParams.putAll(remoteParams); + } + + InterpreterResult result = convert(remoteResult); + return result; + } + } + ); + + } + + @Override + public void cancel(final InterpreterContext context) { + if (!isOpened) { + LOGGER.warn("Cancel is called when RemoterInterpreter is not opened for " + className); + return; + } + RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); + interpreterProcess.callRemoteFunction(new RemoteInterpreterProcess.RemoteFunction() { + @Override + public Void call(Client client) throws Exception { + client.cancel(sessionId, className, convert(context)); + return null; + } + }); + } + + @Override + public FormType getFormType() { + if (formType != null) { + return formType; + } + + // it is possible to call getFormType before it is opened + synchronized (this) { + if (!isOpened) { + open(); + } + } + RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); + FormType type = interpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public FormType call(Client client) throws Exception { + formType = FormType.valueOf(client.getFormType(sessionId, className)); + return formType; + } + }); + return type; + } + + @Override + public int getProgress(final InterpreterContext context) { + if (!isOpened) { + LOGGER.warn("getProgress is called when RemoterInterpreter is not opened for " + className); + return 0; + } + RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); + return interpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public Integer call(Client client) throws Exception { + return client.getProgress(sessionId, className, convert(context)); + } + }); + } + + + @Override + public List completion(final String buf, final int cursor, + final InterpreterContext interpreterContext) { + if (!isOpened) { + LOGGER.warn("completion is called when RemoterInterpreter is not opened for " + className); + return new ArrayList<>(); + } + RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); + return interpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction>() { + @Override + public List call(Client client) throws Exception { + return client.completion(sessionId, className, buf, cursor, + convert(interpreterContext)); + } + }); + } + + public String getStatus(final String jobId) { + if (!isOpened) { + LOGGER.warn("getStatus is called when RemoteInterpreter is not opened for " + className); + return Job.Status.UNKNOWN.name(); + } + RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); + return interpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public String call(Client client) throws Exception { + return client.getStatus(sessionId, jobId); + } + }); + } + + //TODO(zjffdu) Share the Scheduler in the same session or in the same InterpreterGroup ? + @Override + public Scheduler getScheduler() { + int maxConcurrency = Integer.parseInt( + property.getProperty("zeppelin.interpreter.max.poolsize", + ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_MAX_POOL_SIZE.getIntValue() + "")); + return SchedulerFactory.singleton().createOrGetRemoteScheduler( + RemoteInterpreter.class.getName() + "-" + sessionId, + sessionId, this, maxConcurrency); + } + + private RemoteInterpreterContext convert(InterpreterContext ic) { + return new RemoteInterpreterContext(ic.getNoteId(), ic.getParagraphId(), ic.getReplName(), + ic.getParagraphTitle(), ic.getParagraphText(), gson.toJson(ic.getAuthenticationInfo()), + gson.toJson(ic.getConfig()), gson.toJson(ic.getGui()), gson.toJson(ic.getRunners())); + } + + private InterpreterResult convert(RemoteInterpreterResult result) { + InterpreterResult r = new InterpreterResult( + InterpreterResult.Code.valueOf(result.getCode())); + + for (RemoteInterpreterResultMessage m : result.getMsg()) { + r.add(InterpreterResult.Type.valueOf(m.getType()), m.getData()); + } + + return r; + } + + /** + * Push local angular object registry to + * remote interpreter. This method should be + * call ONLY once when the first Interpreter is created + */ + private void pushAngularObjectRegistryToRemote(Client client) throws TException { + final AngularObjectRegistry angularObjectRegistry = this.getInterpreterGroup() + .getAngularObjectRegistry(); + if (angularObjectRegistry != null && angularObjectRegistry.getRegistry() != null) { + final Map> registry = angularObjectRegistry + .getRegistry(); + LOGGER.info("Push local angular object registry from ZeppelinServer to" + + " remote interpreter group {}", this.getInterpreterGroup().getId()); + final java.lang.reflect.Type registryType = new TypeToken>>() { + }.getType(); + client.angularRegistryPush(gson.toJson(registry, registryType)); + } + } + + @Override + public String toString() { + return "RemoteInterpreter_" + className + "_" + sessionId; + } +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java index 26c9d7994ed..f3bce2fa091 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java @@ -29,6 +29,7 @@ import org.apache.zeppelin.interpreter.RemoteZeppelinServerResource; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterEvent; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterEventType; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; import org.apache.zeppelin.interpreter.thrift.ZeppelinServerResourceParagraphRunner; import org.apache.zeppelin.resource.Resource; @@ -38,6 +39,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.ByteBuffer; @@ -84,7 +86,6 @@ public void setInterpreterGroup(InterpreterGroup interpreterGroup) { @Override public void run() { - Client client = null; AppendOutputRunner runner = new AppendOutputRunner(listener); ScheduledFuture appendFuture = appendService.scheduleWithFixedDelay( runner, 0, AppendOutputRunner.BUFFER_TIME_MS, TimeUnit.MILLISECONDS); @@ -100,26 +101,14 @@ public void run() { continue; } - try { - client = interpreterProcess.getClient(); - } catch (Exception e1) { - logger.error("Can't get RemoteInterpreterEvent", e1); - waitQuietly(); - continue; - } - - RemoteInterpreterEvent event = null; - boolean broken = false; - try { - event = client.getEvent(); - } catch (TException e) { - broken = true; - logger.error("Can't get RemoteInterpreterEvent", e); - waitQuietly(); - continue; - } finally { - interpreterProcess.releaseClient(client, broken); - } + RemoteInterpreterEvent event = interpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public RemoteInterpreterEvent call(Client client) throws Exception { + return client.getEvent(); + } + } + ); AngularObjectRegistry angularObjectRegistry = interpreterGroup.getAngularObjectRegistry(); @@ -286,10 +275,7 @@ private void progressRemoteZeppelinControlEvent( boolean broken = false; final Gson gson = new Gson(); final String eventOwnerKey = reqResourceBody.getOwnerKey(); - Client interpreterServerMain = null; try { - interpreterServerMain = interpreterProcess.getClient(); - final Client eventClient = interpreterServerMain; if (resourceType == RemoteZeppelinServerResource.Type.PARAGRAPH_RUNNERS) { final List remoteRunners = new LinkedList<>(); @@ -308,7 +294,6 @@ private void progressRemoteZeppelinControlEvent( @Override public void onFinished(Object resultObject) { - boolean clientBroken = false; if (resultObject != null && resultObject instanceof List) { List runnerList = (List) resultObject; @@ -324,15 +309,15 @@ public void onFinished(Object resultObject) { resResource.setResourceType(RemoteZeppelinServerResource.Type.PARAGRAPH_RUNNERS); resResource.setData(remoteRunners); - try { - eventClient.onReceivedZeppelinResource(resResource.toJson()); - } catch (Exception e) { - clientBroken = true; - logger.error("Can't get RemoteInterpreterEvent", e); - waitQuietly(); - } finally { - interpreterProcess.releaseClient(eventClient, clientBroken); - } + interpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public Void call(Client client) throws Exception { + client.onReceivedZeppelinResource(resResource.toJson()); + return null; + } + } + ); } } @@ -346,39 +331,32 @@ public void onError() { reqRunnerContext.getNoteId(), reqRunnerContext.getParagraphId(), callBackEvent); } } catch (Exception e) { - broken = true; logger.error("Can't get RemoteInterpreterEvent", e); waitQuietly(); - } finally { - interpreterProcess.releaseClient(interpreterServerMain, broken); } } - private void sendResourcePoolResponseGetAll(ResourceSet resourceSet) { - Client client = null; - boolean broken = false; - try { - client = interpreterProcess.getClient(); - List resourceList = new LinkedList<>(); - Gson gson = new Gson(); - for (Resource r : resourceSet) { - resourceList.add(gson.toJson(r)); - } - client.resourcePoolResponseGetAll(resourceList); - } catch (Exception e) { - logger.error(e.getMessage(), e); - broken = true; - } finally { - if (client != null) { - interpreterProcess.releaseClient(client, broken); - } - } + private void sendResourcePoolResponseGetAll(final ResourceSet resourceSet) { + interpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public Void call(Client client) throws Exception { + List resourceList = new LinkedList<>(); + for (Resource r : resourceSet) { + resourceList.add(r.toJson()); + } + client.resourcePoolResponseGetAll(resourceList); + return null; + } + } + ); } private ResourceSet getAllResourcePoolExcept() { ResourceSet resourceSet = new ResourceSet(); - for (InterpreterGroup intpGroup : InterpreterGroup.getAll()) { + for (InterpreterGroup intpGroup : interpreterGroup.getInterpreterSetting() + .getInterpreterSettingManager().getAllInterpreterGroup()) { if (intpGroup.getId().equals(interpreterGroup.getId())) { continue; } @@ -390,115 +368,94 @@ private ResourceSet getAllResourcePoolExcept() { resourceSet.addAll(localPool.getAll()); } } else if (interpreterProcess.isRunning()) { - Client client = null; - boolean broken = false; - try { - client = remoteInterpreterProcess.getClient(); - List resourceList = client.resourcePoolGetAll(); - Gson gson = new Gson(); - for (String res : resourceList) { - resourceSet.add(Resource.fromJson(res)); - } - } catch (Exception e) { - logger.error(e.getMessage(), e); - broken = true; - } finally { - if (client != null) { - intpGroup.getRemoteInterpreterProcess().releaseClient(client, broken); - } + List resourceList = remoteInterpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction>() { + @Override + public List call(Client client) throws Exception { + return client.resourcePoolGetAll(); + } + } + ); + for (String res : resourceList) { + resourceSet.add(Resource.fromJson(res)); } } } return resourceSet; } - private void sendResourceResponseGet(ResourceId resourceId, Object o) { - Client client = null; - boolean broken = false; - try { - client = interpreterProcess.getClient(); - Gson gson = new Gson(); - String rid = gson.toJson(resourceId); - ByteBuffer obj; - if (o == null) { - obj = ByteBuffer.allocate(0); - } else { - obj = Resource.serializeObject(o); - } - client.resourceResponseGet(rid, obj); - } catch (Exception e) { - logger.error(e.getMessage(), e); - broken = true; - } finally { - if (client != null) { - interpreterProcess.releaseClient(client, broken); - } - } + private void sendResourceResponseGet(final ResourceId resourceId, final Object o) { + interpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public Void call(Client client) throws Exception { + String rid = resourceId.toJson(); + ByteBuffer obj; + if (o == null) { + obj = ByteBuffer.allocate(0); + } else { + obj = Resource.serializeObject(o); + } + client.resourceResponseGet(rid, obj); + return null; + } + } + ); } - private Object getResource(ResourceId resourceId) { - InterpreterGroup intpGroup = InterpreterGroup.getByInterpreterGroupId( - resourceId.getResourcePoolId()); + private Object getResource(final ResourceId resourceId) { + InterpreterGroup intpGroup = interpreterGroup.getInterpreterSetting() + .getInterpreterSettingManager() + .getInterpreterGroupById(resourceId.getResourcePoolId()); if (intpGroup == null) { return null; } RemoteInterpreterProcess remoteInterpreterProcess = intpGroup.getRemoteInterpreterProcess(); - if (remoteInterpreterProcess == null) { - ResourcePool localPool = intpGroup.getResourcePool(); - if (localPool != null) { - return localPool.get(resourceId.getName()); - } - } else if (interpreterProcess.isRunning()) { - Client client = null; - boolean broken = false; - try { - client = remoteInterpreterProcess.getClient(); - ByteBuffer res = client.resourceGet( - resourceId.getNoteId(), - resourceId.getParagraphId(), - resourceId.getName()); - Object o = Resource.deserializeObject(res); - return o; - } catch (Exception e) { - logger.error(e.getMessage(), e); - broken = true; - } finally { - if (client != null) { - intpGroup.getRemoteInterpreterProcess().releaseClient(client, broken); + ByteBuffer buffer = remoteInterpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public ByteBuffer call(Client client) throws Exception { + return client.resourceGet( + resourceId.getNoteId(), + resourceId.getParagraphId(), + resourceId.getName()); + } } - } - } - return null; - } + ); - public void sendInvokeMethodResult(InvokeResourceMethodEventMessage message, Object o) { - Client client = null; - boolean broken = false; try { - client = interpreterProcess.getClient(); - Gson gson = new Gson(); - String invokeMessage = gson.toJson(message); - ByteBuffer obj; - if (o == null) { - obj = ByteBuffer.allocate(0); - } else { - obj = Resource.serializeObject(o); - } - client.resourceResponseInvokeMethod(invokeMessage, obj); + Object o = Resource.deserializeObject(buffer); + return o; } catch (Exception e) { logger.error(e.getMessage(), e); - broken = true; - } finally { - if (client != null) { - interpreterProcess.releaseClient(client, broken); - } } + return null; } - private Object invokeResourceMethod(InvokeResourceMethodEventMessage message) { - ResourceId resourceId = message.resourceId; - InterpreterGroup intpGroup = InterpreterGroup.getByInterpreterGroupId( - resourceId.getResourcePoolId()); + public void sendInvokeMethodResult(final InvokeResourceMethodEventMessage message, + final Object o) { + interpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public Void call(Client client) throws Exception { + String invokeMessage = message.toJson(); + ByteBuffer obj; + if (o == null) { + obj = ByteBuffer.allocate(0); + } else { + obj = Resource.serializeObject(o); + } + client.resourceResponseInvokeMethod(invokeMessage, obj); + return null; + } + } + ); + } + + private Object invokeResourceMethod(final InvokeResourceMethodEventMessage message) { + final ResourceId resourceId = message.resourceId; + InterpreterGroup intpGroup = interpreterGroup.getInterpreterSetting() + .getInterpreterSettingManager().getInterpreterGroupById(resourceId.getResourcePoolId()); if (intpGroup == null) { return null; } @@ -529,25 +486,25 @@ private Object invokeResourceMethod(InvokeResourceMethodEventMessage message) { return null; } } else if (interpreterProcess.isRunning()) { - Client client = null; - boolean broken = false; + ByteBuffer res = interpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public ByteBuffer call(Client client) throws Exception { + return client.resourceInvokeMethod( + resourceId.getNoteId(), + resourceId.getParagraphId(), + resourceId.getName(), + message.toJson()); + } + } + ); + try { - client = remoteInterpreterProcess.getClient(); - ByteBuffer res = client.resourceInvokeMethod( - resourceId.getNoteId(), - resourceId.getParagraphId(), - resourceId.getName(), - gson.toJson(message)); - Object o = Resource.deserializeObject(res); - return o; + return Resource.deserializeObject(res); } catch (Exception e) { logger.error(e.getMessage(), e); - broken = true; - } finally { - if (client != null) { - intpGroup.getRemoteInterpreterProcess().releaseClient(client, broken); - } } + return null; } return null; } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java similarity index 93% rename from zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java rename to zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java index 1fb9b90771c..19356fb16ed 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java @@ -21,6 +21,7 @@ import org.apache.commons.exec.environment.EnvironmentUtils; import org.apache.zeppelin.helium.ApplicationEventListener; import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -97,6 +98,7 @@ public void start(String userName, Boolean isUserImpersonate) { // start server process try { port = RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces(); + logger.info("Choose port {} for RemoteInterpreterProcess", port); } catch (IOException e1) { throw new InterpreterException(e1); } @@ -172,6 +174,17 @@ public void start(String userName, Boolean isUserImpersonate) { public void stop() { if (isRunning()) { logger.info("kill interpreter process"); + try { + callRemoteFunction(new RemoteFunction() { + @Override + public Void call(RemoteInterpreterService.Client client) throws Exception { + client.shutdown(); + return null; + } + }); + } catch (Exception e) { + logger.warn("ignore the exception when shutting down"); + } watchdog.destroyProcess(); } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java index 1d48a1e6f6e..a78088c4cb5 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java @@ -20,10 +20,13 @@ import org.apache.commons.pool2.impl.GenericObjectPool; import org.apache.thrift.TException; import org.apache.zeppelin.helium.ApplicationEventListener; +import org.apache.zeppelin.interpreter.InterpreterException; import org.apache.zeppelin.interpreter.InterpreterGroup; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; /** @@ -32,9 +35,6 @@ public abstract class RemoteInterpreterProcess { private static final Logger logger = LoggerFactory.getLogger(RemoteInterpreterProcess.class); - // number of sessions that are attached to this process - private final AtomicInteger referenceCount; - private GenericObjectPool clientPool; private final RemoteInterpreterEventPoller remoteInterpreterEventPoller; private final InterpreterContextRunnerPool interpreterContextRunnerPool; @@ -46,16 +46,20 @@ public RemoteInterpreterProcess( ApplicationEventListener appListener) { this(new RemoteInterpreterEventPoller(listener, appListener), connectTimeout); + this.remoteInterpreterEventPoller.setInterpreterProcess(this); } RemoteInterpreterProcess(RemoteInterpreterEventPoller remoteInterpreterEventPoller, int connectTimeout) { this.interpreterContextRunnerPool = new InterpreterContextRunnerPool(); - referenceCount = new AtomicInteger(0); this.remoteInterpreterEventPoller = remoteInterpreterEventPoller; this.connectTimeout = connectTimeout; } + public RemoteInterpreterEventPoller getRemoteInterpreterEventPoller() { + return remoteInterpreterEventPoller; + } + public abstract String getHost(); public abstract int getPort(); public abstract void start(String userName, Boolean isUserImpersonate); @@ -66,37 +70,18 @@ public int getConnectTimeout() { return connectTimeout; } - public int reference(InterpreterGroup interpreterGroup, String userName, - Boolean isUserImpersonate) { - synchronized (referenceCount) { - if (!isRunning()) { - start(userName, isUserImpersonate); - } - - if (clientPool == null) { - clientPool = new GenericObjectPool<>(new ClientFactory(getHost(), getPort())); - clientPool.setTestOnBorrow(true); - - remoteInterpreterEventPoller.setInterpreterGroup(interpreterGroup); - remoteInterpreterEventPoller.setInterpreterProcess(this); - remoteInterpreterEventPoller.start(); - } - return referenceCount.incrementAndGet(); - } - } - - public Client getClient() throws Exception { + public synchronized Client getClient() throws Exception { if (clientPool == null || clientPool.isClosed()) { - return null; + clientPool = new GenericObjectPool<>(new ClientFactory(getHost(), getPort())); } return clientPool.borrowObject(); } - public void releaseClient(Client client) { + private void releaseClient(Client client) { releaseClient(client, false); } - public void releaseClient(Client client, boolean broken) { + private void releaseClient(Client client, boolean broken) { if (broken) { releaseBrokenClient(client); } else { @@ -108,7 +93,7 @@ public void releaseClient(Client client, boolean broken) { } } - public void releaseBrokenClient(Client client) { + private void releaseBrokenClient(Client client) { try { clientPool.invalidateObject(client); } catch (Exception e) { @@ -116,90 +101,6 @@ public void releaseBrokenClient(Client client) { } } - public int dereference() { - synchronized (referenceCount) { - int r = referenceCount.decrementAndGet(); - if (r == 0) { - logger.info("shutdown interpreter process"); - remoteInterpreterEventPoller.shutdown(); - - // first try shutdown - Client client = null; - try { - client = getClient(); - client.shutdown(); - } catch (Exception e) { - // safely ignore exception while client.shutdown() may terminates remote process - logger.info("Exception in RemoteInterpreterProcess while synchronized dereference, can " + - "safely ignore exception while client.shutdown() may terminates remote process"); - logger.debug(e.getMessage(), e); - } finally { - if (client != null) { - // no longer used - releaseBrokenClient(client); - } - } - - clientPool.clear(); - clientPool.close(); - - // wait for some time (connectTimeout) and force kill - // remote process server.serve() loop is not always finishing gracefully - long startTime = System.currentTimeMillis(); - while (System.currentTimeMillis() - startTime < connectTimeout) { - if (this.isRunning()) { - try { - Thread.sleep(500); - } catch (InterruptedException e) { - logger.error("Exception in RemoteInterpreterProcess while synchronized dereference " + - "Thread.sleep", e); - } - } else { - break; - } - } - } - return r; - } - } - - public int referenceCount() { - synchronized (referenceCount) { - return referenceCount.get(); - } - } - - public int getNumActiveClient() { - if (clientPool == null) { - return 0; - } else { - return clientPool.getNumActive(); - } - } - - public int getNumIdleClient() { - if (clientPool == null) { - return 0; - } else { - return clientPool.getNumIdle(); - } - } - - public void setMaxPoolSize(int size) { - if (clientPool != null) { - //Size + 2 for progress poller , cancel operation - clientPool.setMaxTotal(size + 2); - } - } - - public int getMaxPoolSize() { - if (clientPool != null) { - return clientPool.getMaxTotal(); - } else { - return 0; - } - } - /** * Called when angular object is updated in client side to propagate * change to the remote process @@ -239,4 +140,33 @@ public void updateRemoteAngularObject(String name, String noteId, String paragra public InterpreterContextRunnerPool getInterpreterContextRunnerPool() { return interpreterContextRunnerPool; } + + public T callRemoteFunction(RemoteFunction func) { + Client client = null; + boolean broken = false; + try { + client = getClient(); + if (client != null) { + return func.call(client); + } + } catch (TException e) { + broken = true; + throw new InterpreterException(e); + } catch (Exception e1) { + throw new InterpreterException(e1); + } finally { + if (client != null) { + releaseClient(client, broken); + } + } + return null; + } + + /** + * + * @param + */ + public interface RemoteFunction { + T call(Client client) throws Exception; + } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java similarity index 100% rename from zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java rename to zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java index 38534682b34..3d8123efddf 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java @@ -106,6 +106,7 @@ public void run() { @Override public void shutdown() throws TException { + logger.info("Shutting down..."); eventClient.waitForEventQueueBecomesEmpty(DEFAULT_SHUTDOWN_TIMEOUT); if (interpreterGroup != null) { interpreterGroup.close(); @@ -159,7 +160,7 @@ public static void main(String[] args) } @Override - public void createInterpreter(String interpreterGroupId, String sessionKey, String + public void createInterpreter(String interpreterGroupId, String sessionId, String className, Map properties, String userName) throws TException { if (interpreterGroup == null) { interpreterGroup = new InterpreterGroup(interpreterGroupId); @@ -190,20 +191,11 @@ public void createInterpreter(String interpreterGroupId, String sessionKey, Stri replClass.getConstructor(new Class[] {Properties.class}); Interpreter repl = constructor.newInstance(p); repl.setClassloaderUrls(new URL[]{}); - - synchronized (interpreterGroup) { - List interpreters = interpreterGroup.get(sessionKey); - if (interpreters == null) { - interpreters = new LinkedList<>(); - interpreterGroup.put(sessionKey, interpreters); - } - - interpreters.add(new LazyOpenInterpreter(repl)); - } - logger.info("Instantiate interpreter {}", className); repl.setInterpreterGroup(interpreterGroup); repl.setUserName(userName); + + interpreterGroup.addInterpreterToSession(new LazyOpenInterpreter(repl), sessionId); } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { @@ -237,13 +229,13 @@ private void setSystemProperty(Properties properties) { } } - protected Interpreter getInterpreter(String sessionKey, String className) throws TException { + protected Interpreter getInterpreter(String sessionId, String className) throws TException { if (interpreterGroup == null) { throw new TException( new InterpreterException("Interpreter instance " + className + " not created")); } synchronized (interpreterGroup) { - List interpreters = interpreterGroup.get(sessionKey); + List interpreters = interpreterGroup.get(sessionId); if (interpreters == null) { throw new TException( new InterpreterException("Interpreter " + className + " not initialized")); @@ -259,19 +251,20 @@ protected Interpreter getInterpreter(String sessionKey, String className) throws } @Override - public void open(String noteId, String className) throws TException { - Interpreter intp = getInterpreter(noteId, className); + public void open(String sessionId, String className) throws TException { + logger.info(String.format("Open Interpreter %s for session %s ", className, sessionId)); + Interpreter intp = getInterpreter(sessionId, className); intp.open(); } @Override - public void close(String sessionKey, String className) throws TException { + public void close(String sessionId, String className) throws TException { // unload all applications for (String appId : runningApplications.keySet()) { RunningApplication appInfo = runningApplications.get(appId); // see NoteInterpreterLoader.SHARED_SESSION - if (appInfo.noteId.equals(sessionKey) || sessionKey.equals("shared_session")) { + if (appInfo.noteId.equals(sessionId) || sessionId.equals("shared_session")) { try { logger.info("Unload App {} ", appInfo.pkg.getName()); appInfo.app.unload(); @@ -286,7 +279,7 @@ public void close(String sessionKey, String className) throws TException { // close interpreters List interpreters; synchronized (interpreterGroup) { - interpreters = interpreterGroup.get(sessionKey); + interpreters = interpreterGroup.get(sessionId); } if (interpreters != null) { Iterator it = interpreters.iterator(); @@ -322,7 +315,6 @@ public RemoteInterpreterResult interpret(String noteId, String className, String intp, st, context); - scheduler.submit(job); while (!job.isTerminated()) { @@ -566,30 +558,34 @@ public void cancel(String noteId, String className, RemoteInterpreterContext int } @Override - public int getProgress(String noteId, String className, + public int getProgress(String sessionId, String className, RemoteInterpreterContext interpreterContext) throws TException { Integer manuallyProvidedProgress = progressMap.get(interpreterContext.getParagraphId()); if (manuallyProvidedProgress != null) { return manuallyProvidedProgress; } else { - Interpreter intp = getInterpreter(noteId, className); + Interpreter intp = getInterpreter(sessionId, className); + if (intp == null) { + throw new TException("No interpreter {} existed for session {}".format( + className, sessionId)); + } return intp.getProgress(convert(interpreterContext, null)); } } @Override - public String getFormType(String noteId, String className) throws TException { - Interpreter intp = getInterpreter(noteId, className); + public String getFormType(String sessionId, String className) throws TException { + Interpreter intp = getInterpreter(sessionId, className); return intp.getFormType().toString(); } @Override - public List completion(String noteId, + public List completion(String sessionId, String className, String buf, int cursor, RemoteInterpreterContext remoteInterpreterContext) throws TException { - Interpreter intp = getInterpreter(noteId, className); + Interpreter intp = getInterpreter(sessionId, className); List completion = intp.completion(buf, cursor, convert(remoteInterpreterContext, null)); return completion; } @@ -766,16 +762,16 @@ private RemoteInterpreterResult convert(InterpreterResult result, } @Override - public String getStatus(String sessionKey, String jobId) + public String getStatus(String sessionId, String jobId) throws TException { if (interpreterGroup == null) { - return "Unknown"; + return Status.UNKNOWN.name(); } synchronized (interpreterGroup) { - List interpreters = interpreterGroup.get(sessionKey); + List interpreters = interpreterGroup.get(sessionId); if (interpreters == null) { - return "Unknown"; + return Status.UNKNOWN.name(); } for (Interpreter intp : interpreters) { @@ -792,7 +788,7 @@ public String getStatus(String sessionKey, String jobId) } } } - return "Unknown"; + return Status.UNKNOWN.name(); } 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 deleted file mode 100644 index b26995ada2d..00000000000 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/ResourcePoolUtils.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * 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.resource; - -import org.apache.zeppelin.interpreter.InterpreterGroup; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; -import org.slf4j.Logger; - -import java.util.List; - -/** - * Utilities for ResourcePool - */ -public class ResourcePoolUtils { - static Logger logger = org.slf4j.LoggerFactory.getLogger(ResourcePoolUtils.class); - - public static ResourceSet getAllResources() { - return getAllResourcesExcept(null); - } - - public static ResourceSet getAllResourcesExcept(String interpreterGroupExcludsion) { - ResourceSet resourceSet = new ResourceSet(); - for (InterpreterGroup intpGroup : InterpreterGroup.getAll()) { - if (interpreterGroupExcludsion != null && - intpGroup.getId().equals(interpreterGroupExcludsion)) { - continue; - } - - RemoteInterpreterProcess remoteInterpreterProcess = intpGroup.getRemoteInterpreterProcess(); - if (remoteInterpreterProcess == null) { - ResourcePool localPool = intpGroup.getResourcePool(); - if (localPool != null) { - resourceSet.addAll(localPool.getAll()); - } - } else if (remoteInterpreterProcess.isRunning()) { - RemoteInterpreterService.Client client = null; - boolean broken = false; - try { - client = remoteInterpreterProcess.getClient(); - if (client == null) { - // remote interpreter may not started yet or terminated. - continue; - } - List resourceList = client.resourcePoolGetAll(); - for (String res : resourceList) { - resourceSet.add(Resource.fromJson(res)); - } - } catch (Exception e) { - logger.error(e.getMessage(), e); - broken = true; - } finally { - if (client != null) { - intpGroup.getRemoteInterpreterProcess().releaseClient(client, broken); - } - } - } - } - return resourceSet; - } - - public static void removeResourcesBelongsToNote(String noteId) { - removeResourcesBelongsToParagraph(noteId, null); - } - - public static void removeResourcesBelongsToParagraph(String noteId, String paragraphId) { - for (InterpreterGroup intpGroup : InterpreterGroup.getAll()) { - ResourceSet resourceSet = new ResourceSet(); - RemoteInterpreterProcess remoteInterpreterProcess = intpGroup.getRemoteInterpreterProcess(); - if (remoteInterpreterProcess == null) { - ResourcePool localPool = intpGroup.getResourcePool(); - if (localPool != null) { - resourceSet.addAll(localPool.getAll()); - } - if (noteId != null) { - resourceSet = resourceSet.filterByNoteId(noteId); - } - if (paragraphId != null) { - resourceSet = resourceSet.filterByParagraphId(paragraphId); - } - - for (Resource r : resourceSet) { - localPool.remove( - r.getResourceId().getNoteId(), - r.getResourceId().getParagraphId(), - r.getResourceId().getName()); - } - } else if (remoteInterpreterProcess.isRunning()) { - RemoteInterpreterService.Client client = null; - boolean broken = false; - try { - client = remoteInterpreterProcess.getClient(); - List resourceList = client.resourcePoolGetAll(); - for (String res : resourceList) { - resourceSet.add(Resource.fromJson(res)); - } - - if (noteId != null) { - resourceSet = resourceSet.filterByNoteId(noteId); - } - if (paragraphId != null) { - resourceSet = resourceSet.filterByParagraphId(paragraphId); - } - - for (Resource r : resourceSet) { - client.resourceRemove( - r.getResourceId().getNoteId(), - r.getResourceId().getParagraphId(), - r.getResourceId().getName()); - } - } catch (Exception e) { - logger.error(e.getMessage(), e); - broken = true; - } finally { - if (client != null) { - intpGroup.getRemoteInterpreterProcess().releaseClient(client, broken); - } - } - } - } - } -} - diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/Job.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/Job.java index d0025d86b9f..191902a1c32 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/Job.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/Job.java @@ -41,6 +41,7 @@ public abstract class Job { /** * Job status. * + * UNKNOWN - Job is not found in remote * READY - Job is not running, ready to run. * PENDING - Job is submitted to scheduler. but not running yet * RUNNING - Job is running. @@ -48,8 +49,8 @@ public abstract class Job { * ERROR - Job finished run. with error * ABORT - Job finished by abort */ - public static enum Status { - READY, PENDING, RUNNING, FINISHED, ERROR, ABORT; + public enum Status { + UNKNOWN, READY, PENDING, RUNNING, FINISHED, ERROR, ABORT; public boolean isReady() { return this == READY; @@ -70,14 +71,14 @@ public boolean isPending() { Date dateCreated; Date dateStarted; Date dateFinished; - Status status; + volatile Status status; static Logger LOGGER = LoggerFactory.getLogger(Job.class); transient boolean aborted = false; - private String errorMessage; - private transient Throwable exception; + private volatile String errorMessage; + private transient volatile Throwable exception; private transient JobListener listener; private long progressUpdateIntervalMs; 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 f9ddc4e99c2..e41540bfee7 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 @@ -17,11 +17,9 @@ 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.RemoteInterpreterProcess; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; import org.apache.zeppelin.scheduler.Job.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,6 +32,7 @@ /** * RemoteScheduler runs in ZeppelinServer and proxies Scheduler running on RemoteInterpreter + * */ public class RemoteScheduler implements Scheduler { Logger logger = LoggerFactory.getLogger(RemoteScheduler.class); @@ -45,17 +44,17 @@ public class RemoteScheduler implements Scheduler { boolean terminate = false; private String name; private int maxConcurrency; - private final String noteId; - private RemoteInterpreterProcess interpreterProcess; + private final String sessionId; + private RemoteInterpreter remoteInterpreter; - public RemoteScheduler(String name, ExecutorService executor, String noteId, - RemoteInterpreterProcess interpreterProcess, SchedulerListener listener, + public RemoteScheduler(String name, ExecutorService executor, String sessionId, + RemoteInterpreter remoteInterpreter, SchedulerListener listener, int maxConcurrency) { this.name = name; this.executor = executor; this.listener = listener; - this.noteId = noteId; - this.interpreterProcess = interpreterProcess; + this.sessionId = sessionId; + this.remoteInterpreter = remoteInterpreter; this.maxConcurrency = maxConcurrency; } @@ -167,14 +166,15 @@ private class JobStatusPoller extends Thread { private long initialPeriodMsec; private long initialPeriodCheckIntervalMsec; private long checkIntervalMsec; - private boolean terminate; + private volatile boolean terminate; private JobListener listener; private Job job; - Status lastStatus; + volatile Status lastStatus; public JobStatusPoller(long initialPeriodMsec, long initialPeriodCheckIntervalMsec, long checkIntervalMsec, Job job, JobListener listener) { + setName("JobStatusPoller-" + job.getId()); this.initialPeriodMsec = initialPeriodMsec; this.initialPeriodCheckIntervalMsec = initialPeriodCheckIntervalMsec; this.checkIntervalMsec = checkIntervalMsec; @@ -209,7 +209,7 @@ public void run() { } Status newStatus = getStatus(); - if (newStatus == null) { // unknown + if (newStatus == Status.UNKNOWN) { // unknown continue; } @@ -231,7 +231,9 @@ public void shutdown() { private Status getLastStatus() { if (terminate == true) { - if (lastStatus != Status.FINISHED && + if (job.getErrorMessage() != null) { + return Status.ERROR; + } else if (lastStatus != Status.FINISHED && lastStatus != Status.ERROR && lastStatus != Status.ABORT) { return Status.FINISHED; @@ -239,58 +241,35 @@ private Status getLastStatus() { return (lastStatus == null) ? Status.FINISHED : lastStatus; } } else { - return (lastStatus == null) ? Status.FINISHED : lastStatus; + return (lastStatus == null) ? Status.UNKNOWN : lastStatus; } } public synchronized Job.Status getStatus() { - if (interpreterProcess.referenceCount() <= 0) { + if (!remoteInterpreter.isOpened()) { return getLastStatus(); } - - Client client; - try { - client = interpreterProcess.getClient(); - } catch (Exception e) { - logger.error("Can't get status information", e); - lastStatus = Status.ERROR; - return Status.ERROR; - } - - boolean broken = false; - try { - String statusStr = client.getStatus(noteId, job.getId()); - if ("Unknown".equals(statusStr)) { - // not found this job in the remote schedulers. - // maybe not submitted, maybe already finished - //Status status = getLastStatus(); - listener.afterStatusChange(job, null, null); - return job.getStatus(); - } - Status status = Status.valueOf(statusStr); - lastStatus = status; - listener.afterStatusChange(job, null, status); - return status; - } catch (TException e) { - broken = true; - logger.error("Can't get status information", e); - lastStatus = Status.ERROR; - return Status.ERROR; - } catch (Exception e) { - logger.error("Unknown status", e); - lastStatus = Status.ERROR; - return Status.ERROR; - } finally { - interpreterProcess.releaseClient(client, broken); + Status status = Status.valueOf(remoteInterpreter.getStatus(job.getId())); + if (status == Status.UNKNOWN) { + // not found this job in the remote schedulers. + // maybe not submitted, maybe already finished + //Status status = getLastStatus(); + listener.afterStatusChange(job, null, null); + return job.getStatus(); } + lastStatus = status; + listener.afterStatusChange(job, null, status); + return status; } } + //TODO(zjffdu) need to refactor the schdule module which is too complicated private class JobRunner implements Runnable, JobListener { + private final Logger logger = LoggerFactory.getLogger(JobRunner.class); private Scheduler scheduler; private Job job; - private boolean jobExecuted; - boolean jobSubmittedRemotely; + private volatile boolean jobExecuted; + volatile boolean jobSubmittedRemotely; public JobRunner(Scheduler scheduler, Job job) { this.scheduler = scheduler; @@ -338,20 +317,22 @@ public void run() { } // set job status based on result. - Status lastStatus = jobStatusPoller.getStatus(); Object jobResult = job.getReturn(); - if (jobResult != null && jobResult instanceof InterpreterResult) { - if (((InterpreterResult) jobResult).code() == Code.ERROR) { - lastStatus = Status.ERROR; - } - } - if (job.getException() != null) { - lastStatus = Status.ERROR; + if (job.isAborted()) { + job.setStatus(Status.ABORT); + } else if (job.getException() != null) { +// logger.info("Job ABORT, " + job.getId()); + job.setStatus(Status.ERROR); + } else if (jobResult != null && jobResult instanceof InterpreterResult + && ((InterpreterResult) jobResult).code() == Code.ERROR) { +// logger.info("Job Error, " + job.getId()); + job.setStatus(Status.ERROR); + } else { +// logger.info("Job Finished, " + job.getId()); + job.setStatus(Status.FINISHED); } synchronized (queue) { - job.setStatus(lastStatus); - if (listener != null) { listener.jobFinished(scheduler, job); } @@ -374,25 +355,6 @@ public void beforeStatusChange(Job job, Status before, Status after) { @Override public void afterStatusChange(Job job, Status before, Status after) { - if (after == null) { // unknown. maybe before sumitted remotely, maybe already finished. - if (jobExecuted) { - jobSubmittedRemotely = true; - Object jobResult = job.getReturn(); - if (job.isAborted()) { - job.setStatus(Status.ABORT); - } else if (job.getException() != null) { - job.setStatus(Status.ERROR); - } else if (jobResult != null && jobResult instanceof InterpreterResult - && ((InterpreterResult) jobResult).code() == Code.ERROR) { - job.setStatus(Status.ERROR); - } else { - job.setStatus(Status.FINISHED); - } - } - return; - } - - // Update remoteStatus if (jobExecuted == false) { if (after == Status.FINISHED || after == Status.ABORT @@ -402,14 +364,18 @@ public void afterStatusChange(Job job, Status before, Status after) { return; } else if (after == Status.RUNNING) { jobSubmittedRemotely = true; + job.setStatus(Status.RUNNING); +// logger.info("Job RUNNING, " + job.getId()); } } else { jobSubmittedRemotely = true; } - // status polled by status poller - if (job.getStatus() != after) { - job.setStatus(after); + // only set status when it is RUNNING + // We would set other status based on the interpret result + if (after == Status.RUNNING) { +// logger.info("Job RUNNING, " + job.getId()); + job.setStatus(Status.RUNNING); } } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/SchedulerFactory.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/SchedulerFactory.java index af52dec345b..5871ca5befc 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/SchedulerFactory.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/SchedulerFactory.java @@ -24,17 +24,18 @@ import java.util.Map; import java.util.concurrent.ExecutorService; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * TODO(moon) : add description. + * Factory class for creating schedulers + * */ public class SchedulerFactory implements SchedulerListener { private static final Logger logger = LoggerFactory.getLogger(SchedulerFactory.class); - ExecutorService executor; - Map schedulers = new LinkedHashMap<>(); + private ExecutorService executor; + private Map schedulers = new LinkedHashMap<>(); private static SchedulerFactory singleton; private static Long singletonLock = new Long(0); @@ -54,17 +55,17 @@ public static SchedulerFactory singleton() { return singleton; } - public SchedulerFactory() throws Exception { - executor = ExecutorFactory.singleton().createOrGet("schedulerFactory", 100); + SchedulerFactory() throws Exception { + executor = ExecutorFactory.singleton().createOrGet("SchedulerFactory", 100); } public void destroy() { - ExecutorFactory.singleton().shutdown("schedulerFactory"); + ExecutorFactory.singleton().shutdown("SchedulerFactory"); } public Scheduler createOrGetFIFOScheduler(String name) { synchronized (schedulers) { - if (schedulers.containsKey(name) == false) { + if (!schedulers.containsKey(name)) { Scheduler s = new FIFOScheduler(name, executor, this); schedulers.put(name, s); executor.execute(s); @@ -75,7 +76,7 @@ public Scheduler createOrGetFIFOScheduler(String name) { public Scheduler createOrGetParallelScheduler(String name, int maxConcurrency) { synchronized (schedulers) { - if (schedulers.containsKey(name) == false) { + if (!schedulers.containsKey(name)) { Scheduler s = new ParallelScheduler(name, executor, this, maxConcurrency); schedulers.put(name, s); executor.execute(s); @@ -86,17 +87,17 @@ public Scheduler createOrGetParallelScheduler(String name, int maxConcurrency) { public Scheduler createOrGetRemoteScheduler( String name, - String noteId, - RemoteInterpreterProcess interpreterProcess, + String sessionId, + RemoteInterpreter remoteInterpreter, int maxConcurrency) { synchronized (schedulers) { - if (schedulers.containsKey(name) == false) { + if (!schedulers.containsKey(name)) { Scheduler s = new RemoteScheduler( name, executor, - noteId, - interpreterProcess, + sessionId, + remoteInterpreter, this, maxConcurrency); schedulers.put(name, s); @@ -106,38 +107,24 @@ public Scheduler createOrGetRemoteScheduler( } } - public Scheduler removeScheduler(String name) { + public void removeScheduler(String name) { synchronized (schedulers) { Scheduler s = schedulers.remove(name); if (s != null) { s.stop(); } } - return null; - } - - public Collection listScheduler(String name) { - List s = new LinkedList<>(); - synchronized (schedulers) { - for (Scheduler ss : schedulers.values()) { - s.add(ss); - } - } - return s; } @Override public void jobStarted(Scheduler scheduler, Job job) { - logger.info("Job " + job.getJobName() + " started by scheduler " + scheduler.getName()); + logger.info("Job " + job.getId() + " started by scheduler " + scheduler.getName()); } @Override public void jobFinished(Scheduler scheduler, Job job) { - logger.info("Job " + job.getJobName() + " finished by scheduler " + scheduler.getName()); + logger.info("Job " + job.getId() + " finished by scheduler " + scheduler.getName()); } - - - } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/TableDataProxy.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/TableDataProxy.java index 8673476ed42..19265287ae5 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/TableDataProxy.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/TableDataProxy.java @@ -17,7 +17,6 @@ package org.apache.zeppelin.tabledata; import org.apache.zeppelin.resource.Resource; -import org.apache.zeppelin.resource.ResourcePoolUtils; import java.util.Iterator; diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/IdHashes.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/IdHashes.java new file mode 100644 index 00000000000..14c03a11cf0 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/IdHashes.java @@ -0,0 +1,76 @@ +/* + * 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.util; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Generate Tiny ID. + */ +public class IdHashes { + private static final char[] DICTIONARY = new char[] {'1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z'}; + + /** + * encodes the given string into the base of the dictionary provided in the constructor. + * + * @param value the number to encode. + * @return the encoded string. + */ + private static String encode(Long value) { + + List result = new ArrayList<>(); + BigInteger base = new BigInteger("" + DICTIONARY.length); + int exponent = 1; + BigInteger remaining = new BigInteger(value.toString()); + while (true) { + BigInteger a = base.pow(exponent); // 16^1 = 16 + BigInteger b = remaining.mod(a); // 119 % 16 = 7 | 112 % 256 = 112 + BigInteger c = base.pow(exponent - 1); + BigInteger d = b.divide(c); + + // if d > dictionary.length, we have a problem. but BigInteger doesnt have + // a greater than method :-( hope for the best. theoretically, d is always + // an index of the dictionary! + result.add(DICTIONARY[d.intValue()]); + remaining = remaining.subtract(b); // 119 - 7 = 112 | 112 - 112 = 0 + + // finished? + if (remaining.equals(BigInteger.ZERO)) { + break; + } + + exponent++; + } + + // need to reverse it, since the start of the list contains the least significant values + StringBuffer sb = new StringBuffer(); + for (int i = result.size() - 1; i >= 0; i--) { + sb.append(result.get(i)); + } + return sb.toString(); + } + + public static String generateId() { + return encode(System.currentTimeMillis() + new Random().nextInt()); + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/util/Util.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/Util.java similarity index 98% rename from zeppelin-zengine/src/main/java/org/apache/zeppelin/util/Util.java rename to zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/Util.java index be45b9ef097..6153f499bd6 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/util/Util.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/Util.java @@ -17,7 +17,7 @@ package org.apache.zeppelin.util; -import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang.StringUtils; import java.io.IOException; import java.util.Properties; diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java new file mode 100644 index 00000000000..21d75262a05 --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java @@ -0,0 +1,74 @@ +package org.apache.zeppelin.interpreter; + +import org.apache.commons.io.FileUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.display.AngularObjectRegistryListener; +import org.apache.zeppelin.helium.ApplicationEventListener; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; +import org.junit.After; +import org.junit.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; + +import static org.mockito.Mockito.mock; + + +/** + * This class will load configuration files under + * src/test/resources/interpreter + * src/test/resources/conf + * + * to construct InterpreterSettingManager and InterpreterFactory properly + * + */ +public abstract class AbstractInterpreterTest { + protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractInterpreterTest.class); + private static final String INTERPRETER_SCRIPT = + System.getProperty("os.name").startsWith("Windows") ? + "../bin/interpreter.cmd" : + "../bin/interpreter.sh"; + + protected InterpreterSettingManager interpreterSettingManager; + protected InterpreterFactory interpreterFactory; + protected File testRootDir; + protected File interpreterDir; + protected File confDir; + protected File notebookDir; + protected ZeppelinConfiguration conf; + + @Before + public void setUp() throws Exception { + // copy the resources files to a temp folder + testRootDir = new File(System.getProperty("java.io.tmpdir") + "/Zeppelin_Test_" + System.currentTimeMillis()); + testRootDir.mkdirs(); + LOGGER.info("Create tmp directory: {} as root folder of ZEPPELIN_INTERPRETER_DIR & ZEPPELIN_CONF_DIR", testRootDir.getAbsolutePath()); + interpreterDir = new File(testRootDir, "interpreter"); + confDir = new File(testRootDir, "conf"); + notebookDir = new File(testRootDir, "notebook"); + + interpreterDir.mkdirs(); + confDir.mkdirs(); + notebookDir.mkdirs(); + + FileUtils.copyDirectory(new File("src/test/resources/interpreter"), interpreterDir); + FileUtils.copyDirectory(new File("src/test/resources/conf"), confDir); + + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONF_DIR.getVarName(), confDir.getAbsolutePath()); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_DIR.getVarName(), interpreterDir.getAbsolutePath()); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), notebookDir.getAbsolutePath()); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_REMOTE_RUNNER.getVarName(), INTERPRETER_SCRIPT); + + conf = new ZeppelinConfiguration(); + interpreterSettingManager = new InterpreterSettingManager(conf, + mock(AngularObjectRegistryListener.class), mock(RemoteInterpreterProcessListener.class), mock(ApplicationEventListener.class)); + interpreterFactory = new InterpreterFactory(interpreterSettingManager); + } + + @After + public void tearDown() throws Exception { + interpreterSettingManager.close(); + FileUtils.deleteDirectory(testRootDir); + } +} diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DoubleEchoInterpreter.java similarity index 58% rename from zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DoubleEchoInterpreter.java index 1b1306a78f0..8eea4b25a5f 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DoubleEchoInterpreter.java @@ -14,62 +14,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.zeppelin.interpreter.mock; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import org.apache.zeppelin.interpreter.Interpreter; -import org.apache.zeppelin.interpreter.InterpreterContext; -import org.apache.zeppelin.interpreter.InterpreterResult; -import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; -import org.apache.zeppelin.scheduler.Scheduler; -import org.apache.zeppelin.scheduler.SchedulerFactory; +package org.apache.zeppelin.interpreter; + +import java.util.Properties; -public class MockInterpreter1 extends Interpreter{ - Map vars = new HashMap<>(); +/** + * + */ +public class DoubleEchoInterpreter extends Interpreter { - public MockInterpreter1(Properties property) { + public DoubleEchoInterpreter(Properties property) { super(property); } @Override public void open() { + } @Override public void close() { + } @Override public InterpreterResult interpret(String st, InterpreterContext context) { - return new InterpreterResult(InterpreterResult.Code.SUCCESS, "repl1: "+st); + return new InterpreterResult(InterpreterResult.Code.SUCCESS, st + "," + st); } @Override public void cancel(InterpreterContext context) { + } @Override public FormType getFormType() { - return FormType.SIMPLE; + return null; } @Override public int getProgress(InterpreterContext context) { return 0; } - - @Override - public Scheduler getScheduler() { - return SchedulerFactory.singleton().createOrGetFIFOScheduler("test_"+this.hashCode()); - } - - @Override - public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { - return null; - } } diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DummyInterpreter.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DummyInterpreter.java deleted file mode 100644 index a7a6eb9b715..00000000000 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DummyInterpreter.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.apache.zeppelin.interpreter; - -import java.util.Properties; - -/** - * - */ -public class DummyInterpreter extends Interpreter { - - public DummyInterpreter(Properties property) { - super(property); - } - - @Override - public void open() { - - } - - @Override - public void close() { - - } - - @Override - public InterpreterResult interpret(String st, InterpreterContext context) { - return null; - } - - @Override - public void cancel(InterpreterContext context) { - - } - - @Override - public FormType getFormType() { - return null; - } - - @Override - public int getProgress(InterpreterContext context) { - return 0; - } -} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/EchoInterpreter.java similarity index 50% rename from zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/EchoInterpreter.java index ca688dcb6e7..e7a04f3b5f9 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/EchoInterpreter.java @@ -15,32 +15,51 @@ * limitations under the License. */ -package org.apache.zeppelin.interpreter; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.apache.zeppelin.common.JsonSerializable; -import org.sonatype.aether.repository.RemoteRepository; +package org.apache.zeppelin.interpreter; -import java.util.List; -import java.util.Map; +import java.util.Properties; /** - * + * Just return the received statement back */ -public class InterpreterInfoSaving implements JsonSerializable { +public class EchoInterpreter extends Interpreter { + + public EchoInterpreter(Properties property) { + super(property); + } + + @Override + public void open() { + + } + + @Override + public void close() { - private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + } - public Map interpreterSettings; - public Map> interpreterBindings; - public List interpreterRepositories; + @Override + public InterpreterResult interpret(String st, InterpreterContext context) { + if (Boolean.parseBoolean(property.getProperty("zeppelin.interpreter.echo.fail", "false"))) { + return new InterpreterResult(InterpreterResult.Code.ERROR); + } else { + return new InterpreterResult(InterpreterResult.Code.SUCCESS, st); + } + } + + @Override + public void cancel(InterpreterContext context) { + + } - public String toJson() { - return gson.toJson(this); + @Override + public FormType getFormType() { + return FormType.NATIVE; } - public static InterpreterInfoSaving fromJson(String json) { - return gson.fromJson(json, InterpreterInfoSaving.class); + @Override + public int getProgress(InterpreterContext context) { + return 0; } } diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java new file mode 100644 index 00000000000..f3137d9c895 --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java @@ -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. + */ + +package org.apache.zeppelin.interpreter; + +import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; +import org.junit.Test; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class InterpreterFactoryTest extends AbstractInterpreterTest { + + @Test + public void testGetFactory() throws IOException { + // no default interpreter because there's no interpreter setting binded to this note + assertNull(interpreterFactory.getInterpreter("user1", "note1", "")); + + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "") instanceof RemoteInterpreter); + RemoteInterpreter remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", ""); + // EchoInterpreter is the default interpreter (see zeppelin-interpreter/src/test/resources/conf/interpreter.json) + assertEquals(EchoInterpreter.class.getName(), remoteInterpreter.getClassName()); + + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "test") instanceof RemoteInterpreter); + remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test"); + assertEquals(EchoInterpreter.class.getName(), remoteInterpreter.getClassName()); + + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "echo") instanceof RemoteInterpreter); + remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "echo"); + assertEquals(EchoInterpreter.class.getName(), remoteInterpreter.getClassName()); + + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "double_echo") instanceof RemoteInterpreter); + remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "double_echo"); + assertEquals(DoubleEchoInterpreter.class.getName(), remoteInterpreter.getClassName()); + } + + @Test(expected = InterpreterException.class) + public void testUnknownRepl1() throws IOException { + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); + interpreterFactory.getInterpreter("user1", "note1", "test.unknown_repl"); + } + + @Test + public void testUnknownRepl2() throws IOException { + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); + assertNull(interpreterFactory.getInterpreter("user1", "note1", "unknown_repl")); + } +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterGroupTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterGroupTest.java new file mode 100644 index 00000000000..11607bb9e80 --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterGroupTest.java @@ -0,0 +1,90 @@ +/* + * 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.interpreter; + +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonatype.aether.RepositoryException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.junit.Assert.assertEquals; + + +public class InterpreterGroupTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterGroupTest.class); + + private InterpreterSetting interpreterSetting; + + @Before + public void setUp() throws IOException, RepositoryException { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.SCOPED); + interpreterOption.setRemote(false); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); + InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + interpreterInfos.add(interpreterInfo2); + interpreterSetting = new InterpreterSetting.Builder() + .setId("id") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .create(); + } + + @Test + public void testInterpreterGroup() { + InterpreterGroup interpreterGroup = new InterpreterGroup("group_1", interpreterSetting); + assertEquals(0, interpreterGroup.getSessionNum()); + + // create session_1 + List interpreters = interpreterGroup.getOrCreateSession("user1", "session_1"); + assertEquals(2, interpreters.size()); + assertEquals(EchoInterpreter.class.getName(), interpreters.get(0).getClassName()); + assertEquals(DoubleEchoInterpreter.class.getName(), interpreters.get(1).getClassName()); + assertEquals(1, interpreterGroup.getSessionNum()); + + // get the same interpreters when interpreterGroup.getOrCreateSession is invoked again + assertEquals(interpreters, interpreterGroup.getOrCreateSession("user1", "session_1")); + assertEquals(1, interpreterGroup.getSessionNum()); + + // create session_2 + List interpreters2 = interpreterGroup.getOrCreateSession("user1", "session_2"); + assertEquals(2, interpreters2.size()); + assertEquals(EchoInterpreter.class.getName(), interpreters2.get(0).getClassName()); + assertEquals(DoubleEchoInterpreter.class.getName(), interpreters2.get(1).getClassName()); + assertEquals(2, interpreterGroup.getSessionNum()); + + // close session_1 + interpreterGroup.close("session_1"); + assertEquals(1, interpreterGroup.getSessionNum()); + + // close InterpreterGroup + interpreterGroup.close(); + assertEquals(0, interpreterGroup.getSessionNum()); + } +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterOutputChangeWatcherTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterOutputChangeWatcherTest.java index e37680905bb..f3a30fbd274 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterOutputChangeWatcherTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterOutputChangeWatcherTest.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; import org.junit.Before; @@ -29,7 +30,7 @@ public class InterpreterOutputChangeWatcherTest implements InterpreterOutputChangeListener { private File tmpDir; private File fileChanged; - private int numChanged; + private AtomicInteger numChanged; private InterpreterOutputChangeWatcher watcher; @Before @@ -40,7 +41,7 @@ public void setUp() throws Exception { tmpDir = new File(System.getProperty("java.io.tmpdir")+"/ZeppelinLTest_"+System.currentTimeMillis()); tmpDir.mkdirs(); fileChanged = null; - numChanged = 0; + numChanged = new AtomicInteger(0); } @After @@ -66,7 +67,7 @@ else if(file.isDirectory()){ @Test public void test() throws IOException, InterruptedException { assertNull(fileChanged); - assertEquals(0, numChanged); + assertEquals(0, numChanged.get()); Thread.sleep(1000); // create new file @@ -92,14 +93,14 @@ public void test() throws IOException, InterruptedException { } assertNotNull(fileChanged); - assertEquals(1, numChanged); + assertEquals(1, numChanged.get()); } @Override public void fileChanged(File file) { fileChanged = file; - numChanged++; + numChanged.incrementAndGet(); synchronized(this) { notify(); diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java new file mode 100644 index 00000000000..c74760ff106 --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java @@ -0,0 +1,270 @@ +/* + * 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.interpreter; + +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.dep.Dependency; +import org.apache.zeppelin.display.AngularObjectRegistryListener; +import org.apache.zeppelin.helium.ApplicationEventListener; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; +import org.junit.Test; +import org.sonatype.aether.RepositoryException; +import org.sonatype.aether.repository.RemoteRepository; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + + +public class InterpreterSettingManagerTest extends AbstractInterpreterTest { + + @Test + public void testInitInterpreterSettingManager() throws IOException, RepositoryException { + assertEquals(2, interpreterSettingManager.get().size()); + InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("test"); + assertEquals("test", interpreterSetting.getName()); + assertEquals("test", interpreterSetting.getGroup()); + assertEquals(2, interpreterSetting.getInterpreterInfos().size()); + // 3 other builtin properties: + // * zeppelin.interpeter.output.limit + // * zeppelin.interpreter.localRepo + // * zeppelin.interpreter.max.poolsize + assertEquals(6, interpreterSetting.getJavaProperties().size()); + assertEquals("value_1", interpreterSetting.getJavaProperties().getProperty("property_1")); + assertEquals("new_value_2", interpreterSetting.getJavaProperties().getProperty("property_2")); + assertEquals("value_3", interpreterSetting.getJavaProperties().getProperty("property_3")); + assertEquals("shared", interpreterSetting.getOption().perNote); + assertEquals("shared", interpreterSetting.getOption().perUser); + assertEquals(0, interpreterSetting.getDependencies().size()); + assertNotNull(interpreterSetting.getAngularObjectRegistryListener()); + assertNotNull(interpreterSetting.getRemoteInterpreterProcessListener()); + assertNotNull(interpreterSetting.getAppEventListener()); + assertNotNull(interpreterSetting.getDependencyResolver()); + assertNotNull(interpreterSetting.getInterpreterSettingManager()); + + List repositories = interpreterSettingManager.getRepositories(); + assertEquals(2, repositories.size()); + assertEquals("central", repositories.get(0).getId()); + + // Load it again + InterpreterSettingManager interpreterSettingManager2 = new InterpreterSettingManager(conf, + mock(AngularObjectRegistryListener.class), mock(RemoteInterpreterProcessListener.class), mock(ApplicationEventListener.class)); + assertEquals(2, interpreterSettingManager2.get().size()); + interpreterSetting = interpreterSettingManager2.getByName("test"); + assertEquals("test", interpreterSetting.getName()); + assertEquals("test", interpreterSetting.getGroup()); + assertEquals(2, interpreterSetting.getInterpreterInfos().size()); + assertEquals(6, interpreterSetting.getJavaProperties().size()); + assertEquals("value_1", interpreterSetting.getJavaProperties().getProperty("property_1")); + assertEquals("new_value_2", interpreterSetting.getJavaProperties().getProperty("property_2")); + assertEquals("value_3", interpreterSetting.getJavaProperties().getProperty("property_3")); + assertEquals("shared", interpreterSetting.getOption().perNote); + assertEquals("shared", interpreterSetting.getOption().perUser); + assertEquals(0, interpreterSetting.getDependencies().size()); + + repositories = interpreterSettingManager2.getRepositories(); + assertEquals(2, repositories.size()); + assertEquals("central", repositories.get(0).getId()); + + } + + @Test + public void testCreateUpdateRemoveSetting() throws IOException { + // create new interpreter setting + InterpreterOption option = new InterpreterOption(); + option.setPerNote("scoped"); + option.setPerUser("scoped"); + Map properties = new HashMap<>(); + properties.put("property_4", new InterpreterProperty("property_4","value_4")); + + try { + interpreterSettingManager.createNewSetting("test2", "test", new ArrayList(), option, properties); + fail("Should fail due to interpreter already existed"); + } catch (IOException e) { + assertTrue(e.getMessage().contains("already existed")); + } + + interpreterSettingManager.createNewSetting("test3", "test", new ArrayList(), option, properties); + assertEquals(3, interpreterSettingManager.get().size()); + InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("test3"); + assertEquals("test3", interpreterSetting.getName()); + assertEquals("test", interpreterSetting.getGroup()); + // 3 other builtin properties: + // * zeppelin.interpeter.output.limit + // * zeppelin.interpreter.localRepo + // * zeppelin.interpreter.max.poolsize + assertEquals(4, interpreterSetting.getJavaProperties().size()); + assertEquals("value_4", interpreterSetting.getJavaProperties().getProperty("property_4")); + assertEquals("scoped", interpreterSetting.getOption().perNote); + assertEquals("scoped", interpreterSetting.getOption().perUser); + assertEquals(0, interpreterSetting.getDependencies().size()); + assertNotNull(interpreterSetting.getAngularObjectRegistryListener()); + assertNotNull(interpreterSetting.getRemoteInterpreterProcessListener()); + assertNotNull(interpreterSetting.getAppEventListener()); + assertNotNull(interpreterSetting.getDependencyResolver()); + assertNotNull(interpreterSetting.getInterpreterSettingManager()); + + // load it again, it should be saved in interpreter-setting.json. So we can restore it properly + InterpreterSettingManager interpreterSettingManager2 = new InterpreterSettingManager(conf, + mock(AngularObjectRegistryListener.class), mock(RemoteInterpreterProcessListener.class), mock(ApplicationEventListener.class)); + assertEquals(3, interpreterSettingManager2.get().size()); + interpreterSetting = interpreterSettingManager2.getByName("test3"); + assertEquals("test3", interpreterSetting.getName()); + assertEquals("test", interpreterSetting.getGroup()); + assertEquals(6, interpreterSetting.getJavaProperties().size()); + assertEquals("value_4", interpreterSetting.getJavaProperties().getProperty("property_4")); + assertEquals("scoped", interpreterSetting.getOption().perNote); + assertEquals("scoped", interpreterSetting.getOption().perUser); + assertEquals(0, interpreterSetting.getDependencies().size()); + + // update interpreter setting + InterpreterOption newOption = new InterpreterOption(); + newOption.setPerNote("scoped"); + newOption.setPerUser("isolated"); + Map newProperties = new HashMap<>(properties); + newProperties.put("property_4", new InterpreterProperty("property_4", "new_value_4")); + List newDependencies = new ArrayList<>(); + newDependencies.add(new Dependency("com.databricks:spark-avro_2.11:3.1.0")); + interpreterSettingManager.setPropertyAndRestart(interpreterSetting.getId(), newOption, newProperties, newDependencies); + interpreterSetting = interpreterSettingManager.get(interpreterSetting.getId()); + assertEquals("test3", interpreterSetting.getName()); + assertEquals("test", interpreterSetting.getGroup()); + assertEquals(4, interpreterSetting.getJavaProperties().size()); + assertEquals("new_value_4", interpreterSetting.getJavaProperties().getProperty("property_4")); + assertEquals("scoped", interpreterSetting.getOption().perNote); + assertEquals("isolated", interpreterSetting.getOption().perUser); + assertEquals(1, interpreterSetting.getDependencies().size()); + assertNotNull(interpreterSetting.getAngularObjectRegistryListener()); + assertNotNull(interpreterSetting.getRemoteInterpreterProcessListener()); + assertNotNull(interpreterSetting.getAppEventListener()); + assertNotNull(interpreterSetting.getDependencyResolver()); + assertNotNull(interpreterSetting.getInterpreterSettingManager()); + + // restart in note page + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); + interpreterSettingManager.setInterpreterBinding("user2", "note2", interpreterSettingManager.getSettingIds()); + interpreterSettingManager.setInterpreterBinding("user3", "note3", interpreterSettingManager.getSettingIds()); + // create 3 sessions as it is scoped mode + interpreterSetting.getOption().setPerUser("scoped"); + interpreterSetting.getDefaultInterpreter("user1", "note1"); + interpreterSetting.getDefaultInterpreter("user2", "note2"); + interpreterSetting.getDefaultInterpreter("user3", "note3"); + InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); + assertEquals(3, interpreterGroup.getSessionNum()); + // only close user1's session + interpreterSettingManager.restart(interpreterSetting.getId(), "note1", "user1"); + assertEquals(2, interpreterGroup.getSessionNum()); + // close all the sessions + interpreterSettingManager.restart(interpreterSetting.getId(), "note1", "anonymous"); + assertEquals(0, interpreterGroup.getSessionNum()); + + // remove interpreter setting + interpreterSettingManager.remove(interpreterSetting.getId()); + assertEquals(2, interpreterSettingManager.get().size()); + + // load it again + InterpreterSettingManager interpreterSettingManager3 = new InterpreterSettingManager(new ZeppelinConfiguration(), + mock(AngularObjectRegistryListener.class), mock(RemoteInterpreterProcessListener.class), mock(ApplicationEventListener.class)); + assertEquals(2, interpreterSettingManager3.get().size()); + + } + + @Test + public void testInterpreterBinding() throws IOException { + assertNull(interpreterSettingManager.getInterpreterBinding("note1")); + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getInterpreterSettingIds()); + assertEquals(interpreterSettingManager.getInterpreterSettingIds(), interpreterSettingManager.getInterpreterBinding("note1")); + } + + @Test + public void testUpdateInterpreterBinding_PerNoteShared() throws IOException { + InterpreterSetting defaultInterpreterSetting = interpreterSettingManager.get().get(0); + defaultInterpreterSetting.getOption().setPerNote("shared"); + + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getInterpreterSettingIds()); + // create interpreter of the first binded interpreter setting + interpreterFactory.getInterpreter("user1", "note1", ""); + assertEquals(1, defaultInterpreterSetting.getAllInterpreterGroups().size()); + + // choose the first setting + List newSettingIds = new ArrayList<>(); + newSettingIds.add(interpreterSettingManager.getInterpreterSettingIds().get(1)); + + interpreterSettingManager.setInterpreterBinding("user1", "note1", newSettingIds); + assertEquals(newSettingIds, interpreterSettingManager.getInterpreterBinding("note1")); + // InterpreterGroup will still be alive as it is shared + assertEquals(1, defaultInterpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void testUpdateInterpreterBinding_PerNoteIsolated() throws IOException { + InterpreterSetting defaultInterpreterSetting = interpreterSettingManager.get().get(0); + defaultInterpreterSetting.getOption().setPerNote("isolated"); + + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getInterpreterSettingIds()); + // create interpreter of the first binded interpreter setting + interpreterFactory.getInterpreter("user1", "note1", ""); + assertEquals(1, defaultInterpreterSetting.getAllInterpreterGroups().size()); + + // choose the first setting + List newSettingIds = new ArrayList<>(); + newSettingIds.add(interpreterSettingManager.getInterpreterSettingIds().get(1)); + + interpreterSettingManager.setInterpreterBinding("user1", "note1", newSettingIds); + assertEquals(newSettingIds, interpreterSettingManager.getInterpreterBinding("note1")); + // InterpreterGroup will be closed as it is only belong to this note + assertEquals(0, defaultInterpreterSetting.getAllInterpreterGroups().size()); + + } + + @Test + public void testUpdateInterpreterBinding_PerNoteScoped() throws IOException { + InterpreterSetting defaultInterpreterSetting = interpreterSettingManager.get().get(0); + defaultInterpreterSetting.getOption().setPerNote("scoped"); + + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreterBinding("user1", "note2", interpreterSettingManager.getInterpreterSettingIds()); + // create 2 interpreter of the first binded interpreter setting for note1 and note2 + interpreterFactory.getInterpreter("user1", "note1", ""); + interpreterFactory.getInterpreter("user1", "note2", ""); + assertEquals(1, defaultInterpreterSetting.getAllInterpreterGroups().size()); + assertEquals(2, defaultInterpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + // choose the first setting + List newSettingIds = new ArrayList<>(); + newSettingIds.add(interpreterSettingManager.getInterpreterSettingIds().get(1)); + + interpreterSettingManager.setInterpreterBinding("user1", "note1", newSettingIds); + assertEquals(newSettingIds, interpreterSettingManager.getInterpreterBinding("note1")); + // InterpreterGroup will be still alive but session belong to note1 will be closed + assertEquals(1, defaultInterpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, defaultInterpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + } +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java new file mode 100644 index 00000000000..3c061a99a4c --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java @@ -0,0 +1,411 @@ +/* + * 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.interpreter; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class InterpreterSettingTest { + + @Test + public void testCreateInterpreters() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.SHARED); + interpreterOption.setRemote(false); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); + InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + interpreterInfos.add(interpreterInfo2); + InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() + .setId("id") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .create(); + + // create default interpreter for user1 and note1 + assertEquals(EchoInterpreter.class.getName(), interpreterSetting.getDefaultInterpreter("user1", "note1").getClassName()); + + // create interpreter echo for user1 and note1 + assertEquals(EchoInterpreter.class.getName(), interpreterSetting.getInterpreter("user1", "note1", "echo").getClassName()); + assertEquals(interpreterSetting.getDefaultInterpreter("user1", "note1"), interpreterSetting.getInterpreter("user1", "note1", "echo")); + + // create interpreter double_echo for user1 and note1 + assertEquals(DoubleEchoInterpreter.class.getName(), interpreterSetting.getInterpreter("user1", "note1", "double_echo").getClassName()); + + // create non-existed interpreter + assertNull(interpreterSetting.getInterpreter("user1", "note1", "invalid_echo")); + } + + @Test + public void testSharedMode() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.SHARED); + interpreterOption.setRemote(false); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); + InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + interpreterInfos.add(interpreterInfo2); + InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() + .setId("id") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .create(); + + // create default interpreter for user1 and note1 + interpreterSetting.getDefaultInterpreter("user1", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + + // create default interpreter for user2 and note1 + interpreterSetting.getDefaultInterpreter("user2", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + + // create default interpreter user1 and note2 + interpreterSetting.getDefaultInterpreter("user1", "note2"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + + // only 1 session is created, this session is shared across users and notes + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + interpreterSetting.closeInterpreters("note1", "user1"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void testPerUserScopedMode() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.SCOPED); + interpreterOption.setRemote(true); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); + InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + interpreterInfos.add(interpreterInfo2); + InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() + .setId("id") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .create(); + + // create interpreter for user1 and note1 + interpreterSetting.getDefaultInterpreter("user1", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + // create interpreter for user2 and note1 + interpreterSetting.getDefaultInterpreter("user2", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + interpreterSetting.closeInterpreters("user1", "note1"); + // InterpreterGroup is still there, but one session is removed + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + interpreterSetting.closeInterpreters("user2", "note1"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void testPerNoteScopedMode() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerNote(InterpreterOption.SCOPED); + interpreterOption.setRemote(true); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); + InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + interpreterInfos.add(interpreterInfo2); + InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() + .setId("id") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .create(); + + // create interpreter for user1 and note1 + interpreterSetting.getDefaultInterpreter("user1", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + // create interpreter for user1 and note2 + interpreterSetting.getDefaultInterpreter("user1", "note2"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + interpreterSetting.closeInterpreters("user1", "note1"); + // InterpreterGroup is still there, but one session is removed + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + interpreterSetting.closeInterpreters("user1", "note2"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void testPerUserIsolatedMode() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.ISOLATED); + interpreterOption.setRemote(true); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); + InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + interpreterInfos.add(interpreterInfo2); + InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() + .setId("id") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .create(); + + // create interpreter for user1 and note1 + interpreterSetting.getDefaultInterpreter("user1", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + // create interpreter for user2 and note1 + interpreterSetting.getDefaultInterpreter("user2", "note1"); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); + + // Each user own one InterpreterGroup and one session per InterpreterGroup + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(1).getSessionNum()); + + interpreterSetting.closeInterpreters("user1", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + interpreterSetting.closeInterpreters("user2", "note1"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void testPerNoteIsolatedMode() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerNote(InterpreterOption.ISOLATED); + interpreterOption.setRemote(true); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); + InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + interpreterInfos.add(interpreterInfo2); + InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() + .setId("id") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .create(); + + // create interpreter for user1 and note1 + interpreterSetting.getDefaultInterpreter("user1", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + // create interpreter for user2 and note2 + interpreterSetting.getDefaultInterpreter("user1", "note2"); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); + // Each user own one InterpreterGroup and one session per InterpreterGroup + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(1).getSessionNum()); + + interpreterSetting.closeInterpreters("user1", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + interpreterSetting.closeInterpreters("user1", "note2"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void testPerUserIsolatedPerNoteScopedMode() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.ISOLATED); + interpreterOption.setPerNote(InterpreterOption.SCOPED); + interpreterOption.setRemote(true); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); + InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + interpreterInfos.add(interpreterInfo2); + InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() + .setId("id") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .create(); + + // create interpreter for user1 and note1 + interpreterSetting.getDefaultInterpreter("user1", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + interpreterSetting.getDefaultInterpreter("user1", "note2"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + // create interpreter for user2 and note1 + interpreterSetting.getDefaultInterpreter("user2", "note1"); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); + + // group1 for user1 has 2 sessions, and group2 for user2 has 1 session + assertEquals(interpreterSetting.getInterpreterGroup("user1", "note1"), interpreterSetting.getInterpreterGroup("user1", "note2")); + assertEquals(2, interpreterSetting.getInterpreterGroup("user1", "note1").getSessionNum()); + assertEquals(2, interpreterSetting.getInterpreterGroup("user1", "note2").getSessionNum()); + assertEquals(1, interpreterSetting.getInterpreterGroup("user2", "note1").getSessionNum()); + + // close one session for user1 + interpreterSetting.closeInterpreters("user1", "note1"); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").getSessionNum()); + + // close another session for user1 + interpreterSetting.closeInterpreters("user1", "note2"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + + // close session for user2 + interpreterSetting.closeInterpreters("user2", "note1"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void testPerUserIsolatedPerNoteIsolatedMode() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.ISOLATED); + interpreterOption.setPerNote(InterpreterOption.ISOLATED); + interpreterOption.setRemote(true); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); + InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + interpreterInfos.add(interpreterInfo2); + InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() + .setId("id") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .create(); + + // create interpreter for user1 and note1 + interpreterSetting.getDefaultInterpreter("user1", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + + // create interpreter for user1 and note2 + interpreterSetting.getDefaultInterpreter("user1", "note2"); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); + + // create interpreter for user2 and note1 + interpreterSetting.getDefaultInterpreter("user2", "note1"); + assertEquals(3, interpreterSetting.getAllInterpreterGroups().size()); + + // create interpreter for user2 and note2 + interpreterSetting.getDefaultInterpreter("user2", "note2"); + assertEquals(4, interpreterSetting.getAllInterpreterGroups().size()); + + for (InterpreterGroup interpreterGroup : interpreterSetting.getAllInterpreterGroups()) { + // each InterpreterGroup has one session + assertEquals(1, interpreterGroup.getSessionNum()); + } + + // close one session for user1 and note1 + interpreterSetting.closeInterpreters("user1", "note1"); + assertEquals(3, interpreterSetting.getAllInterpreterGroups().size()); + + // close one session for user1 and note2 + interpreterSetting.closeInterpreters("user1", "note2"); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); + + // close one session for user2 and note1 + interpreterSetting.closeInterpreters("user2", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + + // close one session for user2 and note2 + interpreterSetting.closeInterpreters("user2", "note2"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void testPerUserScopedPerNoteScopedMode() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.SCOPED); + interpreterOption.setPerNote(InterpreterOption.SCOPED); + interpreterOption.setRemote(true); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); + InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + interpreterInfos.add(interpreterInfo2); + InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() + .setId("id") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .create(); + + // create interpreter for user1 and note1 + interpreterSetting.getDefaultInterpreter("user1", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + // create interpreter for user1 and note2 + interpreterSetting.getDefaultInterpreter("user1", "note2"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + // create interpreter for user2 and note1 + interpreterSetting.getDefaultInterpreter("user2", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(3, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + // create interpreter for user2 and note2 + interpreterSetting.getDefaultInterpreter("user2", "note2"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(4, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + // close one session for user1 and note1 + interpreterSetting.closeInterpreters("user1", "note1"); + assertEquals(3, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + // close one session for user1 and note2 + interpreterSetting.closeInterpreters("user1", "note2"); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + // close one session for user2 and note1 + interpreterSetting.closeInterpreters("user2", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + // close one session for user2 and note2 + interpreterSetting.closeInterpreters("user2", "note2"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterTest.java index 305268c89df..d46eaa710d1 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterTest.java @@ -24,13 +24,14 @@ import static org.junit.Assert.assertEquals; +//TODO(zjffdu) add more test for Interpreter which is a very important class public class InterpreterTest { @Test public void testDefaultProperty() { Properties p = new Properties(); p.put("p1", "v1"); - Interpreter intp = new DummyInterpreter(p); + Interpreter intp = new EchoInterpreter(p); assertEquals(1, intp.getProperty().size()); assertEquals("v1", intp.getProperty().get("p1")); @@ -41,7 +42,7 @@ public void testDefaultProperty() { public void testOverriddenProperty() { Properties p = new Properties(); p.put("p1", "v1"); - Interpreter intp = new DummyInterpreter(p); + Interpreter intp = new EchoInterpreter(p); Properties overriddenProperty = new Properties(); overriddenProperty.put("p1", "v2"); intp.setProperty(overriddenProperty); @@ -73,7 +74,7 @@ public void testPropertyWithReplacedContextFields() { Properties p = new Properties(); p.put("p1", "replName #{noteId}, #{paragraphTitle}, #{paragraphId}, #{paragraphText}, #{replName}, #{noteId}, #{user}," + " #{authenticationInfo}"); - Interpreter intp = new DummyInterpreter(p); + Interpreter intp = new EchoInterpreter(p); intp.setUserName(user); String actual = intp.getProperty("p1"); InterpreterContext.remove(); diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/SleepInterpreter.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/SleepInterpreter.java new file mode 100644 index 00000000000..9deafcfa00c --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/SleepInterpreter.java @@ -0,0 +1,60 @@ +package org.apache.zeppelin.interpreter; + +import org.apache.zeppelin.scheduler.Scheduler; +import org.apache.zeppelin.scheduler.SchedulerFactory; + +import java.util.Properties; + +/** + * Interpreter that only accept long value and sleep for such period + */ +public class SleepInterpreter extends Interpreter { + + public SleepInterpreter(Properties property) { + super(property); + } + + @Override + public void open() { + + } + + @Override + public void close() { + + } + + @Override + public InterpreterResult interpret(String st, InterpreterContext context) { + try { + Thread.sleep(Long.parseLong(st)); + return new InterpreterResult(InterpreterResult.Code.SUCCESS); + } catch (Exception e) { + return new InterpreterResult(InterpreterResult.Code.ERROR, e.getMessage()); + } + } + + @Override + public void cancel(InterpreterContext context) { + + } + + @Override + public FormType getFormType() { + return FormType.NATIVE; + } + + @Override + public Scheduler getScheduler() { + if (Boolean.parseBoolean(property.getProperty("zeppelin.SleepInterpreter.parallel", "false"))) { + return SchedulerFactory.singleton().createOrGetParallelScheduler( + "Parallel-" + SleepInterpreter.class.getName(), 10); + } + return super.getScheduler(); + } + + @Override + public int getProgress(InterpreterContext context) { + return 0; + } +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/install/InstallInterpreterTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/install/InstallInterpreterTest.java similarity index 100% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/install/InstallInterpreterTest.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/install/InstallInterpreterTest.java diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java similarity index 96% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java index b16e9371bf8..a533c12913e 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java @@ -17,11 +17,6 @@ package org.apache.zeppelin.interpreter.mock; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterResult; @@ -29,8 +24,14 @@ import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; -public class MockInterpreter1 extends Interpreter{ -Map vars = new HashMap<>(); +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +public class MockInterpreter1 extends Interpreter { + + Map vars = new HashMap<>(); public MockInterpreter1(Properties property) { super(property); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java similarity index 91% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java index 5b9e8022145..d53716f56c7 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java @@ -17,10 +17,6 @@ package org.apache.zeppelin.interpreter.mock; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; @@ -29,12 +25,18 @@ import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; -public class MockInterpreter11 extends Interpreter{ +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +public class MockInterpreter11 extends Interpreter { Map vars = new HashMap<>(); public MockInterpreter11(Properties property) { super(property); } + boolean open; @Override @@ -53,7 +55,7 @@ public boolean isOpen() { @Override public InterpreterResult interpret(String st, InterpreterContext context) { - return new InterpreterResult(InterpreterResult.Code.SUCCESS, "repl11: "+st); + return new InterpreterResult(InterpreterResult.Code.SUCCESS, "repl11: " + st); } @Override @@ -72,12 +74,12 @@ public int getProgress(InterpreterContext context) { @Override public Scheduler getScheduler() { - return SchedulerFactory.singleton().createOrGetFIFOScheduler("test_"+this.hashCode()); + return SchedulerFactory.singleton().createOrGetFIFOScheduler("test_" + this.hashCode()); } @Override public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { + InterpreterContext interpreterContext) { return null; } -} +} \ No newline at end of file diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java similarity index 100% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java index 7a52f7d36c7..f36df56b57d 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java @@ -17,11 +17,6 @@ package org.apache.zeppelin.interpreter.mock; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterResult; @@ -29,6 +24,11 @@ import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + public class MockInterpreter2 extends Interpreter{ Map vars = new HashMap<>(); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java similarity index 97% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java index c8c64eac254..c9dc5c042f9 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java @@ -17,23 +17,6 @@ package org.apache.zeppelin.interpreter.remote; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Mockito.atMost; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - import org.apache.log4j.AppenderSkeleton; import org.apache.log4j.Level; import org.apache.log4j.Logger; @@ -43,6 +26,19 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.*; + public class AppendOutputRunnerTest { private static final int NUM_EVENTS = 10000; diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java similarity index 78% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java index f7404e35cb0..61e4ef06c0b 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java @@ -17,15 +17,10 @@ package org.apache.zeppelin.interpreter.remote; -import static org.junit.Assert.assertEquals; - -import java.io.File; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; - -import org.apache.zeppelin.display.*; +import org.apache.zeppelin.display.AngularObject; +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.display.AngularObjectRegistryListener; +import org.apache.zeppelin.display.GUI; import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterAngular; import org.apache.zeppelin.resource.LocalResourcePool; @@ -34,17 +29,25 @@ import org.junit.Before; import org.junit.Test; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + public class RemoteAngularObjectTest implements AngularObjectRegistryListener { private static final String INTERPRETER_SCRIPT = - System.getProperty("os.name").startsWith("Windows") ? - "../bin/interpreter.cmd" : - "../bin/interpreter.sh"; + System.getProperty("os.name").startsWith("Windows") ? + "../bin/interpreter.cmd" : + "../bin/interpreter.sh"; - private InterpreterGroup intpGroup; - private HashMap env; private RemoteInterpreter intp; private InterpreterContext context; private RemoteAngularObjectRegistry localRegistry; + private InterpreterSetting interpreterSetting; private AtomicInteger onAdd; private AtomicInteger onUpdate; @@ -56,32 +59,24 @@ public void setUp() throws Exception { onUpdate = new AtomicInteger(0); onRemove = new AtomicInteger(0); - intpGroup = new InterpreterGroup("intpId"); - localRegistry = new RemoteAngularObjectRegistry("intpId", this, intpGroup); - intpGroup.setAngularObjectRegistry(localRegistry); - env = new HashMap<>(); - env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); - - Properties p = new Properties(); - - intp = new RemoteInterpreter( - p, - "note", - MockInterpreterAngular.class.getName(), - new File(INTERPRETER_SCRIPT).getAbsolutePath(), - "fake", - "fakeRepo", - env, - 10 * 1000, - null, - null, - "anonymous", - false - ); - - intpGroup.put("note", new LinkedList()); - intpGroup.get("note").add(intp); - intp.setInterpreterGroup(intpGroup); + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setRemote(true); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(MockInterpreterAngular.class.getName(), "mock", true, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + InterpreterRunner runner = new InterpreterRunner(INTERPRETER_SCRIPT, INTERPRETER_SCRIPT); + interpreterSetting = new InterpreterSetting.Builder() + .setId("test") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .setRunner(runner) + .setInterpreterDir("../interpeters/test") + .create(); + + intp = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); + localRegistry = (RemoteAngularObjectRegistry) intp.getInterpreterGroup().getAngularObjectRegistry(); context = new InterpreterContext( "note", @@ -92,17 +87,17 @@ public void setUp() throws Exception { new AuthenticationInfo(), new HashMap(), new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), + new AngularObjectRegistry(intp.getInterpreterGroup().getId(), null), new LocalResourcePool("pool1"), new LinkedList(), null); intp.open(); + } @After public void tearDown() throws Exception { - intp.close(); - intpGroup.close(); + interpreterSetting.close(); } @Test @@ -147,7 +142,7 @@ public void testAngularObjectRemovalOnZeppelinServerSide() throws InterruptedExc Thread.sleep(500); // waitFor eventpoller pool event String[] result = ret.message().get(0).getData().split(" "); assertEquals("0", result[0]); // size of registry - + // create object ret = intp.interpret("add n1 v1", context); Thread.sleep(500); @@ -172,11 +167,11 @@ public void testAngularObjectAddOnZeppelinServerSide() throws InterruptedExcepti Thread.sleep(500); // waitFor eventpoller pool event String[] result = ret.message().get(0).getData().split(" "); assertEquals("0", result[0]); // size of registry - + // create object localRegistry.addAndNotifyRemoteProcess("n1", "v1", "note", null); - - // get from remote registry + + // get from remote registry ret = intp.interpret("get", context); Thread.sleep(500); // waitFor eventpoller pool event result = ret.message().get(0).getData().split(" "); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPollerTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPollerTest.java similarity index 100% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPollerTest.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPollerTest.java diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java similarity index 79% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java index 3f865cb370d..1687060d6d2 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java @@ -17,22 +17,18 @@ package org.apache.zeppelin.interpreter.remote; -import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.user.AuthenticationInfo; import org.apache.zeppelin.display.GUI; import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterOutputStream; +import org.apache.zeppelin.user.AuthenticationInfo; import org.junit.After; import org.junit.Before; import org.junit.Test; -import java.io.File; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Map; -import java.util.Properties; +import java.util.*; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; /** @@ -43,41 +39,32 @@ public class RemoteInterpreterOutputTestStream implements RemoteInterpreterProce System.getProperty("os.name").startsWith("Windows") ? "../bin/interpreter.cmd" : "../bin/interpreter.sh"; - private InterpreterGroup intpGroup; - private HashMap env; + + private InterpreterSetting interpreterSetting; @Before public void setUp() throws Exception { - intpGroup = new InterpreterGroup(); - intpGroup.put("note", new LinkedList()); - - env = new HashMap<>(); - env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); + InterpreterOption interpreterOption = new InterpreterOption(); + + interpreterOption.setRemote(true); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(MockInterpreterOutputStream.class.getName(), "mock", true, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + InterpreterRunner runner = new InterpreterRunner(INTERPRETER_SCRIPT, INTERPRETER_SCRIPT); + interpreterSetting = new InterpreterSetting.Builder() + .setId("test") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .setRunner(runner) + .setInterpreterDir("../interpeters/test") + .create(); } @After public void tearDown() throws Exception { - intpGroup.close(); - } - - private RemoteInterpreter createMockInterpreter() { - RemoteInterpreter intp = new RemoteInterpreter( - new Properties(), - "note", - MockInterpreterOutputStream.class.getName(), - new File(INTERPRETER_SCRIPT).getAbsolutePath(), - "fake", - "fakeRepo", - env, - 10 * 1000, - this, - null, - "anonymous", - false); - - intpGroup.get("note").add(intp); - intp.setInterpreterGroup(intpGroup); - return intp; + interpreterSetting.close(); } private InterpreterContext createInterpreterContext() { @@ -90,14 +77,14 @@ private InterpreterContext createInterpreterContext() { new AuthenticationInfo(), new HashMap(), new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), + null, null, new LinkedList(), null); } @Test public void testInterpreterResultOnly() { - RemoteInterpreter intp = createMockInterpreter(); + RemoteInterpreter intp = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); InterpreterResult ret = intp.interpret("SUCCESS::staticresult", createInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, ret.code()); assertEquals("staticresult", ret.message().get(0).getData()); @@ -113,7 +100,7 @@ public void testInterpreterResultOnly() { @Test public void testInterpreterOutputStreamOnly() { - RemoteInterpreter intp = createMockInterpreter(); + RemoteInterpreter intp = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); InterpreterResult ret = intp.interpret("SUCCESS:streamresult:", createInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, ret.code()); assertEquals("streamresult", ret.message().get(0).getData()); @@ -125,7 +112,7 @@ public void testInterpreterOutputStreamOnly() { @Test public void testInterpreterResultOutputStreamMixed() { - RemoteInterpreter intp = createMockInterpreter(); + RemoteInterpreter intp = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); InterpreterResult ret = intp.interpret("SUCCESS:stream:static", createInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, ret.code()); assertEquals("stream", ret.message().get(0).getData()); @@ -134,7 +121,7 @@ public void testInterpreterResultOutputStreamMixed() { @Test public void testOutputType() { - RemoteInterpreter intp = createMockInterpreter(); + RemoteInterpreter intp = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); InterpreterResult ret = intp.interpret("SUCCESS:%html hello:", createInterpreterContext()); assertEquals(InterpreterResult.Type.HTML, ret.message().get(0).getType()); 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 new file mode 100644 index 00000000000..ae98dc386e5 --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java @@ -0,0 +1,520 @@ +/* + * 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.interpreter.remote; + +import org.apache.thrift.transport.TTransportException; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.InterpreterResult.Code; +import org.apache.zeppelin.interpreter.remote.mock.GetEnvPropertyInterpreter; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; + +public class RemoteInterpreterTest { + + + private static final String INTERPRETER_SCRIPT = + System.getProperty("os.name").startsWith("Windows") ? + "../bin/interpreter.cmd" : + "../bin/interpreter.sh"; + + private InterpreterSetting interpreterSetting; + + @Before + public void setUp() throws Exception { + InterpreterOption interpreterOption = new InterpreterOption(); + + interpreterOption.setRemote(true); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); + InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); + InterpreterInfo interpreterInfo3 = new InterpreterInfo(SleepInterpreter.class.getName(), "sleep", false, new HashMap()); + InterpreterInfo interpreterInfo4 = new InterpreterInfo(GetEnvPropertyInterpreter.class.getName(), "get", false, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + interpreterInfos.add(interpreterInfo2); + interpreterInfos.add(interpreterInfo3); + interpreterInfos.add(interpreterInfo4); + InterpreterRunner runner = new InterpreterRunner(INTERPRETER_SCRIPT, INTERPRETER_SCRIPT); + interpreterSetting = new InterpreterSetting.Builder() + .setId("test") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .setRunner(runner) + .setInterpreterDir("../interpeters/test") + .create(); + } + + @After + public void tearDown() throws Exception { + interpreterSetting.close(); + } + + @Test + public void testSharedMode() { + interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); + + Interpreter interpreter1 = interpreterSetting.getDefaultInterpreter("user1", "note1"); + Interpreter interpreter2 = interpreterSetting.getDefaultInterpreter("user2", "note1"); + assertTrue(interpreter1 instanceof RemoteInterpreter); + RemoteInterpreter remoteInterpreter1 = (RemoteInterpreter) interpreter1; + assertTrue(interpreter2 instanceof RemoteInterpreter); + RemoteInterpreter remoteInterpreter2 = (RemoteInterpreter) interpreter2; + + InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + null, null, new ArrayList(), null); + assertEquals("hello", remoteInterpreter1.interpret("hello", context1).message().get(0).getData()); + assertEquals(Interpreter.FormType.NATIVE, interpreter1.getFormType()); + assertEquals(0, remoteInterpreter1.getProgress(context1)); + assertNotNull(remoteInterpreter1.getOrCreateInterpreterProcess()); + assertTrue(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess().isRunning()); + + assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); + assertEquals(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess(), + remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess()); + + // Call InterpreterGroup.close instead of Interpreter.close, otherwise we will have the + // RemoteInterpreterProcess leakage. + remoteInterpreter1.getInterpreterGroup().close(remoteInterpreter1.getSessionId()); + assertNull(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess()); + try { + assertEquals("hello", remoteInterpreter1.interpret("hello", context1).message().get(0).getData()); + fail("Should not be able to call interpret after interpreter is closed"); + } catch (Exception e) { + e.printStackTrace(); + } + + try { + assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); + fail("Should not be able to call getProgress after RemoterInterpreterProcess is stoped"); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + public void testScopedMode() { + interpreterSetting.getOption().setPerUser(InterpreterOption.SCOPED); + + Interpreter interpreter1 = interpreterSetting.getDefaultInterpreter("user1", "note1"); + Interpreter interpreter2 = interpreterSetting.getDefaultInterpreter("user2", "note1"); + assertTrue(interpreter1 instanceof RemoteInterpreter); + RemoteInterpreter remoteInterpreter1 = (RemoteInterpreter) interpreter1; + assertTrue(interpreter2 instanceof RemoteInterpreter); + RemoteInterpreter remoteInterpreter2 = (RemoteInterpreter) interpreter2; + + InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + null, null, new ArrayList(), null); + assertEquals("hello", remoteInterpreter1.interpret("hello", context1).message().get(0).getData()); + assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); + assertEquals(Interpreter.FormType.NATIVE, interpreter1.getFormType()); + assertEquals(0, remoteInterpreter1.getProgress(context1)); + + assertNotNull(remoteInterpreter1.getOrCreateInterpreterProcess()); + assertTrue(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess().isRunning()); + + assertEquals(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess(), + remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess()); + // Call InterpreterGroup.close instead of Interpreter.close, otherwise we will have the + // RemoteInterpreterProcess leakage. + remoteInterpreter1.getInterpreterGroup().close(remoteInterpreter1.getSessionId()); + try { + assertEquals("hello", remoteInterpreter1.interpret("hello", context1).message().get(0).getData()); + fail("Should not be able to call interpret after interpreter is closed"); + } catch (Exception e) { + e.printStackTrace(); + } + + assertTrue(remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess().isRunning()); + assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); + remoteInterpreter2.getInterpreterGroup().close(remoteInterpreter2.getSessionId()); + try { + assertEquals("hello", remoteInterpreter2.interpret("hello", context1)); + fail("Should not be able to call interpret after interpreter is closed"); + } catch (Exception e) { + e.printStackTrace(); + } + assertNull(remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess()); + } + + @Test + public void testIsolatedMode() { + interpreterSetting.getOption().setPerUser(InterpreterOption.ISOLATED); + + Interpreter interpreter1 = interpreterSetting.getDefaultInterpreter("user1", "note1"); + Interpreter interpreter2 = interpreterSetting.getDefaultInterpreter("user2", "note1"); + assertTrue(interpreter1 instanceof RemoteInterpreter); + RemoteInterpreter remoteInterpreter1 = (RemoteInterpreter) interpreter1; + assertTrue(interpreter2 instanceof RemoteInterpreter); + RemoteInterpreter remoteInterpreter2 = (RemoteInterpreter) interpreter2; + + InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + null, null, new ArrayList(), null); + assertEquals("hello", remoteInterpreter1.interpret("hello", context1).message().get(0).getData()); + assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); + assertEquals(Interpreter.FormType.NATIVE, interpreter1.getFormType()); + assertEquals(0, remoteInterpreter1.getProgress(context1)); + assertNotNull(remoteInterpreter1.getOrCreateInterpreterProcess()); + assertTrue(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess().isRunning()); + + assertNotEquals(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess(), + remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess()); + // Call InterpreterGroup.close instead of Interpreter.close, otherwise we will have the + // RemoteInterpreterProcess leakage. + remoteInterpreter1.getInterpreterGroup().close(remoteInterpreter1.getSessionId()); + assertNull(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess()); + assertTrue(remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess().isRunning()); + try { + remoteInterpreter1.interpret("hello", context1); + fail("Should not be able to call getProgress after interpreter is closed"); + } catch (Exception e) { + e.printStackTrace(); + } + + assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); + remoteInterpreter2.getInterpreterGroup().close(remoteInterpreter2.getSessionId()); + try { + assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); + fail("Should not be able to call interpret after interpreter is closed"); + } catch (Exception e) { + e.printStackTrace(); + } + assertNull(remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess()); + + } + +// @Test +// public void testExecuteIncorrectPrecode() throws TTransportException, IOException { +// interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); +// interpreterSetting.getProperties().setProperty("zeppelin.SleepInterpreter.precode", "fail test"); +// +// Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); +// InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", +// "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), +// null, null, new ArrayList(), null); +// assertEquals(Code.ERROR, interpreter1.interpret("10", context1).code()); +// } +// +// @Test +// public void testExecuteCorrectPrecode() throws TTransportException, IOException { +// interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); +// interpreterSetting.getProperties().setProperty("zeppelin.SleepInterpreter.precode", "1"); +// +// Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); +// InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", +// "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), +// null, null, new ArrayList(), null); +// assertEquals(Code.SUCCESS, interpreter1.interpret("10", context1).code()); +// } + + @Test + public void testRemoteInterperterErrorStatus() throws TTransportException, IOException { + interpreterSetting.setProperty("zeppelin.interpreter.echo.fail", "true"); + interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); + + Interpreter interpreter1 = interpreterSetting.getDefaultInterpreter("user1", "note1"); + assertTrue(interpreter1 instanceof RemoteInterpreter); + RemoteInterpreter remoteInterpreter1 = (RemoteInterpreter) interpreter1; + + InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + null, null, new ArrayList(), null); + assertEquals(Code.ERROR, remoteInterpreter1.interpret("hello", context1).code()); + } + + @Test + public void testFIFOScheduler() throws InterruptedException { + interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); + // by default SleepInterpreter would use FIFOScheduler + + final Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); + final InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + null, null, new ArrayList(), null); + // run this dummy interpret method first to launch the RemoteInterpreterProcess to avoid the + // time overhead of launching the process. + interpreter1.interpret("1", context1); + Thread thread1 = new Thread() { + @Override + public void run() { + assertEquals(Code.SUCCESS, interpreter1.interpret("100", context1).code()); + } + }; + Thread thread2 = new Thread() { + @Override + public void run() { + assertEquals(Code.SUCCESS, interpreter1.interpret("100", context1).code()); + } + }; + long start = System.currentTimeMillis(); + thread1.start(); + thread2.start(); + thread1.join(); + thread2.join(); + long end = System.currentTimeMillis(); + assertTrue((end - start) >= 200); + } + + @Test + public void testParallelScheduler() throws InterruptedException { + interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); + interpreterSetting.setProperty("zeppelin.SleepInterpreter.parallel", "true"); + + final Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); + final InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + null, null, new ArrayList(), null); + + // run this dummy interpret method first to launch the RemoteInterpreterProcess to avoid the + // time overhead of launching the process. + interpreter1.interpret("1", context1); + Thread thread1 = new Thread() { + @Override + public void run() { + assertEquals(Code.SUCCESS, interpreter1.interpret("100", context1).code()); + } + }; + Thread thread2 = new Thread() { + @Override + public void run() { + assertEquals(Code.SUCCESS, interpreter1.interpret("100", context1).code()); + } + }; + long start = System.currentTimeMillis(); + thread1.start(); + thread2.start(); + thread1.join(); + thread2.join(); + long end = System.currentTimeMillis(); + assertTrue((end - start) <= 200); + } + +// @Test +// public void testRunOrderPreserved() throws InterruptedException { +// Properties p = new Properties(); +// intpGroup.put("note", new LinkedList()); +// +// final RemoteInterpreter intpA = createMockInterpreterA(p); +// +// intpGroup.get("note").add(intpA); +// intpA.setInterpreterGroup(intpGroup); +// +// intpA.open(); +// +// int concurrency = 3; +// final List results = new LinkedList<>(); +// +// Scheduler scheduler = intpA.getScheduler(); +// for (int i = 0; i < concurrency; i++) { +// final String jobId = Integer.toString(i); +// scheduler.submit(new Job(jobId, Integer.toString(i), null, 200) { +// private Object r; +// +// @Override +// public Object getReturn() { +// return r; +// } +// +// @Override +// public void setResult(Object results) { +// this.r = results; +// } +// +// @Override +// public int progress() { +// return 0; +// } +// +// @Override +// public Map info() { +// return null; +// } +// +// @Override +// protected Object jobRun() throws Throwable { +// InterpreterResult ret = intpA.interpret(getJobName(), new InterpreterContext( +// "note", +// jobId, +// null, +// "title", +// "text", +// new AuthenticationInfo(), +// new HashMap(), +// new GUI(), +// new AngularObjectRegistry(intpGroup.getId(), null), +// new LocalResourcePool("pool1"), +// new LinkedList(), null)); +// +// synchronized (results) { +// results.addAll(ret.message()); +// results.notify(); +// } +// return null; +// } +// +// @Override +// protected boolean jobAbort() { +// return false; +// } +// +// }); +// } +// +// // wait for job finished +// synchronized (results) { +// while (results.size() != concurrency) { +// results.wait(300); +// } +// } +// +// int i = 0; +// for (InterpreterResultMessage result : results) { +// assertEquals(Integer.toString(i++), result.getData()); +// } +// assertEquals(concurrency, i); +// +// intpA.close(); +// } + + +// @Test +// public void testRemoteInterpreterSharesTheSameSchedulerInstanceInTheSameGroup() { +// Properties p = new Properties(); +// intpGroup.put("note", new LinkedList()); +// +// RemoteInterpreter intpA = createMockInterpreterA(p); +// +// intpGroup.get("note").add(intpA); +// intpA.setInterpreterGroup(intpGroup); +// +// RemoteInterpreter intpB = createMockInterpreterB(p); +// +// intpGroup.get("note").add(intpB); +// intpB.setInterpreterGroup(intpGroup); +// +// intpA.open(); +// intpB.open(); +// +// assertEquals(intpA.getScheduler(), intpB.getScheduler()); +// } + +// @Test +// public void testMultiInterpreterSession() { +// Properties p = new Properties(); +// intpGroup.put("sessionA", new LinkedList()); +// intpGroup.put("sessionB", new LinkedList()); +// +// RemoteInterpreter intpAsessionA = createMockInterpreterA(p, "sessionA"); +// intpGroup.get("sessionA").add(intpAsessionA); +// intpAsessionA.setInterpreterGroup(intpGroup); +// +// RemoteInterpreter intpBsessionA = createMockInterpreterB(p, "sessionA"); +// intpGroup.get("sessionA").add(intpBsessionA); +// intpBsessionA.setInterpreterGroup(intpGroup); +// +// intpAsessionA.open(); +// intpBsessionA.open(); +// +// assertEquals(intpAsessionA.getScheduler(), intpBsessionA.getScheduler()); +// +// RemoteInterpreter intpAsessionB = createMockInterpreterA(p, "sessionB"); +// intpGroup.get("sessionB").add(intpAsessionB); +// intpAsessionB.setInterpreterGroup(intpGroup); +// +// RemoteInterpreter intpBsessionB = createMockInterpreterB(p, "sessionB"); +// intpGroup.get("sessionB").add(intpBsessionB); +// intpBsessionB.setInterpreterGroup(intpGroup); +// +// intpAsessionB.open(); +// intpBsessionB.open(); +// +// assertEquals(intpAsessionB.getScheduler(), intpBsessionB.getScheduler()); +// assertNotEquals(intpAsessionA.getScheduler(), intpAsessionB.getScheduler()); +// } + +// @Test +// public void should_push_local_angular_repo_to_remote() throws Exception { +// //Given +// final Client client = mock(Client.class); +// final RemoteInterpreter intr = null; +//// new RemoteInterpreter(new Properties(), "noteId", +//// MockInterpreterA.class.getName(), "runner", "path", "localRepo", env, 10 * 1000, null, +//// null, "anonymous", false); +// final AngularObjectRegistry registry = new AngularObjectRegistry("spark", null); +// registry.add("name", "DuyHai DOAN", "nodeId", "paragraphId"); +// final InterpreterGroup interpreterGroup = new InterpreterGroup("groupId"); +// interpreterGroup.setAngularObjectRegistry(registry); +// intr.setInterpreterGroup(interpreterGroup); +// +// final java.lang.reflect.Type registryType = new TypeToken>>() { +// }.getType(); +// final Gson gson = new Gson(); +// final String expected = gson.toJson(registry.getRegistry(), registryType); +// +// //When +//// intr.pushAngularObjectRegistryToRemote(client); +// +// //Then +// Mockito.verify(client).angularRegistryPush(expected); +// } + + @Test + public void testEnvStringPattern() { + assertFalse(RemoteInterpreterUtils.isEnvString(null)); + assertFalse(RemoteInterpreterUtils.isEnvString("")); + assertFalse(RemoteInterpreterUtils.isEnvString("abcDEF")); + assertFalse(RemoteInterpreterUtils.isEnvString("ABC-DEF")); + assertTrue(RemoteInterpreterUtils.isEnvString("ABCDEF")); + assertTrue(RemoteInterpreterUtils.isEnvString("ABC_DEF")); + assertTrue(RemoteInterpreterUtils.isEnvString("ABC_DEF123")); + } + + @Test + public void testEnvironmentAndProperty() { + interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); + interpreterSetting.setProperty("ENV_1", "VALUE_1"); + interpreterSetting.setProperty("property_1", "value_1"); + + final Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "get"); + final InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + null, null, new ArrayList(), null); + + assertEquals("VALUE_1", interpreter1.interpret("getEnv ENV_1", context1).message().get(0).getData()); + assertEquals("null", interpreter1.interpret("getEnv ENV_2", context1).message().get(0).getData()); + + assertEquals("value_1", interpreter1.interpret("getProperty property_1", context1).message().get(0).getData()); + assertEquals("null", interpreter1.interpret("getProperty property_2", context1).message().get(0).getData()); + } + +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java similarity index 94% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java index 975d6ea3c76..5f7426a5fbb 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java @@ -17,12 +17,11 @@ package org.apache.zeppelin.interpreter.remote; -import static org.junit.Assert.assertTrue; +import org.junit.Test; import java.io.IOException; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterUtils; -import org.junit.Test; +import static org.junit.Assert.assertTrue; public class RemoteInterpreterUtilsTest { diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterEnv.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/GetEnvPropertyInterpreter.java similarity index 83% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterEnv.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/GetEnvPropertyInterpreter.java index 12e11f77b20..a039a59861b 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterEnv.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/GetEnvPropertyInterpreter.java @@ -16,7 +16,9 @@ */ package org.apache.zeppelin.interpreter.remote.mock; -import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; @@ -25,9 +27,9 @@ import java.util.Properties; -public class MockInterpreterEnv extends Interpreter { +public class GetEnvPropertyInterpreter extends Interpreter { - public MockInterpreterEnv(Properties property) { + public GetEnvPropertyInterpreter(Properties property) { super(property); } @@ -43,9 +45,9 @@ public void close() { public InterpreterResult interpret(String st, InterpreterContext context) { String[] cmd = st.split(" "); if (cmd[0].equals("getEnv")) { - return new InterpreterResult(InterpreterResult.Code.SUCCESS, System.getenv(cmd[1])); + return new InterpreterResult(InterpreterResult.Code.SUCCESS, System.getenv(cmd[1]) == null ? "null" : System.getenv(cmd[1])); } else if (cmd[0].equals("getProperty")){ - return new InterpreterResult(InterpreterResult.Code.SUCCESS, System.getProperty(cmd[1])); + return new InterpreterResult(InterpreterResult.Code.SUCCESS, System.getProperty(cmd[1]) == null ? "null" : System.getProperty(cmd[1])); } else { return new InterpreterResult(InterpreterResult.Code.ERROR, cmd[0]); } diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java similarity index 97% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java index 50d988875bb..5a3e57c06f5 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java @@ -17,19 +17,18 @@ package org.apache.zeppelin.interpreter.remote.mock; -import java.util.List; -import java.util.Properties; - import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterException; -import org.apache.zeppelin.interpreter.InterpreterPropertyBuilder; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResult.Code; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; +import java.util.List; +import java.util.Properties; + public class MockInterpreterA extends Interpreter { private String lastSt; diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java similarity index 98% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java index d4b26ad186d..ec89241136e 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java @@ -17,19 +17,18 @@ package org.apache.zeppelin.interpreter.remote.mock; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; - import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.AngularObjectWatcher; 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.thrift.InterpreterCompletion; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + public class MockInterpreterAngular extends Interpreter { AtomicInteger numWatch = new AtomicInteger(0); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java similarity index 89% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java index 7103335ac33..ff3ff9f0872 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java @@ -17,20 +17,14 @@ package org.apache.zeppelin.interpreter.remote.mock; -import java.util.List; -import java.util.Properties; - -import org.apache.zeppelin.interpreter.Interpreter; -import org.apache.zeppelin.interpreter.InterpreterContext; -import org.apache.zeppelin.interpreter.InterpreterException; -import org.apache.zeppelin.interpreter.InterpreterGroup; -import org.apache.zeppelin.interpreter.InterpreterPropertyBuilder; -import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.InterpreterResult.Code; -import org.apache.zeppelin.interpreter.WrappedInterpreter; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.scheduler.Scheduler; +import java.util.List; +import java.util.Properties; + public class MockInterpreterB extends Interpreter { public MockInterpreterB(Properties property) { diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java similarity index 91% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java index 349315c8ed1..1890cbc94e5 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java @@ -16,7 +16,10 @@ */ package org.apache.zeppelin.interpreter.remote.mock; -import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java similarity index 95% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java index c4ff6abf6f4..ee9f15cb787 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java @@ -17,22 +17,19 @@ package org.apache.zeppelin.interpreter.remote.mock; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; - import com.google.gson.Gson; -import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.display.AngularObjectWatcher; 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.thrift.InterpreterCompletion; import org.apache.zeppelin.resource.Resource; import org.apache.zeppelin.resource.ResourcePool; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + public class MockInterpreterResourcePool extends Interpreter { AtomicInteger numWatch = new AtomicInteger(0); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java similarity index 77% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java rename to zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java index ebb51004285..a1afe0e9d6a 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java @@ -17,83 +17,86 @@ package org.apache.zeppelin.scheduler; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import java.io.File; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Map; -import java.util.Properties; - -import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.interpreter.*; -import org.apache.zeppelin.user.AuthenticationInfo; import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterContextRunner; +import org.apache.zeppelin.interpreter.InterpreterInfo; +import org.apache.zeppelin.interpreter.InterpreterOption; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterRunner; +import org.apache.zeppelin.interpreter.InterpreterSetting; import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterA; import org.apache.zeppelin.resource.LocalResourcePool; import org.apache.zeppelin.scheduler.Job.Status; +import org.apache.zeppelin.user.AuthenticationInfo; import org.junit.After; import org.junit.Before; import org.junit.Test; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + public class RemoteSchedulerTest implements RemoteInterpreterProcessListener { private static final String INTERPRETER_SCRIPT = - System.getProperty("os.name").startsWith("Windows") ? - "../bin/interpreter.cmd" : - "../bin/interpreter.sh"; + System.getProperty("os.name").startsWith("Windows") ? + "../bin/interpreter.cmd" : + "../bin/interpreter.sh"; + + private InterpreterSetting interpreterSetting; private SchedulerFactory schedulerSvc; private static final int TICK_WAIT = 100; private static final int MAX_WAIT_CYCLES = 100; @Before - public void setUp() throws Exception{ + public void setUp() throws Exception { schedulerSvc = new SchedulerFactory(); + + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setRemote(true); + InterpreterInfo interpreterInfo1 = new InterpreterInfo(MockInterpreterA.class.getName(), "mock", true, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + InterpreterRunner runner = new InterpreterRunner(INTERPRETER_SCRIPT, INTERPRETER_SCRIPT); + interpreterSetting = new InterpreterSetting.Builder() + .setId("test") + .setName("test") + .setGroup("test") + .setInterpreterInfos(interpreterInfos) + .setOption(interpreterOption) + .setRunner(runner) + .setInterpreterDir("../interpeters/test") + .create(); } @After - public void tearDown(){ - + public void tearDown() { + interpreterSetting.close(); } @Test public void test() throws Exception { - Properties p = new Properties(); - final InterpreterGroup intpGroup = new InterpreterGroup(); - Map env = new HashMap<>(); - env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); - - final RemoteInterpreter intpA = new RemoteInterpreter( - p, - "note", - MockInterpreterA.class.getName(), - new File(INTERPRETER_SCRIPT).getAbsolutePath(), - "fake", - "fakeRepo", - env, - 10 * 1000, - this, - null, - "anonymous", - false); - - intpGroup.put("note", new LinkedList()); - intpGroup.get("note").add(intpA); - intpA.setInterpreterGroup(intpGroup); + final RemoteInterpreter intpA = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); intpA.open(); Scheduler scheduler = schedulerSvc.createOrGetRemoteScheduler("test", "note", - intpA.getInterpreterProcess(), + intpA, 10); Job job = new Job("jobId", "jobName", null, 200) { Object results; + @Override public Object getReturn() { return results; @@ -120,7 +123,7 @@ protected Object jobRun() throws Throwable { new AuthenticationInfo(), new HashMap(), new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), + null, new LocalResourcePool("pool1"), new LinkedList(), null)); return "1000"; @@ -145,7 +148,7 @@ public void setResult(Object results) { } assertTrue(job.isRunning()); - Thread.sleep(5*TICK_WAIT); + Thread.sleep(5 * TICK_WAIT); assertEquals(0, scheduler.getJobsWaiting().size()); assertEquals(1, scheduler.getJobsRunning().size()); @@ -165,34 +168,10 @@ public void setResult(Object results) { @Test public void testAbortOnPending() throws Exception { - Properties p = new Properties(); - final InterpreterGroup intpGroup = new InterpreterGroup(); - Map env = new HashMap<>(); - env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); - - final RemoteInterpreter intpA = new RemoteInterpreter( - p, - "note", - MockInterpreterA.class.getName(), - new File(INTERPRETER_SCRIPT).getAbsolutePath(), - "fake", - "fakeRepo", - env, - 10 * 1000, - this, - null, - "anonymous", - false); - - intpGroup.put("note", new LinkedList()); - intpGroup.get("note").add(intpA); - intpA.setInterpreterGroup(intpGroup); - + final RemoteInterpreter intpA = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); intpA.open(); - Scheduler scheduler = schedulerSvc.createOrGetRemoteScheduler("test", "note", - intpA.getInterpreterProcess(), - 10); + Scheduler scheduler = schedulerSvc.createOrGetRemoteScheduler("test", "note", intpA, 10); Job job1 = new Job("jobId1", "jobName1", null, 200) { Object results; @@ -205,7 +184,7 @@ public void testAbortOnPending() throws Exception { new AuthenticationInfo(), new HashMap(), new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), + null, new LocalResourcePool("pool1"), new LinkedList(), null); @@ -255,7 +234,7 @@ public void setResult(Object results) { new AuthenticationInfo(), new HashMap(), new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), + null, new LocalResourcePool("pool1"), new LinkedList(), null); @@ -358,7 +337,7 @@ public void onRemoteRunParagraph(String noteId, String PsaragraphID) throws Exce } @Override - public void onParaInfosReceived(String noteId, String paragraphId, - String interpreterSettingId, Map metaInfos) { + public void onParaInfosReceived(String noteId, String paragraphId, + String interpreterSettingId, Map metaInfos) { } } diff --git a/zeppelin-interpreter/src/test/resources/conf/interpreter.json b/zeppelin-interpreter/src/test/resources/conf/interpreter.json new file mode 100644 index 00000000000..45e1d601fd3 --- /dev/null +++ b/zeppelin-interpreter/src/test/resources/conf/interpreter.json @@ -0,0 +1,115 @@ +{ + "interpreterSettings": { + "2C3RWCVAG": { + "id": "2C3RWCVAG", + "name": "test", + "group": "test", + "properties": { + "property_1": "value_1", + "property_2": "new_value_2", + "property_3": "value_3" + }, + "status": "READY", + "interpreterGroup": [ + { + "name": "echo", + "class": "org.apache.zeppelin.interpreter.EchoInterpreter", + "defaultInterpreter": true, + "editor": { + "language": "java", + "editOnDblClick": false + } + } + ], + "dependencies": [], + "option": { + "remote": true, + "port": -1, + "perNote": "shared", + "perUser": "shared", + "isExistingProcess": false, + "setPermission": false, + "users": [], + "isUserImpersonate": false + } + }, + + "2CKWE7B19": { + "id": "2CKWE7B19", + "name": "test2", + "group": "test", + "properties": { + "property_1": "value_1", + "property_2": "new_value_2", + "property_3": "value_3" + }, + "status": "READY", + "interpreterGroup": [ + { + "name": "echo", + "class": "org.apache.zeppelin.interpreter.EchoInterpreter", + "defaultInterpreter": true, + "editor": { + "language": "java", + "editOnDblClick": false + } + } + ], + "dependencies": [], + "option": { + "remote": true, + "port": -1, + "perNote": "shared", + "perUser": "shared", + "isExistingProcess": false, + "setPermission": false, + "users": [], + "isUserImpersonate": false + } + } + }, + "interpreterBindings": { + "2C6793KRV": [ + "2C48Y7FSJ", + "2C63XW4XE", + "2C66GE1VB", + "2C5VH924X", + "2C4BJDRRZ", + "2C3SQSB7V", + "2C4HKDCQW", + "2C3DR183X", + "2C66Z9XPQ", + "2C3PTPMUH", + "2C69WE69N", + "2C5SRRXHM", + "2C4ZD49PF", + "2C6V3D44K", + "2C4UB1UZA", + "2C5S1R21W", + "2C5DCRVGM", + "2C686X8ZH", + "2C3RWCVAG", + "2C3JKFMJU", + "2C3VECEG2" + ] + }, + "interpreterRepositories": [ + { + "id": "central", + "type": "default", + "url": "http://repo1.maven.org/maven2/", + "releasePolicy": { + "enabled": true, + "updatePolicy": "daily", + "checksumPolicy": "warn" + }, + "snapshotPolicy": { + "enabled": true, + "updatePolicy": "daily", + "checksumPolicy": "warn" + }, + "mirroredRepositories": [], + "repositoryManager": false + } + ] +} \ No newline at end of file diff --git a/zeppelin-interpreter/src/test/resources/interpreter/test/interpreter-setting.json b/zeppelin-interpreter/src/test/resources/interpreter/test/interpreter-setting.json new file mode 100644 index 00000000000..1ba1b94b397 --- /dev/null +++ b/zeppelin-interpreter/src/test/resources/interpreter/test/interpreter-setting.json @@ -0,0 +1,42 @@ +[ + { + "group": "test", + "name": "double_echo", + "className": "org.apache.zeppelin.interpreter.DoubleEchoInterpreter", + "properties": { + "property_1": { + "envName": "PROPERTY_1", + "propertyName": "property_1", + "defaultValue": "value_1", + "description": "desc_1" + }, + "property_2": { + "envName": "PROPERTY_2", + "propertyName": "property_2", + "defaultValue": "value_2", + "description": "desc_2" + } + } + }, + + { + "group": "test", + "name": "echo", + "defaultInterpreter": true, + "className": "org.apache.zeppelin.interpreter.EchoInterpreter", + "properties": { + "property_1": { + "envName": "PROPERTY_1", + "propertyName": "property_1", + "defaultValue": "value_1", + "description": "desc_1" + }, + "property_2": { + "envName": "PROPERTY_2", + "propertyName": "property_2", + "defaultValue": "value_2", + "description": "desc_2" + } + } + } +] diff --git a/zeppelin-interpreter/src/test/resources/log4j.properties b/zeppelin-interpreter/src/test/resources/log4j.properties index d8a783974e3..6f346916cc9 100644 --- a/zeppelin-interpreter/src/test/resources/log4j.properties +++ b/zeppelin-interpreter/src/test/resources/log4j.properties @@ -26,4 +26,6 @@ log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c:%L - %m%n # # Root logger option -log4j.rootLogger=INFO, stdout \ No newline at end of file +log4j.rootLogger=INFO, stdout +log4j.logger.org.apache.zeppelin.interpreter=DEBUG +log4j.logger.org.apache.zeppelin.scheduler=DEBUG \ No newline at end of file diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/InterpreterRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/InterpreterRestApi.java index cd0210e4f28..c1dba5c417b 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/InterpreterRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/InterpreterRestApi.java @@ -185,7 +185,7 @@ public Response restartSetting(String message, @PathParam("settingId") String se String noteId = request == null ? null : request.getNoteId(); if (null == noteId) { - interpreterSettingManager.close(setting); + interpreterSettingManager.close(settingId); } else { interpreterSettingManager.restart(settingId, noteId, SecurityUtils.getPrincipal()); } @@ -208,7 +208,7 @@ public Response restartSetting(String message, @PathParam("settingId") String se @GET @ZeppelinApi public Response listInterpreter(String message) { - Map m = interpreterSettingManager.getAvailableInterpreterSettings(); + Map m = interpreterSettingManager.getInterpreterSettingTemplates(); return new JsonResponse<>(Status.OK, "", m).build(); } 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 745347048a2..53ee1146107 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 @@ -93,13 +93,11 @@ public class ZeppelinServer extends Application { private NotebookRepoSync notebookRepo; private NotebookAuthorization notebookAuthorization; private Credentials credentials; - private DependencyResolver depResolver; public ZeppelinServer() throws Exception { ZeppelinConfiguration conf = ZeppelinConfiguration.create(); - this.depResolver = new DependencyResolver( - conf.getString(ConfVars.ZEPPELIN_INTERPRETER_LOCALREPO)); + InterpreterOutput.limit = conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_OUTPUT_LIMIT); @@ -129,13 +127,26 @@ public ZeppelinServer() throws Exception { new File(conf.getRelativeDir("zeppelin-web/src/app/spell"))); } + this.schedulerFactory = SchedulerFactory.singleton(); + this.interpreterSettingManager = new InterpreterSettingManager(conf, notebookWsServer, + notebookWsServer, notebookWsServer); + this.replFactory = new InterpreterFactory(interpreterSettingManager); + this.notebookRepo = new NotebookRepoSync(conf); + this.noteSearchService = new LuceneSearch(); + this.notebookAuthorization = NotebookAuthorization.init(conf); + this.credentials = new Credentials(conf.credentialsPersist(), conf.getCredentialsPath()); + notebook = new Notebook(conf, + notebookRepo, schedulerFactory, replFactory, interpreterSettingManager, notebookWsServer, + noteSearchService, notebookAuthorization, credentials); + ZeppelinServer.helium = new Helium( conf.getHeliumConfPath(), conf.getHeliumRegistry(), new File(conf.getRelativeDir(ConfVars.ZEPPELIN_DEP_LOCALREPO), "helium-registry-cache"), heliumBundleFactory, - heliumApplicationFactory); + heliumApplicationFactory, + interpreterSettingManager); // create bundle try { @@ -144,20 +155,6 @@ public ZeppelinServer() throws Exception { LOG.error(e.getMessage(), e); } - this.schedulerFactory = new SchedulerFactory(); - this.interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, - new InterpreterOption(true)); - this.replFactory = new InterpreterFactory(conf, notebookWsServer, - notebookWsServer, heliumApplicationFactory, depResolver, SecurityUtils.isAuthenticated(), - interpreterSettingManager); - this.notebookRepo = new NotebookRepoSync(conf); - this.noteSearchService = new LuceneSearch(); - this.notebookAuthorization = NotebookAuthorization.init(conf); - this.credentials = new Credentials(conf.credentialsPersist(), conf.getCredentialsPath()); - notebook = new Notebook(conf, - notebookRepo, schedulerFactory, replFactory, interpreterSettingManager, notebookWsServer, - noteSearchService, notebookAuthorization, credentials); - // to update notebook from application event from remote process. heliumApplicationFactory.setNotebook(notebook); // to update fire websocket event on application event. @@ -206,7 +203,7 @@ public static void main(String[] args) throws InterruptedException { LOG.info("Shutting down Zeppelin Server ... "); try { jettyWebServer.stop(); - notebook.getInterpreterSettingManager().shutdown(); + notebook.getInterpreterSettingManager().close(); notebook.close(); Thread.sleep(3000); } catch (Exception e) { 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 3ddeec034e2..102ca1aa343 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 @@ -41,12 +41,7 @@ import org.apache.zeppelin.display.Input; import org.apache.zeppelin.helium.ApplicationEventListener; import org.apache.zeppelin.helium.HeliumPackage; -import org.apache.zeppelin.interpreter.Interpreter; -import org.apache.zeppelin.interpreter.InterpreterContextRunner; -import org.apache.zeppelin.interpreter.InterpreterGroup; -import org.apache.zeppelin.interpreter.InterpreterResult; -import org.apache.zeppelin.interpreter.InterpreterResultMessage; -import org.apache.zeppelin.interpreter.InterpreterSetting; +import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; @@ -463,7 +458,8 @@ private void broadcastToNoteBindedInterpreter(String interpreterGroupId, Message Notebook notebook = notebook(); List notes = notebook.getAllNotes(); for (Note note : notes) { - List ids = notebook.getInterpreterSettingManager().getInterpreters(note.getId()); + List ids = notebook.getInterpreterSettingManager() + .getInterpreterBinding(note.getId()); for (String id : ids) { if (id.equals(interpreterGroupId)) { broadcast(note.getId(), m); @@ -1003,7 +999,7 @@ private void createNote(NotebookSocket conn, HashSet userAndRoles, Noteb List interpreterSettingIds = new LinkedList<>(); interpreterSettingIds.add(defaultInterpreterId); for (String interpreterSettingId : notebook.getInterpreterSettingManager(). - getDefaultInterpreterSettingList()) { + getInterpreterSettingIds()) { if (!interpreterSettingId.equals(defaultInterpreterId)) { interpreterSettingIds.add(interpreterSettingId); } @@ -1363,12 +1359,13 @@ private void angularObjectUpdated(NotebookSocket conn, HashSet userAndRo List settings = notebook.getInterpreterSettingManager().getInterpreterSettings(note.getId()); for (InterpreterSetting setting : settings) { - if (setting.getInterpreterGroup(user, note.getId()) == null) { + if (setting.getOrCreateInterpreterGroup(user, note.getId()) == null) { continue; } - if (interpreterGroupId.equals(setting.getInterpreterGroup(user, note.getId()).getId())) { + if (interpreterGroupId.equals(setting.getOrCreateInterpreterGroup(user, note.getId()) + .getId())) { AngularObjectRegistry angularObjectRegistry = - setting.getInterpreterGroup(user, note.getId()).getAngularObjectRegistry(); + setting.getOrCreateInterpreterGroup(user, note.getId()).getAngularObjectRegistry(); // first trying to get local registry ao = angularObjectRegistry.get(varName, noteId, paragraphId); @@ -1405,12 +1402,13 @@ private void angularObjectUpdated(NotebookSocket conn, HashSet userAndRo List settings = notebook.getInterpreterSettingManager().getInterpreterSettings(note.getId()); for (InterpreterSetting setting : settings) { - if (setting.getInterpreterGroup(user, n.getId()) == null) { + if (setting.getOrCreateInterpreterGroup(user, n.getId()) == null) { continue; } - if (interpreterGroupId.equals(setting.getInterpreterGroup(user, n.getId()).getId())) { + if (interpreterGroupId.equals(setting.getOrCreateInterpreterGroup(user, n.getId()) + .getId())) { AngularObjectRegistry angularObjectRegistry = - setting.getInterpreterGroup(user, n.getId()).getAngularObjectRegistry(); + setting.getOrCreateInterpreterGroup(user, n.getId()).getAngularObjectRegistry(); this.broadcastExcept(n.getId(), new Message(OP.ANGULAR_OBJECT_UPDATE).put("angularObject", ao) .put("interpreterGroupId", interpreterGroupId).put("noteId", n.getId()) @@ -2283,13 +2281,13 @@ private void sendAllAngularObjects(Note note, String user, NotebookSocket conn) for (InterpreterSetting intpSetting : settings) { AngularObjectRegistry registry = - intpSetting.getInterpreterGroup(user, note.getId()).getAngularObjectRegistry(); + intpSetting.getOrCreateInterpreterGroup(user, note.getId()).getAngularObjectRegistry(); List objects = registry.getAllWithGlobal(note.getId()); for (AngularObject object : objects) { conn.send(serializeMessage( new Message(OP.ANGULAR_OBJECT_UPDATE).put("angularObject", object) .put("interpreterGroupId", - intpSetting.getInterpreterGroup(user, note.getId()).getId()) + intpSetting.getOrCreateInterpreterGroup(user, note.getId()).getId()) .put("noteId", note.getId()).put("paragraphId", object.getParagraphId()))); } } @@ -2335,7 +2333,7 @@ public void onRemove(String interpreterGroupId, String name, String noteId, Stri } List settingIds = - notebook.getInterpreterSettingManager().getInterpreters(note.getId()); + notebook.getInterpreterSettingManager().getInterpreterBinding(note.getId()); for (String id : settingIds) { if (interpreterGroupId.contains(id)) { broadcast(note.getId(), 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 a7907db286b..e2f171f9424 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 @@ -307,10 +307,9 @@ private static boolean isActiveSparkHome(File dir) { protected static void shutDown() throws Exception { if (!wasRunning) { // restart interpreter to stop all interpreter processes - List settingList = ZeppelinServer.notebook.getInterpreterSettingManager() - .getDefaultInterpreterSettingList(); - for (String setting : settingList) { - ZeppelinServer.notebook.getInterpreterSettingManager().restart(setting); + List settingList = ZeppelinServer.notebook.getInterpreterSettingManager().get(); + for (InterpreterSetting setting : settingList) { + ZeppelinServer.notebook.getInterpreterSettingManager().restart(setting.getId()); } if (shiroIni != null) { FileUtils.deleteQuietly(shiroIni); diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/InterpreterRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/InterpreterRestApiTest.java index 28541bd3fe9..72dd8a7dce9 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/InterpreterRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/InterpreterRestApiTest.java @@ -80,7 +80,7 @@ public void getAvailableInterpreters() throws IOException { // then assertThat(get, isAllowed()); - assertEquals(ZeppelinServer.notebook.getInterpreterSettingManager().getAvailableInterpreterSettings().size(), + assertEquals(ZeppelinServer.notebook.getInterpreterSettingManager().getInterpreterSettingTemplates().size(), body.entrySet().size()); get.releaseConnection(); } @@ -110,7 +110,7 @@ public void testGetNonExistInterpreterSetting() throws IOException { @Test public void testSettingsCRUD() throws IOException { // when: call create setting API - String rawRequest = "{\"name\":\"md2\",\"group\":\"md\"," + + String rawRequest = "{\"name\":\"md3\",\"group\":\"md\"," + "\"properties\":{\"propname\": {\"value\": \"propvalue\", \"name\": \"propname\", \"type\": \"textarea\"}}," + "\"interpreterGroup\":[{\"class\":\"org.apache.zeppelin.markdown.Markdown\",\"name\":\"md\"}]," + "\"dependencies\":[]," + @@ -367,7 +367,7 @@ public void testAddDeleteRepository() throws IOException { @Test public void testGetMetadataInfo() throws IOException { - String jsonRequest = "{\"name\":\"spark\",\"group\":\"spark\"," + + String jsonRequest = "{\"name\":\"spark_new\",\"group\":\"spark\"," + "\"properties\":{\"propname\": {\"value\": \"propvalue\", \"name\": \"propname\", \"type\": \"textarea\"}}," + "\"interpreterGroup\":[{\"class\":\"org.apache.zeppelin.markdown.Markdown\",\"name\":\"md\"}]," + "\"dependencies\":[]," + diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java index 8da36a61bfa..10d77b2dc8e 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java @@ -30,6 +30,7 @@ import org.apache.zeppelin.notebook.socket.Message; import org.apache.zeppelin.notebook.socket.Message.OP; import org.apache.zeppelin.rest.AbstractTestRestApi; +import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.server.ZeppelinServer; import org.apache.zeppelin.user.AuthenticationInfo; import org.junit.AfterClass; @@ -95,7 +96,7 @@ public void checkInvalidOrigin(){ } @Test - public void testMakeSureNoAngularObjectBroadcastToWebsocketWhoFireTheEvent() throws IOException { + public void testMakeSureNoAngularObjectBroadcastToWebsocketWhoFireTheEvent() throws IOException, InterruptedException { // create a notebook Note note1 = notebook.createNote(anonymous); @@ -104,7 +105,7 @@ public void testMakeSureNoAngularObjectBroadcastToWebsocketWhoFireTheEvent() thr List settings = notebook.getInterpreterSettingManager().getInterpreterSettings(note1.getId()); for (InterpreterSetting setting : settings) { if (setting.getName().equals("md")) { - interpreterGroup = setting.getInterpreterGroup("anonymous", "sharedProcess"); + interpreterGroup = setting.getOrCreateInterpreterGroup("anonymous", "sharedProcess"); break; } } @@ -115,6 +116,14 @@ public void testMakeSureNoAngularObjectBroadcastToWebsocketWhoFireTheEvent() thr p1.setAuthenticationInfo(anonymous); note1.run(p1.getId()); + // wait for paragraph finished + while(true) { + if (p1.getStatus() == Job.Status.FINISHED) { + break; + } + Thread.sleep(100); + } + // add angularObject interpreterGroup.getAngularObjectRegistry().add("object1", "value1", note1.getId(), null); diff --git a/zeppelin-server/src/test/resources/log4j.properties b/zeppelin-server/src/test/resources/log4j.properties index b0d1067bc46..83689930c1a 100644 --- a/zeppelin-server/src/test/resources/log4j.properties +++ b/zeppelin-server/src/test/resources/log4j.properties @@ -33,7 +33,6 @@ log4j.logger.org.apache.hadoop.mapred=WARN log4j.logger.org.apache.hadoop.hive.ql=WARN log4j.logger.org.apache.hadoop.hive.metastore=WARN log4j.logger.org.apache.haadoop.hive.service.HiveServer=WARN -log4j.logger.org.apache.zeppelin.scheduler=WARN log4j.logger.org.quartz=WARN log4j.logger.DataNucleus=WARN diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index b3d5c63b4fc..3ae382afc9d 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -61,18 +61,21 @@ - org.slf4j - slf4j-api + ${project.groupId} + zeppelin-interpreter + ${project.version} + tests + test org.slf4j - slf4j-log4j12 + slf4j-api - commons-configuration - commons-configuration + org.slf4j + slf4j-log4j12 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 index 17a3529d915..5eecd6b1cd3 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java @@ -19,11 +19,12 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterSettingManager; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; 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.apache.zeppelin.resource.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,19 +48,22 @@ public class Helium { private final HeliumBundleFactory bundleFactory; private final HeliumApplicationFactory applicationFactory; + private final InterpreterSettingManager interpreterSettingManager; public Helium( String heliumConfPath, String registryPaths, File registryCacheDir, HeliumBundleFactory bundleFactory, - HeliumApplicationFactory applicationFactory) + HeliumApplicationFactory applicationFactory, + InterpreterSettingManager interpreterSettingManager) throws IOException { this.heliumConfPath = heliumConfPath; this.registryPaths = registryPaths; this.registryCacheDir = registryCacheDir; this.bundleFactory = bundleFactory; this.applicationFactory = applicationFactory; + this.interpreterSettingManager = interpreterSettingManager; heliumConf = loadConf(heliumConfPath); allPackages = getAllPackageInfo(); } @@ -350,7 +354,7 @@ public HeliumPackageSuggestion suggestApp(Paragraph paragraph) { allResources = resourcePool.getAll(); } } else { - allResources = ResourcePoolUtils.getAllResources(); + allResources = interpreterSettingManager.getAllResources(); } for (List pkgs : allPackages.values()) { @@ -478,4 +482,39 @@ private static Map> createMixedConfig(Map resourceList = remoteInterpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction>() { + @Override + public List call(RemoteInterpreterService.Client client) throws Exception { + return client.resourcePoolGetAll(); + } + } + ); + for (String res : resourceList) { + resourceSet.add(Resource.fromJson(res)); + } + } + } + return resourceSet; + } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumApplicationFactory.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumApplicationFactory.java index 84368a76b47..5f5330c17a5 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumApplicationFactory.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumApplicationFactory.java @@ -105,38 +105,33 @@ public void run() { private void load(RemoteInterpreterProcess intpProcess, ApplicationState appState) throws Exception { - RemoteInterpreterService.Client client = null; - synchronized (appState) { if (appState.getStatus() == ApplicationState.Status.LOADED) { // already loaded return; } - try { - appStatusChange(paragraph, appState.getId(), ApplicationState.Status.LOADING); - String pkgInfo = pkg.toJson(); - String appId = appState.getId(); - - client = intpProcess.getClient(); - RemoteApplicationResult ret = client.loadApplication( - appId, - pkgInfo, - paragraph.getNote().getId(), - paragraph.getId()); - - if (ret.isSuccess()) { - appStatusChange(paragraph, appState.getId(), ApplicationState.Status.LOADED); - } else { - throw new ApplicationException(ret.getMsg()); - } - } catch (TException e) { - intpProcess.releaseBrokenClient(client); - throw e; - } finally { - if (client != null) { - intpProcess.releaseClient(client); - } + appStatusChange(paragraph, appState.getId(), ApplicationState.Status.LOADING); + final String pkgInfo = pkg.toJson(); + final String appId = appState.getId(); + + RemoteApplicationResult ret = intpProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public RemoteApplicationResult call(RemoteInterpreterService.Client client) + throws Exception { + return client.loadApplication( + appId, + pkgInfo, + paragraph.getNote().getId(), + paragraph.getId()); + } + } + ); + if (ret.isSuccess()) { + appStatusChange(paragraph, appState.getId(), ApplicationState.Status.LOADED); + } else { + throw new ApplicationException(ret.getMsg()); } } } @@ -199,7 +194,7 @@ public void run() { } } - private void unload(ApplicationState appsToUnload) throws ApplicationException { + private void unload(final ApplicationState appsToUnload) throws ApplicationException { synchronized (appsToUnload) { if (appsToUnload.getStatus() != ApplicationState.Status.LOADED) { throw new ApplicationException( @@ -217,26 +212,19 @@ private void unload(ApplicationState appsToUnload) throws ApplicationException { throw new ApplicationException("Target interpreter process is not running"); } - RemoteInterpreterService.Client client; - try { - client = intpProcess.getClient(); - } catch (Exception e) { - throw new ApplicationException(e); - } - - try { - RemoteApplicationResult ret = client.unloadApplication(appsToUnload.getId()); - - if (ret.isSuccess()) { - appStatusChange(paragraph, appsToUnload.getId(), ApplicationState.Status.UNLOADED); - } else { - throw new ApplicationException(ret.getMsg()); - } - } catch (TException e) { - intpProcess.releaseBrokenClient(client); - throw new ApplicationException(e); - } finally { - intpProcess.releaseClient(client); + RemoteApplicationResult ret = intpProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public RemoteApplicationResult call(RemoteInterpreterService.Client client) + throws Exception { + return client.unloadApplication(appsToUnload.getId()); + } + } + ); + if (ret.isSuccess()) { + appStatusChange(paragraph, appsToUnload.getId(), ApplicationState.Status.UNLOADED); + } else { + throw new ApplicationException(ret.getMsg()); } } } @@ -286,7 +274,7 @@ public void run() { } } - private void run(ApplicationState app) throws ApplicationException { + private void run(final ApplicationState app) throws ApplicationException { synchronized (app) { if (app.getStatus() != ApplicationState.Status.LOADED) { throw new ApplicationException( @@ -303,29 +291,19 @@ private void run(ApplicationState app) throws ApplicationException { if (intpProcess == null) { throw new ApplicationException("Target interpreter process is not running"); } - RemoteInterpreterService.Client client = null; - try { - client = intpProcess.getClient(); - } catch (Exception e) { - throw new ApplicationException(e); - } - - try { - RemoteApplicationResult ret = client.runApplication(app.getId()); - - if (ret.isSuccess()) { - // success - } else { - throw new ApplicationException(ret.getMsg()); - } - } catch (TException e) { - intpProcess.releaseBrokenClient(client); - client = null; - throw new ApplicationException(e); - } finally { - if (client != null) { - intpProcess.releaseClient(client); - } + RemoteApplicationResult ret = intpProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public RemoteApplicationResult call(RemoteInterpreterService.Client client) + throws Exception { + return client.runApplication(app.getId()); + } + } + ); + if (ret.isSuccess()) { + // success + } else { + throw new ApplicationException(ret.getMsg()); } } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java deleted file mode 100644 index 9403b4f34e2..00000000000 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java +++ /dev/null @@ -1,423 +0,0 @@ -/* - * 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.interpreter; - -import com.google.common.base.Joiner; -import com.google.common.base.Preconditions; -import org.apache.commons.lang.NullArgumentException; -import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; -import org.apache.zeppelin.dep.DependencyResolver; -import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.display.AngularObjectRegistryListener; -import org.apache.zeppelin.helium.ApplicationEventListener; -import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonatype.aether.RepositoryException; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -/** - * Manage interpreters. - */ -public class InterpreterFactory implements InterpreterGroupFactory { - private static final Logger logger = LoggerFactory.getLogger(InterpreterFactory.class); - - private Map cleanCl = - Collections.synchronizedMap(new HashMap()); - - private ZeppelinConfiguration conf; - - private final InterpreterSettingManager interpreterSettingManager; - private AngularObjectRegistryListener angularObjectRegistryListener; - private final RemoteInterpreterProcessListener remoteInterpreterProcessListener; - private final ApplicationEventListener appEventListener; - - private boolean shiroEnabled; - - private Map env = new HashMap<>(); - - private Interpreter devInterpreter; - - public InterpreterFactory(ZeppelinConfiguration conf, - AngularObjectRegistryListener angularObjectRegistryListener, - RemoteInterpreterProcessListener remoteInterpreterProcessListener, - ApplicationEventListener appEventListener, DependencyResolver depResolver, - boolean shiroEnabled, InterpreterSettingManager interpreterSettingManager) - throws InterpreterException, IOException, RepositoryException { - this.conf = conf; - this.angularObjectRegistryListener = angularObjectRegistryListener; - this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; - this.appEventListener = appEventListener; - this.shiroEnabled = shiroEnabled; - - this.interpreterSettingManager = interpreterSettingManager; - //TODO(jl): Fix it not to use InterpreterGroupFactory - interpreterSettingManager.setInterpreterGroupFactory(this); - - logger.info("shiroEnabled: {}", shiroEnabled); - } - - /** - * @param id interpreterGroup id. Combination of interpreterSettingId + noteId/userId/shared - * depends on interpreter mode - */ - @Override - public InterpreterGroup createInterpreterGroup(String id, InterpreterOption option) - throws InterpreterException, NullArgumentException { - - //When called from REST API without option we receive NPE - if (option == null) { - throw new NullArgumentException("option"); - } - - AngularObjectRegistry angularObjectRegistry; - - InterpreterGroup interpreterGroup = new InterpreterGroup(id); - if (option.isRemote()) { - angularObjectRegistry = - new RemoteAngularObjectRegistry(id, angularObjectRegistryListener, interpreterGroup); - } else { - angularObjectRegistry = new AngularObjectRegistry(id, angularObjectRegistryListener); - - // TODO(moon) : create distributed resource pool for local interpreters and set - } - - interpreterGroup.setAngularObjectRegistry(angularObjectRegistry); - return interpreterGroup; - } - - public void createInterpretersForNote(InterpreterSetting interpreterSetting, String user, - String noteId, String interpreterSessionKey) { - InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup(user, noteId); - InterpreterOption option = interpreterSetting.getOption(); - Properties properties = interpreterSetting.getFlatProperties(); - // if interpreters are already there, wait until they're being removed - synchronized (interpreterGroup) { - long interpreterRemovalWaitStart = System.nanoTime(); - // interpreter process supposed to be terminated by RemoteInterpreterProcess.dereference() - // in ZEPPELIN_INTERPRETER_CONNECT_TIMEOUT msec. However, if termination of the process and - // removal from interpreter group take too long, throw an error. - long minTimeout = 10L * 1000 * 1000000; // 10 sec - long interpreterRemovalWaitTimeout = Math.max(minTimeout, - conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_CONNECT_TIMEOUT) * 1000000L * 2); - while (interpreterGroup.containsKey(interpreterSessionKey)) { - if (System.nanoTime() - interpreterRemovalWaitStart > interpreterRemovalWaitTimeout) { - throw new InterpreterException("Can not create interpreter"); - } - try { - interpreterGroup.wait(1000); - } catch (InterruptedException e) { - logger.debug(e.getMessage(), e); - } - } - } - - logger.info("Create interpreter instance {} for note {}", interpreterSetting.getName(), noteId); - - List interpreterInfos = interpreterSetting.getInterpreterInfos(); - String path = interpreterSetting.getPath(); - InterpreterRunner runner = interpreterSetting.getInterpreterRunner(); - Interpreter interpreter; - for (InterpreterInfo info : interpreterInfos) { - if (option.isRemote()) { - if (option.isExistingProcess()) { - interpreter = - connectToRemoteRepl(interpreterSessionKey, info.getClassName(), option.getHost(), - option.getPort(), properties, interpreterSetting.getId(), user, - option.isUserImpersonate); - } else { - interpreter = createRemoteRepl(path, interpreterSessionKey, info.getClassName(), - properties, interpreterSetting.getId(), user, option.isUserImpersonate(), runner); - } - } else { - interpreter = createRepl(interpreterSetting.getPath(), info.getClassName(), properties); - } - - synchronized (interpreterGroup) { - List interpreters = interpreterGroup.get(interpreterSessionKey); - if (null == interpreters) { - interpreters = new ArrayList<>(); - interpreterGroup.put(interpreterSessionKey, interpreters); - } - if (info.isDefaultInterpreter()) { - interpreters.add(0, interpreter); - } else { - interpreters.add(interpreter); - } - } - logger.info("Interpreter {} {} created", interpreter.getClassName(), interpreter.hashCode()); - interpreter.setInterpreterGroup(interpreterGroup); - } - } - - private Interpreter createRepl(String dirName, String className, Properties property) - throws InterpreterException { - logger.info("Create repl {} from {}", className, dirName); - - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - try { - - URLClassLoader ccl = cleanCl.get(dirName); - if (ccl == null) { - // classloader fallback - ccl = URLClassLoader.newInstance(new URL[]{}, oldcl); - } - - boolean separateCL = true; - try { // check if server's classloader has driver already. - Class cls = this.getClass().forName(className); - if (cls != null) { - separateCL = false; - } - } catch (Exception e) { - logger.error("exception checking server classloader driver", e); - } - - URLClassLoader cl; - - if (separateCL == true) { - cl = URLClassLoader.newInstance(new URL[]{}, ccl); - } else { - cl = ccl; - } - Thread.currentThread().setContextClassLoader(cl); - - Class replClass = (Class) cl.loadClass(className); - Constructor constructor = - replClass.getConstructor(new Class[]{Properties.class}); - Interpreter repl = constructor.newInstance(property); - repl.setClassloaderUrls(ccl.getURLs()); - LazyOpenInterpreter intp = new LazyOpenInterpreter(new ClassloaderInterpreter(repl, cl)); - return intp; - } catch (SecurityException e) { - throw new InterpreterException(e); - } catch (NoSuchMethodException e) { - throw new InterpreterException(e); - } catch (IllegalArgumentException e) { - throw new InterpreterException(e); - } catch (InstantiationException e) { - throw new InterpreterException(e); - } catch (IllegalAccessException e) { - throw new InterpreterException(e); - } catch (InvocationTargetException e) { - throw new InterpreterException(e); - } catch (ClassNotFoundException e) { - throw new InterpreterException(e); - } finally { - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - private Interpreter connectToRemoteRepl(String interpreterSessionKey, String className, - String host, int port, Properties property, String interpreterSettingId, String userName, - Boolean isUserImpersonate) { - int connectTimeout = conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_CONNECT_TIMEOUT); - int maxPoolSize = conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_MAX_POOL_SIZE); - String localRepoPath = conf.getInterpreterLocalRepoPath() + "/" + interpreterSettingId; - LazyOpenInterpreter intp = new LazyOpenInterpreter( - new RemoteInterpreter(property, interpreterSessionKey, className, host, port, localRepoPath, - connectTimeout, maxPoolSize, remoteInterpreterProcessListener, appEventListener, - userName, isUserImpersonate, conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_OUTPUT_LIMIT))); - return intp; - } - - Interpreter createRemoteRepl(String interpreterPath, String interpreterSessionKey, - String className, Properties property, String interpreterSettingId, - String userName, Boolean isUserImpersonate, InterpreterRunner interpreterRunner) { - int connectTimeout = conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_CONNECT_TIMEOUT); - String localRepoPath = conf.getInterpreterLocalRepoPath() + "/" + interpreterSettingId; - int maxPoolSize = conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_MAX_POOL_SIZE); - String interpreterRunnerPath; - String interpreterGroupName = interpreterSettingManager.get(interpreterSettingId).getName(); - if (null != interpreterRunner) { - interpreterRunnerPath = interpreterRunner.getPath(); - Path p = Paths.get(interpreterRunnerPath); - if (!p.isAbsolute()) { - interpreterRunnerPath = Joiner.on(File.separator) - .join(interpreterPath, interpreterRunnerPath); - } - } else { - interpreterRunnerPath = conf.getInterpreterRemoteRunnerPath(); - } - - RemoteInterpreter remoteInterpreter = - new RemoteInterpreter(property, interpreterSessionKey, className, - interpreterRunnerPath, interpreterPath, localRepoPath, connectTimeout, maxPoolSize, - remoteInterpreterProcessListener, appEventListener, userName, isUserImpersonate, - conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_OUTPUT_LIMIT), interpreterGroupName); - remoteInterpreter.addEnv(env); - - return new LazyOpenInterpreter(remoteInterpreter); - } - - private List createOrGetInterpreterList(String user, String noteId, - InterpreterSetting setting) { - InterpreterGroup interpreterGroup = setting.getInterpreterGroup(user, noteId); - synchronized (interpreterGroup) { - String interpreterSessionKey = - interpreterSettingManager.getInterpreterSessionKey(user, noteId, setting); - if (!interpreterGroup.containsKey(interpreterSessionKey)) { - createInterpretersForNote(setting, user, noteId, interpreterSessionKey); - } - return interpreterGroup.get(interpreterSessionKey); - } - } - - private InterpreterSetting getInterpreterSettingByGroup(List settings, - String group) { - Preconditions.checkNotNull(group, "group should be not null"); - - for (InterpreterSetting setting : settings) { - if (group.equals(setting.getName())) { - return setting; - } - } - return null; - } - - private String getInterpreterClassFromInterpreterSetting(InterpreterSetting setting, - String name) { - Preconditions.checkNotNull(name, "name should be not null"); - - for (InterpreterInfo info : setting.getInterpreterInfos()) { - String infoName = info.getName(); - if (null != info.getName() && name.equals(infoName)) { - return info.getClassName(); - } - } - return null; - } - - private Interpreter getInterpreter(String user, String noteId, InterpreterSetting setting, - String name) { - Preconditions.checkNotNull(noteId, "noteId should be not null"); - Preconditions.checkNotNull(setting, "setting should be not null"); - Preconditions.checkNotNull(name, "name should be not null"); - - String className; - if (null != (className = getInterpreterClassFromInterpreterSetting(setting, name))) { - List interpreterGroup = createOrGetInterpreterList(user, noteId, setting); - for (Interpreter interpreter : interpreterGroup) { - if (className.equals(interpreter.getClassName())) { - return interpreter; - } - } - } - return null; - } - - public Interpreter getInterpreter(String user, String noteId, String replName) { - List settings = interpreterSettingManager.getInterpreterSettings(noteId); - InterpreterSetting setting; - Interpreter interpreter; - - if (settings == null || settings.size() == 0) { - return null; - } - - if (replName == null || replName.trim().length() == 0) { - // get default settings (first available) - // TODO(jl): Fix it in case of returning null - InterpreterSetting defaultSettings = interpreterSettingManager - .getDefaultInterpreterSetting(settings); - return createOrGetInterpreterList(user, noteId, defaultSettings).get(0); - } - - String[] replNameSplit = replName.split("\\."); - if (replNameSplit.length == 2) { - String group = null; - String name = null; - group = replNameSplit[0]; - name = replNameSplit[1]; - - setting = getInterpreterSettingByGroup(settings, group); - - if (null != setting) { - interpreter = getInterpreter(user, noteId, setting, name); - - if (null != interpreter) { - return interpreter; - } - } - - throw new InterpreterException(replName + " interpreter not found"); - - } else { - // first assume replName is 'name' of interpreter. ('groupName' is ommitted) - // search 'name' from first (default) interpreter group - // TODO(jl): Handle with noteId to support defaultInterpreter per note. - setting = interpreterSettingManager.getDefaultInterpreterSetting(settings); - - interpreter = getInterpreter(user, noteId, setting, replName); - - if (null != interpreter) { - return interpreter; - } - - // next, assume replName is 'group' of interpreter ('name' is ommitted) - // search interpreter group and return first interpreter. - setting = getInterpreterSettingByGroup(settings, replName); - - if (null != setting) { - List interpreters = createOrGetInterpreterList(user, noteId, setting); - if (null != interpreters) { - return interpreters.get(0); - } - } - - // Support the legacy way to use it - for (InterpreterSetting s : settings) { - if (s.getGroup().equals(replName)) { - List interpreters = createOrGetInterpreterList(user, noteId, s); - if (null != interpreters) { - return interpreters.get(0); - } - } - } - } - - return null; - } - - public Map getEnv() { - return env; - } - - public void setEnv(Map env) { - this.env = env; - } - - -} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroupFactory.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroupFactory.java deleted file mode 100644 index 3b9be400dd2..00000000000 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroupFactory.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.interpreter; - -import org.apache.commons.lang.NullArgumentException; - -/** - * Created InterpreterGroup - */ -public interface InterpreterGroupFactory { - InterpreterGroup createInterpreterGroup(String interpreterGroupId, InterpreterOption option); -} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java deleted file mode 100644 index 752b4e28824..00000000000 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java +++ /dev/null @@ -1,459 +0,0 @@ -/* - * 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.interpreter; - -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -import org.apache.zeppelin.dep.Dependency; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.annotations.SerializedName; -import com.google.gson.internal.StringMap; - -import static org.apache.zeppelin.notebook.utility.IdHashes.generateId; - -/** - * Interpreter settings - */ -public class InterpreterSetting { - - private static final Logger logger = LoggerFactory.getLogger(InterpreterSetting.class); - private static final String SHARED_PROCESS = "shared_process"; - private String id; - private String name; - // always be null in case of InterpreterSettingRef - private String group; - private transient Map infos; - - // Map of the note and paragraphs which has runtime infos generated by this interpreter setting. - // This map is used to clear the infos in paragraph when the interpretersetting is restarted - private transient Map> runtimeInfosToBeCleared; - - /** - * properties can be either Map or - * Map - * properties should be: - * - Map when Interpreter instances are saved to - * `conf/interpreter.json` file - * - Map when Interpreters are registered - * : this is needed after https://github.com/apache/zeppelin/pull/1145 - * which changed the way of getting default interpreter setting AKA interpreterSettingsRef - */ - private Object properties; - private Status status; - private String errorReason; - - @SerializedName("interpreterGroup") - private List interpreterInfos; - private final transient Map interpreterGroupRef = new HashMap<>(); - private List dependencies = new LinkedList<>(); - private InterpreterOption option; - private transient String path; - - @SerializedName("runner") - private InterpreterRunner interpreterRunner; - - @Deprecated - private transient InterpreterGroupFactory interpreterGroupFactory; - - private final transient ReentrantReadWriteLock.ReadLock interpreterGroupReadLock; - private final transient ReentrantReadWriteLock.WriteLock interpreterGroupWriteLock; - - public InterpreterSetting() { - ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - interpreterGroupReadLock = lock.readLock(); - interpreterGroupWriteLock = lock.writeLock(); - } - - public InterpreterSetting(String id, String name, String group, - List interpreterInfos, Object properties, List dependencies, - InterpreterOption option, String path, InterpreterRunner runner) { - this(); - this.id = id; - this.name = name; - this.group = group; - this.interpreterInfos = interpreterInfos; - this.properties = properties; - this.dependencies = dependencies; - this.option = option; - this.path = path; - this.status = Status.READY; - this.interpreterRunner = runner; - } - - public InterpreterSetting(String name, String group, List interpreterInfos, - Object properties, List dependencies, InterpreterOption option, String path, - InterpreterRunner runner) { - this(generateId(), name, group, interpreterInfos, properties, dependencies, option, path, - runner); - } - - /** - * Create interpreter from interpreterSettingRef - * - * @param o interpreterSetting from interpreterSettingRef - */ - public InterpreterSetting(InterpreterSetting o) { - this(generateId(), o.getName(), o.getGroup(), o.getInterpreterInfos(), o.getProperties(), - o.getDependencies(), o.getOption(), o.getPath(), o.getInterpreterRunner()); - } - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public String getGroup() { - return group; - } - - private String getInterpreterProcessKey(String user, String noteId) { - InterpreterOption option = getOption(); - String key; - if (getOption().isExistingProcess) { - key = Constants.EXISTING_PROCESS; - } else if (getOption().isProcess()) { - key = (option.perUserIsolated() ? user : "") + ":" + (option.perNoteIsolated() ? noteId : ""); - } else { - key = SHARED_PROCESS; - } - - //logger.debug("getInterpreterProcessKey: {} for InterpreterSetting Id: {}, Name: {}", - // key, getId(), getName()); - return key; - } - - private boolean isEqualInterpreterKeyProcessKey(String refKey, String processKey) { - InterpreterOption option = getOption(); - int validCount = 0; - if (getOption().isProcess() - && !(option.perUserIsolated() == true && option.perNoteIsolated() == true)) { - - List processList = Arrays.asList(processKey.split(":")); - List refList = Arrays.asList(refKey.split(":")); - - if (refList.size() <= 1 || processList.size() <= 1) { - return refKey.equals(processKey); - } - - if (processList.get(0).equals("") || processList.get(0).equals(refList.get(0))) { - validCount = validCount + 1; - } - - if (processList.get(1).equals("") || processList.get(1).equals(refList.get(1))) { - validCount = validCount + 1; - } - - return (validCount >= 2); - } else { - return refKey.equals(processKey); - } - } - - String getInterpreterSessionKey(String user, String noteId) { - InterpreterOption option = getOption(); - String key; - if (option.isExistingProcess()) { - key = Constants.EXISTING_PROCESS; - } else if (option.perNoteScoped() && option.perUserScoped()) { - key = user + ":" + noteId; - } else if (option.perUserScoped()) { - key = user; - } else if (option.perNoteScoped()) { - key = noteId; - } else { - key = "shared_session"; - } - - logger.debug("Interpreter session key: {}, for note: {}, user: {}, InterpreterSetting Name: " + - "{}", key, noteId, user, getName()); - return key; - } - - public InterpreterGroup getInterpreterGroup(String user, String noteId) { - String key = getInterpreterProcessKey(user, noteId); - if (!interpreterGroupRef.containsKey(key)) { - String interpreterGroupId = getId() + ":" + key; - InterpreterGroup intpGroup = - interpreterGroupFactory.createInterpreterGroup(interpreterGroupId, getOption()); - - interpreterGroupWriteLock.lock(); - logger.debug("create interpreter group with groupId:" + interpreterGroupId); - interpreterGroupRef.put(key, intpGroup); - interpreterGroupWriteLock.unlock(); - } - try { - interpreterGroupReadLock.lock(); - return interpreterGroupRef.get(key); - } finally { - interpreterGroupReadLock.unlock(); - } - } - - public Collection getAllInterpreterGroups() { - try { - interpreterGroupReadLock.lock(); - return new LinkedList<>(interpreterGroupRef.values()); - } finally { - interpreterGroupReadLock.unlock(); - } - } - - void closeAndRemoveInterpreterGroup(String noteId, String user) { - if (user.equals("anonymous")) { - user = ""; - } - String processKey = getInterpreterProcessKey(user, noteId); - String sessionKey = getInterpreterSessionKey(user, noteId); - List groupToRemove = new LinkedList<>(); - InterpreterGroup groupItem; - for (String intpKey : new HashSet<>(interpreterGroupRef.keySet())) { - if (isEqualInterpreterKeyProcessKey(intpKey, processKey)) { - interpreterGroupWriteLock.lock(); - // TODO(jl): interpreterGroup has two or more sessionKeys inside it. thus we should not - // remove interpreterGroup if it has two or more values. - groupItem = interpreterGroupRef.get(intpKey); - interpreterGroupWriteLock.unlock(); - groupToRemove.add(groupItem); - } - for (InterpreterGroup groupToClose : groupToRemove) { - // TODO(jl): Fix the logic removing session. Now, it's handled into groupToClose.clsose() - groupToClose.close(interpreterGroupRef, intpKey, sessionKey); - } - groupToRemove.clear(); - } - - //Remove session because all interpreters in this session are closed - //TODO(jl): Change all code to handle interpreter one by one or all at once - - } - - void closeAndRemoveAllInterpreterGroups() { - for (String processKey : new HashSet<>(interpreterGroupRef.keySet())) { - InterpreterGroup interpreterGroup = interpreterGroupRef.get(processKey); - for (String sessionKey : new HashSet<>(interpreterGroup.keySet())) { - interpreterGroup.close(interpreterGroupRef, processKey, sessionKey); - } - } - } - - void shutdownAndRemoveAllInterpreterGroups() { - for (InterpreterGroup interpreterGroup : interpreterGroupRef.values()) { - interpreterGroup.shutdown(); - } - } - - public Object getProperties() { - return properties; - } - - public Properties getFlatProperties() { - Properties p = new Properties(); - if (properties != null) { - Map propertyMap = (Map) properties; - for (String key : propertyMap.keySet()) { - InterpreterProperty tmp = propertyMap.get(key); - p.put(tmp.getName() != null ? tmp.getName() : key, - tmp.getValue() != null ? tmp.getValue().toString() : null); - } - } - return p; - } - - public List getDependencies() { - if (dependencies == null) { - return new LinkedList<>(); - } - return dependencies; - } - - public void setDependencies(List dependencies) { - this.dependencies = dependencies; - } - - public InterpreterOption getOption() { - if (option == null) { - option = new InterpreterOption(); - } - - return option; - } - - public void setOption(InterpreterOption option) { - this.option = option; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public List getInterpreterInfos() { - return interpreterInfos; - } - - void setInterpreterGroupFactory(InterpreterGroupFactory interpreterGroupFactory) { - this.interpreterGroupFactory = interpreterGroupFactory; - } - - void appendDependencies(List dependencies) { - for (Dependency dependency : dependencies) { - if (!this.dependencies.contains(dependency)) { - this.dependencies.add(dependency); - } - } - } - - void setInterpreterOption(InterpreterOption interpreterOption) { - this.option = interpreterOption; - } - - public void setProperties(Map p) { - this.properties = p; - } - - void setGroup(String group) { - this.group = group; - } - - void setName(String name) { - this.name = name; - } - - /*** - * Interpreter status - */ - public enum Status { - DOWNLOADING_DEPENDENCIES, - ERROR, - READY - } - - public Status getStatus() { - return status; - } - - public void setStatus(Status status) { - this.status = status; - } - - public String getErrorReason() { - return errorReason; - } - - public void setErrorReason(String errorReason) { - this.errorReason = errorReason; - } - - public void setInfos(Map infos) { - this.infos = infos; - } - - public Map getInfos() { - return infos; - } - - public InterpreterRunner getInterpreterRunner() { - return interpreterRunner; - } - - public void setInterpreterRunner(InterpreterRunner interpreterRunner) { - this.interpreterRunner = interpreterRunner; - } - - public void addNoteToPara(String noteId, String paraId) { - if (runtimeInfosToBeCleared == null) { - runtimeInfosToBeCleared = new HashMap<>(); - } - Set paraIdSet = runtimeInfosToBeCleared.get(noteId); - if (paraIdSet == null) { - paraIdSet = new HashSet<>(); - runtimeInfosToBeCleared.put(noteId, paraIdSet); - } - paraIdSet.add(paraId); - } - - public Map> getNoteIdAndParaMap() { - return runtimeInfosToBeCleared; - } - - public void clearNoteIdAndParaMap() { - runtimeInfosToBeCleared = null; - } - - // For backward compatibility of interpreter.json format after ZEPPELIN-2654 - public void convertPermissionsFromUsersToOwners(JsonObject jsonObject) { - if (jsonObject != null) { - JsonObject option = jsonObject.getAsJsonObject("option"); - if (option != null) { - JsonArray users = option.getAsJsonArray("users"); - if (users != null) { - if (this.option.getOwners() == null) { - this.option.owners = new LinkedList<>(); - } - for (JsonElement user : users) { - this.option.getOwners().add(user.getAsString()); - } - } - } - } - } - - // For backward compatibility of interpreter.json format after ZEPPELIN-2403 - public void convertFlatPropertiesToPropertiesWithWidgets() { - StringMap newProperties = new StringMap(); - if (properties != null && properties instanceof StringMap) { - StringMap p = (StringMap) properties; - - for (Object o : p.entrySet()) { - Map.Entry entry = (Map.Entry) o; - if (!(entry.getValue() instanceof StringMap)) { - StringMap newProperty = new StringMap(); - newProperty.put("name", entry.getKey()); - newProperty.put("value", entry.getValue()); - newProperty.put("type", InterpreterPropertyType.TEXTAREA.getValue()); - newProperties.put(entry.getKey().toString(), newProperty); - } else { - // already converted - return; - } - } - - this.properties = newProperties; - } - } -} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java deleted file mode 100644 index 12545d6ba69..00000000000 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java +++ /dev/null @@ -1,1136 +0,0 @@ -/* - * 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.interpreter; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.lang.reflect.Type; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.charset.StandardCharsets; -import java.nio.file.DirectoryStream.Filter; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.PosixFilePermission; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.EnumSet; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.ArrayUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; -import org.apache.zeppelin.dep.Dependency; -import org.apache.zeppelin.dep.DependencyResolver; -import org.apache.zeppelin.interpreter.Interpreter.RegisteredInterpreter; -import org.apache.zeppelin.scheduler.Job; -import org.apache.zeppelin.scheduler.Job.Status; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonatype.aether.RepositoryException; -import org.sonatype.aether.repository.Authentication; -import org.sonatype.aether.repository.Proxy; -import org.sonatype.aether.repository.RemoteRepository; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.internal.StringMap; -import com.google.gson.reflect.TypeToken; - -import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; -import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; - -/** - * TBD - */ -public class InterpreterSettingManager { - - private static final Logger logger = LoggerFactory.getLogger(InterpreterSettingManager.class); - private static final String SHARED_SESSION = "shared_session"; - private static final Map DEFAULT_EDITOR = ImmutableMap.of( - "language", (Object) "text", - "editOnDblClick", false); - - private final ZeppelinConfiguration zeppelinConfiguration; - private final Path interpreterDirPath; - private final Path interpreterBindingPath; - - /** - * This is only references with default settings, name and properties - * key: InterpreterSetting.name - */ - private final Map interpreterSettingsRef; - /** - * This is used by creating and running Interpreters - * key: InterpreterSetting.id <- This is becuase backward compatibility - */ - private final Map interpreterSettings; - private final Map> interpreterBindings; - - private final DependencyResolver dependencyResolver; - private final List interpreterRepositories; - - private final InterpreterOption defaultOption; - - private final Map cleanCl; - - @Deprecated - private String[] interpreterClassList; - private String[] interpreterGroupOrderList; - private InterpreterGroupFactory interpreterGroupFactory; - - private final Gson gson; - - public InterpreterSettingManager(ZeppelinConfiguration zeppelinConfiguration, - DependencyResolver dependencyResolver, InterpreterOption interpreterOption) - throws IOException, RepositoryException { - this.zeppelinConfiguration = zeppelinConfiguration; - this.interpreterDirPath = Paths.get(zeppelinConfiguration.getInterpreterDir()); - logger.debug("InterpreterRootPath: {}", interpreterDirPath); - this.interpreterBindingPath = Paths.get(zeppelinConfiguration.getInterpreterSettingPath()); - logger.debug("InterpreterBindingPath: {}", interpreterBindingPath); - - this.interpreterSettingsRef = Maps.newConcurrentMap(); - this.interpreterSettings = Maps.newConcurrentMap(); - this.interpreterBindings = Maps.newConcurrentMap(); - - this.dependencyResolver = dependencyResolver; - this.interpreterRepositories = dependencyResolver.getRepos(); - - this.defaultOption = interpreterOption; - - this.cleanCl = Collections.synchronizedMap(new HashMap()); - - String replsConf = zeppelinConfiguration.getString(ConfVars.ZEPPELIN_INTERPRETERS); - this.interpreterClassList = replsConf.split(","); - String groupOrder = zeppelinConfiguration.getString(ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER); - this.interpreterGroupOrderList = groupOrder.split(","); - - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.setPrettyPrinting(); - this.gson = gsonBuilder.create(); - - init(); - } - - /** - * Remember this method doesn't keep current connections after being called - */ - private void loadFromFile() { - if (!Files.exists(interpreterBindingPath)) { - // nothing to read - return; - } - InterpreterInfoSaving infoSaving; - try (BufferedReader jsonReader = - Files.newBufferedReader(interpreterBindingPath, StandardCharsets.UTF_8)) { - JsonParser jsonParser = new JsonParser(); - JsonObject jsonObject = jsonParser.parse(jsonReader).getAsJsonObject(); - infoSaving = gson.fromJson(jsonObject.toString(), InterpreterInfoSaving.class); - - for (String k : infoSaving.interpreterSettings.keySet()) { - InterpreterSetting setting = infoSaving.interpreterSettings.get(k); - - setting.convertFlatPropertiesToPropertiesWithWidgets(); - - List infos = setting.getInterpreterInfos(); - - // Convert json StringMap to Properties - StringMap p = (StringMap) setting.getProperties(); - Map properties = new HashMap(); - for (String key : p.keySet()) { - StringMap fields = (StringMap) p.get(key); - String type = InterpreterPropertyType.TEXTAREA.getValue(); - try { - type = InterpreterPropertyType.byValue(fields.get("type")).getValue(); - } catch (Exception e) { - logger.warn("Incorrect type of property {} in settings {}", key, - setting.getId()); - } - properties.put(key, new InterpreterProperty(key, fields.get("value"), type)); - } - setting.setProperties(properties); - - // Always use separate interpreter process - // While we decided to turn this feature on always (without providing - // enable/disable option on GUI). - // previously created setting should turn this feature on here. - setting.getOption().setRemote(true); - - setting.convertPermissionsFromUsersToOwners( - jsonObject.getAsJsonObject("interpreterSettings").getAsJsonObject(setting.getId())); - - // Update transient information from InterpreterSettingRef - InterpreterSetting interpreterSettingObject = - interpreterSettingsRef.get(setting.getGroup()); - if (interpreterSettingObject == null) { - logger.warn("can't get InterpreterSetting " + - "Information From loaded Interpreter Setting Ref - {} ", setting.getGroup()); - continue; - } - String depClassPath = interpreterSettingObject.getPath(); - setting.setPath(depClassPath); - - for (InterpreterInfo info : infos) { - if (info.getEditor() == null) { - Map editor = getEditorFromSettingByClassName(interpreterSettingObject, - info.getClassName()); - info.setEditor(editor); - } - } - - setting.setInterpreterGroupFactory(interpreterGroupFactory); - - loadInterpreterDependencies(setting); - interpreterSettings.put(k, setting); - } - - interpreterBindings.putAll(infoSaving.interpreterBindings); - - if (infoSaving.interpreterRepositories != null) { - for (RemoteRepository repo : infoSaving.interpreterRepositories) { - if (!dependencyResolver.getRepos().contains(repo)) { - this.interpreterRepositories.add(repo); - } - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - public void saveToFile() throws IOException { - String jsonString; - - synchronized (interpreterSettings) { - InterpreterInfoSaving info = new InterpreterInfoSaving(); - info.interpreterBindings = interpreterBindings; - info.interpreterSettings = interpreterSettings; - info.interpreterRepositories = interpreterRepositories; - - jsonString = info.toJson(); - } - - if (!Files.exists(interpreterBindingPath)) { - Files.createFile(interpreterBindingPath); - - try { - Set permissions = EnumSet.of(OWNER_READ, OWNER_WRITE); - Files.setPosixFilePermissions(interpreterBindingPath, permissions); - } catch (UnsupportedOperationException e) { - // File system does not support Posix file permissions (likely windows) - continue anyway. - logger.warn("unable to setPosixFilePermissions on '{}'.", interpreterBindingPath); - } - } - - FileOutputStream fos = new FileOutputStream(interpreterBindingPath.toFile(), false); - OutputStreamWriter out = new OutputStreamWriter(fos); - out.append(jsonString); - out.close(); - fos.close(); - } - - //TODO(jl): Fix it to remove InterpreterGroupFactory - public void setInterpreterGroupFactory(InterpreterGroupFactory interpreterGroupFactory) { - for (InterpreterSetting setting : interpreterSettings.values()) { - setting.setInterpreterGroupFactory(interpreterGroupFactory); - } - this.interpreterGroupFactory = interpreterGroupFactory; - } - - private void init() throws InterpreterException, IOException, RepositoryException { - String interpreterJson = zeppelinConfiguration.getInterpreterJson(); - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - - if (Files.exists(interpreterDirPath)) { - for (Path interpreterDir : Files - .newDirectoryStream(interpreterDirPath, new Filter() { - @Override - public boolean accept(Path entry) throws IOException { - return Files.exists(entry) && Files.isDirectory(entry); - } - })) { - String interpreterDirString = interpreterDir.toString(); - - /** - * Register interpreter by the following ordering - * 1. Register it from path {ZEPPELIN_HOME}/interpreter/{interpreter_name}/ - * interpreter-setting.json - * 2. Register it from interpreter-setting.json in classpath - * {ZEPPELIN_HOME}/interpreter/{interpreter_name} - * 3. Register it by Interpreter.register - */ - if (!registerInterpreterFromPath(interpreterDirString, interpreterJson)) { - if (!registerInterpreterFromResource(cl, interpreterDirString, interpreterJson)) { - /* - * TODO(jongyoul) - * - Remove these codes below because of legacy code - * - Support ThreadInterpreter - */ - URLClassLoader ccl = new URLClassLoader( - recursiveBuildLibList(interpreterDir.toFile()), cl); - for (String className : interpreterClassList) { - try { - // Load classes - Class.forName(className, true, ccl); - Set interpreterKeys = Interpreter.registeredInterpreters.keySet(); - for (String interpreterKey : interpreterKeys) { - if (className - .equals(Interpreter.registeredInterpreters.get(interpreterKey) - .getClassName())) { - Interpreter.registeredInterpreters.get(interpreterKey) - .setPath(interpreterDirString); - logger.info("Interpreter " + interpreterKey + " found. class=" + className); - cleanCl.put(interpreterDirString, ccl); - } - } - } catch (Throwable t) { - // nothing to do - } - } - } - } - } - } - - for (RegisteredInterpreter registeredInterpreter : Interpreter.registeredInterpreters - .values()) { - logger - .debug("Registered: {} -> {}. Properties: {}", registeredInterpreter.getInterpreterKey(), - registeredInterpreter.getClassName(), registeredInterpreter.getProperties()); - } - - // RegisteredInterpreters -> interpreterSettingRef - InterpreterInfo interpreterInfo; - for (RegisteredInterpreter r : Interpreter.registeredInterpreters.values()) { - interpreterInfo = - new InterpreterInfo(r.getClassName(), r.getName(), r.isDefaultInterpreter(), - r.getEditor()); - add(r.getGroup(), interpreterInfo, r.getProperties(), defaultOption, r.getPath(), - r.getRunner()); - } - - for (String settingId : interpreterSettingsRef.keySet()) { - InterpreterSetting setting = interpreterSettingsRef.get(settingId); - logger.info("InterpreterSettingRef name {}", setting.getName()); - } - - loadFromFile(); - - // if no interpreter settings are loaded, create default set - if (0 == interpreterSettings.size()) { - Map temp = new HashMap<>(); - InterpreterSetting interpreterSetting; - for (InterpreterSetting setting : interpreterSettingsRef.values()) { - interpreterSetting = createFromInterpreterSettingRef(setting); - temp.put(setting.getName(), interpreterSetting); - } - - for (String group : interpreterGroupOrderList) { - if (null != (interpreterSetting = temp.remove(group))) { - interpreterSettings.put(interpreterSetting.getId(), interpreterSetting); - } - } - - for (InterpreterSetting setting : temp.values()) { - interpreterSettings.put(setting.getId(), setting); - } - - saveToFile(); - } - - for (String settingId : interpreterSettings.keySet()) { - InterpreterSetting setting = interpreterSettings.get(settingId); - logger.info("InterpreterSetting group {} : id={}, name={}", setting.getGroup(), settingId, - setting.getName()); - } - } - - private boolean registerInterpreterFromResource(ClassLoader cl, String interpreterDir, - String interpreterJson) throws IOException, RepositoryException { - URL[] urls = recursiveBuildLibList(new File(interpreterDir)); - ClassLoader tempClassLoader = new URLClassLoader(urls, cl); - - Enumeration interpreterSettings = tempClassLoader.getResources(interpreterJson); - if (!interpreterSettings.hasMoreElements()) { - return false; - } - for (URL url : Collections.list(interpreterSettings)) { - try (InputStream inputStream = url.openStream()) { - logger.debug("Reading {} from {}", interpreterJson, url); - List registeredInterpreterList = - getInterpreterListFromJson(inputStream); - registerInterpreters(registeredInterpreterList, interpreterDir); - } - } - return true; - } - - private boolean registerInterpreterFromPath(String interpreterDir, String interpreterJson) - throws IOException, RepositoryException { - - Path interpreterJsonPath = Paths.get(interpreterDir, interpreterJson); - if (Files.exists(interpreterJsonPath)) { - logger.debug("Reading {}", interpreterJsonPath); - List registeredInterpreterList = - getInterpreterListFromJson(interpreterJsonPath); - registerInterpreters(registeredInterpreterList, interpreterDir); - return true; - } - return false; - } - - private List getInterpreterListFromJson(Path filename) - throws FileNotFoundException { - return getInterpreterListFromJson(new FileInputStream(filename.toFile())); - } - - private List getInterpreterListFromJson(InputStream stream) { - Type registeredInterpreterListType = new TypeToken>() { - }.getType(); - return gson.fromJson(new InputStreamReader(stream), registeredInterpreterListType); - } - - private void registerInterpreters(List registeredInterpreters, - String absolutePath) throws IOException, RepositoryException { - - for (RegisteredInterpreter registeredInterpreter : registeredInterpreters) { - InterpreterInfo interpreterInfo = - new InterpreterInfo(registeredInterpreter.getClassName(), registeredInterpreter.getName(), - registeredInterpreter.isDefaultInterpreter(), registeredInterpreter.getEditor()); - // use defaultOption if it is not specified in interpreter-setting.json - InterpreterOption option = registeredInterpreter.getOption() == null ? defaultOption : - registeredInterpreter.getOption(); - add(registeredInterpreter.getGroup(), interpreterInfo, registeredInterpreter.getProperties(), - option, absolutePath, registeredInterpreter.getRunner()); - } - - } - - public InterpreterSetting getDefaultInterpreterSetting(List settings) { - if (settings == null || settings.isEmpty()) { - return null; - } - return settings.get(0); - } - - public InterpreterSetting getDefaultInterpreterSetting(String noteId) { - return getDefaultInterpreterSetting(getInterpreterSettings(noteId)); - } - - public List getInterpreterSettings(String noteId) { - List interpreterSettingIds = getNoteInterpreterSettingBinding(noteId); - LinkedList settings = new LinkedList<>(); - - Iterator iter = interpreterSettingIds.iterator(); - while (iter.hasNext()) { - String id = iter.next(); - InterpreterSetting setting = get(id); - if (setting == null) { - // interpreter setting is removed from factory. remove id from here, too - iter.remove(); - } else { - settings.add(setting); - } - } - return settings; - } - - private List getNoteInterpreterSettingBinding(String noteId) { - LinkedList bindings = new LinkedList<>(); - List settingIds = interpreterBindings.get(noteId); - if (settingIds != null) { - bindings.addAll(settingIds); - } - return bindings; - } - - private InterpreterSetting createFromInterpreterSettingRef(String name) { - Preconditions.checkNotNull(name, "reference name should be not null"); - InterpreterSetting settingRef = interpreterSettingsRef.get(name); - return createFromInterpreterSettingRef(settingRef); - } - - private InterpreterSetting createFromInterpreterSettingRef(InterpreterSetting o) { - // should return immutable objects - List infos = (null == o.getInterpreterInfos()) ? - new ArrayList() : new ArrayList<>(o.getInterpreterInfos()); - List deps = (null == o.getDependencies()) ? - new ArrayList() : new ArrayList<>(o.getDependencies()); - Map props = - convertInterpreterProperties((Map) o.getProperties()); - InterpreterOption option = InterpreterOption.fromInterpreterOption(o.getOption()); - - InterpreterSetting setting = new InterpreterSetting(o.getName(), o.getName(), - infos, props, deps, option, o.getPath(), o.getInterpreterRunner()); - setting.setInterpreterGroupFactory(interpreterGroupFactory); - return setting; - } - - private Map convertInterpreterProperties( - Map defaultProperties) { - Map properties = new HashMap<>(); - - for (String key : defaultProperties.keySet()) { - DefaultInterpreterProperty defaultInterpreterProperty = defaultProperties.get(key); - properties.put(key, new InterpreterProperty(key, defaultInterpreterProperty.getValue(), - defaultInterpreterProperty.getType())); - } - return properties; - } - - public Map getEditorSetting(Interpreter interpreter, String user, String noteId, - String replName) { - Map editor = DEFAULT_EDITOR; - String group = StringUtils.EMPTY; - try { - String defaultSettingName = getDefaultInterpreterSetting(noteId).getName(); - List intpSettings = getInterpreterSettings(noteId); - for (InterpreterSetting intpSetting : intpSettings) { - String[] replNameSplit = replName.split("\\."); - if (replNameSplit.length == 2) { - group = replNameSplit[0]; - } - // when replName is 'name' of interpreter - if (defaultSettingName.equals(intpSetting.getName())) { - editor = getEditorFromSettingByClassName(intpSetting, interpreter.getClassName()); - } - // when replName is 'alias name' of interpreter or 'group' of interpreter - if (replName.equals(intpSetting.getName()) || group.equals(intpSetting.getName())) { - editor = getEditorFromSettingByClassName(intpSetting, interpreter.getClassName()); - break; - } - } - } catch (NullPointerException e) { - // Use `debug` level because this log occurs frequently - logger.debug("Couldn't get interpreter editor setting"); - } - return editor; - } - - public Map getEditorFromSettingByClassName(InterpreterSetting intpSetting, - String className) { - List intpInfos = intpSetting.getInterpreterInfos(); - for (InterpreterInfo intpInfo : intpInfos) { - - if (className.equals(intpInfo.getClassName())) { - if (intpInfo.getEditor() == null) { - break; - } - return intpInfo.getEditor(); - } - } - return DEFAULT_EDITOR; - } - - private void loadInterpreterDependencies(final InterpreterSetting setting) { - setting.setStatus(InterpreterSetting.Status.DOWNLOADING_DEPENDENCIES); - setting.setErrorReason(null); - interpreterSettings.put(setting.getId(), setting); - synchronized (interpreterSettings) { - final Thread t = new Thread() { - public void run() { - try { - // dependencies to prevent library conflict - File localRepoDir = new File(zeppelinConfiguration.getInterpreterLocalRepoPath() + "/" + - setting.getId()); - if (localRepoDir.exists()) { - try { - FileUtils.forceDelete(localRepoDir); - } catch (FileNotFoundException e) { - logger.info("A file that does not exist cannot be deleted, nothing to worry", e); - } - } - - // load dependencies - List deps = setting.getDependencies(); - if (deps != null) { - for (Dependency d : deps) { - File destDir = new File( - zeppelinConfiguration.getRelativeDir(ConfVars.ZEPPELIN_DEP_LOCALREPO)); - - if (d.getExclusions() != null) { - dependencyResolver.load(d.getGroupArtifactVersion(), d.getExclusions(), - new File(destDir, setting.getId())); - } else { - dependencyResolver - .load(d.getGroupArtifactVersion(), new File(destDir, setting.getId())); - } - } - } - - setting.setStatus(InterpreterSetting.Status.READY); - setting.setErrorReason(null); - } catch (Exception e) { - logger.error(String.format("Error while downloading repos for interpreter group : %s," + - " go to interpreter setting page click on edit and save it again to make " + - "this interpreter work properly. : %s", - setting.getGroup(), e.getLocalizedMessage()), e); - setting.setErrorReason(e.getLocalizedMessage()); - setting.setStatus(InterpreterSetting.Status.ERROR); - } finally { - interpreterSettings.put(setting.getId(), setting); - } - } - }; - t.start(); - } - } - - /** - * Overwrite dependency jar under local-repo/{interpreterId} - * if jar file in original path is changed - */ - private void copyDependenciesFromLocalPath(final InterpreterSetting setting) { - setting.setStatus(InterpreterSetting.Status.DOWNLOADING_DEPENDENCIES); - interpreterSettings.put(setting.getId(), setting); - synchronized (interpreterSettings) { - final Thread t = new Thread() { - public void run() { - try { - List deps = setting.getDependencies(); - if (deps != null) { - for (Dependency d : deps) { - File destDir = new File( - zeppelinConfiguration.getRelativeDir(ConfVars.ZEPPELIN_DEP_LOCALREPO)); - - int numSplits = d.getGroupArtifactVersion().split(":").length; - if (!(numSplits >= 3 && numSplits <= 6)) { - dependencyResolver.copyLocalDependency(d.getGroupArtifactVersion(), - new File(destDir, setting.getId())); - } - } - } - setting.setStatus(InterpreterSetting.Status.READY); - } catch (Exception e) { - logger.error(String.format("Error while copying deps for interpreter group : %s," + - " go to interpreter setting page click on edit and save it again to make " + - "this interpreter work properly.", - setting.getGroup()), e); - setting.setErrorReason(e.getLocalizedMessage()); - setting.setStatus(InterpreterSetting.Status.ERROR); - } finally { - interpreterSettings.put(setting.getId(), setting); - } - } - }; - t.start(); - } - } - - /** - * Return ordered interpreter setting list. - * The list does not contain more than one setting from the same interpreter class. - * Order by InterpreterClass (order defined by ZEPPELIN_INTERPRETERS), Interpreter setting name - */ - public List getDefaultInterpreterSettingList() { - // this list will contain default interpreter setting list - List defaultSettings = new LinkedList<>(); - - // to ignore the same interpreter group - Map interpreterGroupCheck = new HashMap<>(); - - List sortedSettings = get(); - - for (InterpreterSetting setting : sortedSettings) { - if (defaultSettings.contains(setting.getId())) { - continue; - } - - if (!interpreterGroupCheck.containsKey(setting.getName())) { - defaultSettings.add(setting.getId()); - interpreterGroupCheck.put(setting.getName(), true); - } - } - return defaultSettings; - } - - List getRegisteredInterpreterList() { - return new ArrayList<>(Interpreter.registeredInterpreters.values()); - } - - - private boolean findDefaultInterpreter(List infos) { - for (InterpreterInfo interpreterInfo : infos) { - if (interpreterInfo.isDefaultInterpreter()) { - return true; - } - } - return false; - } - - public InterpreterSetting createNewSetting(String name, String group, - List dependencies, InterpreterOption option, Map p) - throws IOException { - if (name.indexOf(".") >= 0) { - throw new IOException("'.' is invalid for InterpreterSetting name."); - } - InterpreterSetting setting = createFromInterpreterSettingRef(group); - setting.setName(name); - setting.setGroup(group); - setting.appendDependencies(dependencies); - setting.setInterpreterOption(option); - setting.setProperties(p); - setting.setInterpreterGroupFactory(interpreterGroupFactory); - interpreterSettings.put(setting.getId(), setting); - loadInterpreterDependencies(setting); - saveToFile(); - return setting; - } - - private InterpreterSetting add(String group, InterpreterInfo interpreterInfo, - Map interpreterProperties, InterpreterOption option, - String path, InterpreterRunner runner) - throws InterpreterException, IOException, RepositoryException { - ArrayList infos = new ArrayList<>(); - infos.add(interpreterInfo); - return add(group, infos, new ArrayList(), option, interpreterProperties, path, - runner); - } - - /** - * @param group InterpreterSetting reference name - */ - public InterpreterSetting add(String group, ArrayList interpreterInfos, - List dependencies, InterpreterOption option, - Map interpreterProperties, String path, - InterpreterRunner runner) { - Preconditions.checkNotNull(group, "name should not be null"); - Preconditions.checkNotNull(interpreterInfos, "interpreterInfos should not be null"); - Preconditions.checkNotNull(dependencies, "dependencies should not be null"); - Preconditions.checkNotNull(option, "option should not be null"); - Preconditions.checkNotNull(interpreterProperties, "properties should not be null"); - - InterpreterSetting interpreterSetting; - - synchronized (interpreterSettingsRef) { - if (interpreterSettingsRef.containsKey(group)) { - interpreterSetting = interpreterSettingsRef.get(group); - - // Append InterpreterInfo - List infos = interpreterSetting.getInterpreterInfos(); - boolean hasDefaultInterpreter = findDefaultInterpreter(infos); - for (InterpreterInfo interpreterInfo : interpreterInfos) { - if (!infos.contains(interpreterInfo)) { - if (!hasDefaultInterpreter && interpreterInfo.isDefaultInterpreter()) { - hasDefaultInterpreter = true; - infos.add(0, interpreterInfo); - } else { - infos.add(interpreterInfo); - } - } - } - - // Append dependencies - List dependencyList = interpreterSetting.getDependencies(); - for (Dependency dependency : dependencies) { - if (!dependencyList.contains(dependency)) { - dependencyList.add(dependency); - } - } - - // Append properties - Map properties = - (Map) interpreterSetting.getProperties(); - for (String key : interpreterProperties.keySet()) { - if (!properties.containsKey(key)) { - properties.put(key, interpreterProperties.get(key)); - } - } - - } else { - interpreterSetting = - new InterpreterSetting(group, null, interpreterInfos, interpreterProperties, - dependencies, option, path, runner); - interpreterSettingsRef.put(group, interpreterSetting); - } - } - - if (dependencies.size() > 0) { - loadInterpreterDependencies(interpreterSetting); - } - - interpreterSetting.setInterpreterGroupFactory(interpreterGroupFactory); - return interpreterSetting; - } - - /** - * map interpreter ids into noteId - * - * @param noteId note id - * @param ids InterpreterSetting id list - */ - public void setInterpreters(String user, String noteId, List ids) throws IOException { - putNoteInterpreterSettingBinding(user, noteId, ids); - } - - private void putNoteInterpreterSettingBinding(String user, String noteId, - List settingList) throws IOException { - List unBindedSettings = new LinkedList<>(); - - synchronized (interpreterSettings) { - List oldSettings = interpreterBindings.get(noteId); - if (oldSettings != null) { - for (String oldSettingId : oldSettings) { - if (!settingList.contains(oldSettingId)) { - unBindedSettings.add(oldSettingId); - } - } - } - interpreterBindings.put(noteId, settingList); - saveToFile(); - - for (String settingId : unBindedSettings) { - InterpreterSetting setting = get(settingId); - removeInterpretersForNote(setting, user, noteId); - } - } - } - - public void removeInterpretersForNote(InterpreterSetting interpreterSetting, String user, - String noteId) { - //TODO(jl): This is only for hotfix. You should fix it as a beautiful way - InterpreterOption interpreterOption = interpreterSetting.getOption(); - if (!(InterpreterOption.SHARED.equals(interpreterOption.perNote) - && InterpreterOption.SHARED.equals(interpreterOption.perUser))) { - interpreterSetting.closeAndRemoveInterpreterGroup(noteId, ""); - } - } - - public String getInterpreterSessionKey(String user, String noteId, InterpreterSetting setting) { - InterpreterOption option = setting.getOption(); - String key; - if (option.isExistingProcess()) { - key = Constants.EXISTING_PROCESS; - } else if (option.perNoteScoped() && option.perUserScoped()) { - key = user + ":" + noteId; - } else if (option.perUserScoped()) { - key = user; - } else if (option.perNoteScoped()) { - key = noteId; - } else { - key = SHARED_SESSION; - } - - logger.debug("Interpreter session key: {}, for note: {}, user: {}, InterpreterSetting Name: " + - "{}", key, noteId, user, setting.getName()); - return key; - } - - - public List getInterpreters(String noteId) { - return getNoteInterpreterSettingBinding(noteId); - } - - public void closeNote(String user, String noteId) { - // close interpreters in this note session - List settings = getInterpreterSettings(noteId); - if (settings == null || settings.size() == 0) { - return; - } - - logger.info("closeNote: {}", noteId); - for (InterpreterSetting setting : settings) { - removeInterpretersForNote(setting, user, noteId); - } - } - - public Map getAvailableInterpreterSettings() { - return interpreterSettingsRef; - } - - private URL[] recursiveBuildLibList(File path) throws MalformedURLException { - URL[] urls = new URL[0]; - if (path == null || !path.exists()) { - return urls; - } else if (path.getName().startsWith(".")) { - return urls; - } else if (path.isDirectory()) { - File[] files = path.listFiles(); - if (files != null) { - for (File f : files) { - urls = (URL[]) ArrayUtils.addAll(urls, recursiveBuildLibList(f)); - } - } - return urls; - } else { - return new URL[]{path.toURI().toURL()}; - } - } - - public List getRepositories() { - return this.interpreterRepositories; - } - - public void addRepository(String id, String url, boolean snapshot, Authentication auth, - Proxy proxy) throws IOException { - dependencyResolver.addRepo(id, url, snapshot, auth, proxy); - saveToFile(); - } - - public void removeRepository(String id) throws IOException { - dependencyResolver.delRepo(id); - saveToFile(); - } - - public void removeNoteInterpreterSettingBinding(String user, String noteId) throws IOException { - List settingIds = interpreterBindings.remove(noteId); - if (settingIds != null) { - for (String settingId : settingIds) { - InterpreterSetting setting = get(settingId); - if (setting != null) { - this.removeInterpretersForNote(setting, user, noteId); - } - } - } - saveToFile(); - } - - /** - * Change interpreter property and restart - */ - public void setPropertyAndRestart(String id, InterpreterOption option, - Map properties, - List dependencies) throws IOException { - synchronized (interpreterSettings) { - InterpreterSetting intpSetting = interpreterSettings.get(id); - if (intpSetting != null) { - try { - stopJobAllInterpreter(intpSetting); - - intpSetting.closeAndRemoveAllInterpreterGroups(); - intpSetting.setOption(option); - intpSetting.setProperties(properties); - intpSetting.setDependencies(dependencies); - loadInterpreterDependencies(intpSetting); - - saveToFile(); - } catch (Exception e) { - loadFromFile(); - throw e; - } - } else { - throw new InterpreterException("Interpreter setting id " + id + " not found"); - } - } - } - - public void restart(String settingId, String noteId, String user) { - InterpreterSetting intpSetting = interpreterSettings.get(settingId); - Preconditions.checkNotNull(intpSetting); - synchronized (interpreterSettings) { - intpSetting = interpreterSettings.get(settingId); - // Check if dependency in specified path is changed - // If it did, overwrite old dependency jar with new one - if (intpSetting != null) { - //clean up metaInfos - intpSetting.setInfos(null); - copyDependenciesFromLocalPath(intpSetting); - - stopJobAllInterpreter(intpSetting); - if (user.equals("anonymous")) { - intpSetting.closeAndRemoveAllInterpreterGroups(); - } else { - intpSetting.closeAndRemoveInterpreterGroup(noteId, user); - } - - } else { - throw new InterpreterException("Interpreter setting id " + settingId + " not found"); - } - } - } - - public void restart(String id) { - restart(id, "", "anonymous"); - } - - private void stopJobAllInterpreter(InterpreterSetting intpSetting) { - if (intpSetting != null) { - for (InterpreterGroup intpGroup : intpSetting.getAllInterpreterGroups()) { - for (List interpreters : intpGroup.values()) { - for (Interpreter intp : interpreters) { - for (Job job : intp.getScheduler().getJobsRunning()) { - job.abort(); - job.setStatus(Status.ABORT); - logger.info("Job " + job.getJobName() + " aborted "); - } - for (Job job : intp.getScheduler().getJobsWaiting()) { - job.abort(); - job.setStatus(Status.ABORT); - logger.info("Job " + job.getJobName() + " aborted "); - } - } - } - } - } - } - - public InterpreterSetting get(String name) { - synchronized (interpreterSettings) { - return interpreterSettings.get(name); - } - } - - public void remove(String id) throws IOException { - synchronized (interpreterSettings) { - if (interpreterSettings.containsKey(id)) { - InterpreterSetting intp = interpreterSettings.get(id); - intp.closeAndRemoveAllInterpreterGroups(); - - interpreterSettings.remove(id); - for (List settings : interpreterBindings.values()) { - Iterator it = settings.iterator(); - while (it.hasNext()) { - String settingId = it.next(); - if (settingId.equals(id)) { - it.remove(); - } - } - } - saveToFile(); - } - } - - File localRepoDir = new File(zeppelinConfiguration.getInterpreterLocalRepoPath() + "/" + id); - FileUtils.deleteDirectory(localRepoDir); - } - - /** - * Get interpreter settings - */ - public List get() { - synchronized (interpreterSettings) { - List orderedSettings = new LinkedList<>(); - - Map> nameInterpreterSettingMap = new HashMap<>(); - for (InterpreterSetting interpreterSetting : interpreterSettings.values()) { - String group = interpreterSetting.getGroup(); - if (!nameInterpreterSettingMap.containsKey(group)) { - nameInterpreterSettingMap.put(group, new ArrayList()); - } - nameInterpreterSettingMap.get(group).add(interpreterSetting); - } - - for (String groupName : interpreterGroupOrderList) { - List interpreterSettingList = - nameInterpreterSettingMap.remove(groupName); - if (null != interpreterSettingList) { - for (InterpreterSetting interpreterSetting : interpreterSettingList) { - orderedSettings.add(interpreterSetting); - } - } - } - - List settings = new ArrayList<>(); - - for (List interpreterSettingList : nameInterpreterSettingMap.values()) { - for (InterpreterSetting interpreterSetting : interpreterSettingList) { - settings.add(interpreterSetting); - } - } - - Collections.sort(settings, new Comparator() { - @Override - public int compare(InterpreterSetting o1, InterpreterSetting o2) { - return o1.getName().compareTo(o2.getName()); - } - }); - - orderedSettings.addAll(settings); - - return orderedSettings; - } - } - - public void close(InterpreterSetting interpreterSetting) { - interpreterSetting.closeAndRemoveAllInterpreterGroups(); - } - - public void close() { - List closeThreads = new LinkedList<>(); - synchronized (interpreterSettings) { - Collection intpSettings = interpreterSettings.values(); - for (final InterpreterSetting intpSetting : intpSettings) { - Thread t = new Thread() { - public void run() { - intpSetting.closeAndRemoveAllInterpreterGroups(); - } - }; - t.start(); - closeThreads.add(t); - } - } - - for (Thread t : closeThreads) { - try { - t.join(); - } catch (InterruptedException e) { - logger.error("Can't close interpreterGroup", e); - } - } - } - - public void shutdown() { - List closeThreads = new LinkedList<>(); - synchronized (interpreterSettings) { - Collection intpSettings = interpreterSettings.values(); - for (final InterpreterSetting intpSetting : intpSettings) { - Thread t = new Thread() { - public void run() { - intpSetting.shutdownAndRemoveAllInterpreterGroups(); - } - }; - t.start(); - closeThreads.add(t); - } - } - - for (Thread t : closeThreads) { - try { - t.join(); - } catch (InterruptedException e) { - logger.error("Can't close interpreterGroup", e); - } - } - } -} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java deleted file mode 100644 index 12e0caa435d..00000000000 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java +++ /dev/null @@ -1,597 +0,0 @@ -/* - * 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.interpreter.remote; - -import java.util.*; - -import org.apache.commons.lang3.StringUtils; -import org.apache.thrift.TException; -import org.apache.zeppelin.display.AngularObject; -import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.display.GUI; -import org.apache.zeppelin.helium.ApplicationEventListener; -import org.apache.zeppelin.display.Input; -import org.apache.zeppelin.interpreter.*; -import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterContext; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterResult; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterResultMessage; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; -import org.apache.zeppelin.scheduler.Scheduler; -import org.apache.zeppelin.scheduler.SchedulerFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; - -/** - * Proxy for Interpreter instance that runs on separate process - */ -public class RemoteInterpreter extends Interpreter { - private static final Logger logger = LoggerFactory.getLogger(RemoteInterpreter.class); - - private final RemoteInterpreterProcessListener remoteInterpreterProcessListener; - private final ApplicationEventListener applicationEventListener; - private Gson gson = new Gson(); - private String interpreterRunner; - private String interpreterPath; - private String localRepoPath; - private String className; - private String sessionKey; - private FormType formType; - private boolean initialized; - private Map env; - private int connectTimeout; - private int maxPoolSize; - private String host; - private int port; - private String userName; - private Boolean isUserImpersonate; - private int outputLimit = Constants.ZEPPELIN_INTERPRETER_OUTPUT_LIMIT; - private String interpreterGroupName; - - /** - * Remote interpreter and manage interpreter process - */ - public RemoteInterpreter(Properties property, String sessionKey, String className, - String interpreterRunner, String interpreterPath, String localRepoPath, int connectTimeout, - int maxPoolSize, RemoteInterpreterProcessListener remoteInterpreterProcessListener, - ApplicationEventListener appListener, String userName, Boolean isUserImpersonate, - int outputLimit, String interpreterGroupName) { - super(property); - this.sessionKey = sessionKey; - this.className = className; - initialized = false; - this.interpreterRunner = interpreterRunner; - this.interpreterPath = interpreterPath; - this.localRepoPath = localRepoPath; - env = getEnvFromInterpreterProperty(property); - this.connectTimeout = connectTimeout; - this.maxPoolSize = maxPoolSize; - this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; - this.applicationEventListener = appListener; - this.userName = userName; - this.isUserImpersonate = isUserImpersonate; - this.outputLimit = outputLimit; - this.interpreterGroupName = interpreterGroupName; - } - - - /** - * Connect to existing process - */ - public RemoteInterpreter(Properties property, String sessionKey, String className, String host, - int port, String localRepoPath, int connectTimeout, int maxPoolSize, - RemoteInterpreterProcessListener remoteInterpreterProcessListener, - ApplicationEventListener appListener, String userName, Boolean isUserImpersonate, - int outputLimit) { - super(property); - this.sessionKey = sessionKey; - this.className = className; - initialized = false; - this.host = host; - this.port = port; - this.localRepoPath = localRepoPath; - this.connectTimeout = connectTimeout; - this.maxPoolSize = maxPoolSize; - this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; - this.applicationEventListener = appListener; - this.userName = userName; - this.isUserImpersonate = isUserImpersonate; - this.outputLimit = outputLimit; - } - - - // VisibleForTesting - public RemoteInterpreter(Properties property, String sessionKey, String className, - String interpreterRunner, String interpreterPath, String localRepoPath, - Map env, int connectTimeout, - RemoteInterpreterProcessListener remoteInterpreterProcessListener, - ApplicationEventListener appListener, String userName, Boolean isUserImpersonate) { - super(property); - this.className = className; - this.sessionKey = sessionKey; - this.interpreterRunner = interpreterRunner; - this.interpreterPath = interpreterPath; - this.localRepoPath = localRepoPath; - env.putAll(getEnvFromInterpreterProperty(property)); - this.env = env; - this.connectTimeout = connectTimeout; - this.maxPoolSize = 10; - this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; - this.applicationEventListener = appListener; - this.userName = userName; - this.isUserImpersonate = isUserImpersonate; - } - - private Map getEnvFromInterpreterProperty(Properties property) { - Map env = new HashMap(); - StringBuilder sparkConfBuilder = new StringBuilder(); - for (String key : property.stringPropertyNames()) { - if (RemoteInterpreterUtils.isEnvString(key)) { - env.put(key, property.getProperty(key)); - } - if (key.equals("master")) { - sparkConfBuilder.append(" --master " + property.getProperty("master")); - } - if (isSparkConf(key, property.getProperty(key))) { - sparkConfBuilder.append(" --conf " + key + "=" + - toShellFormat(property.getProperty(key))); - } - } - env.put("ZEPPELIN_SPARK_CONF", sparkConfBuilder.toString()); - return env; - } - - private String toShellFormat(String value) { - if (value.contains("\'") && value.contains("\"")) { - throw new RuntimeException("Spark property value could not contain both \" and '"); - } else if (value.contains("\'")) { - return "\"" + value + "\""; - } else { - return "\'" + value + "\'"; - } - } - - static boolean isSparkConf(String key, String value) { - return !StringUtils.isEmpty(key) && key.startsWith("spark.") && !StringUtils.isEmpty(value); - } - - @Override - public String getClassName() { - return className; - } - - private boolean connectToExistingProcess() { - return host != null && port > 0; - } - - public RemoteInterpreterProcess getInterpreterProcess() { - InterpreterGroup intpGroup = getInterpreterGroup(); - if (intpGroup == null) { - return null; - } - - synchronized (intpGroup) { - if (intpGroup.getRemoteInterpreterProcess() == null) { - RemoteInterpreterProcess remoteProcess; - if (connectToExistingProcess()) { - remoteProcess = new RemoteInterpreterRunningProcess( - connectTimeout, - remoteInterpreterProcessListener, - applicationEventListener, - host, - port); - } else { - // create new remote process - remoteProcess = new RemoteInterpreterManagedProcess( - interpreterRunner, interpreterPath, localRepoPath, env, connectTimeout, - remoteInterpreterProcessListener, applicationEventListener, interpreterGroupName); - } - - intpGroup.setRemoteInterpreterProcess(remoteProcess); - } - - return intpGroup.getRemoteInterpreterProcess(); - } - } - - public synchronized void init() { - if (initialized == true) { - return; - } - - RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); - - final InterpreterGroup interpreterGroup = getInterpreterGroup(); - - interpreterProcess.setMaxPoolSize( - Math.max(this.maxPoolSize, interpreterProcess.getMaxPoolSize())); - String groupId = interpreterGroup.getId(); - - synchronized (interpreterProcess) { - Client client = null; - try { - client = interpreterProcess.getClient(); - } catch (Exception e1) { - throw new InterpreterException(e1); - } - - boolean broken = false; - try { - logger.info("Create remote interpreter {}", getClassName()); - if (localRepoPath != null) { - property.put("zeppelin.interpreter.localRepo", localRepoPath); - } - - property.put("zeppelin.interpreter.output.limit", Integer.toString(outputLimit)); - client.createInterpreter(groupId, sessionKey, - getClassName(), (Map) property, userName); - // Push angular object loaded from JSON file to remote interpreter - if (!interpreterGroup.isAngularRegistryPushed()) { - pushAngularObjectRegistryToRemote(client); - interpreterGroup.setAngularRegistryPushed(true); - } - - } catch (TException e) { - logger.error("Failed to create interpreter: {}", getClassName()); - throw new InterpreterException(e); - } finally { - // TODO(jongyoul): Fixed it when not all of interpreter in same interpreter group are broken - interpreterProcess.releaseClient(client, broken); - } - } - initialized = true; - } - - - @Override - public void open() { - InterpreterGroup interpreterGroup = getInterpreterGroup(); - - synchronized (interpreterGroup) { - // initialize all interpreters in this interpreter group - List interpreters = interpreterGroup.get(sessionKey); - // TODO(jl): this open method is called by LazyOpenInterpreter.open(). It, however, - // initializes all of interpreters with same sessionKey. But LazyOpenInterpreter assumes if it - // doesn't call open method, it's not open. It causes problem while running intp.close() - // In case of Spark, this method initializes all of interpreters and init() method increases - // reference count of RemoteInterpreterProcess. But while closing this interpreter group, all - // other interpreters doesn't do anything because those LazyInterpreters aren't open. - // But for now, we have to initialise all of interpreters for some reasons. - // See Interpreter.getInterpreterInTheSameSessionByClassName(String) - RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); - if (!initialized) { - // reference per session - interpreterProcess.reference(interpreterGroup, userName, isUserImpersonate); - } - for (Interpreter intp : new ArrayList<>(interpreters)) { - Interpreter p = intp; - while (p instanceof WrappedInterpreter) { - p = ((WrappedInterpreter) p).getInnerInterpreter(); - } - try { - ((RemoteInterpreter) p).init(); - } catch (InterpreterException e) { - logger.error("Failed to initialize interpreter: {}. Remove it from interpreterGroup", - p.getClassName()); - interpreters.remove(p); - } - } - } - } - - @Override - public void close() { - InterpreterGroup interpreterGroup = getInterpreterGroup(); - synchronized (interpreterGroup) { - // close all interpreters in this session - List interpreters = interpreterGroup.get(sessionKey); - // TODO(jl): this open method is called by LazyOpenInterpreter.open(). It, however, - // initializes all of interpreters with same sessionKey. But LazyOpenInterpreter assumes if it - // doesn't call open method, it's not open. It causes problem while running intp.close() - // In case of Spark, this method initializes all of interpreters and init() method increases - // reference count of RemoteInterpreterProcess. But while closing this interpreter group, all - // other interpreters doesn't do anything because those LazyInterpreters aren't open. - // But for now, we have to initialise all of interpreters for some reasons. - // See Interpreter.getInterpreterInTheSameSessionByClassName(String) - if (initialized) { - // dereference per session - getInterpreterProcess().dereference(); - } - for (Interpreter intp : new ArrayList<>(interpreters)) { - Interpreter p = intp; - while (p instanceof WrappedInterpreter) { - p = ((WrappedInterpreter) p).getInnerInterpreter(); - } - try { - ((RemoteInterpreter) p).closeInterpreter(); - } catch (InterpreterException e) { - logger.error("Failed to initialize interpreter: {}. Remove it from interpreterGroup", - p.getClassName()); - interpreters.remove(p); - } - } - } - } - - public void closeInterpreter() { - if (this.initialized == false) { - return; - } - RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); - Client client = null; - boolean broken = false; - try { - client = interpreterProcess.getClient(); - if (client != null) { - client.close(sessionKey, className); - } - } catch (TException e) { - broken = true; - throw new InterpreterException(e); - } catch (Exception e1) { - throw new InterpreterException(e1); - } finally { - if (client != null) { - interpreterProcess.releaseClient(client, broken); - } - this.initialized = false; - } - } - - @Override - public InterpreterResult interpret(String st, InterpreterContext context) { - if (logger.isDebugEnabled()) { - logger.debug("st:\n{}", st); - } - - FormType form = getFormType(); - RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); - Client client = null; - try { - client = interpreterProcess.getClient(); - } catch (Exception e1) { - throw new InterpreterException(e1); - } - - InterpreterContextRunnerPool interpreterContextRunnerPool = interpreterProcess - .getInterpreterContextRunnerPool(); - - List runners = context.getRunners(); - if (runners != null && runners.size() != 0) { - // assume all runners in this InterpreterContext have the same note id - String noteId = runners.get(0).getNoteId(); - - interpreterContextRunnerPool.clear(noteId); - interpreterContextRunnerPool.addAll(noteId, runners); - } - - boolean broken = false; - try { - - final GUI currentGUI = context.getGui(); - RemoteInterpreterResult remoteResult = client.interpret( - sessionKey, className, st, convert(context)); - - Map remoteConfig = (Map) gson.fromJson( - remoteResult.getConfig(), new TypeToken>() { - }.getType()); - context.getConfig().clear(); - context.getConfig().putAll(remoteConfig); - - if (form == FormType.NATIVE) { - GUI remoteGui = GUI.fromJson(remoteResult.getGui()); - currentGUI.clear(); - currentGUI.setParams(remoteGui.getParams()); - currentGUI.setForms(remoteGui.getForms()); - } else if (form == FormType.SIMPLE) { - final Map currentForms = currentGUI.getForms(); - final Map currentParams = currentGUI.getParams(); - final GUI remoteGUI = GUI.fromJson(remoteResult.getGui()); - final Map remoteForms = remoteGUI.getForms(); - final Map remoteParams = remoteGUI.getParams(); - currentForms.putAll(remoteForms); - currentParams.putAll(remoteParams); - } - - InterpreterResult result = convert(remoteResult); - return result; - } catch (TException e) { - broken = true; - throw new InterpreterException(e); - } finally { - interpreterProcess.releaseClient(client, broken); - } - } - - @Override - public void cancel(InterpreterContext context) { - RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); - Client client = null; - try { - client = interpreterProcess.getClient(); - } catch (Exception e1) { - throw new InterpreterException(e1); - } - - boolean broken = false; - try { - client.cancel(sessionKey, className, convert(context)); - } catch (TException e) { - broken = true; - throw new InterpreterException(e); - } finally { - interpreterProcess.releaseClient(client, broken); - } - } - - @Override - public FormType getFormType() { - open(); - - if (formType != null) { - return formType; - } - - RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); - Client client = null; - try { - client = interpreterProcess.getClient(); - } catch (Exception e1) { - throw new InterpreterException(e1); - } - - boolean broken = false; - try { - formType = FormType.valueOf(client.getFormType(sessionKey, className)); - return formType; - } catch (TException e) { - broken = true; - throw new InterpreterException(e); - } finally { - interpreterProcess.releaseClient(client, broken); - } - } - - @Override - public int getProgress(InterpreterContext context) { - RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); - if (interpreterProcess == null || !interpreterProcess.isRunning()) { - return 0; - } - - Client client = null; - try { - client = interpreterProcess.getClient(); - } catch (Exception e1) { - throw new InterpreterException(e1); - } - - boolean broken = false; - try { - return client.getProgress(sessionKey, className, convert(context)); - } catch (TException e) { - broken = true; - throw new InterpreterException(e); - } finally { - interpreterProcess.releaseClient(client, broken); - } - } - - - @Override - public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { - RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); - Client client = null; - try { - client = interpreterProcess.getClient(); - } catch (Exception e1) { - throw new InterpreterException(e1); - } - - boolean broken = false; - try { - List completion = client.completion(sessionKey, className, buf, cursor, - convert(interpreterContext)); - return completion; - } catch (TException e) { - broken = true; - throw new InterpreterException(e); - } finally { - interpreterProcess.releaseClient(client, broken); - } - } - - @Override - public Scheduler getScheduler() { - int maxConcurrency = maxPoolSize; - RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); - if (interpreterProcess == null) { - return null; - } else { - return SchedulerFactory.singleton().createOrGetRemoteScheduler( - RemoteInterpreter.class.getName() + sessionKey + interpreterProcess.hashCode(), - sessionKey, interpreterProcess, maxConcurrency); - } - } - - private String getInterpreterGroupKey(InterpreterGroup interpreterGroup) { - return interpreterGroup.getId(); - } - - private RemoteInterpreterContext convert(InterpreterContext ic) { - return new RemoteInterpreterContext(ic.getNoteId(), ic.getParagraphId(), ic.getReplName(), - ic.getParagraphTitle(), ic.getParagraphText(), ic.getAuthenticationInfo().toJson(), - gson.toJson(ic.getConfig()), ic.getGui().toJson(), gson.toJson(ic.getRunners())); - } - - private InterpreterResult convert(RemoteInterpreterResult result) { - InterpreterResult r = new InterpreterResult( - InterpreterResult.Code.valueOf(result.getCode())); - - for (RemoteInterpreterResultMessage m : result.getMsg()) { - r.add(InterpreterResult.Type.valueOf(m.getType()), m.getData()); - } - - return r; - } - - /** - * Push local angular object registry to - * remote interpreter. This method should be - * call ONLY inside the init() method - */ - void pushAngularObjectRegistryToRemote(Client client) throws TException { - final AngularObjectRegistry angularObjectRegistry = this.getInterpreterGroup() - .getAngularObjectRegistry(); - - if (angularObjectRegistry != null && angularObjectRegistry.getRegistry() != null) { - final Map> registry = angularObjectRegistry - .getRegistry(); - - logger.info("Push local angular object registry from ZeppelinServer to" + - " remote interpreter group {}", this.getInterpreterGroup().getId()); - - final java.lang.reflect.Type registryType = new TypeToken>>() { - }.getType(); - - Gson gson = new Gson(); - client.angularRegistryPush(gson.toJson(registry, registryType)); - } - } - - public Map getEnv() { - return env; - } - - public void addEnv(Map env) { - if (this.env == null) { - this.env = new HashMap<>(); - } - this.env.putAll(env); - } - - //Only for test - public String getInterpreterRunner() { - return interpreterRunner; - } -} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/ApplicationState.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/ApplicationState.java index 1505db9ada3..bc71d893221 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/ApplicationState.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/ApplicationState.java @@ -17,7 +17,6 @@ package org.apache.zeppelin.notebook; import org.apache.zeppelin.helium.HeliumPackage; -import org.apache.zeppelin.interpreter.InterpreterGroup; /** * Current state of application diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java index 198e278e04b..4a93d08011b 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java @@ -41,7 +41,6 @@ import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.notebook.repo.NotebookRepo; import org.apache.zeppelin.notebook.utility.IdHashes; -import org.apache.zeppelin.resource.ResourcePoolUtils; import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.scheduler.Job.Status; import org.apache.zeppelin.search.SearchService; @@ -126,11 +125,6 @@ private void generateId() { id = IdHashes.generateId(); } - private String getDefaultInterpreterName() { - InterpreterSetting setting = interpreterSettingManager.getDefaultInterpreterSetting(getId()); - return null != setting ? setting.getName() : StringUtils.EMPTY; - } - public boolean isPersonalizedMode() { Object v = getConfig().get("personalizedMode"); return null != v && "true".equals(v); @@ -385,7 +379,7 @@ public void insertParagraph(Paragraph paragraph, int index) { */ public Paragraph removeParagraph(String user, String paragraphId) { removeAllAngularObjectInParagraph(user, paragraphId); - ResourcePoolUtils.removeResourcesBelongsToParagraph(getId(), paragraphId); + interpreterSettingManager.removeResourcesBelongsToParagraph(getId(), paragraphId); synchronized (paragraphs) { Iterator i = paragraphs.iterator(); while (i.hasNext()) { @@ -690,7 +684,7 @@ private void snapshotAngularObjectRegistry(String user) { } for (InterpreterSetting setting : settings) { - InterpreterGroup intpGroup = setting.getInterpreterGroup(user, id); + InterpreterGroup intpGroup = setting.getOrCreateInterpreterGroup(user, id); AngularObjectRegistry registry = intpGroup.getAngularObjectRegistry(); angularObjects.put(intpGroup.getId(), registry.getAllWithGlobal(id)); } @@ -705,7 +699,7 @@ private void removeAllAngularObjectInParagraph(String user, String paragraphId) } for (InterpreterSetting setting : settings) { - InterpreterGroup intpGroup = setting.getInterpreterGroup(user, id); + InterpreterGroup intpGroup = setting.getOrCreateInterpreterGroup(user, id); AngularObjectRegistry registry = intpGroup.getAngularObjectRegistry(); if (registry instanceof RemoteAngularObjectRegistry) { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java index a0c1dff8059..fd3111b6c8d 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java @@ -60,7 +60,6 @@ import org.apache.zeppelin.notebook.repo.NotebookRepo; import org.apache.zeppelin.notebook.repo.NotebookRepo.Revision; import org.apache.zeppelin.notebook.repo.NotebookRepoSync; -import org.apache.zeppelin.resource.ResourcePoolUtils; import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.scheduler.SchedulerFactory; import org.apache.zeppelin.search.SearchService; @@ -140,7 +139,7 @@ public Note createNote(AuthenticationInfo subject) throws IOException { Preconditions.checkNotNull(subject, "AuthenticationInfo should not be null"); Note note; if (conf.getBoolean(ConfVars.ZEPPELIN_NOTEBOOK_AUTO_INTERPRETER_BINDING)) { - note = createNote(interpreterSettingManager.getDefaultInterpreterSettingList(), subject); + note = createNote(interpreterSettingManager.getInterpreterSettingIds(), subject); } else { note = createNote(null, subject); } @@ -270,8 +269,8 @@ public void bindInterpretersToNote(String user, String id, List interpre } } - interpreterSettingManager.setInterpreters(user, note.getId(), interpreterSettingIds); - // comment out while note.getNoteReplLoader().setInterpreters(...) do the same + interpreterSettingManager.setInterpreterBinding(user, note.getId(), interpreterSettingIds); + // comment out while note.getNoteReplLoader().setInterpreterBinding(...) do the same // replFactory.putNoteInterpreterSettingBinding(id, interpreterSettingIds); } } @@ -279,7 +278,7 @@ public void bindInterpretersToNote(String user, String id, List interpre List getBindedInterpreterSettingsIds(String id) { Note note = getNote(id); if (note != null) { - return interpreterSettingManager.getInterpreters(note.getId()); + return interpreterSettingManager.getInterpreterBinding(note.getId()); } else { return new LinkedList<>(); } @@ -313,9 +312,10 @@ public boolean hasFolder(String folderId) { } public void moveNoteToTrash(String noteId) { - for (InterpreterSetting interpreterSetting : interpreterSettingManager - .getInterpreterSettings(noteId)) { - interpreterSettingManager.removeInterpretersForNote(interpreterSetting, "", noteId); + try { + interpreterSettingManager.setInterpreterBinding("", noteId, new ArrayList()); + } catch (IOException e) { + e.printStackTrace(); } } @@ -339,7 +339,7 @@ public void removeNote(String id, AuthenticationInfo subject) { // remove from all interpreter instance's angular object registry for (InterpreterSetting settings : interpreterSettingManager.get()) { AngularObjectRegistry registry = - settings.getInterpreterGroup(subject.getUser(), id).getAngularObjectRegistry(); + settings.getOrCreateInterpreterGroup(subject.getUser(), id).getAngularObjectRegistry(); if (registry instanceof RemoteAngularObjectRegistry) { // remove paragraph scope object for (Paragraph p : note.getParagraphs()) { @@ -374,7 +374,7 @@ public void removeNote(String id, AuthenticationInfo subject) { } } - ResourcePoolUtils.removeResourcesBelongsToNote(id); + interpreterSettingManager.removeResourcesBelongsToNote(id); fireNoteRemoveEvent(note); @@ -521,7 +521,8 @@ public Note loadNoteFromRepo(String id, AuthenticationInfo subject) { SnapshotAngularObject snapshot = angularObjectSnapshot.get(name); List settings = interpreterSettingManager.get(); for (InterpreterSetting setting : settings) { - InterpreterGroup intpGroup = setting.getInterpreterGroup(subject.getUser(), note.getId()); + InterpreterGroup intpGroup = setting.getOrCreateInterpreterGroup(subject.getUser(), + note.getId()); if (intpGroup.getId().equals(snapshot.getIntpGroupId())) { AngularObjectRegistry registry = intpGroup.getAngularObjectRegistry(); String noteId = snapshot.getAngularObject().getNoteId(); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java index 37138e63c42..bfe45662f4a 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java @@ -93,10 +93,10 @@ public class Paragraph extends Job implements Cloneable, JsonSerializable { // since zeppelin-0.7.0, zeppelin stores multiple results of the paragraph // see ZEPPELIN-212 - Object results; + volatile Object results; // For backward compatibility of note.json format after ZEPPELIN-212 - Object result; + volatile Object result; private Map runtimeInfos; /** @@ -157,7 +157,7 @@ public Paragraph getUserParagraph(String user) { } @Override - public void setResult(Object results) { + public synchronized void setResult(Object results) { this.results = results; } @@ -354,7 +354,7 @@ public InterpreterResult getResult() { } @Override - public Object getReturn() { + public synchronized Object getReturn() { return results; } @@ -401,6 +401,7 @@ protected Object jobRun() throws Throwable { logger.error("Can not find interpreter name " + repl); throw new RuntimeException("Can not find interpreter for " + getRequiredReplName()); } + //TODO(zjffdu) check interpreter setting status in interpreter setting itself InterpreterSetting intp = getInterpreterSettingById(repl.getInterpreterGroup().getId()); while (intp.getStatus().equals( org.apache.zeppelin.interpreter.InterpreterSetting.Status.DOWNLOADING_DEPENDENCIES)) { @@ -560,8 +561,10 @@ private InterpreterContext getInterpreterContextWithoutRunner(InterpreterOutput if (!interpreterSettingManager.getInterpreterSettings(note.getId()).isEmpty()) { InterpreterSetting intpGroup = interpreterSettingManager.getInterpreterSettings(note.getId()).get(0); - registry = intpGroup.getInterpreterGroup(getUser(), note.getId()).getAngularObjectRegistry(); - resourcePool = intpGroup.getInterpreterGroup(getUser(), note.getId()).getResourcePool(); + registry = intpGroup.getOrCreateInterpreterGroup(getUser(), note.getId()) + .getAngularObjectRegistry(); + resourcePool = intpGroup.getOrCreateInterpreterGroup(getUser(), note.getId()) + .getResourcePool(); } List runners = new LinkedList<>(); @@ -591,8 +594,10 @@ private InterpreterContext getInterpreterContext(InterpreterOutput output) { if (!interpreterSettingManager.getInterpreterSettings(note.getId()).isEmpty()) { InterpreterSetting intpGroup = interpreterSettingManager.getInterpreterSettings(note.getId()).get(0); - registry = intpGroup.getInterpreterGroup(getUser(), note.getId()).getAngularObjectRegistry(); - resourcePool = intpGroup.getInterpreterGroup(getUser(), note.getId()).getResourcePool(); + registry = intpGroup.getOrCreateInterpreterGroup(getUser(), note.getId()) + .getAngularObjectRegistry(); + resourcePool = intpGroup.getOrCreateInterpreterGroup(getUser(), note.getId()) + .getResourcePool(); } List runners = new LinkedList<>(); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumApplicationFactoryTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumApplicationFactoryTest.java index 305258afa4a..c204711f573 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumApplicationFactoryTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumApplicationFactoryTest.java @@ -16,14 +16,14 @@ */ package org.apache.zeppelin.helium; -import com.google.common.collect.Maps; import org.apache.commons.io.FileUtils; import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.dep.Dependency; import org.apache.zeppelin.dep.DependencyResolver; +import org.apache.zeppelin.display.AngularObjectRegistryListener; import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.mock.MockInterpreter1; import org.apache.zeppelin.interpreter.mock.MockInterpreter2; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; import org.apache.zeppelin.notebook.*; import org.apache.zeppelin.notebook.repo.VFSNotebookRepo; import org.apache.zeppelin.scheduler.Job; @@ -45,14 +45,9 @@ import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; -public class HeliumApplicationFactoryTest implements JobListenerFactory { - private File tmpDir; - private File notebookDir; - private ZeppelinConfiguration conf; +public class HeliumApplicationFactoryTest extends AbstractInterpreterTest implements JobListenerFactory { + private SchedulerFactory schedulerFactory; - private DependencyResolver depResolver; - private InterpreterFactory factory; - private InterpreterSettingManager interpreterSettingManager; private VFSNotebookRepo notebookRepo; private Notebook notebook; private HeliumApplicationFactory heliumAppFactory; @@ -60,46 +55,15 @@ public class HeliumApplicationFactoryTest implements JobListenerFactory { @Before public void setUp() throws Exception { - tmpDir = new File(System.getProperty("java.io.tmpdir")+"/ZepelinLTest_"+System.currentTimeMillis()); - tmpDir.mkdirs(); - File confDir = new File(tmpDir, "conf"); - confDir.mkdirs(); - notebookDir = new File(tmpDir + "/notebook"); - notebookDir.mkdirs(); - - File home = new File(getClass().getClassLoader().getResource("note").getFile()) // zeppelin/zeppelin-zengine/target/test-classes/note - .getParentFile() // zeppelin/zeppelin-zengine/target/test-classes - .getParentFile() // zeppelin/zeppelin-zengine/target - .getParentFile() // zeppelin/zeppelin-zengine - .getParentFile(); // zeppelin - - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName(), home.getAbsolutePath()); - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONF_DIR.getVarName(), tmpDir.getAbsolutePath() + "/conf"); - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), notebookDir.getAbsolutePath()); - - conf = new ZeppelinConfiguration(); - - this.schedulerFactory = new SchedulerFactory(); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER.getVarName(), "mock1,mock2"); + super.setUp(); + this.schedulerFactory = SchedulerFactory.singleton(); heliumAppFactory = new HeliumApplicationFactory(); - depResolver = new DependencyResolver(tmpDir.getAbsolutePath() + "/local-repo"); - interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); - factory = new InterpreterFactory(conf, null, null, heliumAppFactory, depResolver, false, interpreterSettingManager); - HashMap env = new HashMap<>(); - env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); - factory.setEnv(env); - - ArrayList interpreterInfos = new ArrayList<>(); - interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); - interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), - Maps.newHashMap(), "mock1", null); - interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(true), new HashMap()); - - ArrayList interpreterInfos2 = new ArrayList<>(); - interpreterInfos2.add(new InterpreterInfo(MockInterpreter2.class.getName(), "mock2", true, new HashMap())); - interpreterSettingManager.add("mock2", interpreterInfos2, new ArrayList(), new InterpreterOption(), - Maps.newHashMap(), "mock2", null); - interpreterSettingManager.createNewSetting("mock2", "mock2", new ArrayList(), new InterpreterOption(), new HashMap()); + // set AppEventListener properly + for (InterpreterSetting interpreterSetting : interpreterSettingManager.get()) { + interpreterSetting.setAppEventListener(heliumAppFactory); + } SearchService search = mock(SearchService.class); notebookRepo = new VFSNotebookRepo(conf); @@ -108,7 +72,7 @@ public void setUp() throws Exception { conf, notebookRepo, schedulerFactory, - factory, + interpreterFactory, interpreterSettingManager, this, search, @@ -124,16 +88,7 @@ public void setUp() throws Exception { @After public void tearDown() throws Exception { - List settings = interpreterSettingManager.get(); - for (InterpreterSetting setting : settings) { - for (InterpreterGroup intpGroup : setting.getAllInterpreterGroups()) { - intpGroup.close(); - } - } - - FileUtils.deleteDirectory(tmpDir); - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONF_DIR.getVarName(), - ZeppelinConfiguration.ConfVars.ZEPPELIN_CONF_DIR.getStringValue()); + super.tearDown(); } @@ -150,7 +105,7 @@ public void testLoadRunUnloadApplication() "", ""); Note note1 = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters("user", note1.getId(),interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding("user", note1.getId(),interpreterSettingManager.getInterpreterSettingIds()); Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -196,7 +151,7 @@ public void testUnloadOnParagraphRemove() throws IOException { "", ""); Note note1 = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters("user", note1.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding("user", note1.getId(), interpreterSettingManager.getInterpreterSettingIds()); Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -236,7 +191,7 @@ public void testUnloadOnInterpreterUnbind() throws IOException { "", ""); Note note1 = notebook.createNote(anonymous); - notebook.bindInterpretersToNote("user", note1.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + notebook.bindInterpretersToNote("user", note1.getId(), interpreterSettingManager.getInterpreterSettingIds()); Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -297,7 +252,7 @@ public void testUnloadOnInterpreterRestart() throws IOException { "", ""); Note note1 = notebook.createNote(anonymous); - notebook.bindInterpretersToNote("user", note1.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + notebook.bindInterpretersToNote("user", note1.getId(), interpreterSettingManager.getInterpreterSettingIds()); String mock1IntpSettingId = null; for (InterpreterSetting setting : notebook.getBindedInterpreterSettings(note1.getId())) { if (setting.getName().equals("mock1")) { diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumTest.java index 6b4932d0594..bdd639e3bb6 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumTest.java @@ -52,7 +52,7 @@ public void testSaveLoadConf() throws IOException, URISyntaxException, TaskRunne // given File heliumConf = new File(tmpDir, "helium.conf"); Helium helium = new Helium(heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), - null, null, null); + null, null, null, null); assertFalse(heliumConf.exists()); // when @@ -63,14 +63,14 @@ public void testSaveLoadConf() throws IOException, URISyntaxException, TaskRunne // then load without exception Helium heliumRestored = new Helium( - heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), null, null, null); + heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), null, null, null, null); } @Test public void testRestoreRegistryInstances() throws IOException, URISyntaxException, TaskRunnerException { File heliumConf = new File(tmpDir, "helium.conf"); Helium helium = new Helium( - heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), null, null, null); + heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), null, null, null, null); HeliumTestRegistry registry1 = new HeliumTestRegistry("r1", "r1"); HeliumTestRegistry registry2 = new HeliumTestRegistry("r2", "r2"); helium.addRegistry(registry1); @@ -105,7 +105,7 @@ public void testRestoreRegistryInstances() throws IOException, URISyntaxExceptio public void testRefresh() throws IOException, URISyntaxException, TaskRunnerException { File heliumConf = new File(tmpDir, "helium.conf"); Helium helium = new Helium( - heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), null, null, null); + heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), null, null, null, null); HeliumTestRegistry registry1 = new HeliumTestRegistry("r1", "r1"); helium.addRegistry(registry1); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java deleted file mode 100644 index aaa8864e8cd..00000000000 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java +++ /dev/null @@ -1,497 +0,0 @@ -/* - * 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.interpreter; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang.NullArgumentException; -import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; -import org.apache.zeppelin.dep.Dependency; -import org.apache.zeppelin.dep.DependencyResolver; -import org.apache.zeppelin.interpreter.mock.MockInterpreter1; -import org.apache.zeppelin.interpreter.mock.MockInterpreter2; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; -import org.apache.zeppelin.notebook.JobListenerFactory; -import org.apache.zeppelin.notebook.Note; -import org.apache.zeppelin.notebook.Notebook; -import org.apache.zeppelin.notebook.NotebookAuthorization; -import org.apache.zeppelin.notebook.repo.NotebookRepo; -import org.apache.zeppelin.notebook.repo.VFSNotebookRepo; -import org.apache.zeppelin.scheduler.SchedulerFactory; -import org.apache.zeppelin.search.SearchService; -import org.apache.zeppelin.user.AuthenticationInfo; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.quartz.SchedulerException; -import org.sonatype.aether.RepositoryException; - -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class InterpreterFactoryTest { - - private InterpreterFactory factory; - private InterpreterSettingManager interpreterSettingManager; - private File tmpDir; - private ZeppelinConfiguration conf; - private InterpreterContext context; - private Notebook notebook; - private NotebookRepo notebookRepo; - private DependencyResolver depResolver; - private SchedulerFactory schedulerFactory; - private NotebookAuthorization notebookAuthorization; - @Mock - private JobListenerFactory jobListenerFactory; - - @Before - public void setUp() throws Exception { - tmpDir = new File(System.getProperty("java.io.tmpdir")+"/ZeppelinLTest_"+System.currentTimeMillis()); - tmpDir.mkdirs(); - new File(tmpDir, "conf").mkdirs(); - FileUtils.copyDirectory(new File("src/test/resources/interpreter"), new File(tmpDir, "interpreter")); - - System.setProperty(ConfVars.ZEPPELIN_HOME.getVarName(), tmpDir.getAbsolutePath()); - System.setProperty(ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER.getVarName(), - "mock1,mock2,mock11,dev"); - conf = new ZeppelinConfiguration(); - schedulerFactory = new SchedulerFactory(); - depResolver = new DependencyResolver(tmpDir.getAbsolutePath() + "/local-repo"); - interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); - factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); - context = new InterpreterContext("note", "id", null, "title", "text", null, null, null, null, null, null, null); - - ArrayList interpreterInfos = new ArrayList<>(); - interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); - interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), - Maps.newHashMap(), "mock1", null); - Map intp1Properties = new HashMap(); - intp1Properties.put("PROPERTY_1", - new InterpreterProperty("PROPERTY_1", "VALUE_1")); - intp1Properties.put("property_2", - new InterpreterProperty("property_2", "value_2")); - interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(true), intp1Properties); - - ArrayList interpreterInfos2 = new ArrayList<>(); - interpreterInfos2.add(new InterpreterInfo(MockInterpreter2.class.getName(), "mock2", true, new HashMap())); - interpreterSettingManager.add("mock2", interpreterInfos2, new ArrayList(), new InterpreterOption(), - Maps.newHashMap(), "mock2", null); - interpreterSettingManager.createNewSetting("mock2", "mock2", new ArrayList(), new InterpreterOption(), new HashMap()); - - SearchService search = mock(SearchService.class); - notebookRepo = new VFSNotebookRepo(conf); - notebookAuthorization = NotebookAuthorization.init(conf); - notebook = new Notebook(conf, notebookRepo, schedulerFactory, factory, interpreterSettingManager, jobListenerFactory, search, - notebookAuthorization, null); - } - - @After - public void tearDown() throws Exception { - FileUtils.deleteDirectory(tmpDir); - } - - @Test - public void testBasic() { - List all = interpreterSettingManager.get(); - InterpreterSetting mock1Setting = null; - for (InterpreterSetting setting : all) { - if (setting.getName().equals("mock1")) { - mock1Setting = setting; - break; - } - } - -// mock1Setting = factory.createNewSetting("mock11", "mock1", new ArrayList(), new InterpreterOption(false), new Properties()); - - InterpreterGroup interpreterGroup = mock1Setting.getInterpreterGroup("user", "sharedProcess"); - factory.createInterpretersForNote(mock1Setting, "user", "sharedProcess", "session"); - - // get interpreter - assertNotNull("get Interpreter", interpreterGroup.get("session").get(0)); - - // try to get unavailable interpreter - assertNull(interpreterSettingManager.get("unknown")); - - // restart interpreter - interpreterSettingManager.restart(mock1Setting.getId()); - assertNull(mock1Setting.getInterpreterGroup("user", "sharedProcess").get("session")); - } - - @Test - public void testRemoteRepl() throws Exception { - interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); - ArrayList interpreterInfos = new ArrayList<>(); - interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); - interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), - Maps.newHashMap(), "mock1", null); - Map intp1Properties = new HashMap(); - intp1Properties.put("PROPERTY_1", - new InterpreterProperty("PROPERTY_1", "VALUE_1")); - intp1Properties.put("property_2", new InterpreterProperty("property_2", "value_2")); - interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(true), intp1Properties); - factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); - List all = interpreterSettingManager.get(); - InterpreterSetting mock1Setting = null; - for (InterpreterSetting setting : all) { - if (setting.getName().equals("mock1")) { - mock1Setting = setting; - break; - } - } - InterpreterGroup interpreterGroup = mock1Setting.getInterpreterGroup("user", "sharedProcess"); - factory.createInterpretersForNote(mock1Setting, "user", "sharedProcess", "session"); - // get interpreter - assertNotNull("get Interpreter", interpreterGroup.get("session").get(0)); - assertTrue(interpreterGroup.get("session").get(0) instanceof LazyOpenInterpreter); - LazyOpenInterpreter lazyInterpreter = (LazyOpenInterpreter)(interpreterGroup.get("session").get(0)); - assertTrue(lazyInterpreter.getInnerInterpreter() instanceof RemoteInterpreter); - RemoteInterpreter remoteInterpreter = (RemoteInterpreter) lazyInterpreter.getInnerInterpreter(); - assertEquals("VALUE_1", remoteInterpreter.getEnv().get("PROPERTY_1")); - assertEquals("value_2", remoteInterpreter.getProperty("property_2")); - } - - /** - * 2 users' interpreters in scoped mode. Each user has one session. Restarting user1's interpreter - * won't affect user2's interpreter - * @throws Exception - */ - @Test - public void testRestartInterpreterInScopedMode() throws Exception { - interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); - ArrayList interpreterInfos = new ArrayList<>(); - interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); - interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), - Maps.newHashMap(), "mock1", null); - Map intp1Properties = new HashMap(); - intp1Properties.put("PROPERTY_1", - new InterpreterProperty("PROPERTY_1", "VALUE_1")); - intp1Properties.put("property_2", - new InterpreterProperty("property_2", "value_2")); - interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(true), intp1Properties); - factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); - List all = interpreterSettingManager.get(); - InterpreterSetting mock1Setting = null; - for (InterpreterSetting setting : all) { - if (setting.getName().equals("mock1")) { - mock1Setting = setting; - break; - } - } - mock1Setting.getOption().setPerUser("scoped"); - mock1Setting.getOption().setPerNote("shared"); - // set remote as false so that we won't create new remote interpreter process - mock1Setting.getOption().setRemote(false); - mock1Setting.getOption().setHost("localhost"); - mock1Setting.getOption().setPort(2222); - InterpreterGroup interpreterGroup = mock1Setting.getInterpreterGroup("user1", "sharedProcess"); - factory.createInterpretersForNote(mock1Setting, "user1", "sharedProcess", "user1"); - factory.createInterpretersForNote(mock1Setting, "user2", "sharedProcess", "user2"); - - LazyOpenInterpreter interpreter1 = (LazyOpenInterpreter)interpreterGroup.get("user1").get(0); - interpreter1.open(); - LazyOpenInterpreter interpreter2 = (LazyOpenInterpreter)interpreterGroup.get("user2").get(0); - interpreter2.open(); - - mock1Setting.closeAndRemoveInterpreterGroup("sharedProcess", "user1"); - assertFalse(interpreter1.isOpen()); - assertTrue(interpreter2.isOpen()); - } - - /** - * 2 users' interpreters in isolated mode. Each user has one interpreterGroup. Restarting user1's interpreter - * won't affect user2's interpreter - * @throws Exception - */ - @Test - public void testRestartInterpreterInIsolatedMode() throws Exception { - interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); - ArrayList interpreterInfos = new ArrayList<>(); - interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); - interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), - Maps.newHashMap(), "mock1", null); - Map intp1Properties = new HashMap(); - intp1Properties.put("PROPERTY_1", - new InterpreterProperty("PROPERTY_1", "VALUE_1")); - intp1Properties.put("property_2", - new InterpreterProperty("property_2", "value_2")); - interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(true), intp1Properties); - factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); - List all = interpreterSettingManager.get(); - InterpreterSetting mock1Setting = null; - for (InterpreterSetting setting : all) { - if (setting.getName().equals("mock1")) { - mock1Setting = setting; - break; - } - } - mock1Setting.getOption().setPerUser("isolated"); - mock1Setting.getOption().setPerNote("shared"); - // set remote as false so that we won't create new remote interpreter process - mock1Setting.getOption().setRemote(false); - mock1Setting.getOption().setHost("localhost"); - mock1Setting.getOption().setPort(2222); - InterpreterGroup interpreterGroup1 = mock1Setting.getInterpreterGroup("user1", "note1"); - InterpreterGroup interpreterGroup2 = mock1Setting.getInterpreterGroup("user2", "note2"); - factory.createInterpretersForNote(mock1Setting, "user1", "note1", "shared_session"); - factory.createInterpretersForNote(mock1Setting, "user2", "note2", "shared_session"); - - LazyOpenInterpreter interpreter1 = (LazyOpenInterpreter)interpreterGroup1.get("shared_session").get(0); - interpreter1.open(); - LazyOpenInterpreter interpreter2 = (LazyOpenInterpreter)interpreterGroup2.get("shared_session").get(0); - interpreter2.open(); - - mock1Setting.closeAndRemoveInterpreterGroup("note1", "user1"); - assertFalse(interpreter1.isOpen()); - assertTrue(interpreter2.isOpen()); - } - - @Test - public void testFactoryDefaultList() throws IOException, RepositoryException { - // get default settings - List all = interpreterSettingManager.getDefaultInterpreterSettingList(); - assertTrue(interpreterSettingManager.get().size() >= all.size()); - } - - @Test - public void testExceptions() throws InterpreterException, IOException, RepositoryException { - List all = interpreterSettingManager.getDefaultInterpreterSettingList(); - // add setting with null option & properties expected nullArgumentException.class - try { - interpreterSettingManager.add("mock2", new ArrayList(), new LinkedList(), new InterpreterOption(false), Collections.EMPTY_MAP, "", null); - } catch(NullArgumentException e) { - assertEquals("Test null option" , e.getMessage(),new NullArgumentException("option").getMessage()); - } - try { - interpreterSettingManager.add("mock2", new ArrayList(), new LinkedList(), new InterpreterOption(false), Collections.EMPTY_MAP, "", null); - } catch (NullArgumentException e){ - assertEquals("Test null properties" , e.getMessage(),new NullArgumentException("properties").getMessage()); - } - } - - - @Test - public void testSaveLoad() throws IOException, RepositoryException { - // interpreter settings - int numInterpreters = interpreterSettingManager.get().size(); - - // check if file saved - assertTrue(new File(conf.getInterpreterSettingPath()).exists()); - - interpreterSettingManager.createNewSetting("new-mock1", "mock1", new LinkedList(), new InterpreterOption(false), new HashMap()); - assertEquals(numInterpreters + 1, interpreterSettingManager.get().size()); - - interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); - - /* - Current situation, if InterpreterSettinfRef doesn't have the key of InterpreterSetting, it would be ignored. - Thus even though interpreter.json have several interpreterSetting in that file, it would be ignored and would not be initialized from loadFromFile. - In this case, only "mock11" would be referenced from file under interpreter/mock, and "mock11" group would be initialized. - */ - // TODO(jl): Decide how to handle the know referenced interpreterSetting. - assertEquals(1, interpreterSettingManager.get().size()); - } - - @Test - public void testInterpreterSettingPropertyClass() throws IOException, RepositoryException { - // check if default interpreter reference's property type is map - Map interpreterSettingRefs = interpreterSettingManager.getAvailableInterpreterSettings(); - InterpreterSetting intpSetting = interpreterSettingRefs.get("mock1"); - Map intpProperties = - (Map) intpSetting.getProperties(); - assertTrue(intpProperties instanceof Map); - - // check if interpreter instance is saved as Properties in conf/interpreter.json file - Map properties = new HashMap(); - properties.put("key1", new InterpreterProperty("key1", "value1", "type1")); - properties.put("key2", new InterpreterProperty("key2", "value2", "type2")); - - interpreterSettingManager.createNewSetting("newMock", "mock1", new LinkedList(), new InterpreterOption(false), properties); - - String confFilePath = conf.getInterpreterSettingPath(); - byte[] encoded = Files.readAllBytes(Paths.get(confFilePath)); - String json = new String(encoded, "UTF-8"); - - InterpreterInfoSaving infoSaving = InterpreterInfoSaving.fromJson(json); - Map interpreterSettings = infoSaving.interpreterSettings; - for (String key : interpreterSettings.keySet()) { - InterpreterSetting setting = interpreterSettings.get(key); - if (setting.getName().equals("newMock")) { - assertEquals(setting.getProperties().toString(), properties.toString()); - } - } - } - - @Test - public void testInterpreterAliases() throws IOException, RepositoryException { - interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); - factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); - final InterpreterInfo info1 = new InterpreterInfo("className1", "name1", true, null); - final InterpreterInfo info2 = new InterpreterInfo("className2", "name1", true, null); - interpreterSettingManager.add("group1", new ArrayList() {{ - add(info1); - }}, new ArrayList(), new InterpreterOption(true), Collections.EMPTY_MAP, "/path1", null); - interpreterSettingManager.add("group2", new ArrayList(){{ - add(info2); - }}, new ArrayList(), new InterpreterOption(true), Collections.EMPTY_MAP, "/path2", null); - - final InterpreterSetting setting1 = interpreterSettingManager.createNewSetting("test-group1", "group1", new ArrayList(), new InterpreterOption(true), new HashMap()); - final InterpreterSetting setting2 = interpreterSettingManager.createNewSetting("test-group2", "group1", new ArrayList(), new InterpreterOption(true), new HashMap()); - - interpreterSettingManager.setInterpreters("user", "note", new ArrayList() {{ - add(setting1.getId()); - add(setting2.getId()); - }}); - - assertEquals("className1", factory.getInterpreter("user1", "note", "test-group1").getClassName()); - assertEquals("className1", factory.getInterpreter("user1", "note", "group1").getClassName()); - } - - @Test - public void testMultiUser() throws IOException, RepositoryException { - interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); - factory = new InterpreterFactory(conf, null, null, null, depResolver, true, interpreterSettingManager); - final InterpreterInfo info1 = new InterpreterInfo("className1", "name1", true, null); - interpreterSettingManager.add("group1", new ArrayList(){{ - add(info1); - }}, new ArrayList(), new InterpreterOption(true), Collections.EMPTY_MAP, "/path1", null); - - InterpreterOption perUserInterpreterOption = new InterpreterOption(true, InterpreterOption.ISOLATED, InterpreterOption.SHARED); - final InterpreterSetting setting1 = interpreterSettingManager.createNewSetting("test-group1", "group1", new ArrayList(), perUserInterpreterOption, new HashMap()); - - interpreterSettingManager.setInterpreters("user1", "note", new ArrayList() {{ - add(setting1.getId()); - }}); - - interpreterSettingManager.setInterpreters("user2", "note", new ArrayList() {{ - add(setting1.getId()); - }}); - - assertNotEquals(factory.getInterpreter("user1", "note", "test-group1"), factory.getInterpreter("user2", "note", "test-group1")); - } - - - @Test - public void testInvalidInterpreterSettingName() { - try { - interpreterSettingManager.createNewSetting("new.mock1", "mock1", new LinkedList(), new InterpreterOption(false), new HashMap()); - fail("expect fail because of invalid InterpreterSetting Name"); - } catch (IOException e) { - assertEquals("'.' is invalid for InterpreterSetting name.", e.getMessage()); - } - } - - - @Test - public void getEditorSetting() throws IOException, RepositoryException, SchedulerException { - List intpIds = new ArrayList<>(); - for(InterpreterSetting intpSetting: interpreterSettingManager.get()) { - if (intpSetting.getName().startsWith("mock1")) { - intpIds.add(intpSetting.getId()); - } - } - Note note = notebook.createNote(intpIds, new AuthenticationInfo("anonymous")); - - Interpreter interpreter = factory.getInterpreter("user1", note.getId(), "mock11"); - // get editor setting from interpreter-setting.json - Map editor = interpreterSettingManager.getEditorSetting(interpreter, "user1", note.getId(), "mock11"); - assertEquals("java", editor.get("language")); - - // when interpreter is not loaded via interpreter-setting.json - // or editor setting doesn't exit - editor = interpreterSettingManager.getEditorSetting(factory.getInterpreter("user1", note.getId(), "mock1"),"user1", note.getId(), "mock1"); - assertEquals(null, editor.get("language")); - - // when interpreter is not bound to note - editor = interpreterSettingManager.getEditorSetting(factory.getInterpreter("user1", note.getId(), "mock11"),"user1", note.getId(), "mock2"); - assertEquals("text", editor.get("language")); - } - - @Test - public void registerCustomInterpreterRunner() throws IOException { - InterpreterSettingManager spyInterpreterSettingManager = spy(interpreterSettingManager); - - doNothing().when(spyInterpreterSettingManager).saveToFile(); - - ArrayList interpreterInfos1 = new ArrayList<>(); - interpreterInfos1.add(new InterpreterInfo("name1.class", "name1", true, Maps.newHashMap())); - - spyInterpreterSettingManager.add("normalGroup1", interpreterInfos1, Lists.newArrayList(), new InterpreterOption(true), Maps.newHashMap(), "/normalGroup1", null); - - spyInterpreterSettingManager.createNewSetting("normalGroup1", "normalGroup1", Lists.newArrayList(), new InterpreterOption(true), new HashMap()); - - ArrayList interpreterInfos2 = new ArrayList<>(); - interpreterInfos2.add(new InterpreterInfo("name1.class", "name1", true, Maps.newHashMap())); - - InterpreterRunner mockInterpreterRunner = mock(InterpreterRunner.class); - - when(mockInterpreterRunner.getPath()).thenReturn("custom-linux-path.sh"); - - spyInterpreterSettingManager.add("customGroup1", interpreterInfos2, Lists.newArrayList(), new InterpreterOption(true), Maps.newHashMap(), "/customGroup1", mockInterpreterRunner); - - spyInterpreterSettingManager.createNewSetting("customGroup1", "customGroup1", Lists.newArrayList(), new InterpreterOption(true), new HashMap()); - - spyInterpreterSettingManager.setInterpreters("anonymous", "noteCustome", spyInterpreterSettingManager.getDefaultInterpreterSettingList()); - - factory.getInterpreter("anonymous", "noteCustome", "customGroup1"); - - verify(mockInterpreterRunner, times(1)).getPath(); - } - - @Test - public void interpreterRunnerTest() { - InterpreterRunner mockInterpreterRunner = mock(InterpreterRunner.class); - String testInterpreterRunner = "relativePath.sh"; - when(mockInterpreterRunner.getPath()).thenReturn(testInterpreterRunner); // This test only for Linux - Interpreter i = factory.createRemoteRepl("path1", "sessionKey", "className", new Properties(), interpreterSettingManager.get().get(0).getId(), "userName", false, mockInterpreterRunner); - String interpreterRunner = ((RemoteInterpreter) ((LazyOpenInterpreter) i).getInnerInterpreter()).getInterpreterRunner(); - assertNotEquals(interpreterRunner, testInterpreterRunner); - - testInterpreterRunner = "/AbsolutePath.sh"; - when(mockInterpreterRunner.getPath()).thenReturn(testInterpreterRunner); - i = factory.createRemoteRepl("path1", "sessionKey", "className", new Properties(), interpreterSettingManager.get().get(0).getId(), "userName", false, mockInterpreterRunner); - interpreterRunner = ((RemoteInterpreter) ((LazyOpenInterpreter) i).getInnerInterpreter()).getInterpreterRunner(); - assertEquals(interpreterRunner, testInterpreterRunner); - } -} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java deleted file mode 100644 index 1aab7572758..00000000000 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java +++ /dev/null @@ -1,327 +0,0 @@ -package org.apache.zeppelin.interpreter; - -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -import org.junit.Test; - -import org.apache.zeppelin.dep.Dependency; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; - -public class InterpreterSettingTest { - - @Test - public void sharedModeCloseandRemoveInterpreterGroupTest() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerUser(InterpreterOption.SHARED); - InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); - - interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { - @Override - public InterpreterGroup createInterpreterGroup(String interpreterGroupId, - InterpreterOption option) { - return new InterpreterGroup(interpreterGroupId); - } - }); - - Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); - List interpreterList1 = new ArrayList<>(); - interpreterList1.add(mockInterpreter1); - InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); - - // This won't effect anything - Interpreter mockInterpreter2 = mock(RemoteInterpreter.class); - List interpreterList2 = new ArrayList<>(); - interpreterList2.add(mockInterpreter2); - interpreterGroup = interpreterSetting.getInterpreterGroup("user2", "note1"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user2", "note1"), interpreterList2); - - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); - - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user2"); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - } - - @Test - public void perUserScopedModeCloseAndRemoveInterpreterGroupTest() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerUser(InterpreterOption.SCOPED); - InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); - - interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { - @Override - public InterpreterGroup createInterpreterGroup(String interpreterGroupId, - InterpreterOption option) { - return new InterpreterGroup(interpreterGroupId); - } - }); - - Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); - List interpreterList1 = new ArrayList<>(); - interpreterList1.add(mockInterpreter1); - InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); - - Interpreter mockInterpreter2 = mock(RemoteInterpreter.class); - List interpreterList2 = new ArrayList<>(); - interpreterList2.add(mockInterpreter2); - interpreterGroup = interpreterSetting.getInterpreterGroup("user2", "note1"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user2", "note1"), interpreterList2); - - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(2, interpreterSetting.getInterpreterGroup("user1", "note1").size()); - assertEquals(2, interpreterSetting.getInterpreterGroup("user2", "note1").size()); - - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); - assertEquals(1, interpreterSetting.getInterpreterGroup("user2","note1").size()); - - // Check if non-existed key works or not - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); - assertEquals(1, interpreterSetting.getInterpreterGroup("user2","note1").size()); - - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user2"); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - } - - @Test - public void perUserIsolatedModeCloseAndRemoveInterpreterGroupTest() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerUser(InterpreterOption.ISOLATED); - InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); - - interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { - @Override - public InterpreterGroup createInterpreterGroup(String interpreterGroupId, - InterpreterOption option) { - return new InterpreterGroup(interpreterGroupId); - } - }); - - Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); - List interpreterList1 = new ArrayList<>(); - interpreterList1.add(mockInterpreter1); - InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); - - Interpreter mockInterpreter2 = mock(RemoteInterpreter.class); - List interpreterList2 = new ArrayList<>(); - interpreterList2.add(mockInterpreter2); - interpreterGroup = interpreterSetting.getInterpreterGroup("user2", "note1"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user2", "note1"), interpreterList2); - - assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user2", "note1").size()); - - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); - assertEquals(1, interpreterSetting.getInterpreterGroup("user2","note1").size()); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user2"); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - } - - @Test - public void perNoteScopedModeCloseAndRemoveInterpreterGroupTest() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerNote(InterpreterOption.SCOPED); - InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); - - interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { - @Override - public InterpreterGroup createInterpreterGroup(String interpreterGroupId, - InterpreterOption option) { - return new InterpreterGroup(interpreterGroupId); - } - }); - - Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); - List interpreterList1 = new ArrayList<>(); - interpreterList1.add(mockInterpreter1); - InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); - - Interpreter mockInterpreter2 = mock(RemoteInterpreter.class); - List interpreterList2 = new ArrayList<>(); - interpreterList2.add(mockInterpreter2); - interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note2"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note2"), interpreterList2); - - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(2, interpreterSetting.getInterpreterGroup("user1", "note1").size()); - assertEquals(2, interpreterSetting.getInterpreterGroup("user1", "note2").size()); - - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note2").size()); - - // Check if non-existed key works or not - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note2").size()); - - interpreterSetting.closeAndRemoveInterpreterGroup("note2", "user1"); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - } - - @Test - public void perNoteIsolatedModeCloseAndRemoveInterpreterGroupTest() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerNote(InterpreterOption.ISOLATED); - InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); - - interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { - @Override - public InterpreterGroup createInterpreterGroup(String interpreterGroupId, - InterpreterOption option) { - return new InterpreterGroup(interpreterGroupId); - } - }); - - Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); - List interpreterList1 = new ArrayList<>(); - interpreterList1.add(mockInterpreter1); - InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); - - Interpreter mockInterpreter2 = mock(RemoteInterpreter.class); - List interpreterList2 = new ArrayList<>(); - interpreterList2.add(mockInterpreter2); - interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note2"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note2"), interpreterList2); - - assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note2").size()); - - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note2").size()); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - - interpreterSetting.closeAndRemoveInterpreterGroup("note2", "user1"); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - } - - @Test - public void perNoteScopedModeRemoveInterpreterGroupWhenNoteIsRemoved() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerNote(InterpreterOption.SCOPED); - InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); - - interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { - @Override - public InterpreterGroup createInterpreterGroup(String interpreterGroupId, - InterpreterOption option) { - return new InterpreterGroup(interpreterGroupId); - } - }); - - Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); - List interpreterList1 = new ArrayList<>(); - interpreterList1.add(mockInterpreter1); - InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); - - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); - - // This method will be called when remove note - interpreterSetting.closeAndRemoveInterpreterGroup("note1",""); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - // Be careful that getInterpreterGroup makes interpreterGroup if it doesn't exist - assertEquals(0, interpreterSetting.getInterpreterGroup("user1","note1").size()); - } - - @Test - public void perNoteIsolatedModeRemoveInterpreterGroupWhenNoteIsRemoved() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerNote(InterpreterOption.ISOLATED); - InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); - - interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { - @Override - public InterpreterGroup createInterpreterGroup(String interpreterGroupId, - InterpreterOption option) { - return new InterpreterGroup(interpreterGroupId); - } - }); - - Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); - List interpreterList1 = new ArrayList<>(); - interpreterList1.add(mockInterpreter1); - InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); - - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); - - // This method will be called when remove note - interpreterSetting.closeAndRemoveInterpreterGroup("note1",""); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - // Be careful that getInterpreterGroup makes interpreterGroup if it doesn't exist - assertEquals(0, interpreterSetting.getInterpreterGroup("user1","note1").size()); - } - - @Test - public void perUserScopedModeNeverRemoveInterpreterGroupWhenNoteIsRemoved() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerUser(InterpreterOption.SCOPED); - InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); - - interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { - @Override - public InterpreterGroup createInterpreterGroup(String interpreterGroupId, - InterpreterOption option) { - return new InterpreterGroup(interpreterGroupId); - } - }); - - Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); - List interpreterList1 = new ArrayList<>(); - interpreterList1.add(mockInterpreter1); - InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); - - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); - - // This method will be called when remove note - interpreterSetting.closeAndRemoveInterpreterGroup("note1",""); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - // Be careful that getInterpreterGroup makes interpreterGroup if it doesn't exist - assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note1").size()); - } - - @Test - public void perUserIsolatedModeNeverRemoveInterpreterGroupWhenNoteIsRemoved() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerUser(InterpreterOption.ISOLATED); - InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); - - interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { - @Override - public InterpreterGroup createInterpreterGroup(String interpreterGroupId, - InterpreterOption option) { - return new InterpreterGroup(interpreterGroupId); - } - }); - - Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); - List interpreterList1 = new ArrayList<>(); - interpreterList1.add(mockInterpreter1); - InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); - interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); - - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); - - // This method will be called when remove note - interpreterSetting.closeAndRemoveInterpreterGroup("note1",""); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - // Be careful that getInterpreterGroup makes interpreterGroup if it doesn't exist - assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note1").size()); - } -} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessTest.java deleted file mode 100644 index b85d7ef2fb0..00000000000 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessTest.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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.interpreter.remote; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.mockito.Mockito.*; - -import java.util.HashMap; -import java.util.Properties; - -import org.apache.thrift.TException; -import org.apache.thrift.transport.TTransportException; -import org.apache.zeppelin.interpreter.Constants; -import org.apache.zeppelin.interpreter.InterpreterException; -import org.apache.zeppelin.interpreter.InterpreterGroup; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; -import org.junit.Test; - -public class RemoteInterpreterProcessTest { - private static final String INTERPRETER_SCRIPT = - System.getProperty("os.name").startsWith("Windows") ? - "../bin/interpreter.cmd" : - "../bin/interpreter.sh"; - private static final int DUMMY_PORT=3678; - - @Test - public void testStartStop() { - InterpreterGroup intpGroup = new InterpreterGroup(); - RemoteInterpreterManagedProcess rip = new RemoteInterpreterManagedProcess( - INTERPRETER_SCRIPT, "nonexists", "fakeRepo", new HashMap(), - 10 * 1000, null, null,"fakeName"); - assertFalse(rip.isRunning()); - assertEquals(0, rip.referenceCount()); - assertEquals(1, rip.reference(intpGroup, "anonymous", false)); - assertEquals(2, rip.reference(intpGroup, "anonymous", false)); - assertEquals(true, rip.isRunning()); - assertEquals(1, rip.dereference()); - assertEquals(true, rip.isRunning()); - assertEquals(0, rip.dereference()); - assertEquals(false, rip.isRunning()); - } - - @Test - public void testClientFactory() throws Exception { - InterpreterGroup intpGroup = new InterpreterGroup(); - RemoteInterpreterManagedProcess rip = new RemoteInterpreterManagedProcess( - INTERPRETER_SCRIPT, "nonexists", "fakeRepo", new HashMap(), - mock(RemoteInterpreterEventPoller.class), 10 * 1000, "fakeName"); - rip.reference(intpGroup, "anonymous", false); - assertEquals(0, rip.getNumActiveClient()); - assertEquals(0, rip.getNumIdleClient()); - - Client client = rip.getClient(); - assertEquals(1, rip.getNumActiveClient()); - assertEquals(0, rip.getNumIdleClient()); - - rip.releaseClient(client); - assertEquals(0, rip.getNumActiveClient()); - assertEquals(1, rip.getNumIdleClient()); - - rip.dereference(); - } - - @Test - public void testStartStopRemoteInterpreter() throws TException, InterruptedException { - RemoteInterpreterServer server = new RemoteInterpreterServer(3678); - server.start(); - boolean running = false; - long startTime = System.currentTimeMillis(); - while (System.currentTimeMillis() - startTime < 10 * 1000) { - if (server.isRunning()) { - running = true; - break; - } else { - Thread.sleep(200); - } - } - Properties properties = new Properties(); - properties.setProperty(Constants.ZEPPELIN_INTERPRETER_PORT, "3678"); - properties.setProperty(Constants.ZEPPELIN_INTERPRETER_HOST, "localhost"); - InterpreterGroup intpGroup = mock(InterpreterGroup.class); - when(intpGroup.getProperty()).thenReturn(properties); - when(intpGroup.containsKey(Constants.EXISTING_PROCESS)).thenReturn(true); - - RemoteInterpreterProcess rip = new RemoteInterpreterManagedProcess( - INTERPRETER_SCRIPT, - "nonexists", - "fakeRepo", - new HashMap(), - mock(RemoteInterpreterEventPoller.class) - , 10 * 1000, - "fakeName"); - assertFalse(rip.isRunning()); - assertEquals(0, rip.referenceCount()); - assertEquals(1, rip.reference(intpGroup, "anonymous", false)); - assertEquals(true, rip.isRunning()); - } - - - @Test - public void testPropagateError() throws TException, InterruptedException { - InterpreterGroup intpGroup = new InterpreterGroup(); - RemoteInterpreterManagedProcess rip = new RemoteInterpreterManagedProcess( - "echo hello_world", "nonexists", "fakeRepo", new HashMap(), - 10 * 1000, null, null, "fakeName"); - assertFalse(rip.isRunning()); - assertEquals(0, rip.referenceCount()); - try { - assertEquals(1, rip.reference(intpGroup, "anonymous", false)); - } catch (InterpreterException e) { - e.getMessage().contains("hello_world"); - } - assertEquals(0, rip.referenceCount()); - } -} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java deleted file mode 100644 index 95235e51a97..00000000000 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java +++ /dev/null @@ -1,975 +0,0 @@ -/* - * 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.interpreter.remote; - -import static org.junit.Assert.*; - -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import org.apache.thrift.transport.TTransportException; -import org.apache.zeppelin.display.AngularObject; -import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterEnv; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterResultMessage; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; -import org.apache.zeppelin.user.AuthenticationInfo; -import org.apache.zeppelin.display.GUI; -import org.apache.zeppelin.interpreter.*; -import org.apache.zeppelin.interpreter.InterpreterResult.Code; -import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterA; -import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterB; -import org.apache.zeppelin.resource.LocalResourcePool; -import org.apache.zeppelin.scheduler.Job; -import org.apache.zeppelin.scheduler.Job.Status; -import org.apache.zeppelin.scheduler.Scheduler; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mockito; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; - -public class RemoteInterpreterTest { - - - private static final String INTERPRETER_SCRIPT = - System.getProperty("os.name").startsWith("Windows") ? - "../bin/interpreter.cmd" : - "../bin/interpreter.sh"; - - private InterpreterGroup intpGroup; - private HashMap env; - - @Before - public void setUp() throws Exception { - intpGroup = new InterpreterGroup(); - env = new HashMap<>(); - env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); - } - - @After - public void tearDown() throws Exception { - intpGroup.close(); - } - - private RemoteInterpreter createMockInterpreterA(Properties p) { - return createMockInterpreterA(p, "note"); - } - - private RemoteInterpreter createMockInterpreterA(Properties p, String noteId) { - return new RemoteInterpreter( - p, - noteId, - MockInterpreterA.class.getName(), - new File(INTERPRETER_SCRIPT).getAbsolutePath(), - "fake", - "fakeRepo", - env, - 10 * 1000, - null, - null, - "anonymous", - false); - } - - private RemoteInterpreter createMockInterpreterB(Properties p) { - return createMockInterpreterB(p, "note"); - } - - private RemoteInterpreter createMockInterpreterB(Properties p, String noteId) { - return new RemoteInterpreter( - p, - noteId, - MockInterpreterB.class.getName(), - new File(INTERPRETER_SCRIPT).getAbsolutePath(), - "fake", - "fakeRepo", - env, - 10 * 1000, - null, - null, - "anonymous", - false); - } - - @Test - public void testRemoteInterperterCall() throws TTransportException, IOException { - Properties p = new Properties(); - intpGroup.put("note", new LinkedList()); - - RemoteInterpreter intpA = createMockInterpreterA(p); - - intpGroup.get("note").add(intpA); - - intpA.setInterpreterGroup(intpGroup); - - RemoteInterpreter intpB = createMockInterpreterB(p); - - intpGroup.get("note").add(intpB); - intpB.setInterpreterGroup(intpGroup); - - - RemoteInterpreterProcess process = intpA.getInterpreterProcess(); - process.equals(intpB.getInterpreterProcess()); - - assertFalse(process.isRunning()); - assertEquals(0, process.getNumIdleClient()); - assertEquals(0, process.referenceCount()); - - intpA.open(); // initializa all interpreters in the same group - assertTrue(process.isRunning()); - assertEquals(1, process.getNumIdleClient()); - assertEquals(1, process.referenceCount()); - - intpA.interpret("1", - new InterpreterContext( - "note", - "id", - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null)); - - intpB.open(); - assertEquals(1, process.referenceCount()); - - intpA.close(); - assertEquals(0, process.referenceCount()); - intpB.close(); - assertEquals(0, process.referenceCount()); - - assertFalse(process.isRunning()); - - } - - @Test - public void testExecuteIncorrectPrecode() throws TTransportException, IOException { - Properties p = new Properties(); - p.put("zeppelin.MockInterpreterA.precode", "fail test"); - intpGroup.put("note", new LinkedList()); - - RemoteInterpreter intpA = createMockInterpreterA(p); - - intpGroup.get("note").add(intpA); - - intpA.setInterpreterGroup(intpGroup); - - RemoteInterpreterProcess process = intpA.getInterpreterProcess(); - - intpA.open(); - - InterpreterResult result = intpA.interpret("1", - new InterpreterContext( - "note", - "id", - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null)); - - - - intpA.close(); - assertEquals(Code.ERROR, result.code()); - } - - @Test - public void testExecuteCorrectPrecode() throws TTransportException, IOException { - Properties p = new Properties(); - p.put("zeppelin.MockInterpreterA.precode", "2"); - intpGroup.put("note", new LinkedList()); - - RemoteInterpreter intpA = createMockInterpreterA(p); - - intpGroup.get("note").add(intpA); - - intpA.setInterpreterGroup(intpGroup); - - RemoteInterpreterProcess process = intpA.getInterpreterProcess(); - - intpA.open(); - - InterpreterResult result = intpA.interpret("1", - new InterpreterContext( - "note", - "id", - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null)); - - - - intpA.close(); - assertEquals(Code.SUCCESS, result.code()); - assertEquals("1", result.message().get(0).getData()); - } - - @Test - public void testRemoteInterperterErrorStatus() throws TTransportException, IOException { - Properties p = new Properties(); - - RemoteInterpreter intpA = createMockInterpreterA(p); - - intpGroup.put("note", new LinkedList()); - intpGroup.get("note").add(intpA); - intpA.setInterpreterGroup(intpGroup); - - intpA.open(); - InterpreterResult ret = intpA.interpret("non numeric value", - new InterpreterContext( - "noteId", - "id", - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null)); - - assertEquals(Code.ERROR, ret.code()); - } - - @Test - public void testRemoteSchedulerSharing() throws TTransportException, IOException { - Properties p = new Properties(); - intpGroup.put("note", new LinkedList()); - - RemoteInterpreter intpA = new RemoteInterpreter( - p, - "note", - MockInterpreterA.class.getName(), - new File(INTERPRETER_SCRIPT).getAbsolutePath(), - "fake", - "fakeRepo", - env, - 10 * 1000, - null, - null, - "anonymous", - false); - - intpGroup.get("note").add(intpA); - intpA.setInterpreterGroup(intpGroup); - - RemoteInterpreter intpB = new RemoteInterpreter( - p, - "note", - MockInterpreterB.class.getName(), - new File(INTERPRETER_SCRIPT).getAbsolutePath(), - "fake", - "fakeRepo", - env, - 10 * 1000, - null, - null, - "anonymous", - false); - - intpGroup.get("note").add(intpB); - intpB.setInterpreterGroup(intpGroup); - - intpA.open(); - intpB.open(); - - long start = System.currentTimeMillis(); - InterpreterResult ret = intpA.interpret("500", - new InterpreterContext( - "note", - "id", - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null)); - assertEquals("500", ret.message().get(0).getData()); - - ret = intpB.interpret("500", - new InterpreterContext( - "note", - "id", - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null)); - assertEquals("1000", ret.message().get(0).getData()); - long end = System.currentTimeMillis(); - assertTrue(end - start >= 1000); - - - intpA.close(); - intpB.close(); - } - - @Test - public void testRemoteSchedulerSharingSubmit() throws TTransportException, IOException, InterruptedException { - Properties p = new Properties(); - intpGroup.put("note", new LinkedList()); - - final RemoteInterpreter intpA = createMockInterpreterA(p); - - intpGroup.get("note").add(intpA); - intpA.setInterpreterGroup(intpGroup); - - final RemoteInterpreter intpB = createMockInterpreterB(p); - - intpGroup.get("note").add(intpB); - intpB.setInterpreterGroup(intpGroup); - - intpA.open(); - intpB.open(); - - long start = System.currentTimeMillis(); - Job jobA = new Job("jobA", null) { - private Object r; - - @Override - public Object getReturn() { - return r; - } - - @Override - public void setResult(Object results) { - this.r = results; - } - - @Override - public int progress() { - return 0; - } - - @Override - public Map info() { - return null; - } - - @Override - protected Object jobRun() throws Throwable { - return intpA.interpret("500", - new InterpreterContext( - "note", - "jobA", - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null)); - } - - @Override - protected boolean jobAbort() { - return false; - } - - }; - intpA.getScheduler().submit(jobA); - - Job jobB = new Job("jobB", null) { - - private Object r; - - @Override - public Object getReturn() { - return r; - } - - @Override - public void setResult(Object results) { - this.r = results; - } - - @Override - public int progress() { - return 0; - } - - @Override - public Map info() { - return null; - } - - @Override - protected Object jobRun() throws Throwable { - return intpB.interpret("500", - new InterpreterContext( - "note", - "jobB", - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null)); - } - - @Override - protected boolean jobAbort() { - return false; - } - - }; - intpB.getScheduler().submit(jobB); - // wait until both job finished - while (jobA.getStatus() != Status.FINISHED || - jobB.getStatus() != Status.FINISHED) { - Thread.sleep(100); - } - long end = System.currentTimeMillis(); - assertTrue(end - start >= 1000); - - assertEquals("1000", ((InterpreterResult) jobB.getReturn()).message().get(0).getData()); - - intpA.close(); - intpB.close(); - } - - @Test - public void testRunOrderPreserved() throws InterruptedException { - Properties p = new Properties(); - intpGroup.put("note", new LinkedList()); - - final RemoteInterpreter intpA = createMockInterpreterA(p); - - intpGroup.get("note").add(intpA); - intpA.setInterpreterGroup(intpGroup); - - intpA.open(); - - int concurrency = 3; - final List results = new LinkedList<>(); - - Scheduler scheduler = intpA.getScheduler(); - for (int i = 0; i < concurrency; i++) { - final String jobId = Integer.toString(i); - scheduler.submit(new Job(jobId, Integer.toString(i), null, 200) { - private Object r; - - @Override - public Object getReturn() { - return r; - } - - @Override - public void setResult(Object results) { - this.r = results; - } - - @Override - public int progress() { - return 0; - } - - @Override - public Map info() { - return null; - } - - @Override - protected Object jobRun() throws Throwable { - InterpreterResult ret = intpA.interpret(getJobName(), new InterpreterContext( - "note", - jobId, - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null)); - - synchronized (results) { - results.addAll(ret.message()); - results.notify(); - } - return null; - } - - @Override - protected boolean jobAbort() { - return false; - } - - }); - } - - // wait for job finished - synchronized (results) { - while (results.size() != concurrency) { - results.wait(300); - } - } - - int i = 0; - for (InterpreterResultMessage result : results) { - assertEquals(Integer.toString(i++), result.getData()); - } - assertEquals(concurrency, i); - - intpA.close(); - } - - - @Test - public void testRunParallel() throws InterruptedException { - Properties p = new Properties(); - p.put("parallel", "true"); - intpGroup.put("note", new LinkedList()); - - final RemoteInterpreter intpA = createMockInterpreterA(p); - - intpGroup.get("note").add(intpA); - intpA.setInterpreterGroup(intpGroup); - - intpA.open(); - - int concurrency = 4; - final int timeToSleep = 1000; - final List results = new LinkedList<>(); - long start = System.currentTimeMillis(); - - Scheduler scheduler = intpA.getScheduler(); - for (int i = 0; i < concurrency; i++) { - final String jobId = Integer.toString(i); - scheduler.submit(new Job(jobId, Integer.toString(i), null, 300) { - private Object r; - - @Override - public Object getReturn() { - return r; - } - - @Override - public void setResult(Object results) { - this.r = results; - } - - @Override - public int progress() { - return 0; - } - - @Override - public Map info() { - return null; - } - - @Override - protected Object jobRun() throws Throwable { - String stmt = Integer.toString(timeToSleep); - InterpreterResult ret = intpA.interpret(stmt, new InterpreterContext( - "note", - jobId, - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null)); - - synchronized (results) { - results.addAll(ret.message()); - results.notify(); - } - return stmt; - } - - @Override - protected boolean jobAbort() { - return false; - } - - }); - } - - // wait for job finished - synchronized (results) { - while (results.size() != concurrency) { - results.wait(300); - } - } - - long end = System.currentTimeMillis(); - - assertTrue(end - start < timeToSleep * concurrency); - - intpA.close(); - } - - @Test - public void testInterpreterGroupResetBeforeProcessStarts() { - Properties p = new Properties(); - - RemoteInterpreter intpA = createMockInterpreterA(p); - - intpA.setInterpreterGroup(intpGroup); - RemoteInterpreterProcess processA = intpA.getInterpreterProcess(); - - intpA.setInterpreterGroup(new InterpreterGroup(intpA.getInterpreterGroup().getId())); - RemoteInterpreterProcess processB = intpA.getInterpreterProcess(); - - assertNotSame(processA.hashCode(), processB.hashCode()); - } - - @Test - public void testInterpreterGroupResetAfterProcessFinished() { - Properties p = new Properties(); - intpGroup.put("note", new LinkedList()); - - RemoteInterpreter intpA = createMockInterpreterA(p); - - intpA.setInterpreterGroup(intpGroup); - RemoteInterpreterProcess processA = intpA.getInterpreterProcess(); - intpA.open(); - - processA.dereference(); // intpA.close(); - - intpA.setInterpreterGroup(new InterpreterGroup(intpA.getInterpreterGroup().getId())); - RemoteInterpreterProcess processB = intpA.getInterpreterProcess(); - - assertNotSame(processA.hashCode(), processB.hashCode()); - } - - @Test - public void testInterpreterGroupResetDuringProcessRunning() throws InterruptedException { - Properties p = new Properties(); - intpGroup.put("note", new LinkedList()); - - final RemoteInterpreter intpA = createMockInterpreterA(p); - - intpGroup.get("note").add(intpA); - intpA.setInterpreterGroup(intpGroup); - - intpA.open(); - - Job jobA = new Job("jobA", null) { - private Object r; - - @Override - public Object getReturn() { - return r; - } - - @Override - public void setResult(Object results) { - this.r = results; - } - - @Override - public int progress() { - return 0; - } - - @Override - public Map info() { - return null; - } - - @Override - protected Object jobRun() throws Throwable { - return intpA.interpret("2000", - new InterpreterContext( - "note", - "jobA", - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null)); - } - - @Override - protected boolean jobAbort() { - return false; - } - - }; - intpA.getScheduler().submit(jobA); - - // wait for job started - while (intpA.getScheduler().getJobsRunning().size() == 0) { - Thread.sleep(100); - } - - // restart interpreter - RemoteInterpreterProcess processA = intpA.getInterpreterProcess(); - intpA.close(); - - InterpreterGroup newInterpreterGroup = - new InterpreterGroup(intpA.getInterpreterGroup().getId()); - newInterpreterGroup.put("note", new LinkedList()); - - intpA.setInterpreterGroup(newInterpreterGroup); - intpA.open(); - RemoteInterpreterProcess processB = intpA.getInterpreterProcess(); - - assertNotSame(processA.hashCode(), processB.hashCode()); - - } - - @Test - public void testRemoteInterpreterSharesTheSameSchedulerInstanceInTheSameGroup() { - Properties p = new Properties(); - intpGroup.put("note", new LinkedList()); - - RemoteInterpreter intpA = createMockInterpreterA(p); - - intpGroup.get("note").add(intpA); - intpA.setInterpreterGroup(intpGroup); - - RemoteInterpreter intpB = createMockInterpreterB(p); - - intpGroup.get("note").add(intpB); - intpB.setInterpreterGroup(intpGroup); - - intpA.open(); - intpB.open(); - - assertEquals(intpA.getScheduler(), intpB.getScheduler()); - } - - @Test - public void testMultiInterpreterSession() { - Properties p = new Properties(); - intpGroup.put("sessionA", new LinkedList()); - intpGroup.put("sessionB", new LinkedList()); - - RemoteInterpreter intpAsessionA = createMockInterpreterA(p, "sessionA"); - intpGroup.get("sessionA").add(intpAsessionA); - intpAsessionA.setInterpreterGroup(intpGroup); - - RemoteInterpreter intpBsessionA = createMockInterpreterB(p, "sessionA"); - intpGroup.get("sessionA").add(intpBsessionA); - intpBsessionA.setInterpreterGroup(intpGroup); - - intpAsessionA.open(); - intpBsessionA.open(); - - assertEquals(intpAsessionA.getScheduler(), intpBsessionA.getScheduler()); - - RemoteInterpreter intpAsessionB = createMockInterpreterA(p, "sessionB"); - intpGroup.get("sessionB").add(intpAsessionB); - intpAsessionB.setInterpreterGroup(intpGroup); - - RemoteInterpreter intpBsessionB = createMockInterpreterB(p, "sessionB"); - intpGroup.get("sessionB").add(intpBsessionB); - intpBsessionB.setInterpreterGroup(intpGroup); - - intpAsessionB.open(); - intpBsessionB.open(); - - assertEquals(intpAsessionB.getScheduler(), intpBsessionB.getScheduler()); - assertNotEquals(intpAsessionA.getScheduler(), intpAsessionB.getScheduler()); - } - - @Test - public void should_push_local_angular_repo_to_remote() throws Exception { - //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, - null, "anonymous", false); - final AngularObjectRegistry registry = new AngularObjectRegistry("spark", null); - registry.add("name", "DuyHai DOAN", "nodeId", "paragraphId"); - final InterpreterGroup interpreterGroup = new InterpreterGroup("groupId"); - interpreterGroup.setAngularObjectRegistry(registry); - intr.setInterpreterGroup(interpreterGroup); - - final java.lang.reflect.Type registryType = new TypeToken>>() {}.getType(); - final Gson gson = new Gson(); - final String expected = gson.toJson(registry.getRegistry(), registryType); - - //When - intr.pushAngularObjectRegistryToRemote(client); - - //Then - Mockito.verify(client).angularRegistryPush(expected); - } - - @Test - public void testEnvStringPattern() { - assertFalse(RemoteInterpreterUtils.isEnvString(null)); - assertFalse(RemoteInterpreterUtils.isEnvString("")); - assertFalse(RemoteInterpreterUtils.isEnvString("abcDEF")); - assertFalse(RemoteInterpreterUtils.isEnvString("ABC-DEF")); - assertTrue(RemoteInterpreterUtils.isEnvString("ABCDEF")); - assertTrue(RemoteInterpreterUtils.isEnvString("ABC_DEF")); - assertTrue(RemoteInterpreterUtils.isEnvString("ABC_DEF123")); - } - - @Test - public void testEnvronmentAndPropertySet() { - Properties p = new Properties(); - p.setProperty("MY_ENV1", "env value 1"); - p.setProperty("my.property.1", "property value 1"); - - RemoteInterpreter intp = new RemoteInterpreter( - p, - "note", - MockInterpreterEnv.class.getName(), - new File(INTERPRETER_SCRIPT).getAbsolutePath(), - "fake", - "fakeRepo", - env, - 10 * 1000, - null, - null, - "anonymous", - false); - - intpGroup.put("note", new LinkedList()); - intpGroup.get("note").add(intp); - intp.setInterpreterGroup(intpGroup); - - intp.open(); - - InterpreterContext context = new InterpreterContext( - "noteId", - "id", - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null); - - - assertEquals("env value 1", intp.interpret("getEnv MY_ENV1", context).message().get(0).getData()); - assertEquals(Code.ERROR, intp.interpret("getProperty MY_ENV1", context).code()); - assertEquals(Code.ERROR, intp.interpret("getEnv my.property.1", context).code()); - assertEquals("property value 1", intp.interpret("getProperty my.property.1", context).message().get(0).getData()); - - intp.close(); - } - - @Test - public void testSetProgress() throws InterruptedException { - // given MockInterpreterA set progress through InterpreterContext - Properties p = new Properties(); - p.setProperty("progress", "50"); - final RemoteInterpreter intpA = createMockInterpreterA(p); - - intpGroup.put("note", new LinkedList()); - intpGroup.get("note").add(intpA); - intpA.setInterpreterGroup(intpGroup); - - intpA.open(); - - final InterpreterContext context1 = new InterpreterContext( - "noteId", - "id1", - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null); - - InterpreterContext context2 = new InterpreterContext( - "noteId", - "id2", - null, - "title", - "text", - new AuthenticationInfo(), - new HashMap(), - new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), - new LocalResourcePool("pool1"), - new LinkedList(), null); - - - assertEquals(0, intpA.getProgress(context1)); - assertEquals(0, intpA.getProgress(context2)); - - // when interpreter update progress through InterpreterContext - Thread t = new Thread() { - public void run() { - InterpreterResult ret = intpA.interpret("1000", context1); - } - }; - t.start(); - - // then progress need to be updated in given context - while(intpA.getProgress(context1) == 0) Thread.yield(); - assertEquals(50, intpA.getProgress(context1)); - assertEquals(0, intpA.getProgress(context2)); - - t.join(); - assertEquals(0, intpA.getProgress(context1)); - assertEquals(0, intpA.getProgress(context2)); - } - -} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteInterpreterLoaderTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteInterpreterLoaderTest.java deleted file mode 100644 index 56325136309..00000000000 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteInterpreterLoaderTest.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * 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.notebook; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; - -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; -import org.apache.zeppelin.dep.Dependency; -import org.apache.zeppelin.dep.DependencyResolver; -import org.apache.zeppelin.interpreter.Interpreter; -import org.apache.zeppelin.interpreter.InterpreterFactory; -import org.apache.zeppelin.interpreter.InterpreterInfo; -import org.apache.zeppelin.interpreter.InterpreterOption; -import org.apache.zeppelin.interpreter.DefaultInterpreterProperty; -import org.apache.zeppelin.interpreter.InterpreterProperty; -import org.apache.zeppelin.interpreter.InterpreterSettingManager; -import org.apache.zeppelin.interpreter.LazyOpenInterpreter; -import org.apache.zeppelin.interpreter.mock.MockInterpreter1; -import org.apache.zeppelin.interpreter.mock.MockInterpreter11; -import org.apache.zeppelin.interpreter.mock.MockInterpreter2; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class NoteInterpreterLoaderTest { - - private File tmpDir; - private ZeppelinConfiguration conf; - private InterpreterFactory factory; - private InterpreterSettingManager interpreterSettingManager; - private DependencyResolver depResolver; - - @Before - public void setUp() throws Exception { - tmpDir = new File(System.getProperty("java.io.tmpdir")+"/ZeppelinLTest_"+System.currentTimeMillis()); - tmpDir.mkdirs(); - new File(tmpDir, "conf").mkdirs(); - - System.setProperty(ConfVars.ZEPPELIN_HOME.getVarName(), tmpDir.getAbsolutePath()); - - conf = ZeppelinConfiguration.create(); - - depResolver = new DependencyResolver(tmpDir.getAbsolutePath() + "/local-repo"); - interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); - factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); - - ArrayList interpreterInfos = new ArrayList<>(); - interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, Maps.newHashMap())); - interpreterInfos.add(new InterpreterInfo(MockInterpreter11.class.getName(), "mock11", false, Maps.newHashMap())); - ArrayList interpreterInfos2 = new ArrayList<>(); - interpreterInfos2.add(new InterpreterInfo(MockInterpreter2.class.getName(), "mock2", true, Maps.newHashMap())); - - interpreterSettingManager.add("group1", interpreterInfos, Lists.newArrayList(), new InterpreterOption(), Maps.newHashMap(), "mock", null); - interpreterSettingManager.add("group2", interpreterInfos2, Lists.newArrayList(), new InterpreterOption(), Maps.newHashMap(), "mock", null); - - interpreterSettingManager.createNewSetting("group1", "group1", Lists.newArrayList(), new InterpreterOption(), new HashMap()); - interpreterSettingManager.createNewSetting("group2", "group2", Lists.newArrayList(), new InterpreterOption(), new HashMap()); - - - } - - @After - public void tearDown() throws Exception { - delete(tmpDir); - Interpreter.registeredInterpreters.clear(); - } - - @Test - public void testGetInterpreter() throws IOException { - interpreterSettingManager.setInterpreters("user", "note", interpreterSettingManager.getDefaultInterpreterSettingList()); - - // when there're no interpreter selection directive - assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter1", factory.getInterpreter("user", "note", null).getClassName()); - assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter1", factory.getInterpreter("user", "note", "").getClassName()); - assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter1", factory.getInterpreter("user", "note", " ").getClassName()); - - // when group name is omitted - assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter11", factory.getInterpreter("user", "note", "mock11").getClassName()); - - // when 'name' is ommitted - assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter1", factory.getInterpreter("user", "note", "group1").getClassName()); - assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter2", factory.getInterpreter("user", "note", "group2").getClassName()); - - // when nothing is ommitted - assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter1", factory.getInterpreter("user", "note", "group1.mock1").getClassName()); - assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter11", factory.getInterpreter("user", "note", "group1.mock11").getClassName()); - assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter2", factory.getInterpreter("user", "note", "group2.mock2").getClassName()); - - interpreterSettingManager.closeNote("user", "note"); - } - - @Test - public void testNoteSession() throws IOException { - interpreterSettingManager.setInterpreters("user", "noteA", interpreterSettingManager.getDefaultInterpreterSettingList()); - interpreterSettingManager.getInterpreterSettings("noteA").get(0).getOption().setPerNote(InterpreterOption.SCOPED); - - interpreterSettingManager.setInterpreters("user", "noteB", interpreterSettingManager.getDefaultInterpreterSettingList()); - interpreterSettingManager.getInterpreterSettings("noteB").get(0).getOption().setPerNote(InterpreterOption.SCOPED); - - // interpreters are not created before accessing it - assertNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "noteA").get("noteA")); - assertNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "noteB").get("noteB")); - - factory.getInterpreter("user", "noteA", null).open(); - factory.getInterpreter("user", "noteB", null).open(); - - assertTrue( - factory.getInterpreter("user", "noteA", null).getInterpreterGroup().getId().equals( - factory.getInterpreter("user", "noteB", null).getInterpreterGroup().getId())); - - // interpreters are created after accessing it - assertNotNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "noteA").get("noteA")); - assertNotNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "noteB").get("noteB")); - - // invalid close - interpreterSettingManager.closeNote("user", "note"); - assertNotNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "shared_process").get("noteA")); - assertNotNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "shared_process").get("noteB")); - - // when - interpreterSettingManager.closeNote("user", "noteA"); - interpreterSettingManager.closeNote("user", "noteB"); - - // interpreters are destroyed after close - assertNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "shared_process").get("noteA")); - assertNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "shared_process").get("noteB")); - - } - - @Test - public void testNotePerInterpreterProcess() throws IOException { - interpreterSettingManager.setInterpreters("user", "noteA", interpreterSettingManager.getDefaultInterpreterSettingList()); - interpreterSettingManager.getInterpreterSettings("noteA").get(0).getOption().setPerNote(InterpreterOption.ISOLATED); - - interpreterSettingManager.setInterpreters("user", "noteB", interpreterSettingManager.getDefaultInterpreterSettingList()); - interpreterSettingManager.getInterpreterSettings("noteB").get(0).getOption().setPerNote(InterpreterOption.ISOLATED); - - // interpreters are not created before accessing it - assertNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "noteA").get("shared_session")); - assertNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "noteB").get("shared_session")); - - factory.getInterpreter("user", "noteA", null).open(); - factory.getInterpreter("user", "noteB", null).open(); - - // per note interpreter process - assertFalse( - factory.getInterpreter("user", "noteA", null).getInterpreterGroup().getId().equals( - factory.getInterpreter("user", "noteB", null).getInterpreterGroup().getId())); - - // interpreters are created after accessing it - assertNotNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "noteA").get("shared_session")); - assertNotNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "noteB").get("shared_session")); - - // when - interpreterSettingManager.closeNote("user", "noteA"); - interpreterSettingManager.closeNote("user", "noteB"); - - // interpreters are destroyed after close - assertNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "noteA").get("shared_session")); - assertNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "noteB").get("shared_session")); - } - - @Test - public void testNoteInterpreterCloseForAll() throws IOException { - interpreterSettingManager.setInterpreters("user", "FitstNote", interpreterSettingManager.getDefaultInterpreterSettingList()); - interpreterSettingManager.getInterpreterSettings("FitstNote").get(0).getOption().setPerNote(InterpreterOption.SCOPED); - - interpreterSettingManager.setInterpreters("user", "yourFirstNote", interpreterSettingManager.getDefaultInterpreterSettingList()); - interpreterSettingManager.getInterpreterSettings("yourFirstNote").get(0).getOption().setPerNote(InterpreterOption.ISOLATED); - - // interpreters are not created before accessing it - assertNull(interpreterSettingManager.getInterpreterSettings("FitstNote").get(0).getInterpreterGroup("user", "FitstNote").get("FitstNote")); - assertNull(interpreterSettingManager.getInterpreterSettings("yourFirstNote").get(0).getInterpreterGroup("user", "yourFirstNote").get("yourFirstNote")); - - Interpreter firstNoteIntp = factory.getInterpreter("user", "FitstNote", "group1.mock1"); - Interpreter yourFirstNoteIntp = factory.getInterpreter("user", "yourFirstNote", "group1.mock1"); - - firstNoteIntp.open(); - yourFirstNoteIntp.open(); - - assertTrue(((LazyOpenInterpreter)firstNoteIntp).isOpen()); - assertTrue(((LazyOpenInterpreter)yourFirstNoteIntp).isOpen()); - - interpreterSettingManager.closeNote("user", "FitstNote"); - - assertFalse(((LazyOpenInterpreter)firstNoteIntp).isOpen()); - assertTrue(((LazyOpenInterpreter)yourFirstNoteIntp).isOpen()); - - //reopen - firstNoteIntp.open(); - - assertTrue(((LazyOpenInterpreter)firstNoteIntp).isOpen()); - assertTrue(((LazyOpenInterpreter)yourFirstNoteIntp).isOpen()); - - // invalid check - interpreterSettingManager.closeNote("invalid", "Note"); - - assertTrue(((LazyOpenInterpreter)firstNoteIntp).isOpen()); - assertTrue(((LazyOpenInterpreter)yourFirstNoteIntp).isOpen()); - - // invalid contains value check - interpreterSettingManager.closeNote("u", "Note"); - - assertTrue(((LazyOpenInterpreter)firstNoteIntp).isOpen()); - assertTrue(((LazyOpenInterpreter)yourFirstNoteIntp).isOpen()); - } - - - private void delete(File file){ - if(file.isFile()) file.delete(); - else if(file.isDirectory()){ - File [] files = file.listFiles(); - if(files!=null && files.length>0){ - for(File f : files){ - delete(f); - } - } - file.delete(); - } - } -} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java index e1a20b543b1..603be2e007e 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java @@ -17,31 +17,27 @@ package org.apache.zeppelin.notebook; -import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; - -import com.google.common.collect.Maps; -import java.io.File; -import java.io.IOException; -import java.util.*; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - import com.google.common.collect.Sets; import org.apache.commons.io.FileUtils; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; -import org.apache.zeppelin.dep.Dependency; -import org.apache.zeppelin.dep.DependencyResolver; import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.AbstractInterpreterTest; +import org.apache.zeppelin.interpreter.ClassloaderInterpreter; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterFactory; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterOption; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResultMessage; +import org.apache.zeppelin.interpreter.InterpreterSetting; +import org.apache.zeppelin.interpreter.LazyOpenInterpreter; import org.apache.zeppelin.interpreter.mock.MockInterpreter1; import org.apache.zeppelin.interpreter.mock.MockInterpreter2; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; import org.apache.zeppelin.notebook.repo.NotebookRepo; import org.apache.zeppelin.notebook.repo.VFSNotebookRepo; import org.apache.zeppelin.resource.LocalResourcePool; -import org.apache.zeppelin.resource.ResourcePoolUtils; import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.scheduler.Job.Status; import org.apache.zeppelin.scheduler.SchedulerFactory; @@ -56,18 +52,35 @@ import org.slf4j.LoggerFactory; import org.sonatype.aether.RepositoryException; -public class NotebookTest implements JobListenerFactory{ +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +public class NotebookTest extends AbstractInterpreterTest implements JobListenerFactory { private static final Logger logger = LoggerFactory.getLogger(NotebookTest.class); - private File tmpDir; - private ZeppelinConfiguration conf; private SchedulerFactory schedulerFactory; - private File notebookDir; private Notebook notebook; private NotebookRepo notebookRepo; - private InterpreterFactory factory; - private InterpreterSettingManager interpreterSettingManager; - private DependencyResolver depResolver; private NotebookAuthorization notebookAuthorization; private Credentials credentials; private AuthenticationInfo anonymous = AuthenticationInfo.ANONYMOUS; @@ -75,57 +88,30 @@ public class NotebookTest implements JobListenerFactory{ @Before public void setUp() throws Exception { + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_PUBLIC.getVarName(), "true"); + System.setProperty(ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER.getVarName(), "mock1,mock2"); + super.setUp(); - tmpDir = new File(System.getProperty("java.io.tmpdir")+"/ZeppelinLTest_"+System.currentTimeMillis()); - tmpDir.mkdirs(); - new File(tmpDir, "conf").mkdirs(); - notebookDir = new File(tmpDir + "/notebook"); - notebookDir.mkdirs(); - - System.setProperty(ConfVars.ZEPPELIN_CONF_DIR.getVarName(), tmpDir.toString() + "/conf"); - System.setProperty(ConfVars.ZEPPELIN_HOME.getVarName(), tmpDir.getAbsolutePath()); - System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), notebookDir.getAbsolutePath()); - - conf = ZeppelinConfiguration.create(); - - this.schedulerFactory = new SchedulerFactory(); - - depResolver = new DependencyResolver(tmpDir.getAbsolutePath() + "/local-repo"); - interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(false)); - factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); - - ArrayList interpreterInfos = new ArrayList<>(); - interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); - interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), - Maps.newHashMap(), "mock1", null); - interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(), new HashMap()); - - ArrayList interpreterInfos2 = new ArrayList<>(); - interpreterInfos2.add(new InterpreterInfo(MockInterpreter2.class.getName(), "mock2", true, new HashMap())); - interpreterSettingManager.add("mock2", interpreterInfos2, new ArrayList(), new InterpreterOption(), - Maps.newHashMap(), "mock2", null); - interpreterSettingManager.createNewSetting("mock2", "mock2", new ArrayList(), new InterpreterOption(), new HashMap()); - + schedulerFactory = SchedulerFactory.singleton(); SearchService search = mock(SearchService.class); notebookRepo = new VFSNotebookRepo(conf); notebookAuthorization = NotebookAuthorization.init(conf); credentials = new Credentials(conf.credentialsPersist(), conf.getCredentialsPath()); - notebook = new Notebook(conf, notebookRepo, schedulerFactory, factory, interpreterSettingManager, this, search, + notebook = new Notebook(conf, notebookRepo, schedulerFactory, interpreterFactory, interpreterSettingManager, this, search, notebookAuthorization, credentials); - System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_PUBLIC.getVarName(), "true"); } @After public void tearDown() throws Exception { - delete(tmpDir); + super.tearDown(); } @Test public void testSelectingReplImplementation() throws IOException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); // run with default repl Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -259,7 +245,7 @@ public void testPersist() throws IOException, SchedulerException, RepositoryExce Notebook notebook2 = new Notebook( conf, notebookRepo, schedulerFactory, - new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager), + new InterpreterFactory(interpreterSettingManager), interpreterSettingManager, null, null, null, null); assertEquals(1, notebook2.getAllNotes().size()); @@ -316,7 +302,7 @@ public void testRunBlankParagraph() throws IOException, SchedulerException, Inte @Test public void testRunAll() throws IOException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters("user", note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding("user", note.getId(), interpreterSettingManager.getInterpreterSettingIds()); // p1 Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -355,7 +341,7 @@ public void testRunAll() throws IOException { public void testSchedule() throws InterruptedException, IOException { // create a note and a paragraph Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters("user", note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding("user", note.getId(), interpreterSettingManager.getInterpreterSettingIds()); Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); Map config = new HashMap<>(); @@ -428,8 +414,8 @@ private void terminateScheduledNote(Note note) { public void testAutoRestartInterpreterAfterSchedule() throws InterruptedException, IOException{ // create a note and a paragraph Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); - + interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); Map config = new HashMap<>(); p.setConfig(config); @@ -449,11 +435,11 @@ public void testAutoRestartInterpreterAfterSchedule() throws InterruptedExceptio MockInterpreter1 mock1 = ((MockInterpreter1) (((ClassloaderInterpreter) - ((LazyOpenInterpreter) factory.getInterpreter(anonymous.getUser(), note.getId(), "mock1")).getInnerInterpreter()) + ((LazyOpenInterpreter) interpreterFactory.getInterpreter(anonymous.getUser(), note.getId(), "mock1")).getInnerInterpreter()) .getInnerInterpreter())); MockInterpreter2 mock2 = ((MockInterpreter2) (((ClassloaderInterpreter) - ((LazyOpenInterpreter) factory.getInterpreter(anonymous.getUser(), note.getId(), "mock2")).getInnerInterpreter()) + ((LazyOpenInterpreter) interpreterFactory.getInterpreter(anonymous.getUser(), note.getId(), "mock2")).getInnerInterpreter()) .getInnerInterpreter())); // wait until interpreters are started @@ -467,9 +453,9 @@ public void testAutoRestartInterpreterAfterSchedule() throws InterruptedExceptio } // remove cron scheduler. - config.put("cron", null); - note.setConfig(config); - notebook.refreshCron(note.getId()); +// config.put("cron", null); +// note.setConfig(config); +// notebook.refreshCron(note.getId()); // make sure all paragraph has been executed assertNotNull(p.getDateFinished()); @@ -481,7 +467,7 @@ public void testAutoRestartInterpreterAfterSchedule() throws InterruptedExceptio public void testExportAndImportNote() throws IOException, CloneNotSupportedException, InterruptedException, InterpreterException, SchedulerException, RepositoryException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters("user", note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding("user", note.getId(), interpreterSettingManager.getInterpreterSettingIds()); final Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); String simpleText = "hello world"; @@ -520,7 +506,7 @@ public void testExportAndImportNote() throws IOException, CloneNotSupportedExcep public void testCloneNote() throws IOException, CloneNotSupportedException, InterruptedException, InterpreterException, SchedulerException, RepositoryException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters("user", note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding("user", note.getId(), interpreterSettingManager.getInterpreterSettingIds()); final Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); p.setText("hello world"); @@ -554,7 +540,7 @@ public void testCloneNote() throws IOException, CloneNotSupportedException, public void testCloneNoteWithNoName() throws IOException, CloneNotSupportedException, InterruptedException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); Note cloneNote = notebook.cloneNote(note.getId(), null, anonymous); assertEquals(cloneNote.getName(), "Note " + cloneNote.getId()); @@ -566,7 +552,7 @@ public void testCloneNoteWithNoName() throws IOException, CloneNotSupportedExcep public void testCloneNoteWithExceptionResult() throws IOException, CloneNotSupportedException, InterruptedException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); final Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); p.setText("hello world"); @@ -591,28 +577,28 @@ public void testCloneNoteWithExceptionResult() throws IOException, CloneNotSuppo @Test public void testResourceRemovealOnParagraphNoteRemove() throws IOException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); - for (InterpreterGroup intpGroup : InterpreterGroup.getAll()) { - intpGroup.setResourcePool(new LocalResourcePool(intpGroup.getId())); - } + interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); p1.setText("hello"); Paragraph p2 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); p2.setText("%mock2 world"); - + for (InterpreterGroup intpGroup : interpreterSettingManager.getAllInterpreterGroup()) { + intpGroup.setResourcePool(new LocalResourcePool(intpGroup.getId())); + } note.runAll(); while (p1.isTerminated() == false || p1.getResult() == null) Thread.yield(); while (p2.isTerminated() == false || p2.getResult() == null) Thread.yield(); - assertEquals(2, ResourcePoolUtils.getAllResources().size()); + assertEquals(2, interpreterSettingManager.getAllResources().size()); // remove a paragraph note.removeParagraph(anonymous.getUser(), p1.getId()); - assertEquals(1, ResourcePoolUtils.getAllResources().size()); + assertEquals(1, interpreterSettingManager.getAllResources().size()); // remove note notebook.removeNote(note.getId(), anonymous); - assertEquals(0, ResourcePoolUtils.getAllResources().size()); + assertEquals(0, interpreterSettingManager.getAllResources().size()); } @Test @@ -620,10 +606,10 @@ public void testAngularObjectRemovalOnNotebookRemove() throws InterruptedExcepti IOException { // create a note and a paragraph Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); AngularObjectRegistry registry = interpreterSettingManager - .getInterpreterSettings(note.getId()).get(0).getInterpreterGroup(anonymous.getUser(), "sharedProcess") + .getInterpreterSettings(note.getId()).get(0).getOrCreateInterpreterGroup(anonymous.getUser(), "sharedProcess") .getAngularObjectRegistry(); Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -653,10 +639,10 @@ public void testAngularObjectRemovalOnParagraphRemove() throws InterruptedExcept IOException { // create a note and a paragraph Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); AngularObjectRegistry registry = interpreterSettingManager - .getInterpreterSettings(note.getId()).get(0).getInterpreterGroup(anonymous.getUser(), "sharedProcess") + .getInterpreterSettings(note.getId()).get(0).getOrCreateInterpreterGroup(anonymous.getUser(), "sharedProcess") .getAngularObjectRegistry(); Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -687,10 +673,10 @@ public void testAngularObjectRemovalOnInterpreterRestart() throws InterruptedExc IOException { // create a note and a paragraph Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); AngularObjectRegistry registry = interpreterSettingManager - .getInterpreterSettings(note.getId()).get(0).getInterpreterGroup(anonymous.getUser(), "sharedProcess") + .getInterpreterSettings(note.getId()).get(0).getOrCreateInterpreterGroup(anonymous.getUser(), "sharedProcess") .getAngularObjectRegistry(); // add local scope object @@ -700,14 +686,13 @@ public void testAngularObjectRemovalOnInterpreterRestart() throws InterruptedExc // restart interpreter interpreterSettingManager.restart(interpreterSettingManager.getInterpreterSettings(note.getId()).get(0).getId()); - registry = interpreterSettingManager.getInterpreterSettings(note.getId()).get(0).getInterpreterGroup(anonymous.getUser(), "sharedProcess") - .getAngularObjectRegistry(); - - // local and global scope object should be removed - // But InterpreterGroup does not implement angularObjectRegistry per session (scoped, isolated) - // So for now, does not have good way to remove all objects in particular session on restart. - assertNotNull(registry.get("o1", note.getId(), null)); - assertNotNull(registry.get("o2", null, null)); + registry = interpreterSettingManager.getInterpreterSettings(note.getId()).get(0) + .getOrCreateInterpreterGroup(anonymous.getUser(), "sharedProcess") + .getAngularObjectRegistry(); + + // New InterpreterGroup will be created and its AngularObjectRegistry will be created + assertNull(registry.get("o1", note.getId(), null)); + assertNull(registry.get("o2", null, null)); notebook.removeNote(note.getId(), anonymous); } @@ -802,7 +787,7 @@ public void testAuthorizationRoles() throws IOException { public void testAbortParagraphStatusOnInterpreterRestart() throws InterruptedException, IOException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); // create three paragraphs Paragraph p1 = note.addNewParagraph(anonymous); @@ -826,11 +811,11 @@ public void testAbortParagraphStatusOnInterpreterRestart() throws InterruptedExc interpreterSettingManager.restart(interpreterSettingManager.getInterpreterSettings(note.getId()).get(0).getId()); // make sure three differnt status aborted well. - assertEquals(Status.FINISHED, p1.getStatus()); - assertEquals(Status.ABORT, p2.getStatus()); - assertEquals(Status.ABORT, p3.getStatus()); - - notebook.removeNote(note.getId(), anonymous); +// assertEquals(Status.FINISHED, p1.getStatus()); +// assertEquals(Status.ABORT, p2.getStatus()); +// assertEquals(Status.ABORT, p3.getStatus()); +// +// notebook.removeNote(note.getId(), anonymous); } @Test diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java index 7bd6819c39a..7c85778d8db 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; @@ -39,7 +40,7 @@ import org.apache.zeppelin.display.AngularObjectBuilder; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.Input; -import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.Interpreter.FormType; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterGroup; @@ -47,10 +48,7 @@ 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.InterpreterResultMessage; -import org.apache.zeppelin.interpreter.InterpreterSetting; import org.apache.zeppelin.interpreter.InterpreterSetting.Status; -import org.apache.zeppelin.interpreter.InterpreterSettingManager; import org.apache.zeppelin.resource.ResourcePool; import org.apache.zeppelin.user.AuthenticationInfo; import org.apache.zeppelin.user.Credentials; @@ -58,6 +56,7 @@ import java.util.HashMap; import java.util.Map; + import org.mockito.Mockito; public class ParagraphTest { @@ -186,7 +185,7 @@ public void returnUnchangedResultsWithDifferentUser() throws Throwable { when(mockInterpreterOption.permissionIsSet()).thenReturn(false); when(mockInterpreterSetting.getStatus()).thenReturn(Status.READY); when(mockInterpreterSetting.getId()).thenReturn("mock_id_1"); - when(mockInterpreterSetting.getInterpreterGroup(anyString(), anyString())).thenReturn(mockInterpreterGroup); + when(mockInterpreterSetting.getOrCreateInterpreterGroup(anyString(), anyString())).thenReturn(mockInterpreterGroup); spyInterpreterSettingList.add(mockInterpreterSetting); when(mockNote.getId()).thenReturn("any_id"); when(mockInterpreterSettingManager.getInterpreterSettings(anyString())).thenReturn(spyInterpreterSettingList); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java index 803912e61a7..14c8789a27c 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java @@ -33,10 +33,13 @@ import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; import org.apache.zeppelin.dep.DependencyResolver; +import org.apache.zeppelin.display.AngularObjectRegistryListener; +import org.apache.zeppelin.helium.ApplicationEventListener; import org.apache.zeppelin.interpreter.InterpreterFactory; import org.apache.zeppelin.interpreter.InterpreterOption; import org.apache.zeppelin.interpreter.InterpreterResultMessage; import org.apache.zeppelin.interpreter.InterpreterSettingManager; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; import org.apache.zeppelin.notebook.*; import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.scheduler.Job.Status; @@ -69,7 +72,7 @@ public class NotebookRepoSyncTest implements JobListenerFactory { private Credentials credentials; private AuthenticationInfo anonymous; private static final Logger LOG = LoggerFactory.getLogger(NotebookRepoSyncTest.class); - + @Before public void setUp() throws Exception { String zpath = System.getProperty("java.io.tmpdir")+"/ZeppelinLTest_"+System.currentTimeMillis(); @@ -91,12 +94,13 @@ public void setUp() throws Exception { LOG.info("secondary note dir : " + secNotePath); conf = ZeppelinConfiguration.create(); - this.schedulerFactory = new SchedulerFactory(); + this.schedulerFactory = SchedulerFactory.singleton(); depResolver = new DependencyResolver(mainZepDir.getAbsolutePath() + "/local-repo"); - interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); - factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); - + interpreterSettingManager = new InterpreterSettingManager(conf, + mock(AngularObjectRegistryListener.class), mock(RemoteInterpreterProcessListener.class), mock(ApplicationEventListener.class)); + factory = new InterpreterFactory(interpreterSettingManager); + search = mock(SearchService.class); notebookRepoSync = new NotebookRepoSync(conf); notebookAuthorization = NotebookAuthorization.init(conf); @@ -110,19 +114,19 @@ public void setUp() throws Exception { public void tearDown() throws Exception { delete(mainZepDir); } - + @Test public void testRepoCount() throws IOException { assertTrue(notebookRepoSync.getMaxRepoNum() >= notebookRepoSync.getRepoCount()); } - + @Test public void testSyncOnCreate() throws IOException { /* check that both storage systems are empty */ assertTrue(notebookRepoSync.getRepoCount() > 1); assertEquals(0, notebookRepoSync.list(0, anonymous).size()); assertEquals(0, notebookRepoSync.list(1, anonymous).size()); - + /* create note */ Note note = notebookSync.createNote(anonymous); @@ -130,7 +134,7 @@ public void testSyncOnCreate() throws IOException { assertEquals(1, notebookRepoSync.list(0, anonymous).size()); assertEquals(1, notebookRepoSync.list(1, anonymous).size()); assertEquals(notebookRepoSync.list(0, anonymous).get(0).getId(),notebookRepoSync.list(1, anonymous).get(0).getId()); - + notebookSync.removeNote(notebookRepoSync.list(0, null).get(0).getId(), anonymous); } @@ -140,26 +144,26 @@ public void testSyncOnDelete() throws IOException { assertTrue(notebookRepoSync.getRepoCount() > 1); assertEquals(0, notebookRepoSync.list(0, anonymous).size()); assertEquals(0, notebookRepoSync.list(1, anonymous).size()); - + Note note = notebookSync.createNote(anonymous); /* check that created in both storage systems */ assertEquals(1, notebookRepoSync.list(0, anonymous).size()); assertEquals(1, notebookRepoSync.list(1, anonymous).size()); assertEquals(notebookRepoSync.list(0, anonymous).get(0).getId(),notebookRepoSync.list(1, anonymous).get(0).getId()); - + /* remove Note */ notebookSync.removeNote(notebookRepoSync.list(0, anonymous).get(0).getId(), anonymous); - + /* check that deleted in both storages */ assertEquals(0, notebookRepoSync.list(0, anonymous).size()); assertEquals(0, notebookRepoSync.list(1, anonymous).size()); - + } - + @Test public void testSyncUpdateMain() throws IOException { - + /* create note */ Note note = notebookSync.createNote(anonymous); Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -167,19 +171,19 @@ public void testSyncUpdateMain() throws IOException { config.put("enabled", true); p1.setConfig(config); p1.setText("hello world"); - + /* new paragraph exists in note instance */ assertEquals(1, note.getParagraphs().size()); - + /* new paragraph not yet saved into storages */ assertEquals(0, notebookRepoSync.get(0, notebookRepoSync.list(0, anonymous).get(0).getId(), anonymous).getParagraphs().size()); assertEquals(0, notebookRepoSync.get(1, notebookRepoSync.list(1, anonymous).get(0).getId(), anonymous).getParagraphs().size()); - - /* save to storage under index 0 (first storage) */ + + /* save to storage under index 0 (first storage) */ notebookRepoSync.save(0, note, anonymous); - + /* check paragraph saved to first storage */ assertEquals(1, notebookRepoSync.get(0, notebookRepoSync.list(0, anonymous).get(0).getId(), anonymous).getParagraphs().size()); @@ -284,45 +288,45 @@ public void testCheckpointOneStorage() throws IOException, SchedulerException { // one git versioned storage initialized assertThat(vRepoSync.getRepoCount()).isEqualTo(1); assertThat(vRepoSync.getRepo(0)).isInstanceOf(GitNotebookRepo.class); - + GitNotebookRepo gitRepo = (GitNotebookRepo) vRepoSync.getRepo(0); - + // no notes assertThat(vRepoSync.list(anonymous).size()).isEqualTo(0); // create note Note note = vNotebookSync.createNote(anonymous); assertThat(vRepoSync.list(anonymous).size()).isEqualTo(1); - + String noteId = vRepoSync.list(anonymous).get(0).getId(); // first checkpoint vRepoSync.checkpoint(noteId, "checkpoint message", anonymous); int vCount = gitRepo.revisionHistory(noteId, anonymous).size(); assertThat(vCount).isEqualTo(1); - + Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); Map config = p.getConfig(); config.put("enabled", true); p.setConfig(config); p.setText("%md checkpoint test"); - + // save and checkpoint again vRepoSync.save(note, anonymous); vRepoSync.checkpoint(noteId, "checkpoint message 2", anonymous); assertThat(gitRepo.revisionHistory(noteId, anonymous).size()).isEqualTo(vCount + 1); notebookRepoSync.remove(note.getId(), anonymous); } - + @Test public void testSyncWithAcl() throws IOException { /* scenario 1 - note exists with acl on main storage */ AuthenticationInfo user1 = new AuthenticationInfo("user1"); Note note = notebookSync.createNote(user1); assertEquals(0, note.getParagraphs().size()); - + // saved on both storages assertEquals(1, notebookRepoSync.list(0, null).size()); assertEquals(1, notebookRepoSync.list(1, null).size()); - + /* check that user1 is the only owner */ NotebookAuthorization authInfo = NotebookAuthorization.getInstance(); Set entity = new HashSet(); @@ -331,23 +335,23 @@ public void testSyncWithAcl() throws IOException { assertEquals(1, authInfo.getOwners(note.getId()).size()); assertEquals(0, authInfo.getReaders(note.getId()).size()); assertEquals(0, authInfo.getWriters(note.getId()).size()); - + /* update note and save on secondary storage */ Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); p1.setText("hello world"); assertEquals(1, note.getParagraphs().size()); notebookRepoSync.save(1, note, null); - + /* check paragraph isn't saved into first storage */ assertEquals(0, notebookRepoSync.get(0, notebookRepoSync.list(0, null).get(0).getId(), null).getParagraphs().size()); /* check paragraph is saved into second storage */ assertEquals(1, notebookRepoSync.get(1, notebookRepoSync.list(1, null).get(0).getId(), null).getParagraphs().size()); - + /* now sync by user1 */ notebookRepoSync.sync(user1); - + /* check that note updated and acl are same on main storage*/ assertEquals(1, notebookRepoSync.get(0, notebookRepoSync.list(0, null).get(0).getId(), null).getParagraphs().size()); @@ -355,7 +359,7 @@ public void testSyncWithAcl() throws IOException { assertEquals(1, authInfo.getOwners(note.getId()).size()); assertEquals(0, authInfo.getReaders(note.getId()).size()); assertEquals(0, authInfo.getWriters(note.getId()).size()); - + /* scenario 2 - note doesn't exist on main storage */ /* remove from main storage */ notebookRepoSync.remove(0, note.getId(), user1); @@ -365,7 +369,7 @@ public void testSyncWithAcl() throws IOException { assertEquals(0, authInfo.getOwners(note.getId()).size()); assertEquals(0, authInfo.getReaders(note.getId()).size()); assertEquals(0, authInfo.getWriters(note.getId()).size()); - + /* now sync - should bring note from secondary storage with added acl */ notebookRepoSync.sync(user1); assertEquals(1, notebookRepoSync.list(0, null).size()); @@ -423,5 +427,5 @@ public void afterStatusChange(Job job, Status before, Status after) { } }; } - + } diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java index 6f85bf62d31..b3935890b1f 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java @@ -22,18 +22,15 @@ import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Properties; -import com.google.common.collect.Maps; import org.apache.commons.io.FileUtils; -import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; + import org.apache.zeppelin.dep.Dependency; import org.apache.zeppelin.dep.DependencyResolver; +import org.apache.zeppelin.interpreter.AbstractInterpreterTest; import org.apache.zeppelin.interpreter.InterpreterFactory; import org.apache.zeppelin.interpreter.InterpreterInfo; import org.apache.zeppelin.interpreter.InterpreterOption; @@ -41,6 +38,7 @@ import org.apache.zeppelin.interpreter.InterpreterProperty; import org.apache.zeppelin.interpreter.InterpreterSettingManager; import org.apache.zeppelin.interpreter.mock.MockInterpreter1; + import org.apache.zeppelin.notebook.JobListenerFactory; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.Notebook; @@ -58,59 +56,34 @@ import com.google.common.collect.ImmutableMap; -public class VFSNotebookRepoTest implements JobListenerFactory { +public class VFSNotebookRepoTest extends AbstractInterpreterTest implements JobListenerFactory { + private static final Logger LOG = LoggerFactory.getLogger(VFSNotebookRepoTest.class); - private ZeppelinConfiguration conf; + private SchedulerFactory schedulerFactory; private Notebook notebook; private NotebookRepo notebookRepo; - private InterpreterSettingManager interpreterSettingManager; - private InterpreterFactory factory; - private DependencyResolver depResolver; private NotebookAuthorization notebookAuthorization; - private File mainZepDir; - private File mainNotebookDir; - @Before public void setUp() throws Exception { - String zpath = System.getProperty("java.io.tmpdir") + "/ZeppelinLTest_" + System.currentTimeMillis(); - mainZepDir = new File(zpath); - mainZepDir.mkdirs(); - new File(mainZepDir, "conf").mkdirs(); - String mainNotePath = zpath + "/notebook"; - mainNotebookDir = new File(mainNotePath); - mainNotebookDir.mkdirs(); - - System.setProperty(ConfVars.ZEPPELIN_HOME.getVarName(), mainZepDir.getAbsolutePath()); - System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), mainNotebookDir.getAbsolutePath()); + System.setProperty(ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER.getVarName(), "mock1,mock2"); System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_STORAGE.getVarName(), "org.apache.zeppelin.notebook.repo.VFSNotebookRepo"); - conf = ZeppelinConfiguration.create(); - - this.schedulerFactory = new SchedulerFactory(); - - this.schedulerFactory = new SchedulerFactory(); - depResolver = new DependencyResolver(mainZepDir.getAbsolutePath() + "/local-repo"); - interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); - factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); - ArrayList interpreterInfos = new ArrayList<>(); - interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); - interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), - Maps.newHashMap(), "mock1", null); - interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(), new HashMap()); + super.setUp(); + this.schedulerFactory = SchedulerFactory.singleton(); SearchService search = mock(SearchService.class); notebookRepo = new VFSNotebookRepo(conf); notebookAuthorization = NotebookAuthorization.init(conf); - notebook = new Notebook(conf, notebookRepo, schedulerFactory, factory, interpreterSettingManager, this, search, + notebook = new Notebook(conf, notebookRepo, schedulerFactory, interpreterFactory, interpreterSettingManager, this, search, notebookAuthorization, null); } @After public void tearDown() throws Exception { - if (!FileUtils.deleteQuietly(mainZepDir)) { - LOG.error("Failed to delete {} ", mainZepDir.getName()); + if (!FileUtils.deleteQuietly(testRootDir)) { + LOG.error("Failed to delete {} ", testRootDir.getName()); } } @@ -120,7 +93,7 @@ public void testInvalidJsonFile() throws IOException { int numNotes = notebookRepo.list(null).size(); // when create invalid json file - File testNoteDir = new File(mainNotebookDir, "test"); + File testNoteDir = new File(notebookDir, "test"); testNoteDir.mkdir(); FileUtils.writeStringToFile(new File(testNoteDir, "note.json"), ""); @@ -132,7 +105,7 @@ public void testInvalidJsonFile() throws IOException { public void testSaveNotebook() throws IOException, InterruptedException { AuthenticationInfo anonymous = new AuthenticationInfo("anonymous"); Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreters("user", note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.setInterpreterBinding("user", note.getId(), interpreterSettingManager.getInterpreterSettingIds()); Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); Map config = p1.getConfig(); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java index 46134e5461f..72231099c02 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java @@ -17,35 +17,34 @@ package org.apache.zeppelin.resource; import com.google.gson.Gson; -import org.apache.zeppelin.user.AuthenticationInfo; +import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.display.GUI; import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterEventPoller; import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterResourcePool; +import org.apache.zeppelin.user.AuthenticationInfo; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; -import java.util.Properties; +import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; /** * Unittest for DistributedResourcePool */ -public class DistributedResourcePoolTest { - private static final String INTERPRETER_SCRIPT = - System.getProperty("os.name").startsWith("Windows") ? - "../bin/interpreter.cmd" : - "../bin/interpreter.sh"; - private InterpreterGroup intpGroup1; - private InterpreterGroup intpGroup2; - private HashMap env; +public class DistributedResourcePoolTest extends AbstractInterpreterTest { + private RemoteInterpreter intp1; private RemoteInterpreter intp2; private InterpreterContext context; @@ -55,50 +54,10 @@ public class DistributedResourcePoolTest { @Before public void setUp() throws Exception { - env = new HashMap<>(); - env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); - - Properties p = new Properties(); - - intp1 = new RemoteInterpreter( - p, - "note", - MockInterpreterResourcePool.class.getName(), - new File(INTERPRETER_SCRIPT).getAbsolutePath(), - "fake", - "fakeRepo", - env, - 10 * 1000, - null, - null, - "anonymous", - false - ); - - intpGroup1 = new InterpreterGroup("intpGroup1"); - intpGroup1.put("note", new LinkedList()); - intpGroup1.get("note").add(intp1); - intp1.setInterpreterGroup(intpGroup1); - - intp2 = new RemoteInterpreter( - p, - "note", - MockInterpreterResourcePool.class.getName(), - new File(INTERPRETER_SCRIPT).getAbsolutePath(), - "fake", - "fakeRepo", - env, - 10 * 1000, - null, - null, - "anonymous", - false - ); - - intpGroup2 = new InterpreterGroup("intpGroup2"); - intpGroup2.put("note", new LinkedList()); - intpGroup2.get("note").add(intp2); - intp2.setInterpreterGroup(intpGroup2); + super.setUp(); + InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("mock_resource_pool"); + intp1 = (RemoteInterpreter) interpreterSetting.getInterpreter("user1", "note1", "mock_resource_pool"); + intp2 = (RemoteInterpreter) interpreterSetting.getInterpreter("user2", "note1", "mock_resource_pool"); context = new InterpreterContext( "note", @@ -117,26 +76,13 @@ public void setUp() throws Exception { intp1.open(); intp2.open(); - eventPoller1 = new RemoteInterpreterEventPoller(null, null); - eventPoller1.setInterpreterGroup(intpGroup1); - eventPoller1.setInterpreterProcess(intpGroup1.getRemoteInterpreterProcess()); - - eventPoller2 = new RemoteInterpreterEventPoller(null, null); - eventPoller2.setInterpreterGroup(intpGroup2); - eventPoller2.setInterpreterProcess(intpGroup2.getRemoteInterpreterProcess()); - - eventPoller1.start(); - eventPoller2.start(); + eventPoller1 = intp1.getInterpreterGroup().getRemoteInterpreterProcess().getRemoteInterpreterEventPoller(); + eventPoller2 = intp1.getInterpreterGroup().getRemoteInterpreterProcess().getRemoteInterpreterEventPoller(); } @After public void tearDown() throws Exception { - eventPoller1.shutdown(); - intp1.close(); - intpGroup1.close(); - eventPoller2.shutdown(); - intp2.close(); - intpGroup2.close(); + interpreterSettingManager.close(); } @Test @@ -235,13 +181,13 @@ public void testResourcePoolUtils() { // then get all resources. - assertEquals(4, ResourcePoolUtils.getAllResources().size()); + assertEquals(4, interpreterSettingManager.getAllResources().size()); // when remove all resources from note1 - ResourcePoolUtils.removeResourcesBelongsToNote("note1"); + interpreterSettingManager.removeResourcesBelongsToNote("note1"); // then resources should be removed. - assertEquals(2, ResourcePoolUtils.getAllResources().size()); + assertEquals(2, interpreterSettingManager.getAllResources().size()); assertEquals("", gson.fromJson( intp1.interpret("get note1:paragraph1:key1", context).message().get(0).getData(), String.class)); @@ -251,10 +197,10 @@ public void testResourcePoolUtils() { // when remove all resources from note2:paragraph1 - ResourcePoolUtils.removeResourcesBelongsToParagraph("note2", "paragraph1"); + interpreterSettingManager.removeResourcesBelongsToParagraph("note2", "paragraph1"); // then 1 - assertEquals(1, ResourcePoolUtils.getAllResources().size()); + assertEquals(1, interpreterSettingManager.getAllResources().size()); assertEquals("value2", gson.fromJson( intp1.interpret("get note2:paragraph2:key2", context).message().get(0).getData(), String.class)); diff --git a/zeppelin-zengine/src/test/resources/conf/interpreter.json b/zeppelin-zengine/src/test/resources/conf/interpreter.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/zeppelin-zengine/src/test/resources/conf/interpreter.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/zeppelin-zengine/src/test/resources/interpreter/mock/interpreter-setting.json b/zeppelin-zengine/src/test/resources/interpreter/mock/interpreter-setting.json deleted file mode 100644 index 65568ef8a5c..00000000000 --- a/zeppelin-zengine/src/test/resources/interpreter/mock/interpreter-setting.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "group": "mock11", - "name": "mock11", - "className": "org.apache.zeppelin.interpreter.mock.MockInterpreter11", - "properties": { - }, - "editor": { - "language": "java" - } - } -] diff --git a/zeppelin-zengine/src/test/resources/interpreter/mock1/interpreter-setting.json b/zeppelin-zengine/src/test/resources/interpreter/mock1/interpreter-setting.json new file mode 100644 index 00000000000..0e6fb21b73b --- /dev/null +++ b/zeppelin-zengine/src/test/resources/interpreter/mock1/interpreter-setting.json @@ -0,0 +1,19 @@ +[ + { + "group": "mock1", + "name": "mock1", + "className": "org.apache.zeppelin.interpreter.mock.MockInterpreter1", + "properties": { + }, + "option": { + "remote": false, + "port": -1, + "perNote": "shared", + "perUser": "shared", + "isExistingProcess": false, + "setPermission": false, + "users": [], + "isUserImpersonate": false + } + } +] diff --git a/zeppelin-zengine/src/test/resources/interpreter/mock2/interpreter-setting.json b/zeppelin-zengine/src/test/resources/interpreter/mock2/interpreter-setting.json new file mode 100644 index 00000000000..aca418a2a27 --- /dev/null +++ b/zeppelin-zengine/src/test/resources/interpreter/mock2/interpreter-setting.json @@ -0,0 +1,19 @@ +[ + { + "group": "mock2", + "name": "mock2", + "className": "org.apache.zeppelin.interpreter.mock.MockInterpreter2", + "properties": { + }, + "option": { + "remote": false, + "port": -1, + "perNote": "shared", + "perUser": "isolated", + "isExistingProcess": false, + "setPermission": false, + "users": [], + "isUserImpersonate": false + } + } +] diff --git a/zeppelin-zengine/src/test/resources/interpreter/mock_resource_pool/interpreter-setting.json b/zeppelin-zengine/src/test/resources/interpreter/mock_resource_pool/interpreter-setting.json new file mode 100644 index 00000000000..4dfe0a75b5e --- /dev/null +++ b/zeppelin-zengine/src/test/resources/interpreter/mock_resource_pool/interpreter-setting.json @@ -0,0 +1,19 @@ +[ + { + "group": "mock_resource_pool", + "name": "mock_resource_pool", + "className": "org.apache.zeppelin.interpreter.remote.mock.MockInterpreterResourcePool", + "properties": { + }, + "option": { + "remote": true, + "port": -1, + "perNote": "shared", + "perUser": "shared", + "isExistingProcess": false, + "setPermission": false, + "users": [], + "isUserImpersonate": false + } + } +] diff --git a/zeppelin-zengine/src/test/resources/log4j.properties b/zeppelin-zengine/src/test/resources/log4j.properties index 001a222535d..74f619b70ee 100644 --- a/zeppelin-zengine/src/test/resources/log4j.properties +++ b/zeppelin-zengine/src/test/resources/log4j.properties @@ -35,7 +35,6 @@ log4j.logger.org.apache.hadoop.mapred=WARN log4j.logger.org.apache.hadoop.hive.ql=WARN log4j.logger.org.apache.hadoop.hive.metastore=WARN log4j.logger.org.apache.haadoop.hive.service.HiveServer=WARN -log4j.logger.org.apache.zeppelin.scheduler=WARN log4j.logger.org.quartz=WARN log4j.logger.DataNucleus=WARN @@ -45,4 +44,6 @@ log4j.logger.DataNucleus.Datastore=ERROR # Log all JDBC parameters log4j.logger.org.hibernate.type=ALL +log4j.logger.org.apache.zeppelin.interpreter=DEBUG +log4j.logger.org.apache.zeppelin.scheduler=DEBUG From 2a3791020fc74700b3310bef5dea2bb6842d9de1 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Mon, 28 Aug 2017 13:51:57 +0800 Subject: [PATCH 028/492] Revert "[ZEPPELIN-2627] Interpreter refactor" This reverts commit 8d4902e717ba2932ddb0f7110f2a669811f5d1af. ### What is this PR for? Just revert ZEPPELIN-2627 ### What type of PR is it? [Refactoring] ### Todos * [ ] - Task ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-2627 ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2553 from zjffdu/Revert_2627 and squashes the following commits: 222d957 [Jeff Zhang] Revert "[ZEPPELIN-2627] Interpreter refactor" --- .travis.yml | 1 - .../zeppelin/helium/ZeppelinDevServer.java | 8 +- zeppelin-interpreter/pom.xml | 22 - .../zeppelin/interpreter/Interpreter.java | 1 + .../interpreter/InterpreterFactory.java | 115 -- .../interpreter/InterpreterGroup.java | 272 ++-- .../interpreter/InterpreterInfoSaving.java | 101 -- .../interpreter/InterpreterProperty.java | 1 - .../interpreter/InterpreterRunner.java | 11 - .../interpreter/InterpreterSetting.java | 911 ------------- .../InterpreterSettingManager.java | 886 ------------- .../interpreter/remote/RemoteInterpreter.java | 371 ------ .../remote/RemoteInterpreterEventPoller.java | 279 ++-- .../remote/RemoteInterpreterProcess.java | 154 ++- .../remote/RemoteInterpreterServer.java | 58 +- .../zeppelin/resource/ResourcePoolUtils.java | 138 ++ .../org/apache/zeppelin/scheduler/Job.java | 11 +- .../zeppelin/scheduler/RemoteScheduler.java | 134 +- .../zeppelin/scheduler/SchedulerFactory.java | 49 +- .../zeppelin/tabledata/TableDataProxy.java | 1 + .../org/apache/zeppelin/util/IdHashes.java | 76 -- .../interpreter/AbstractInterpreterTest.java | 74 -- .../interpreter/DummyInterpreter.java | 43 + .../interpreter/InterpreterFactoryTest.java | 66 - .../interpreter/InterpreterGroupTest.java | 90 -- .../InterpreterOutputChangeWatcherTest.java | 11 +- .../InterpreterSettingManagerTest.java | 270 ---- .../interpreter/InterpreterSettingTest.java | 411 ------ .../zeppelin/interpreter/InterpreterTest.java | 7 +- .../interpreter/SleepInterpreter.java | 60 - .../remote/RemoteInterpreterTest.java | 520 -------- .../src/test/resources/conf/interpreter.json | 115 -- .../interpreter/test/interpreter-setting.json | 42 - .../src/test/resources/log4j.properties | 4 +- .../zeppelin/rest/InterpreterRestApi.java | 4 +- .../zeppelin/server/ZeppelinServer.java | 35 +- .../zeppelin/socket/NotebookServer.java | 32 +- .../interpreter/mock/MockInterpreter1.java | 40 +- .../zeppelin/rest/AbstractTestRestApi.java | 7 +- .../zeppelin/rest/InterpreterRestApiTest.java | 6 +- .../zeppelin/socket/NotebookServerTest.java | 13 +- .../src/test/resources/log4j.properties | 1 + zeppelin-zengine/pom.xml | 13 +- .../zeppelin/conf/ZeppelinConfiguration.java | 20 +- .../org/apache/zeppelin/helium/Helium.java | 51 +- .../helium/HeliumApplicationFactory.java | 120 +- .../interpreter/InterpreterFactory.java | 423 ++++++ .../interpreter/InterpreterGroupFactory.java | 26 + .../zeppelin/interpreter/InterpreterInfo.java | 0 .../interpreter/InterpreterInfoSaving.java | 53 +- .../interpreter/InterpreterSetting.java | 459 +++++++ .../InterpreterSettingManager.java | 1136 +++++++++++++++++ .../install/InstallInterpreter.java | 4 +- .../remote/RemoteAngularObjectRegistry.java | 76 +- .../interpreter/remote/RemoteInterpreter.java | 597 +++++++++ .../RemoteInterpreterManagedProcess.java | 13 - .../RemoteInterpreterRunningProcess.java | 0 .../zeppelin/notebook/ApplicationState.java | 1 + .../org/apache/zeppelin/notebook/Note.java | 12 +- .../apache/zeppelin/notebook/Notebook.java | 23 +- .../apache/zeppelin/notebook/Paragraph.java | 21 +- .../java/org/apache/zeppelin/util/Util.java | 2 +- .../helium/HeliumApplicationFactoryTest.java | 79 +- .../apache/zeppelin/helium/HeliumTest.java | 8 +- .../interpreter/InterpreterFactoryTest.java | 497 ++++++++ .../interpreter/InterpreterSettingTest.java | 327 +++++ .../install/InstallInterpreterTest.java | 0 .../interpreter/mock/MockInterpreter1.java | 15 +- .../interpreter/mock/MockInterpreter11.java | 20 +- .../interpreter/mock/MockInterpreter2.java | 10 +- .../remote/AppendOutputRunnerTest.java | 30 +- .../remote/RemoteAngularObjectTest.java | 89 +- .../RemoteInterpreterEventPollerTest.java | 0 .../RemoteInterpreterOutputTestStream.java | 67 +- .../remote/RemoteInterpreterProcessTest.java | 131 ++ .../remote/RemoteInterpreterTest.java | 975 ++++++++++++++ .../remote/RemoteInterpreterUtilsTest.java | 5 +- .../remote/mock/MockInterpreterA.java | 7 +- .../remote/mock/MockInterpreterAngular.java | 9 +- .../remote/mock/MockInterpreterB.java | 14 +- .../remote/mock/MockInterpreterEnv.java | 12 +- .../mock/MockInterpreterOutputStream.java | 5 +- .../mock/MockInterpreterResourcePool.java | 11 +- .../notebook/NoteInterpreterLoaderTest.java | 243 ++++ .../zeppelin/notebook/NotebookTest.java | 181 +-- .../zeppelin/notebook/ParagraphTest.java | 9 +- .../notebook/repo/NotebookRepoSyncTest.java | 74 +- .../notebook/repo/VFSNotebookRepoTest.java | 55 +- .../resource/DistributedResourcePoolTest.java | 96 +- .../scheduler/RemoteSchedulerTest.java | 129 +- .../src/test/resources/conf/interpreter.json | 1 - .../interpreter/mock/interpreter-setting.json | 12 + .../mock1/interpreter-setting.json | 19 - .../mock2/interpreter-setting.json | 19 - .../interpreter-setting.json | 19 - .../src/test/resources/log4j.properties | 3 +- 96 files changed, 6420 insertions(+), 5252 deletions(-) delete mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java delete mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java delete mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java delete mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java delete mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/ResourcePoolUtils.java delete mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/IdHashes.java delete mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java create mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DummyInterpreter.java delete mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java delete mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterGroupTest.java delete mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java delete mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java delete mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/SleepInterpreter.java delete mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java delete mode 100644 zeppelin-interpreter/src/test/resources/conf/interpreter.json delete mode 100644 zeppelin-interpreter/src/test/resources/interpreter/test/interpreter-setting.json rename zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DoubleEchoInterpreter.java => zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java (58%) rename {zeppelin-interpreter => zeppelin-zengine}/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java (99%) create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroupFactory.java rename {zeppelin-interpreter => zeppelin-zengine}/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfo.java (100%) rename zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/EchoInterpreter.java => zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java (50%) create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java rename {zeppelin-interpreter => zeppelin-zengine}/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java (98%) rename {zeppelin-interpreter => zeppelin-zengine}/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java (71%) create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java rename {zeppelin-interpreter => zeppelin-zengine}/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java (93%) rename {zeppelin-interpreter => zeppelin-zengine}/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java (100%) rename {zeppelin-interpreter => zeppelin-zengine}/src/main/java/org/apache/zeppelin/util/Util.java (98%) create mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java create mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/install/InstallInterpreterTest.java (100%) rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java (96%) rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java (91%) rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java (100%) rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java (97%) rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java (78%) rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPollerTest.java (100%) rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java (79%) create mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessTest.java create mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java (94%) rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java (97%) rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java (98%) rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java (89%) rename zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/GetEnvPropertyInterpreter.java => zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterEnv.java (83%) rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java (91%) rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java (95%) create mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteInterpreterLoaderTest.java rename {zeppelin-interpreter => zeppelin-zengine}/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java (77%) delete mode 100644 zeppelin-zengine/src/test/resources/conf/interpreter.json create mode 100644 zeppelin-zengine/src/test/resources/interpreter/mock/interpreter-setting.json delete mode 100644 zeppelin-zengine/src/test/resources/interpreter/mock1/interpreter-setting.json delete mode 100644 zeppelin-zengine/src/test/resources/interpreter/mock2/interpreter-setting.json delete mode 100644 zeppelin-zengine/src/test/resources/interpreter/mock_resource_pool/interpreter-setting.json diff --git a/.travis.yml b/.travis.yml index 64ea5596803..97ca60a853e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -162,7 +162,6 @@ after_success: after_failure: - echo "Travis exited with ${TRAVIS_TEST_RESULT}" - find . -name rat.txt | xargs cat - - cat logs/* - cat zeppelin-distribution/target/zeppelin-*-SNAPSHOT/zeppelin-*-SNAPSHOT/logs/zeppelin*.log - cat zeppelin-distribution/target/zeppelin-*-SNAPSHOT/zeppelin-*-SNAPSHOT/logs/zeppelin*.out - cat zeppelin-web/npm-debug.log diff --git a/helium-dev/src/main/java/org/apache/zeppelin/helium/ZeppelinDevServer.java b/helium-dev/src/main/java/org/apache/zeppelin/helium/ZeppelinDevServer.java index 24844693c5f..21ce283abba 100644 --- a/helium-dev/src/main/java/org/apache/zeppelin/helium/ZeppelinDevServer.java +++ b/helium-dev/src/main/java/org/apache/zeppelin/helium/ZeppelinDevServer.java @@ -43,13 +43,13 @@ public ZeppelinDevServer(int port) throws TException { } @Override - protected Interpreter getInterpreter(String sessionId, String className) throws TException { + protected Interpreter getInterpreter(String sessionKey, String className) throws TException { synchronized (this) { InterpreterGroup interpreterGroup = getInterpreterGroup(); if (interpreterGroup == null || interpreterGroup.isEmpty()) { createInterpreter( "dev", - sessionId, + sessionKey, DevInterpreter.class.getName(), new HashMap(), "anonymous"); @@ -57,11 +57,11 @@ protected Interpreter getInterpreter(String sessionId, String className) throws } } - Interpreter intp = super.getInterpreter(sessionId, className); + Interpreter intp = super.getInterpreter(sessionKey, className); interpreter = (DevInterpreter) ( ((LazyOpenInterpreter) intp).getInnerInterpreter()); interpreter.setInterpreterEvent(this); - return super.getInterpreter(sessionId, className); + return super.getInterpreter(sessionKey, className); } @Override diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index d8f9c43931d..384b9d1c59d 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -51,11 +51,6 @@ - - commons-configuration - commons-configuration - - org.apache.thrift libthrift @@ -231,21 +226,4 @@ test - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.0.2 - - - - test-jar - - - - - - diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java index 05599a01a35..74506dd402d 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java @@ -149,6 +149,7 @@ public Scheduler getScheduler() { @ZeppelinApi public Interpreter(Properties property) { + logger.debug("Properties: {}", property); this.property = property; } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java deleted file mode 100644 index a2fdb045b9a..00000000000 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.interpreter; - -import com.google.common.base.Preconditions; -import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonatype.aether.RepositoryException; - -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * //TODO(zjffdu) considering to move to InterpreterSettingManager - * - * Manage interpreters. - */ -public class InterpreterFactory { - private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterFactory.class); - - private final InterpreterSettingManager interpreterSettingManager; - - public InterpreterFactory(InterpreterSettingManager interpreterSettingManager) { - this.interpreterSettingManager = interpreterSettingManager; - } - - private InterpreterSetting getInterpreterSettingByGroup(List settings, - String group) { - - Preconditions.checkNotNull(group, "group should be not null"); - for (InterpreterSetting setting : settings) { - if (group.equals(setting.getName())) { - return setting; - } - } - return null; - } - - public Interpreter getInterpreter(String user, String noteId, String replName) { - List settings = interpreterSettingManager.getInterpreterSettings(noteId); - InterpreterSetting setting; - Interpreter interpreter; - - if (settings == null || settings.size() == 0) { - LOGGER.error("No interpreter is binded to this note: " + noteId); - return null; - } - - if (StringUtils.isBlank(replName)) { - // Get the default interpreter of the first interpreter binding - InterpreterSetting defaultSetting = settings.get(0); - return defaultSetting.getDefaultInterpreter(user, noteId); - } - - String[] replNameSplit = replName.split("\\."); - if (replNameSplit.length == 2) { - String group = replNameSplit[0]; - String name = replNameSplit[1]; - setting = getInterpreterSettingByGroup(settings, group); - if (null != setting) { - interpreter = setting.getInterpreter(user, noteId, name); - if (null != interpreter) { - return interpreter; - } - } - throw new InterpreterException(replName + " interpreter not found"); - - } else { - // first assume replName is 'name' of interpreter. ('groupName' is ommitted) - // search 'name' from first (default) interpreter group - // TODO(jl): Handle with noteId to support defaultInterpreter per note. - setting = settings.get(0); - interpreter = setting.getInterpreter(user, noteId, replName); - - if (null != interpreter) { - return interpreter; - } - - // next, assume replName is 'group' of interpreter ('name' is ommitted) - // search interpreter group and return first interpreter. - setting = getInterpreterSettingByGroup(settings, replName); - - if (null != setting) { - return setting.getDefaultInterpreter(user, noteId); - } - - // Support the legacy way to use it - for (InterpreterSetting s : settings) { - if (s.getGroup().equals(replName)) { - return setting.getDefaultInterpreter(user, noteId); - } - } - } - //TODO(zjffdu) throw InterpreterException instead of return null - return null; - } -} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroup.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroup.java index ae7ad3a9e47..5cbab6bdd9a 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroup.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroup.java @@ -17,7 +17,14 @@ package org.apache.zeppelin.interpreter; -import com.google.common.annotations.VisibleForTesting; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; + import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; import org.apache.zeppelin.resource.ResourcePool; @@ -26,93 +33,91 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - /** - * InterpreterGroup is collections of interpreter sessions. - * One session could include multiple interpreters. - * For example spark, pyspark, sql interpreters are in the same 'spark' interpreter session. + * InterpreterGroup is list of interpreters in the same interpreter group. + * For example spark, pyspark, sql interpreters are in the same 'spark' group + * and InterpreterGroup will have reference to these all interpreters. * * Remember, list of interpreters are dedicated to a session. Session could be shared across user * or notes, so the sessionId could be user or noteId or their combination. - * So InterpreterGroup internally manages map of [sessionId(noteId, user, or + * So InterpreterGroup internally manages map of [interpreterSessionKey(noteId, user, or * their combination), list of interpreters] * - * A InterpreterGroup runs both in zeppelin server process and interpreter process. + * A InterpreterGroup runs on interpreter process. + * And unit of interpreter instantiate, restart, bind, unbind. */ -public class InterpreterGroup { +public class InterpreterGroup extends ConcurrentHashMap> { + String id; private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterGroup.class); - private String id; - // sessionId --> interpreters - private Map> sessions = new ConcurrentHashMap(); - private InterpreterSetting interpreterSetting; - private AngularObjectRegistry angularObjectRegistry; - private InterpreterHookRegistry hookRegistry; - private RemoteInterpreterProcess remoteInterpreterProcess; // attached remote interpreter process - private ResourcePool resourcePool; - private boolean angularRegistryPushed = false; + AngularObjectRegistry angularObjectRegistry; + InterpreterHookRegistry hookRegistry; + RemoteInterpreterProcess remoteInterpreterProcess; // attached remote interpreter process + ResourcePool resourcePool; + boolean angularRegistryPushed = false; - /** - * Create InterpreterGroup with given id, used in InterpreterProcess - * @param id - */ - public InterpreterGroup(String id) { - this.id = id; + // map [notebook session, Interpreters in the group], to support per note session interpreters + //Map> interpreters = new ConcurrentHashMap>(); + + private static final Map allInterpreterGroups = + new ConcurrentHashMap<>(); + + public static InterpreterGroup getByInterpreterGroupId(String id) { + return allInterpreterGroups.get(id); + } + + public static Collection getAll() { + return new LinkedList(allInterpreterGroups.values()); } /** - * Create InterpreterGroup with given id and interpreterSetting, used in ZeppelinServer + * Create InterpreterGroup with given id * @param id - * @param interpreterSetting */ - InterpreterGroup(String id, InterpreterSetting interpreterSetting) { + public InterpreterGroup(String id) { this.id = id; - this.interpreterSetting = interpreterSetting; + allInterpreterGroups.put(id, this); } /** * Create InterpreterGroup with autogenerated id */ - @VisibleForTesting public InterpreterGroup() { - this.id = generateId(); + getId(); + allInterpreterGroups.put(id, this); } private static String generateId() { - return "InterpreterGroup_" + System.currentTimeMillis() + "_" + new Random().nextInt(); + return "InterpreterGroup_" + System.currentTimeMillis() + "_" + + new Random().nextInt(); } public String getId() { - return this.id; - } - - //TODO(zjffdu) change it to getSession. For now just keep this method to reduce code change - public synchronized List get(String sessionId) { - return sessions.get(sessionId); + synchronized (this) { + if (id == null) { + id = generateId(); + } + return id; + } } - //TODO(zjffdu) change it to addSession. For now just keep this method to reduce code change - public synchronized void put(String sessionId, List interpreters) { - this.sessions.put(sessionId, interpreters); - } + /** + * Get combined property of all interpreters in this group + * @return + */ + public Properties getProperty() { + Properties p = new Properties(); - public synchronized void addInterpreterToSession(Interpreter interpreter, String sessionId) { - LOGGER.debug("Add Interpreter {} to session {}", interpreter.getClassName(), sessionId); - List interpreters = get(sessionId); - if (interpreters == null) { - interpreters = new ArrayList<>(); + for (List intpGroupForASession : this.values()) { + for (Interpreter intp : intpGroupForASession) { + p.putAll(intp.getProperty()); + } + // it's okay to break here while every List will have the same property set + break; } - interpreters.add(interpreter); - put(sessionId, interpreters); - } - - //TODO(zjffdu) rename it to a more proper name. - //For now just keep this method to reduce code change - public Collection> values() { - return sessions.values(); + return p; } public AngularObjectRegistry getAngularObjectRegistry() { @@ -131,69 +136,128 @@ public void setInterpreterHookRegistry(InterpreterHookRegistry hookRegistry) { this.hookRegistry = hookRegistry; } - public InterpreterSetting getInterpreterSetting() { - return interpreterSetting; - } - - public synchronized RemoteInterpreterProcess getOrCreateInterpreterProcess() { - if (remoteInterpreterProcess == null) { - LOGGER.info("Create InterperterProcess for InterpreterGroup: " + getId()); - remoteInterpreterProcess = interpreterSetting.createInterpreterProcess(); - } - return remoteInterpreterProcess; - } - public RemoteInterpreterProcess getRemoteInterpreterProcess() { return remoteInterpreterProcess; } + public void setRemoteInterpreterProcess(RemoteInterpreterProcess remoteInterpreterProcess) { + this.remoteInterpreterProcess = remoteInterpreterProcess; + } /** * Close all interpreter instances in this group */ - public synchronized void close() { - LOGGER.info("Close InterpreterGroup: " + id); - for (String sessionId : sessions.keySet()) { - close(sessionId); + public void close() { + LOGGER.info("Close interpreter group " + getId()); + List intpToClose = new LinkedList<>(); + for (List intpGroupForSession : this.values()) { + intpToClose.addAll(intpGroupForSession); } + close(intpToClose); + + // make sure remote interpreter process terminates + if (remoteInterpreterProcess != null) { + while (remoteInterpreterProcess.referenceCount() > 0) { + remoteInterpreterProcess.dereference(); + } + remoteInterpreterProcess = null; + } + allInterpreterGroups.remove(id); } /** - * Close all interpreter instances in this session + * Close all interpreter instances in this group for the session * @param sessionId */ - public synchronized void close(String sessionId) { - LOGGER.info("Close Session: " + sessionId); - close(sessions.remove(sessionId)); - //TODO(zjffdu) whether close InterpreterGroup if there's no session left in Zeppelin Server - if (sessions.isEmpty() && interpreterSetting != null) { - LOGGER.info("Remove this InterpreterGroup {} as all the sessions are closed", id); - interpreterSetting.removeInterpreterGroup(id); - if (remoteInterpreterProcess != null) { - LOGGER.info("Kill RemoteInterpreterProcess"); - remoteInterpreterProcess.stop(); - remoteInterpreterProcess = null; - } - } + public void close(String sessionId) { + LOGGER.info("Close interpreter group " + getId() + " for session: " + sessionId); + final List intpForSession = this.get(sessionId); + + close(intpForSession); + } + + private void close(final Collection intpToClose) { + close(null, null, null, intpToClose); } - public int getSessionNum() { - return sessions.size(); + public void close(final Map interpreterGroupRef, + final String processKey, final String sessionKey) { + LOGGER.info("Close interpreter group " + getId() + " for session: " + sessionKey); + close(interpreterGroupRef, processKey, sessionKey, this.get(sessionKey)); } - private void close(Collection interpreters) { - if (interpreters == null) { + private void close(final Map interpreterGroupRef, + final String processKey, final String sessionKey, final Collection intpToClose) { + if (intpToClose == null) { return; } + Thread t = new Thread() { + public void run() { + for (Interpreter interpreter : intpToClose) { + Scheduler scheduler = interpreter.getScheduler(); + interpreter.close(); + + if (null != scheduler) { + SchedulerFactory.singleton().removeScheduler(scheduler.getName()); + } + } + + if (remoteInterpreterProcess != null) { + //TODO(jl): Because interpreter.close() runs as a seprate thread, we cannot guarantee + // refernceCount is a proper value. And as the same reason, we must not call + // remoteInterpreterProcess.dereference twice - this method also be called by + // interpreter.close(). + + // remoteInterpreterProcess.dereference(); + if (remoteInterpreterProcess.referenceCount() <= 0) { + remoteInterpreterProcess = null; + allInterpreterGroups.remove(id); + } + } + + // TODO(jl): While closing interpreters in a same session, we should remove after all + // interpreters are removed. OMG. It's too dirty!! + if (null != interpreterGroupRef && null != processKey && null != sessionKey) { + InterpreterGroup interpreterGroup = interpreterGroupRef.get(processKey); + if (1 == interpreterGroup.size() && interpreterGroup.containsKey(sessionKey)) { + interpreterGroupRef.remove(processKey); + } else { + interpreterGroup.remove(sessionKey); + } + } + } + }; + + t.start(); + try { + t.join(); + } catch (InterruptedException e) { + LOGGER.error("Can't close interpreter: {}", getId(), e); + } - for (Interpreter interpreter : interpreters) { - Scheduler scheduler = interpreter.getScheduler(); - interpreter.close(); - //TODO(zjffdu) move the close of schedule to Interpreter - if (null != scheduler) { - SchedulerFactory.singleton().removeScheduler(scheduler.getName()); + + } + + /** + * Close all interpreter instances in this group + */ + public void shutdown() { + LOGGER.info("Close interpreter group " + getId()); + + // make sure remote interpreter process terminates + if (remoteInterpreterProcess != null) { + while (remoteInterpreterProcess.referenceCount() > 0) { + remoteInterpreterProcess.dereference(); } + remoteInterpreterProcess = null; } + allInterpreterGroups.remove(id); + + List intpToClose = new LinkedList<>(); + for (List intpGroupForSession : this.values()) { + intpToClose.addAll(intpGroupForSession); + } + close(intpToClose); } public void setResourcePool(ResourcePool resourcePool) { @@ -211,22 +275,4 @@ public boolean isAngularRegistryPushed() { public void setAngularRegistryPushed(boolean angularRegistryPushed) { this.angularRegistryPushed = angularRegistryPushed; } - - public synchronized List getOrCreateSession(String user, String sessionId) { - if (sessions.containsKey(sessionId)) { - return sessions.get(sessionId); - } else { - List interpreters = interpreterSetting.createInterpreters(user, sessionId); - for (Interpreter interpreter : interpreters) { - interpreter.setInterpreterGroup(this); - } - LOGGER.info("Create Session {} in InterpreterGroup {} for user {}", sessionId, id, user); - sessions.put(sessionId, interpreters); - return interpreters; - } - } - - public boolean isEmpty() { - return sessions.isEmpty(); - } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java deleted file mode 100644 index 4ccc262699a..00000000000 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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.interpreter; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.internal.StringMap; -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonatype.aether.repository.RemoteRepository; - -import java.io.BufferedReader; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.PosixFilePermission; -import java.util.*; - -import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; -import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; - -/** - * - */ -public class InterpreterInfoSaving { - - private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterInfoSaving.class); - private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); - - public Map interpreterSettings = new HashMap<>(); - public Map> interpreterBindings = new HashMap<>(); - public List interpreterRepositories = new ArrayList<>(); - - public static InterpreterInfoSaving loadFromFile(Path file) throws IOException { - LOGGER.info("Load interpreter setting from file: " + file); - InterpreterInfoSaving infoSaving = null; - try (BufferedReader json = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { - JsonParser jsonParser = new JsonParser(); - JsonObject jsonObject = jsonParser.parse(json).getAsJsonObject(); - infoSaving = InterpreterInfoSaving.fromJson(jsonObject.toString()); - - if (infoSaving != null && infoSaving.interpreterSettings != null) { - for (InterpreterSetting interpreterSetting : infoSaving.interpreterSettings.values()) { - // Always use separate interpreter process - // While we decided to turn this feature on always (without providing - // enable/disable option on GUI). - // previously created setting should turn this feature on here. - interpreterSetting.getOption().setRemote(true); - interpreterSetting.convertPermissionsFromUsersToOwners( - jsonObject.getAsJsonObject("interpreterSettings") - .getAsJsonObject(interpreterSetting.getId())); - } - } - } - return infoSaving == null ? new InterpreterInfoSaving() : infoSaving; - } - - public void saveToFile(Path file) throws IOException { - if (!Files.exists(file)) { - Files.createFile(file); - try { - Set permissions = EnumSet.of(OWNER_READ, OWNER_WRITE); - Files.setPosixFilePermissions(file, permissions); - } catch (UnsupportedOperationException e) { - // File system does not support Posix file permissions (likely windows) - continue anyway. - LOGGER.warn("unable to setPosixFilePermissions on '{}'.", file); - }; - } - LOGGER.info("Save Interpreter Settings to " + file); - IOUtils.write(this.toJson(), new FileOutputStream(file.toFile())); - } - - public String toJson() { - return gson.toJson(this); - } - - public static InterpreterInfoSaving fromJson(String json) { - return gson.fromJson(json, InterpreterInfoSaving.class); - } -} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterProperty.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterProperty.java index ac40de36a04..0bb3d42dd19 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterProperty.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterProperty.java @@ -34,7 +34,6 @@ public InterpreterProperty(String name, Object value, String type) { public InterpreterProperty(String name, Object value) { this.name = name; this.value = value; - this.type = "textarea"; } public String getName() { diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterRunner.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterRunner.java index ae7bb884ec7..020564b5fb3 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterRunner.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterRunner.java @@ -1,6 +1,5 @@ package org.apache.zeppelin.interpreter; -import com.google.common.annotations.VisibleForTesting; import com.google.gson.annotations.SerializedName; /** @@ -13,16 +12,6 @@ public class InterpreterRunner { @SerializedName("win") private String winPath; - public InterpreterRunner() { - - } - - @VisibleForTesting - public InterpreterRunner(String linuxPath, String winPath) { - this.linuxPath = linuxPath; - this.winPath = winPath; - } - public String getPath() { return System.getProperty("os.name").startsWith("Windows") ? winPath : linuxPath; } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java deleted file mode 100644 index 3f84cd00178..00000000000 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java +++ /dev/null @@ -1,911 +0,0 @@ -/* - * 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.interpreter; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.annotations.SerializedName; -import com.google.gson.internal.StringMap; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.dep.Dependency; -import org.apache.zeppelin.dep.DependencyResolver; -import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.display.AngularObjectRegistryListener; -import org.apache.zeppelin.helium.ApplicationEventListener; -import org.apache.zeppelin.interpreter.remote.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.FileNotFoundException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -import static org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_MAX_POOL_SIZE; -import static org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_OUTPUT_LIMIT; -import static org.apache.zeppelin.util.IdHashes.generateId; - -/** - * Represent one InterpreterSetting in the interpreter setting page - */ -public class InterpreterSetting { - - private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterSetting.class); - private static final String SHARED_PROCESS = "shared_process"; - private static final String SHARED_SESSION = "shared_session"; - private static final Map DEFAULT_EDITOR = ImmutableMap.of( - "language", (Object) "text", - "editOnDblClick", false); - - private String id; - private String name; - // the original interpreter setting template name where it is created from - private String group; - - //TODO(zjffdu) make the interpreter.json consistent with interpreter-setting.json - /** - * properties can be either Properties or Map - * properties should be: - * - Properties when Interpreter instances are saved to `conf/interpreter.json` file - * - Map when Interpreters are registered - * : this is needed after https://github.com/apache/zeppelin/pull/1145 - * which changed the way of getting default interpreter setting AKA interpreterSettingsRef - * Note(mina): In order to simplify the implementation, I chose to change properties - * from Properties to Object instead of creating new classes. - */ - private Object properties = new Properties(); - - private Status status; - private String errorReason; - - @SerializedName("interpreterGroup") - private List interpreterInfos; - - private List dependencies = new ArrayList<>(); - private InterpreterOption option = new InterpreterOption(true); - - @SerializedName("runner") - private InterpreterRunner interpreterRunner; - - /////////////////////////////////////////////////////////////////////////////////////////// - private transient InterpreterSettingManager interpreterSettingManager; - private transient String interpreterDir; - private final transient Map interpreterGroups = - new ConcurrentHashMap<>(); - - private final transient ReentrantReadWriteLock.ReadLock interpreterGroupReadLock; - private final transient ReentrantReadWriteLock.WriteLock interpreterGroupWriteLock; - - private transient AngularObjectRegistryListener angularObjectRegistryListener; - private transient RemoteInterpreterProcessListener remoteInterpreterProcessListener; - private transient ApplicationEventListener appEventListener; - private transient DependencyResolver dependencyResolver; - - private transient Map infos; - - // Map of the note and paragraphs which has runtime infos generated by this interpreter setting. - // This map is used to clear the infos in paragraph when the interpretersetting is restarted - private transient Map> runtimeInfosToBeCleared; - - private transient ZeppelinConfiguration conf = new ZeppelinConfiguration(); - - private transient Map cleanCl = - Collections.synchronizedMap(new HashMap()); - /////////////////////////////////////////////////////////////////////////////////////////// - - - /** - * Builder class for InterpreterSetting - */ - public static class Builder { - private InterpreterSetting interpreterSetting; - - public Builder() { - this.interpreterSetting = new InterpreterSetting(); - } - - public Builder setId(String id) { - interpreterSetting.id = id; - return this; - } - - public Builder setName(String name) { - interpreterSetting.name = name; - return this; - } - - public Builder setGroup(String group) { - interpreterSetting.group = group; - return this; - } - - public Builder setInterpreterInfos(List interpreterInfos) { - interpreterSetting.interpreterInfos = interpreterInfos; - return this; - } - - public Builder setProperties(Object properties) { - interpreterSetting.properties = properties; - return this; - } - - public Builder setOption(InterpreterOption option) { - interpreterSetting.option = option; - return this; - } - - public Builder setInterpreterDir(String interpreterDir) { - interpreterSetting.interpreterDir = interpreterDir; - return this; - } - - public Builder setRunner(InterpreterRunner runner) { - interpreterSetting.interpreterRunner = runner; - return this; - } - - public Builder setDependencies(List dependencies) { - interpreterSetting.dependencies = dependencies; - return this; - } - - public Builder setConf(ZeppelinConfiguration conf) { - interpreterSetting.conf = conf; - return this; - } - - public Builder setDependencyResolver(DependencyResolver dependencyResolver) { - interpreterSetting.dependencyResolver = dependencyResolver; - return this; - } - -// public Builder setInterpreterRunner(InterpreterRunner runner) { -// interpreterSetting.interpreterRunner = runner; -// return this; -// } - - public Builder setIntepreterSettingManager( - InterpreterSettingManager interpreterSettingManager) { - interpreterSetting.interpreterSettingManager = interpreterSettingManager; - return this; - } - - public Builder setRemoteInterpreterProcessListener(RemoteInterpreterProcessListener - remoteInterpreterProcessListener) { - interpreterSetting.remoteInterpreterProcessListener = remoteInterpreterProcessListener; - return this; - } - - public Builder setAngularObjectRegistryListener( - AngularObjectRegistryListener angularObjectRegistryListener) { - interpreterSetting.angularObjectRegistryListener = angularObjectRegistryListener; - return this; - } - - public Builder setApplicationEventListener(ApplicationEventListener applicationEventListener) { - interpreterSetting.appEventListener = applicationEventListener; - return this; - } - - public InterpreterSetting create() { - // post processing - interpreterSetting.postProcessing(); - return interpreterSetting; - } - } - - public InterpreterSetting() { - ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - this.id = generateId(); - interpreterGroupReadLock = lock.readLock(); - interpreterGroupWriteLock = lock.writeLock(); - } - - void postProcessing() { - this.status = Status.READY; - } - - /** - * Create interpreter from InterpreterSettingTemplate - * - * @param o interpreterSetting from InterpreterSettingTemplate - */ - public InterpreterSetting(InterpreterSetting o) { - this(); - this.id = generateId(); - this.name = o.name; - this.group = o.group; - this.properties = convertInterpreterProperties( - (Map) o.getProperties()); - this.interpreterInfos = new ArrayList<>(o.getInterpreterInfos()); - this.option = InterpreterOption.fromInterpreterOption(o.getOption()); - this.dependencies = new ArrayList<>(o.getDependencies()); - this.interpreterDir = o.getInterpreterDir(); - this.interpreterRunner = o.getInterpreterRunner(); - this.conf = o.getConf(); - } - - public AngularObjectRegistryListener getAngularObjectRegistryListener() { - return angularObjectRegistryListener; - } - - public RemoteInterpreterProcessListener getRemoteInterpreterProcessListener() { - return remoteInterpreterProcessListener; - } - - public ApplicationEventListener getAppEventListener() { - return appEventListener; - } - - public DependencyResolver getDependencyResolver() { - return dependencyResolver; - } - - public InterpreterSettingManager getInterpreterSettingManager() { - return interpreterSettingManager; - } - - public void setAngularObjectRegistryListener(AngularObjectRegistryListener - angularObjectRegistryListener) { - this.angularObjectRegistryListener = angularObjectRegistryListener; - } - - public void setAppEventListener(ApplicationEventListener appEventListener) { - this.appEventListener = appEventListener; - } - - public void setRemoteInterpreterProcessListener(RemoteInterpreterProcessListener - remoteInterpreterProcessListener) { - this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; - } - - public void setDependencyResolver(DependencyResolver dependencyResolver) { - this.dependencyResolver = dependencyResolver; - } - - public void setInterpreterSettingManager(InterpreterSettingManager interpreterSettingManager) { - this.interpreterSettingManager = interpreterSettingManager; - } - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public String getGroup() { - return group; - } - - private String getInterpreterGroupId(String user, String noteId) { - String key; - if (option.isExistingProcess) { - key = Constants.EXISTING_PROCESS; - } else if (getOption().isProcess()) { - key = (option.perUserIsolated() ? user : "") + ":" + (option.perNoteIsolated() ? noteId : ""); - } else { - key = SHARED_PROCESS; - } - - //TODO(zjffdu) we encode interpreter setting id into groupId, this is not a good design - return id + ":" + key; - } - - private String getInterpreterSessionId(String user, String noteId) { - String key; - if (option.isExistingProcess()) { - key = Constants.EXISTING_PROCESS; - } else if (option.perNoteScoped() && option.perUserScoped()) { - key = user + ":" + noteId; - } else if (option.perUserScoped()) { - key = user; - } else if (option.perNoteScoped()) { - key = noteId; - } else { - key = SHARED_SESSION; - } - - return key; - } - - public InterpreterGroup getOrCreateInterpreterGroup(String user, String noteId) { - String groupId = getInterpreterGroupId(user, noteId); - try { - interpreterGroupWriteLock.lock(); - if (!interpreterGroups.containsKey(groupId)) { - LOGGER.info("Create InterpreterGroup with groupId {} for user {} and note {}", - groupId, user, noteId); - InterpreterGroup intpGroup = createInterpreterGroup(groupId); - interpreterGroups.put(groupId, intpGroup); - } - return interpreterGroups.get(groupId); - } finally { - interpreterGroupWriteLock.unlock();; - } - } - - void removeInterpreterGroup(String groupId) { - this.interpreterGroups.remove(groupId); - } - - InterpreterGroup getInterpreterGroup(String user, String noteId) { - String groupId = getInterpreterGroupId(user, noteId); - try { - interpreterGroupReadLock.lock(); - return interpreterGroups.get(groupId); - } finally { - interpreterGroupReadLock.unlock();; - } - } - - InterpreterGroup getInterpreterGroup(String groupId) { - return interpreterGroups.get(groupId); - } - - @VisibleForTesting - public ArrayList getAllInterpreterGroups() { - try { - interpreterGroupReadLock.lock(); - return new ArrayList(interpreterGroups.values()); - } finally { - interpreterGroupReadLock.unlock(); - } - } - - Map getEditorFromSettingByClassName(String className) { - for (InterpreterInfo intpInfo : interpreterInfos) { - if (className.equals(intpInfo.getClassName())) { - if (intpInfo.getEditor() == null) { - break; - } - return intpInfo.getEditor(); - } - } - return DEFAULT_EDITOR; - } - - void closeInterpreters(String user, String noteId) { - InterpreterGroup interpreterGroup = getInterpreterGroup(user, noteId); - if (interpreterGroup != null) { - String sessionId = getInterpreterSessionId(user, noteId); - interpreterGroup.close(sessionId); - } - } - - public void close() { - LOGGER.info("Close InterpreterSetting: " + name); - for (InterpreterGroup intpGroup : interpreterGroups.values()) { - intpGroup.close(); - } - interpreterGroups.clear(); - this.runtimeInfosToBeCleared = null; - this.infos = null; - } - - public void setProperties(Object object) { - if (object instanceof StringMap) { - StringMap map = (StringMap) properties; - Properties newProperties = new Properties(); - for (String key : map.keySet()) { - newProperties.put(key, map.get(key)); - } - this.properties = newProperties; - } else { - this.properties = object; - } - } - - - public Object getProperties() { - return properties; - } - - @VisibleForTesting - public void setProperty(String name, String value) { - ((Map) properties).put(name, new InterpreterProperty(name, value)); - } - - // This method is supposed to be only called by InterpreterSetting - // but not InterpreterSetting Template - public Properties getJavaProperties() { - Properties jProperties = new Properties(); - Map iProperties = (Map) properties; - for (Map.Entry entry : iProperties.entrySet()) { - jProperties.setProperty(entry.getKey(), entry.getValue().getValue().toString()); - } - - if (!jProperties.containsKey("zeppelin.interpreter.output.limit")) { - jProperties.setProperty("zeppelin.interpreter.output.limit", - conf.getInt(ZEPPELIN_INTERPRETER_OUTPUT_LIMIT) + ""); - } - - if (!jProperties.containsKey("zeppelin.interpreter.max.poolsize")) { - jProperties.setProperty("zeppelin.interpreter.max.poolsize", - conf.getInt(ZEPPELIN_INTERPRETER_MAX_POOL_SIZE) + ""); - } - - String interpreterLocalRepoPath = conf.getInterpreterLocalRepoPath(); - //TODO(zjffdu) change it to interpreterDir/{interpreter_name} - jProperties.setProperty("zeppelin.interpreter.localRepo", - interpreterLocalRepoPath + "/" + id); - return jProperties; - } - - public ZeppelinConfiguration getConf() { - return conf; - } - - public void setConf(ZeppelinConfiguration conf) { - this.conf = conf; - } - - public List getDependencies() { - return dependencies; - } - - public void setDependencies(List dependencies) { - this.dependencies = dependencies; - loadInterpreterDependencies(); - } - - public InterpreterOption getOption() { - return option; - } - - public void setOption(InterpreterOption option) { - this.option = option; - } - - public String getInterpreterDir() { - return interpreterDir; - } - - public void setInterpreterDir(String interpreterDir) { - this.interpreterDir = interpreterDir; - } - - public List getInterpreterInfos() { - return interpreterInfos; - } - - void appendDependencies(List dependencies) { - for (Dependency dependency : dependencies) { - if (!this.dependencies.contains(dependency)) { - this.dependencies.add(dependency); - } - } - loadInterpreterDependencies(); - } - - void setInterpreterOption(InterpreterOption interpreterOption) { - this.option = interpreterOption; - } - - public void setProperties(Properties p) { - this.properties = p; - } - - void setGroup(String group) { - this.group = group; - } - - void setName(String name) { - this.name = name; - } - - /*** - * Interpreter status - */ - public enum Status { - DOWNLOADING_DEPENDENCIES, - ERROR, - READY - } - - public Status getStatus() { - return status; - } - - public void setStatus(Status status) { - this.status = status; - } - - public String getErrorReason() { - return errorReason; - } - - public void setErrorReason(String errorReason) { - this.errorReason = errorReason; - } - - public void setInterpreterInfos(List interpreterInfos) { - this.interpreterInfos = interpreterInfos; - } - - public void setInfos(Map infos) { - this.infos = infos; - } - - public Map getInfos() { - return infos; - } - - public InterpreterRunner getInterpreterRunner() { - return interpreterRunner; - } - - public void setInterpreterRunner(InterpreterRunner interpreterRunner) { - this.interpreterRunner = interpreterRunner; - } - - public void addNoteToPara(String noteId, String paraId) { - if (runtimeInfosToBeCleared == null) { - runtimeInfosToBeCleared = new HashMap<>(); - } - Set paraIdSet = runtimeInfosToBeCleared.get(noteId); - if (paraIdSet == null) { - paraIdSet = new HashSet<>(); - runtimeInfosToBeCleared.put(noteId, paraIdSet); - } - paraIdSet.add(paraId); - } - - public Map> getNoteIdAndParaMap() { - return runtimeInfosToBeCleared; - } - - public void clearNoteIdAndParaMap() { - runtimeInfosToBeCleared = null; - } - - - //////////////////////////// IMPORTANT //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////// - // This is the only place to create interpreters. For now we always create multiple interpreter - // together (one session). We don't support to create single interpreter yet. - List createInterpreters(String user, String sessionId) { - List interpreters = new ArrayList<>(); - List interpreterInfos = getInterpreterInfos(); - for (InterpreterInfo info : interpreterInfos) { - Interpreter interpreter = null; - if (option.isRemote()) { - interpreter = new RemoteInterpreter(getJavaProperties(), sessionId, - info.getClassName(), user); - } else { - interpreter = createLocalInterpreter(info.getClassName()); - } - - if (info.isDefaultInterpreter()) { - interpreters.add(0, interpreter); - } else { - interpreters.add(interpreter); - } - LOGGER.info("Interpreter {} created for user: {}, sessionId: {}", - interpreter.getClassName(), user, sessionId); - } - return interpreters; - } - - // Create Interpreter in ZeppelinServer for non-remote mode - private Interpreter createLocalInterpreter(String className) - throws InterpreterException { - LOGGER.info("Create Local Interpreter {} from {}", className, interpreterDir); - - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - try { - - URLClassLoader ccl = cleanCl.get(interpreterDir); - if (ccl == null) { - // classloader fallback - ccl = URLClassLoader.newInstance(new URL[]{}, oldcl); - } - - boolean separateCL = true; - try { // check if server's classloader has driver already. - Class cls = this.getClass().forName(className); - if (cls != null) { - separateCL = false; - } - } catch (Exception e) { - LOGGER.error("exception checking server classloader driver", e); - } - - URLClassLoader cl; - - if (separateCL == true) { - cl = URLClassLoader.newInstance(new URL[]{}, ccl); - } else { - cl = ccl; - } - Thread.currentThread().setContextClassLoader(cl); - - Class replClass = (Class) cl.loadClass(className); - Constructor constructor = - replClass.getConstructor(new Class[]{Properties.class}); - Interpreter repl = constructor.newInstance(getJavaProperties()); - repl.setClassloaderUrls(ccl.getURLs()); - LazyOpenInterpreter intp = new LazyOpenInterpreter(new ClassloaderInterpreter(repl, cl)); - return intp; - } catch (SecurityException e) { - throw new InterpreterException(e); - } catch (NoSuchMethodException e) { - throw new InterpreterException(e); - } catch (IllegalArgumentException e) { - throw new InterpreterException(e); - } catch (InstantiationException e) { - throw new InterpreterException(e); - } catch (IllegalAccessException e) { - throw new InterpreterException(e); - } catch (InvocationTargetException e) { - throw new InterpreterException(e); - } catch (ClassNotFoundException e) { - throw new InterpreterException(e); - } finally { - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - RemoteInterpreterProcess createInterpreterProcess() { - RemoteInterpreterProcess remoteInterpreterProcess = null; - int connectTimeout = - conf.getInt(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_CONNECT_TIMEOUT); - String localRepoPath = conf.getInterpreterLocalRepoPath() + "/" + id; - if (option.isExistingProcess()) { - // TODO(zjffdu) remove the existing process approach seems no one is using this. - // use the existing process - remoteInterpreterProcess = new RemoteInterpreterRunningProcess( - connectTimeout, - remoteInterpreterProcessListener, - appEventListener, - option.getHost(), - option.getPort()); - } else { - // create new remote process - remoteInterpreterProcess = new RemoteInterpreterManagedProcess( - interpreterRunner != null ? interpreterRunner.getPath() : - conf.getInterpreterRemoteRunnerPath(), interpreterDir, localRepoPath, - getEnvFromInterpreterProperty(getJavaProperties()), connectTimeout, - remoteInterpreterProcessListener, appEventListener, group); - } - return remoteInterpreterProcess; - } - - private Map getEnvFromInterpreterProperty(Properties property) { - Map env = new HashMap(); - StringBuilder sparkConfBuilder = new StringBuilder(); - for (String key : property.stringPropertyNames()) { - if (RemoteInterpreterUtils.isEnvString(key)) { - env.put(key, property.getProperty(key)); - } - if (key.equals("master")) { - sparkConfBuilder.append(" --master " + property.getProperty("master")); - } - if (isSparkConf(key, property.getProperty(key))) { - sparkConfBuilder.append(" --conf " + key + "=" + - toShellFormat(property.getProperty(key))); - } - } - env.put("ZEPPELIN_SPARK_CONF", sparkConfBuilder.toString()); - return env; - } - - private String toShellFormat(String value) { - if (value.contains("\'") && value.contains("\"")) { - throw new RuntimeException("Spark property value could not contain both \" and '"); - } else if (value.contains("\'")) { - return "\"" + value + "\""; - } else { - return "\'" + value + "\'"; - } - } - - static boolean isSparkConf(String key, String value) { - return !StringUtils.isEmpty(key) && key.startsWith("spark.") && !StringUtils.isEmpty(value); - } - - private List getOrCreateSession(String user, String noteId) { - InterpreterGroup interpreterGroup = getOrCreateInterpreterGroup(user, noteId); - Preconditions.checkNotNull(interpreterGroup, "No InterpreterGroup existed for user {}, " + - "noteId {}", user, noteId); - String sessionId = getInterpreterSessionId(user, noteId); - return interpreterGroup.getOrCreateSession(user, sessionId); - } - - public Interpreter getDefaultInterpreter(String user, String noteId) { - return getOrCreateSession(user, noteId).get(0); - } - - public Interpreter getInterpreter(String user, String noteId, String replName) { - Preconditions.checkNotNull(noteId, "noteId should be not null"); - Preconditions.checkNotNull(replName, "replName should be not null"); - - String className = getInterpreterClassFromInterpreterSetting(replName); - if (className == null) { - return null; - } - List interpreters = getOrCreateSession(user, noteId); - for (Interpreter interpreter : interpreters) { - if (className.equals(interpreter.getClassName())) { - return interpreter; - } - } - return null; - } - - private String getInterpreterClassFromInterpreterSetting(String replName) { - Preconditions.checkNotNull(replName, "replName should be not null"); - - for (InterpreterInfo info : interpreterInfos) { - String infoName = info.getName(); - if (null != info.getName() && replName.equals(infoName)) { - return info.getClassName(); - } - } - return null; - } - - private InterpreterGroup createInterpreterGroup(String groupId) throws InterpreterException { - AngularObjectRegistry angularObjectRegistry; - InterpreterGroup interpreterGroup = new InterpreterGroup(groupId, this); - if (option.isRemote()) { - angularObjectRegistry = - new RemoteAngularObjectRegistry(groupId, angularObjectRegistryListener, interpreterGroup); - } else { - angularObjectRegistry = new AngularObjectRegistry(id, angularObjectRegistryListener); - // TODO(moon) : create distributed resource pool for local interpreters and set - } - - interpreterGroup.setAngularObjectRegistry(angularObjectRegistry); - return interpreterGroup; - } - - private void loadInterpreterDependencies() { - setStatus(Status.DOWNLOADING_DEPENDENCIES); - setErrorReason(null); - Thread t = new Thread() { - public void run() { - try { - // dependencies to prevent library conflict - File localRepoDir = new File(conf.getInterpreterLocalRepoPath() + "/" + getId()); - if (localRepoDir.exists()) { - try { - FileUtils.forceDelete(localRepoDir); - } catch (FileNotFoundException e) { - LOGGER.info("A file that does not exist cannot be deleted, nothing to worry", e); - } - } - - // load dependencies - List deps = getDependencies(); - if (deps != null) { - for (Dependency d : deps) { - File destDir = new File( - conf.getRelativeDir(ZeppelinConfiguration.ConfVars.ZEPPELIN_DEP_LOCALREPO)); - - if (d.getExclusions() != null) { - dependencyResolver.load(d.getGroupArtifactVersion(), d.getExclusions(), - new File(destDir, id)); - } else { - dependencyResolver - .load(d.getGroupArtifactVersion(), new File(destDir, id)); - } - } - } - - setStatus(Status.READY); - setErrorReason(null); - } catch (Exception e) { - LOGGER.error(String.format("Error while downloading repos for interpreter group : %s," + - " go to interpreter setting page click on edit and save it again to make " + - "this interpreter work properly. : %s", - getGroup(), e.getLocalizedMessage()), e); - setErrorReason(e.getLocalizedMessage()); - setStatus(Status.ERROR); - } - } - }; - - t.start(); - } - - //TODO(zjffdu) ugly code, should not use JsonObject as parameter. not readable - public void convertPermissionsFromUsersToOwners(JsonObject jsonObject) { - if (jsonObject != null) { - JsonObject option = jsonObject.getAsJsonObject("option"); - if (option != null) { - JsonArray users = option.getAsJsonArray("users"); - if (users != null) { - if (this.option.getOwners() == null) { - this.option.owners = new LinkedList<>(); - } - for (JsonElement user : users) { - this.option.getOwners().add(user.getAsString()); - } - } - } - } - } - - // For backward compatibility of interpreter.json format after ZEPPELIN-2403 - static Map convertInterpreterProperties(Object properties) { - if (properties != null && properties instanceof StringMap) { - Map newProperties = new HashMap<>(); - StringMap p = (StringMap) properties; - for (Object o : p.entrySet()) { - Map.Entry entry = (Map.Entry) o; - if (!(entry.getValue() instanceof StringMap)) { - InterpreterProperty newProperty = new InterpreterProperty( - entry.getKey().toString(), - entry.getValue(), - InterpreterPropertyType.STRING.getValue()); - newProperties.put(entry.getKey().toString(), newProperty); - } else { - // already converted - return (Map) properties; - } - } - return newProperties; - - } else if (properties instanceof Map) { - Map dProperties = - (Map) properties; - Map newProperties = new HashMap<>(); - for (String key : dProperties.keySet()) { - Object value = dProperties.get(key); - if (value instanceof InterpreterProperty) { - return (Map) properties; - } else if (value instanceof StringMap) { - StringMap stringMap = (StringMap) value; - InterpreterProperty newProperty = new InterpreterProperty( - key, - stringMap.get("value"), - stringMap.get("type").toString()); - - newProperties.put(newProperty.getName(), newProperty); - } else if (value instanceof DefaultInterpreterProperty){ - DefaultInterpreterProperty dProperty = (DefaultInterpreterProperty) value; - InterpreterProperty property = new InterpreterProperty( - key, - dProperty.getValue(), - dProperty.getType() != null ? dProperty.getType() : "string" - // in case user forget to specify type in interpreter-setting.json - ); - newProperties.put(key, property); - } else { - throw new RuntimeException("Can not convert this type of property: " + value.getClass()); - } - } - return newProperties; - } - throw new RuntimeException("Can not convert this type: " + properties.getClass()); - } -} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java deleted file mode 100644 index ed3ebd890d1..00000000000 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java +++ /dev/null @@ -1,886 +0,0 @@ -/* - * 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.interpreter; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang.ArrayUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; -import org.apache.zeppelin.dep.Dependency; -import org.apache.zeppelin.dep.DependencyResolver; -import org.apache.zeppelin.display.AngularObjectRegistryListener; -import org.apache.zeppelin.helium.ApplicationEventListener; -import org.apache.zeppelin.interpreter.Interpreter.RegisteredInterpreter; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; -import org.apache.zeppelin.resource.Resource; -import org.apache.zeppelin.resource.ResourcePool; -import org.apache.zeppelin.resource.ResourceSet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonatype.aether.repository.Authentication; -import org.sonatype.aether.repository.Proxy; -import org.sonatype.aether.repository.RemoteRepository; - -import java.io.*; -import java.lang.reflect.Type; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.DirectoryStream.Filter; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; - -/** - * InterpreterSettingManager is the component which manage all the interpreter settings. - * (load/create/update/remove/get) - * Besides that InterpreterSettingManager also manage the interpreter setting binding. - * TODO(zjffdu) We could move it into another separated component. - */ -public class InterpreterSettingManager { - - private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterSettingManager.class); - private static final Map DEFAULT_EDITOR = ImmutableMap.of( - "language", (Object) "text", - "editOnDblClick", false); - - private final ZeppelinConfiguration conf; - private final Path interpreterDirPath; - private final Path interpreterSettingPath; - - /** - * This is only InterpreterSetting templates with default name and properties - * name --> InterpreterSetting - */ - private final Map interpreterSettingTemplates = - Maps.newConcurrentMap(); - /** - * This is used by creating and running Interpreters - * id --> InterpreterSetting - * TODO(zjffdu) change it to name --> InterpreterSetting - */ - private final Map interpreterSettings = - Maps.newConcurrentMap(); - - /** - * noteId --> list of InterpreterSettingId - */ - private final Map> interpreterBindings = - Maps.newConcurrentMap(); - - private final List interpreterRepositories; - private InterpreterOption defaultOption; - private List interpreterGroupOrderList; - private final Gson gson; - - private AngularObjectRegistryListener angularObjectRegistryListener; - private RemoteInterpreterProcessListener remoteInterpreterProcessListener; - private ApplicationEventListener appEventListener; - private DependencyResolver dependencyResolver; - - - public InterpreterSettingManager(ZeppelinConfiguration zeppelinConfiguration, - AngularObjectRegistryListener angularObjectRegistryListener, - RemoteInterpreterProcessListener - remoteInterpreterProcessListener, - ApplicationEventListener appEventListener) - throws IOException { - this(zeppelinConfiguration, new InterpreterOption(true), - angularObjectRegistryListener, - remoteInterpreterProcessListener, - appEventListener); - } - - @VisibleForTesting - public InterpreterSettingManager(ZeppelinConfiguration conf, - InterpreterOption defaultOption, - AngularObjectRegistryListener angularObjectRegistryListener, - RemoteInterpreterProcessListener - remoteInterpreterProcessListener, - ApplicationEventListener appEventListener) throws IOException { - this.conf = conf; - this.defaultOption = defaultOption; - this.interpreterDirPath = Paths.get(conf.getInterpreterDir()); - LOGGER.debug("InterpreterRootPath: {}", interpreterDirPath); - this.interpreterSettingPath = Paths.get(conf.getInterpreterSettingPath()); - LOGGER.debug("InterpreterBindingPath: {}", interpreterSettingPath); - this.dependencyResolver = new DependencyResolver( - conf.getString(ConfVars.ZEPPELIN_INTERPRETER_LOCALREPO)); - this.interpreterRepositories = dependencyResolver.getRepos(); - this.interpreterGroupOrderList = Arrays.asList(conf.getString( - ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER).split(",")); - this.gson = new GsonBuilder().setPrettyPrinting().create(); - - this.angularObjectRegistryListener = angularObjectRegistryListener; - this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; - this.appEventListener = appEventListener; - init(); - } - - /** - * Load interpreter setting from interpreter-setting.json - */ - private void loadFromFile() { - if (!Files.exists(interpreterSettingPath)) { - // nothing to read - LOGGER.warn("Interpreter Setting file {} doesn't exist", interpreterSettingPath); - return; - } - - try { - InterpreterInfoSaving infoSaving = InterpreterInfoSaving.loadFromFile(interpreterSettingPath); - //TODO(zjffdu) still ugly (should move all to InterpreterInfoSaving) - for (InterpreterSetting savedInterpreterSetting : infoSaving.interpreterSettings.values()) { - savedInterpreterSetting.setConf(conf); - savedInterpreterSetting.setInterpreterSettingManager(this); - savedInterpreterSetting.setAngularObjectRegistryListener(angularObjectRegistryListener); - savedInterpreterSetting.setRemoteInterpreterProcessListener( - remoteInterpreterProcessListener); - savedInterpreterSetting.setAppEventListener(appEventListener); - savedInterpreterSetting.setDependencyResolver(dependencyResolver); - savedInterpreterSetting.setProperties(InterpreterSetting.convertInterpreterProperties( - savedInterpreterSetting.getProperties() - )); - - InterpreterSetting interpreterSettingTemplate = - interpreterSettingTemplates.get(savedInterpreterSetting.getGroup()); - // InterpreterSettingTemplate is from interpreter-setting.json which represent the latest - // InterpreterSetting, while InterpreterSetting is from interpreter.json which represent - // the user saved interpreter setting - if (interpreterSettingTemplate != null) { - savedInterpreterSetting.setInterpreterDir(interpreterSettingTemplate.getInterpreterDir()); - // merge properties from interpreter-setting.json and interpreter.json - Map mergedProperties = - new HashMap<>(InterpreterSetting.convertInterpreterProperties( - interpreterSettingTemplate.getProperties())); - mergedProperties.putAll(InterpreterSetting.convertInterpreterProperties( - savedInterpreterSetting.getProperties())); - savedInterpreterSetting.setProperties(mergedProperties); - // merge InterpreterInfo - savedInterpreterSetting.setInterpreterInfos( - interpreterSettingTemplate.getInterpreterInfos()); - } else { - LOGGER.warn("No InterpreterSetting Template found for InterpreterSetting: " - + savedInterpreterSetting.getGroup()); - } - - // Overwrite the default InterpreterSetting we registered from InterpreterSetting Templates - // remove it first - for (InterpreterSetting setting : interpreterSettings.values()) { - if (setting.getName().equals(savedInterpreterSetting.getName())) { - interpreterSettings.remove(setting.getId()); - } - } - savedInterpreterSetting.postProcessing(); - LOGGER.info("Create Interpreter Setting {} from interpreter.json", - savedInterpreterSetting.getName()); - interpreterSettings.put(savedInterpreterSetting.getId(), savedInterpreterSetting); - } - - interpreterBindings.putAll(infoSaving.interpreterBindings); - - if (infoSaving.interpreterRepositories != null) { - for (RemoteRepository repo : infoSaving.interpreterRepositories) { - if (!dependencyResolver.getRepos().contains(repo)) { - this.interpreterRepositories.add(repo); - } - } - } - } catch (IOException e) { - LOGGER.error("Fail to load interpreter setting configuration file: " - + interpreterSettingPath, e); - } - } - - public void saveToFile() throws IOException { - synchronized (interpreterSettings) { - InterpreterInfoSaving info = new InterpreterInfoSaving(); - info.interpreterBindings = interpreterBindings; - info.interpreterSettings = interpreterSettings; - info.interpreterRepositories = interpreterRepositories; - info.saveToFile(interpreterSettingPath); - } - } - - private void init() throws IOException { - - // 1. detect interpreter setting via interpreter-setting.json in each interpreter folder - // 2. detect interpreter setting in interpreter.json that is saved before - String interpreterJson = conf.getInterpreterJson(); - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - if (Files.exists(interpreterDirPath)) { - for (Path interpreterDir : Files - .newDirectoryStream(interpreterDirPath, new Filter() { - @Override - public boolean accept(Path entry) throws IOException { - return Files.exists(entry) && Files.isDirectory(entry); - } - })) { - String interpreterDirString = interpreterDir.toString(); - - /** - * Register interpreter by the following ordering - * 1. Register it from path {ZEPPELIN_HOME}/interpreter/{interpreter_name}/ - * interpreter-setting.json - * 2. Register it from interpreter-setting.json in classpath - * {ZEPPELIN_HOME}/interpreter/{interpreter_name} - */ - if (!registerInterpreterFromPath(interpreterDirString, interpreterJson)) { - if (!registerInterpreterFromResource(cl, interpreterDirString, interpreterJson)) { - LOGGER.warn("No interpreter-setting.json found in " + interpreterDirPath); - } - } - } - } else { - LOGGER.warn("InterpreterDir {} doesn't exist", interpreterDirPath); - } - - loadFromFile(); - saveToFile(); - } - - private boolean registerInterpreterFromResource(ClassLoader cl, String interpreterDir, - String interpreterJson) throws IOException { - URL[] urls = recursiveBuildLibList(new File(interpreterDir)); - ClassLoader tempClassLoader = new URLClassLoader(urls, cl); - - URL url = tempClassLoader.getResource(interpreterJson); - if (url == null) { - return false; - } - - LOGGER.debug("Reading interpreter-setting.json from {} as Resource", url); - List registeredInterpreterList = - getInterpreterListFromJson(url.openStream()); - registerInterpreterSetting(registeredInterpreterList, interpreterDir); - return true; - } - - private boolean registerInterpreterFromPath(String interpreterDir, String interpreterJson) - throws IOException { - - Path interpreterJsonPath = Paths.get(interpreterDir, interpreterJson); - if (Files.exists(interpreterJsonPath)) { - LOGGER.debug("Reading interpreter-setting.json from file {}", interpreterJsonPath); - List registeredInterpreterList = - getInterpreterListFromJson(new FileInputStream(interpreterJsonPath.toFile())); - registerInterpreterSetting(registeredInterpreterList, interpreterDir); - return true; - } - return false; - } - - private List getInterpreterListFromJson(InputStream stream) { - Type registeredInterpreterListType = new TypeToken>() { - }.getType(); - return gson.fromJson(new InputStreamReader(stream), registeredInterpreterListType); - } - - private void registerInterpreterSetting(List registeredInterpreters, - String interpreterDir) throws IOException { - - Map properties = new HashMap<>(); - List interpreterInfos = new ArrayList<>(); - InterpreterOption option = defaultOption; - String group = null; - InterpreterRunner runner = null; - for (RegisteredInterpreter registeredInterpreter : registeredInterpreters) { - //TODO(zjffdu) merge RegisteredInterpreter & InterpreterInfo - InterpreterInfo interpreterInfo = - new InterpreterInfo(registeredInterpreter.getClassName(), registeredInterpreter.getName(), - registeredInterpreter.isDefaultInterpreter(), registeredInterpreter.getEditor()); - group = registeredInterpreter.getGroup(); - runner = registeredInterpreter.getRunner(); - // use defaultOption if it is not specified in interpreter-setting.json - if (registeredInterpreter.getOption() != null) { - option = registeredInterpreter.getOption(); - } - properties.putAll(registeredInterpreter.getProperties()); - interpreterInfos.add(interpreterInfo); - } - - InterpreterSetting interpreterSettingTemplate = new InterpreterSetting.Builder() - .setGroup(group) - .setName(group) - .setInterpreterInfos(interpreterInfos) - .setProperties(properties) - .setDependencies(new ArrayList()) - .setOption(option) - .setRunner(runner) - .setInterpreterDir(interpreterDir) - .setRunner(runner) - .setConf(conf) - .setIntepreterSettingManager(this) - .create(); - - LOGGER.info("Register InterpreterSettingTemplate & InterpreterSetting: {}", - interpreterSettingTemplate.getName()); - interpreterSettingTemplates.put(interpreterSettingTemplate.getName(), - interpreterSettingTemplate); - - InterpreterSetting interpreterSetting = new InterpreterSetting(interpreterSettingTemplate); - interpreterSetting.setAngularObjectRegistryListener(angularObjectRegistryListener); - interpreterSetting.setRemoteInterpreterProcessListener(remoteInterpreterProcessListener); - interpreterSetting.setAppEventListener(appEventListener); - interpreterSetting.setDependencyResolver(dependencyResolver); - interpreterSetting.setInterpreterSettingManager(this); - interpreterSetting.postProcessing(); - interpreterSettings.put(interpreterSetting.getId(), interpreterSetting); - } - - @VisibleForTesting - public InterpreterSetting getDefaultInterpreterSetting(String noteId) { - return getInterpreterSettings(noteId).get(0); - } - - public List getInterpreterSettings(String noteId) { - List settings = new ArrayList<>(); - synchronized (interpreterSettings) { - List interpreterSettingIds = interpreterBindings.get(noteId); - if (interpreterSettingIds != null) { - for (String settingId : interpreterSettingIds) { - if (interpreterSettings.containsKey(settingId)) { - settings.add(interpreterSettings.get(settingId)); - } else { - LOGGER.warn("InterpreterSetting {} has been removed, but note {} still bind to it.", - settingId, noteId); - } - } - } - } - return settings; - } - - public InterpreterGroup getInterpreterGroupById(String groupId) { - for (InterpreterSetting setting : interpreterSettings.values()) { - InterpreterGroup interpreterGroup = setting.getInterpreterGroup(groupId); - if (interpreterGroup != null) { - return interpreterGroup; - } - } - return null; - } - - //TODO(zjffdu) logic here is a little ugly - public Map getEditorSetting(Interpreter interpreter, String user, String noteId, - String replName) { - Map editor = DEFAULT_EDITOR; - String group = StringUtils.EMPTY; - try { - String defaultSettingName = getDefaultInterpreterSetting(noteId).getName(); - List intpSettings = getInterpreterSettings(noteId); - for (InterpreterSetting intpSetting : intpSettings) { - String[] replNameSplit = replName.split("\\."); - if (replNameSplit.length == 2) { - group = replNameSplit[0]; - } - // when replName is 'name' of interpreter - if (defaultSettingName.equals(intpSetting.getName())) { - editor = intpSetting.getEditorFromSettingByClassName(interpreter.getClassName()); - } - // when replName is 'alias name' of interpreter or 'group' of interpreter - if (replName.equals(intpSetting.getName()) || group.equals(intpSetting.getName())) { - editor = intpSetting.getEditorFromSettingByClassName(interpreter.getClassName()); - break; - } - } - } catch (NullPointerException e) { - // Use `debug` level because this log occurs frequently - LOGGER.debug("Couldn't get interpreter editor setting"); - } - return editor; - } - - public List getAllInterpreterGroup() { - List interpreterGroups = new ArrayList<>(); - for (InterpreterSetting interpreterSetting : interpreterSettings.values()) { - interpreterGroups.addAll(interpreterSetting.getAllInterpreterGroups()); - } - return interpreterGroups; - } - - //TODO(zjffdu) move Resource related api to ResourceManager - public ResourceSet getAllResources() { - return getAllResourcesExcept(null); - } - - private ResourceSet getAllResourcesExcept(String interpreterGroupExcludsion) { - ResourceSet resourceSet = new ResourceSet(); - for (InterpreterGroup intpGroup : getAllInterpreterGroup()) { - if (interpreterGroupExcludsion != null && - intpGroup.getId().equals(interpreterGroupExcludsion)) { - continue; - } - - RemoteInterpreterProcess remoteInterpreterProcess = intpGroup.getRemoteInterpreterProcess(); - if (remoteInterpreterProcess == null) { - ResourcePool localPool = intpGroup.getResourcePool(); - if (localPool != null) { - resourceSet.addAll(localPool.getAll()); - } - } else if (remoteInterpreterProcess.isRunning()) { - List resourceList = remoteInterpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction>() { - @Override - public List call(RemoteInterpreterService.Client client) throws Exception { - return client.resourcePoolGetAll(); - } - }); - for (String res : resourceList) { - resourceSet.add(Resource.fromJson(res)); - } - } - } - return resourceSet; - } - - public void removeResourcesBelongsToParagraph(String noteId, String paragraphId) { - for (InterpreterGroup intpGroup : getAllInterpreterGroup()) { - ResourceSet resourceSet = new ResourceSet(); - RemoteInterpreterProcess remoteInterpreterProcess = intpGroup.getRemoteInterpreterProcess(); - if (remoteInterpreterProcess == null) { - ResourcePool localPool = intpGroup.getResourcePool(); - if (localPool != null) { - resourceSet.addAll(localPool.getAll()); - } - if (noteId != null) { - resourceSet = resourceSet.filterByNoteId(noteId); - } - if (paragraphId != null) { - resourceSet = resourceSet.filterByParagraphId(paragraphId); - } - - for (Resource r : resourceSet) { - localPool.remove( - r.getResourceId().getNoteId(), - r.getResourceId().getParagraphId(), - r.getResourceId().getName()); - } - } else if (remoteInterpreterProcess.isRunning()) { - List resourceList = remoteInterpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction>() { - @Override - public List call(RemoteInterpreterService.Client client) throws Exception { - return client.resourcePoolGetAll(); - } - }); - for (String res : resourceList) { - resourceSet.add(Resource.fromJson(res)); - } - - if (noteId != null) { - resourceSet = resourceSet.filterByNoteId(noteId); - } - if (paragraphId != null) { - resourceSet = resourceSet.filterByParagraphId(paragraphId); - } - - for (final Resource r : resourceSet) { - remoteInterpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - - @Override - public Void call(RemoteInterpreterService.Client client) throws Exception { - client.resourceRemove( - r.getResourceId().getNoteId(), - r.getResourceId().getParagraphId(), - r.getResourceId().getName()); - return null; - } - }); - } - } - } - } - - public void removeResourcesBelongsToNote(String noteId) { - removeResourcesBelongsToParagraph(noteId, null); - } - - /** - * Overwrite dependency jar under local-repo/{interpreterId} - * if jar file in original path is changed - */ - private void copyDependenciesFromLocalPath(final InterpreterSetting setting) { - setting.setStatus(InterpreterSetting.Status.DOWNLOADING_DEPENDENCIES); - synchronized (interpreterSettings) { - final Thread t = new Thread() { - public void run() { - try { - List deps = setting.getDependencies(); - if (deps != null) { - for (Dependency d : deps) { - File destDir = new File( - conf.getRelativeDir(ConfVars.ZEPPELIN_DEP_LOCALREPO)); - - int numSplits = d.getGroupArtifactVersion().split(":").length; - if (!(numSplits >= 3 && numSplits <= 6)) { - dependencyResolver.copyLocalDependency(d.getGroupArtifactVersion(), - new File(destDir, setting.getId())); - } - } - } - setting.setStatus(InterpreterSetting.Status.READY); - } catch (Exception e) { - LOGGER.error(String.format("Error while copying deps for interpreter group : %s," + - " go to interpreter setting page click on edit and save it again to make " + - "this interpreter work properly.", - setting.getGroup()), e); - setting.setErrorReason(e.getLocalizedMessage()); - setting.setStatus(InterpreterSetting.Status.ERROR); - } finally { - - } - } - }; - t.start(); - } - } - - /** - * Return ordered interpreter setting list. - * The list does not contain more than one setting from the same interpreter class. - * Order by InterpreterClass (order defined by ZEPPELIN_INTERPRETERS), Interpreter setting name - */ - public List getInterpreterSettingIds() { - List settingIdList = new ArrayList<>(); - for (InterpreterSetting interpreterSetting : get()) { - settingIdList.add(interpreterSetting.getId()); - } - return settingIdList; - } - - public InterpreterSetting createNewSetting(String name, String group, - List dependencies, InterpreterOption option, Map p) - throws IOException { - - if (name.indexOf(".") >= 0) { - throw new IOException("'.' is invalid for InterpreterSetting name."); - } - // check if name is existed - for (InterpreterSetting interpreterSetting : interpreterSettings.values()) { - if (interpreterSetting.getName().equals(name)) { - throw new IOException("Interpreter " + name + " already existed"); - } - } - InterpreterSetting setting = new InterpreterSetting(interpreterSettingTemplates.get(group)); - setting.setName(name); - setting.setGroup(group); - //TODO(zjffdu) Should use setDependencies - setting.appendDependencies(dependencies); - setting.setInterpreterOption(option); - setting.setProperties(p); - setting.setAppEventListener(appEventListener); - setting.setRemoteInterpreterProcessListener(remoteInterpreterProcessListener); - setting.setDependencyResolver(dependencyResolver); - setting.setAngularObjectRegistryListener(angularObjectRegistryListener); - setting.setInterpreterSettingManager(this); - setting.postProcessing(); - interpreterSettings.put(setting.getId(), setting); - saveToFile(); - return setting; - } - - @VisibleForTesting - public void addInterpreterSetting(InterpreterSetting interpreterSetting) { - interpreterSettingTemplates.put(interpreterSetting.getName(), interpreterSetting); - interpreterSetting.setAppEventListener(appEventListener); - interpreterSetting.setDependencyResolver(dependencyResolver); - interpreterSetting.setAngularObjectRegistryListener(angularObjectRegistryListener); - interpreterSetting.setRemoteInterpreterProcessListener(remoteInterpreterProcessListener); - interpreterSetting.setInterpreterSettingManager(this); - interpreterSettings.put(interpreterSetting.getId(), interpreterSetting); - } - - /** - * map interpreter ids into noteId - * - * @param user user name - * @param noteId note id - * @param settingIdList InterpreterSetting id list - */ - public void setInterpreterBinding(String user, String noteId, List settingIdList) - throws IOException { - List unBindedSettingIdList = new LinkedList<>(); - - synchronized (interpreterSettings) { - List oldSettingIdList = interpreterBindings.get(noteId); - if (oldSettingIdList != null) { - for (String oldSettingId : oldSettingIdList) { - if (!settingIdList.contains(oldSettingId)) { - unBindedSettingIdList.add(oldSettingId); - } - } - } - interpreterBindings.put(noteId, settingIdList); - saveToFile(); - - for (String settingId : unBindedSettingIdList) { - InterpreterSetting interpreterSetting = interpreterSettings.get(settingId); - //TODO(zjffdu) Add test for this scenario - //only close Interpreters when it is note scoped - if (interpreterSetting.getOption().perNoteIsolated() || - interpreterSetting.getOption().perNoteScoped()) { - interpreterSetting.closeInterpreters(user, noteId); - } - } - } - } - - public List getInterpreterBinding(String noteId) { - return interpreterBindings.get(noteId); - } - - @VisibleForTesting - public void closeNote(String user, String noteId) { - // close interpreters in this note session - LOGGER.info("Close Note: {}", noteId); - List settings = getInterpreterSettings(noteId); - for (InterpreterSetting setting : settings) { - setting.closeInterpreters(user, noteId); - } - } - - public Map getInterpreterSettingTemplates() { - return interpreterSettingTemplates; - } - - private URL[] recursiveBuildLibList(File path) throws MalformedURLException { - URL[] urls = new URL[0]; - if (path == null || !path.exists()) { - return urls; - } else if (path.getName().startsWith(".")) { - return urls; - } else if (path.isDirectory()) { - File[] files = path.listFiles(); - if (files != null) { - for (File f : files) { - urls = (URL[]) ArrayUtils.addAll(urls, recursiveBuildLibList(f)); - } - } - return urls; - } else { - return new URL[]{path.toURI().toURL()}; - } - } - - public List getRepositories() { - return this.interpreterRepositories; - } - - public void addRepository(String id, String url, boolean snapshot, Authentication auth, - Proxy proxy) throws IOException { - dependencyResolver.addRepo(id, url, snapshot, auth, proxy); - saveToFile(); - } - - public void removeRepository(String id) throws IOException { - dependencyResolver.delRepo(id); - saveToFile(); - } - - public void removeNoteInterpreterSettingBinding(String user, String noteId) throws IOException { - setInterpreterBinding(user, noteId, new ArrayList()); - interpreterBindings.remove(noteId); - } - - /** - * Change interpreter property and restart - */ - public void setPropertyAndRestart(String id, InterpreterOption option, - Map properties, - List dependencies) throws IOException { - synchronized (interpreterSettings) { - InterpreterSetting intpSetting = interpreterSettings.get(id); - if (intpSetting != null) { - try { - intpSetting.close(); - intpSetting.setOption(option); - intpSetting.setProperties(properties); - intpSetting.setDependencies(dependencies); - intpSetting.postProcessing(); - saveToFile(); - } catch (Exception e) { - loadFromFile(); - throw e; - } - } else { - throw new InterpreterException("Interpreter setting id " + id + " not found"); - } - } - } - - // restart in note page - public void restart(String settingId, String noteId, String user) { - InterpreterSetting intpSetting = interpreterSettings.get(settingId); - Preconditions.checkNotNull(intpSetting); - synchronized (interpreterSettings) { - intpSetting = interpreterSettings.get(settingId); - // Check if dependency in specified path is changed - // If it did, overwrite old dependency jar with new one - if (intpSetting != null) { - //clean up metaInfos - intpSetting.setInfos(null); - copyDependenciesFromLocalPath(intpSetting); - - if (user.equals("anonymous")) { - intpSetting.close(); - } else { - intpSetting.closeInterpreters(user, noteId); - } - - } else { - throw new InterpreterException("Interpreter setting id " + settingId + " not found"); - } - } - } - - public void restart(String id) { - restart(id, "", "anonymous"); - } - - public InterpreterSetting get(String id) { - synchronized (interpreterSettings) { - return interpreterSettings.get(id); - } - } - - @VisibleForTesting - public InterpreterSetting getByName(String name) { - for (InterpreterSetting interpreterSetting : interpreterSettings.values()) { - if (interpreterSetting.getName().equals(name)) { - return interpreterSetting; - } - } - throw new RuntimeException("No InterpreterSetting: " + name); - } - - public void remove(String id) throws IOException { - // 1. close interpreter groups of this interpreter setting - // 2. remove this interpreter setting - // 3. remove this interpreter setting from note binding - // 4. clean local repo directory - LOGGER.info("Remove interpreter setting: " + id); - synchronized (interpreterSettings) { - if (interpreterSettings.containsKey(id)) { - - InterpreterSetting intp = interpreterSettings.get(id); - intp.close(); - interpreterSettings.remove(id); - for (List settings : interpreterBindings.values()) { - Iterator it = settings.iterator(); - while (it.hasNext()) { - String settingId = it.next(); - if (settingId.equals(id)) { - it.remove(); - } - } - } - saveToFile(); - } - } - - File localRepoDir = new File(conf.getInterpreterLocalRepoPath() + "/" + id); - FileUtils.deleteDirectory(localRepoDir); - } - - /** - * Get interpreter settings - */ - public List get() { - synchronized (interpreterSettings) { - List orderedSettings = new ArrayList<>(interpreterSettings.values()); - Collections.sort(orderedSettings, new Comparator() { - @Override - public int compare(InterpreterSetting o1, InterpreterSetting o2) { - int i = interpreterGroupOrderList.indexOf(o1.getGroup()); - int j = interpreterGroupOrderList.indexOf(o2.getGroup()); - if (i < 0) { - LOGGER.warn("InterpreterGroup " + o1.getGroup() - + " is not specified in " + ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER.getVarName()); - // move the unknown interpreter to last - i = Integer.MAX_VALUE; - } - if (j < 0) { - LOGGER.warn("InterpreterGroup " + o2.getGroup() - + " is not specified in " + ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER.getVarName()); - // move the unknown interpreter to last - j = Integer.MAX_VALUE; - } - if (i < j) { - return -1; - } else if (i > j) { - return 1; - } else { - return 0; - } - } - }); - return orderedSettings; - } - } - - @VisibleForTesting - public List getSettingIds() { - List settingIds = new ArrayList<>(); - for (InterpreterSetting interpreterSetting : get()) { - settingIds.add(interpreterSetting.getId()); - } - return settingIds; - } - - public void close(String settingId) { - get(settingId).close(); - } - - public void close() { - List closeThreads = new LinkedList<>(); - synchronized (interpreterSettings) { - Collection intpSettings = interpreterSettings.values(); - for (final InterpreterSetting intpSetting : intpSettings) { - Thread t = new Thread() { - public void run() { - intpSetting.close(); - } - }; - t.start(); - closeThreads.add(t); - } - } - - for (Thread t : closeThreads) { - try { - t.join(); - } catch (InterruptedException e) { - LOGGER.error("Can't close interpreterGroup", e); - } - } - } - -} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java deleted file mode 100644 index bb90dd89fb3..00000000000 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java +++ /dev/null @@ -1,371 +0,0 @@ -/* - * 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.interpreter.remote; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import org.apache.thrift.TException; -import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.display.AngularObject; -import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.display.GUI; -import org.apache.zeppelin.display.Input; -import org.apache.zeppelin.interpreter.*; -import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterContext; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterResult; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterResultMessage; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; -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 java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -/** - * Proxy for Interpreter instance that runs on separate process - */ -public class RemoteInterpreter extends Interpreter { - private static final Logger LOGGER = LoggerFactory.getLogger(RemoteInterpreter.class); - private static final Gson gson = new Gson(); - - - private String className; - private String sessionId; - private String userName; - private FormType formType; - - private RemoteInterpreterProcess interpreterProcess; - private volatile boolean isOpened = false; - private volatile boolean isCreated = false; - - /** - * Remote interpreter and manage interpreter process - */ - public RemoteInterpreter(Properties properties, - String sessionId, - String className, - String userName) { - super(properties); - this.sessionId = sessionId; - this.className = className; - this.userName = userName; - } - - public boolean isOpened() { - return isOpened; - } - - @Override - public String getClassName() { - return className; - } - - public String getSessionId() { - return this.sessionId; - } - - public synchronized RemoteInterpreterProcess getOrCreateInterpreterProcess() { - if (this.interpreterProcess != null) { - return this.interpreterProcess; - } - InterpreterGroup intpGroup = getInterpreterGroup(); - this.interpreterProcess = intpGroup.getOrCreateInterpreterProcess(); - synchronized (interpreterProcess) { - if (!interpreterProcess.isRunning()) { - interpreterProcess.start(userName, false); - interpreterProcess.getRemoteInterpreterEventPoller() - .setInterpreterProcess(interpreterProcess); - interpreterProcess.getRemoteInterpreterEventPoller().setInterpreterGroup(intpGroup); - interpreterProcess.getRemoteInterpreterEventPoller().start(); - } - } - return interpreterProcess; - } - - @Override - public void open() { - synchronized (this) { - if (!isOpened) { - // create all the interpreters of the same session first, then Open the internal interpreter - // of this RemoteInterpreter. - // The why we we create all the interpreter of the session is because some interpreter - // depends on other interpreter. e.g. PySparkInterpreter depends on SparkInterpreter. - // also see method Interpreter.getInterpreterInTheSameSessionByClassName - for (Interpreter interpreter : getInterpreterGroup().getOrCreateSession( - userName, sessionId)) { - ((RemoteInterpreter) interpreter).internal_create(); - } - - interpreterProcess.callRemoteFunction(new RemoteInterpreterProcess.RemoteFunction() { - @Override - public Void call(Client client) throws Exception { - LOGGER.info("Open RemoteInterpreter {}", getClassName()); - client.open(sessionId, className); - // Push angular object loaded from JSON file to remote interpreter - synchronized (getInterpreterGroup()) { - if (!getInterpreterGroup().isAngularRegistryPushed()) { - pushAngularObjectRegistryToRemote(client); - getInterpreterGroup().setAngularRegistryPushed(true); - } - } - return null; - } - }); - isOpened = true; - } - } - } - - private void internal_create() { - synchronized (this) { - if (!isCreated) { - RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); - interpreterProcess.callRemoteFunction(new RemoteInterpreterProcess.RemoteFunction() { - @Override - public Void call(Client client) throws Exception { - LOGGER.info("Create RemoteInterpreter {}", getClassName()); - client.createInterpreter(getInterpreterGroup().getId(), sessionId, - className, (Map) property, userName); - return null; - } - }); - isCreated = true; - } - } - } - - - @Override - public void close() { - if (isOpened) { - RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); - interpreterProcess.callRemoteFunction(new RemoteInterpreterProcess.RemoteFunction() { - @Override - public Void call(Client client) throws Exception { - client.close(sessionId, className); - return null; - } - }); - } else { - LOGGER.warn("close is called when RemoterInterpreter is not opened for " + className); - } - } - - @Override - public InterpreterResult interpret(final String st, final InterpreterContext context) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("st:\n{}", st); - } - - final FormType form = getFormType(); - RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); - InterpreterContextRunnerPool interpreterContextRunnerPool = interpreterProcess - .getInterpreterContextRunnerPool(); - List runners = context.getRunners(); - if (runners != null && runners.size() != 0) { - // assume all runners in this InterpreterContext have the same note id - String noteId = runners.get(0).getNoteId(); - - interpreterContextRunnerPool.clear(noteId); - interpreterContextRunnerPool.addAll(noteId, runners); - } - return interpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public InterpreterResult call(Client client) throws Exception { - - RemoteInterpreterResult remoteResult = client.interpret( - sessionId, className, st, convert(context)); - Map remoteConfig = (Map) gson.fromJson( - remoteResult.getConfig(), new TypeToken>() { - }.getType()); - context.getConfig().clear(); - context.getConfig().putAll(remoteConfig); - GUI currentGUI = context.getGui(); - if (form == FormType.NATIVE) { - GUI remoteGui = GUI.fromJson(remoteResult.getGui()); - currentGUI.clear(); - currentGUI.setParams(remoteGui.getParams()); - currentGUI.setForms(remoteGui.getForms()); - } else if (form == FormType.SIMPLE) { - final Map currentForms = currentGUI.getForms(); - final Map currentParams = currentGUI.getParams(); - final GUI remoteGUI = GUI.fromJson(remoteResult.getGui()); - final Map remoteForms = remoteGUI.getForms(); - final Map remoteParams = remoteGUI.getParams(); - currentForms.putAll(remoteForms); - currentParams.putAll(remoteParams); - } - - InterpreterResult result = convert(remoteResult); - return result; - } - } - ); - - } - - @Override - public void cancel(final InterpreterContext context) { - if (!isOpened) { - LOGGER.warn("Cancel is called when RemoterInterpreter is not opened for " + className); - return; - } - RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); - interpreterProcess.callRemoteFunction(new RemoteInterpreterProcess.RemoteFunction() { - @Override - public Void call(Client client) throws Exception { - client.cancel(sessionId, className, convert(context)); - return null; - } - }); - } - - @Override - public FormType getFormType() { - if (formType != null) { - return formType; - } - - // it is possible to call getFormType before it is opened - synchronized (this) { - if (!isOpened) { - open(); - } - } - RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); - FormType type = interpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public FormType call(Client client) throws Exception { - formType = FormType.valueOf(client.getFormType(sessionId, className)); - return formType; - } - }); - return type; - } - - @Override - public int getProgress(final InterpreterContext context) { - if (!isOpened) { - LOGGER.warn("getProgress is called when RemoterInterpreter is not opened for " + className); - return 0; - } - RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); - return interpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public Integer call(Client client) throws Exception { - return client.getProgress(sessionId, className, convert(context)); - } - }); - } - - - @Override - public List completion(final String buf, final int cursor, - final InterpreterContext interpreterContext) { - if (!isOpened) { - LOGGER.warn("completion is called when RemoterInterpreter is not opened for " + className); - return new ArrayList<>(); - } - RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); - return interpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction>() { - @Override - public List call(Client client) throws Exception { - return client.completion(sessionId, className, buf, cursor, - convert(interpreterContext)); - } - }); - } - - public String getStatus(final String jobId) { - if (!isOpened) { - LOGGER.warn("getStatus is called when RemoteInterpreter is not opened for " + className); - return Job.Status.UNKNOWN.name(); - } - RemoteInterpreterProcess interpreterProcess = getOrCreateInterpreterProcess(); - return interpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public String call(Client client) throws Exception { - return client.getStatus(sessionId, jobId); - } - }); - } - - //TODO(zjffdu) Share the Scheduler in the same session or in the same InterpreterGroup ? - @Override - public Scheduler getScheduler() { - int maxConcurrency = Integer.parseInt( - property.getProperty("zeppelin.interpreter.max.poolsize", - ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_MAX_POOL_SIZE.getIntValue() + "")); - return SchedulerFactory.singleton().createOrGetRemoteScheduler( - RemoteInterpreter.class.getName() + "-" + sessionId, - sessionId, this, maxConcurrency); - } - - private RemoteInterpreterContext convert(InterpreterContext ic) { - return new RemoteInterpreterContext(ic.getNoteId(), ic.getParagraphId(), ic.getReplName(), - ic.getParagraphTitle(), ic.getParagraphText(), gson.toJson(ic.getAuthenticationInfo()), - gson.toJson(ic.getConfig()), gson.toJson(ic.getGui()), gson.toJson(ic.getRunners())); - } - - private InterpreterResult convert(RemoteInterpreterResult result) { - InterpreterResult r = new InterpreterResult( - InterpreterResult.Code.valueOf(result.getCode())); - - for (RemoteInterpreterResultMessage m : result.getMsg()) { - r.add(InterpreterResult.Type.valueOf(m.getType()), m.getData()); - } - - return r; - } - - /** - * Push local angular object registry to - * remote interpreter. This method should be - * call ONLY once when the first Interpreter is created - */ - private void pushAngularObjectRegistryToRemote(Client client) throws TException { - final AngularObjectRegistry angularObjectRegistry = this.getInterpreterGroup() - .getAngularObjectRegistry(); - if (angularObjectRegistry != null && angularObjectRegistry.getRegistry() != null) { - final Map> registry = angularObjectRegistry - .getRegistry(); - LOGGER.info("Push local angular object registry from ZeppelinServer to" + - " remote interpreter group {}", this.getInterpreterGroup().getId()); - final java.lang.reflect.Type registryType = new TypeToken>>() { - }.getType(); - client.angularRegistryPush(gson.toJson(registry, registryType)); - } - } - - @Override - public String toString() { - return "RemoteInterpreter_" + className + "_" + sessionId; - } -} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java index f3bce2fa091..26c9d7994ed 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java @@ -29,7 +29,6 @@ import org.apache.zeppelin.interpreter.RemoteZeppelinServerResource; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterEvent; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterEventType; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; import org.apache.zeppelin.interpreter.thrift.ZeppelinServerResourceParagraphRunner; import org.apache.zeppelin.resource.Resource; @@ -39,7 +38,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.ByteBuffer; @@ -86,6 +84,7 @@ public void setInterpreterGroup(InterpreterGroup interpreterGroup) { @Override public void run() { + Client client = null; AppendOutputRunner runner = new AppendOutputRunner(listener); ScheduledFuture appendFuture = appendService.scheduleWithFixedDelay( runner, 0, AppendOutputRunner.BUFFER_TIME_MS, TimeUnit.MILLISECONDS); @@ -101,14 +100,26 @@ public void run() { continue; } - RemoteInterpreterEvent event = interpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public RemoteInterpreterEvent call(Client client) throws Exception { - return client.getEvent(); - } - } - ); + try { + client = interpreterProcess.getClient(); + } catch (Exception e1) { + logger.error("Can't get RemoteInterpreterEvent", e1); + waitQuietly(); + continue; + } + + RemoteInterpreterEvent event = null; + boolean broken = false; + try { + event = client.getEvent(); + } catch (TException e) { + broken = true; + logger.error("Can't get RemoteInterpreterEvent", e); + waitQuietly(); + continue; + } finally { + interpreterProcess.releaseClient(client, broken); + } AngularObjectRegistry angularObjectRegistry = interpreterGroup.getAngularObjectRegistry(); @@ -275,7 +286,10 @@ private void progressRemoteZeppelinControlEvent( boolean broken = false; final Gson gson = new Gson(); final String eventOwnerKey = reqResourceBody.getOwnerKey(); + Client interpreterServerMain = null; try { + interpreterServerMain = interpreterProcess.getClient(); + final Client eventClient = interpreterServerMain; if (resourceType == RemoteZeppelinServerResource.Type.PARAGRAPH_RUNNERS) { final List remoteRunners = new LinkedList<>(); @@ -294,6 +308,7 @@ private void progressRemoteZeppelinControlEvent( @Override public void onFinished(Object resultObject) { + boolean clientBroken = false; if (resultObject != null && resultObject instanceof List) { List runnerList = (List) resultObject; @@ -309,15 +324,15 @@ public void onFinished(Object resultObject) { resResource.setResourceType(RemoteZeppelinServerResource.Type.PARAGRAPH_RUNNERS); resResource.setData(remoteRunners); - interpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public Void call(Client client) throws Exception { - client.onReceivedZeppelinResource(resResource.toJson()); - return null; - } - } - ); + try { + eventClient.onReceivedZeppelinResource(resResource.toJson()); + } catch (Exception e) { + clientBroken = true; + logger.error("Can't get RemoteInterpreterEvent", e); + waitQuietly(); + } finally { + interpreterProcess.releaseClient(eventClient, clientBroken); + } } } @@ -331,32 +346,39 @@ public void onError() { reqRunnerContext.getNoteId(), reqRunnerContext.getParagraphId(), callBackEvent); } } catch (Exception e) { + broken = true; logger.error("Can't get RemoteInterpreterEvent", e); waitQuietly(); + } finally { + interpreterProcess.releaseClient(interpreterServerMain, broken); } } - private void sendResourcePoolResponseGetAll(final ResourceSet resourceSet) { - interpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public Void call(Client client) throws Exception { - List resourceList = new LinkedList<>(); - for (Resource r : resourceSet) { - resourceList.add(r.toJson()); - } - client.resourcePoolResponseGetAll(resourceList); - return null; - } - } - ); + private void sendResourcePoolResponseGetAll(ResourceSet resourceSet) { + Client client = null; + boolean broken = false; + try { + client = interpreterProcess.getClient(); + List resourceList = new LinkedList<>(); + Gson gson = new Gson(); + for (Resource r : resourceSet) { + resourceList.add(gson.toJson(r)); + } + client.resourcePoolResponseGetAll(resourceList); + } catch (Exception e) { + logger.error(e.getMessage(), e); + broken = true; + } finally { + if (client != null) { + interpreterProcess.releaseClient(client, broken); + } + } } private ResourceSet getAllResourcePoolExcept() { ResourceSet resourceSet = new ResourceSet(); - for (InterpreterGroup intpGroup : interpreterGroup.getInterpreterSetting() - .getInterpreterSettingManager().getAllInterpreterGroup()) { + for (InterpreterGroup intpGroup : InterpreterGroup.getAll()) { if (intpGroup.getId().equals(interpreterGroup.getId())) { continue; } @@ -368,94 +390,115 @@ private ResourceSet getAllResourcePoolExcept() { resourceSet.addAll(localPool.getAll()); } } else if (interpreterProcess.isRunning()) { - List resourceList = remoteInterpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction>() { - @Override - public List call(Client client) throws Exception { - return client.resourcePoolGetAll(); - } - } - ); - for (String res : resourceList) { - resourceSet.add(Resource.fromJson(res)); + Client client = null; + boolean broken = false; + try { + client = remoteInterpreterProcess.getClient(); + List resourceList = client.resourcePoolGetAll(); + Gson gson = new Gson(); + for (String res : resourceList) { + resourceSet.add(Resource.fromJson(res)); + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + broken = true; + } finally { + if (client != null) { + intpGroup.getRemoteInterpreterProcess().releaseClient(client, broken); + } } } } return resourceSet; } - private void sendResourceResponseGet(final ResourceId resourceId, final Object o) { - interpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public Void call(Client client) throws Exception { - String rid = resourceId.toJson(); - ByteBuffer obj; - if (o == null) { - obj = ByteBuffer.allocate(0); - } else { - obj = Resource.serializeObject(o); - } - client.resourceResponseGet(rid, obj); - return null; - } - } - ); + private void sendResourceResponseGet(ResourceId resourceId, Object o) { + Client client = null; + boolean broken = false; + try { + client = interpreterProcess.getClient(); + Gson gson = new Gson(); + String rid = gson.toJson(resourceId); + ByteBuffer obj; + if (o == null) { + obj = ByteBuffer.allocate(0); + } else { + obj = Resource.serializeObject(o); + } + client.resourceResponseGet(rid, obj); + } catch (Exception e) { + logger.error(e.getMessage(), e); + broken = true; + } finally { + if (client != null) { + interpreterProcess.releaseClient(client, broken); + } + } } - private Object getResource(final ResourceId resourceId) { - InterpreterGroup intpGroup = interpreterGroup.getInterpreterSetting() - .getInterpreterSettingManager() - .getInterpreterGroupById(resourceId.getResourcePoolId()); + private Object getResource(ResourceId resourceId) { + InterpreterGroup intpGroup = InterpreterGroup.getByInterpreterGroupId( + resourceId.getResourcePoolId()); if (intpGroup == null) { return null; } RemoteInterpreterProcess remoteInterpreterProcess = intpGroup.getRemoteInterpreterProcess(); - ByteBuffer buffer = remoteInterpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public ByteBuffer call(Client client) throws Exception { - return client.resourceGet( - resourceId.getNoteId(), - resourceId.getParagraphId(), - resourceId.getName()); - } + if (remoteInterpreterProcess == null) { + ResourcePool localPool = intpGroup.getResourcePool(); + if (localPool != null) { + return localPool.get(resourceId.getName()); + } + } else if (interpreterProcess.isRunning()) { + Client client = null; + boolean broken = false; + try { + client = remoteInterpreterProcess.getClient(); + ByteBuffer res = client.resourceGet( + resourceId.getNoteId(), + resourceId.getParagraphId(), + resourceId.getName()); + Object o = Resource.deserializeObject(res); + return o; + } catch (Exception e) { + logger.error(e.getMessage(), e); + broken = true; + } finally { + if (client != null) { + intpGroup.getRemoteInterpreterProcess().releaseClient(client, broken); } - ); + } + } + return null; + } + public void sendInvokeMethodResult(InvokeResourceMethodEventMessage message, Object o) { + Client client = null; + boolean broken = false; try { - Object o = Resource.deserializeObject(buffer); - return o; + client = interpreterProcess.getClient(); + Gson gson = new Gson(); + String invokeMessage = gson.toJson(message); + ByteBuffer obj; + if (o == null) { + obj = ByteBuffer.allocate(0); + } else { + obj = Resource.serializeObject(o); + } + client.resourceResponseInvokeMethod(invokeMessage, obj); } catch (Exception e) { logger.error(e.getMessage(), e); + broken = true; + } finally { + if (client != null) { + interpreterProcess.releaseClient(client, broken); + } } - return null; } - public void sendInvokeMethodResult(final InvokeResourceMethodEventMessage message, - final Object o) { - interpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public Void call(Client client) throws Exception { - String invokeMessage = message.toJson(); - ByteBuffer obj; - if (o == null) { - obj = ByteBuffer.allocate(0); - } else { - obj = Resource.serializeObject(o); - } - client.resourceResponseInvokeMethod(invokeMessage, obj); - return null; - } - } - ); - } - - private Object invokeResourceMethod(final InvokeResourceMethodEventMessage message) { - final ResourceId resourceId = message.resourceId; - InterpreterGroup intpGroup = interpreterGroup.getInterpreterSetting() - .getInterpreterSettingManager().getInterpreterGroupById(resourceId.getResourcePoolId()); + private Object invokeResourceMethod(InvokeResourceMethodEventMessage message) { + ResourceId resourceId = message.resourceId; + InterpreterGroup intpGroup = InterpreterGroup.getByInterpreterGroupId( + resourceId.getResourcePoolId()); if (intpGroup == null) { return null; } @@ -486,25 +529,25 @@ private Object invokeResourceMethod(final InvokeResourceMethodEventMessage messa return null; } } else if (interpreterProcess.isRunning()) { - ByteBuffer res = interpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public ByteBuffer call(Client client) throws Exception { - return client.resourceInvokeMethod( - resourceId.getNoteId(), - resourceId.getParagraphId(), - resourceId.getName(), - message.toJson()); - } - } - ); - + Client client = null; + boolean broken = false; try { - return Resource.deserializeObject(res); + client = remoteInterpreterProcess.getClient(); + ByteBuffer res = client.resourceInvokeMethod( + resourceId.getNoteId(), + resourceId.getParagraphId(), + resourceId.getName(), + gson.toJson(message)); + Object o = Resource.deserializeObject(res); + return o; } catch (Exception e) { logger.error(e.getMessage(), e); + broken = true; + } finally { + if (client != null) { + intpGroup.getRemoteInterpreterProcess().releaseClient(client, broken); + } } - return null; } return null; } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java index a78088c4cb5..1d48a1e6f6e 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java @@ -20,13 +20,10 @@ import org.apache.commons.pool2.impl.GenericObjectPool; import org.apache.thrift.TException; import org.apache.zeppelin.helium.ApplicationEventListener; -import org.apache.zeppelin.interpreter.InterpreterException; import org.apache.zeppelin.interpreter.InterpreterGroup; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; /** @@ -35,6 +32,9 @@ public abstract class RemoteInterpreterProcess { private static final Logger logger = LoggerFactory.getLogger(RemoteInterpreterProcess.class); + // number of sessions that are attached to this process + private final AtomicInteger referenceCount; + private GenericObjectPool clientPool; private final RemoteInterpreterEventPoller remoteInterpreterEventPoller; private final InterpreterContextRunnerPool interpreterContextRunnerPool; @@ -46,20 +46,16 @@ public RemoteInterpreterProcess( ApplicationEventListener appListener) { this(new RemoteInterpreterEventPoller(listener, appListener), connectTimeout); - this.remoteInterpreterEventPoller.setInterpreterProcess(this); } RemoteInterpreterProcess(RemoteInterpreterEventPoller remoteInterpreterEventPoller, int connectTimeout) { this.interpreterContextRunnerPool = new InterpreterContextRunnerPool(); + referenceCount = new AtomicInteger(0); this.remoteInterpreterEventPoller = remoteInterpreterEventPoller; this.connectTimeout = connectTimeout; } - public RemoteInterpreterEventPoller getRemoteInterpreterEventPoller() { - return remoteInterpreterEventPoller; - } - public abstract String getHost(); public abstract int getPort(); public abstract void start(String userName, Boolean isUserImpersonate); @@ -70,18 +66,37 @@ public int getConnectTimeout() { return connectTimeout; } - public synchronized Client getClient() throws Exception { + public int reference(InterpreterGroup interpreterGroup, String userName, + Boolean isUserImpersonate) { + synchronized (referenceCount) { + if (!isRunning()) { + start(userName, isUserImpersonate); + } + + if (clientPool == null) { + clientPool = new GenericObjectPool<>(new ClientFactory(getHost(), getPort())); + clientPool.setTestOnBorrow(true); + + remoteInterpreterEventPoller.setInterpreterGroup(interpreterGroup); + remoteInterpreterEventPoller.setInterpreterProcess(this); + remoteInterpreterEventPoller.start(); + } + return referenceCount.incrementAndGet(); + } + } + + public Client getClient() throws Exception { if (clientPool == null || clientPool.isClosed()) { - clientPool = new GenericObjectPool<>(new ClientFactory(getHost(), getPort())); + return null; } return clientPool.borrowObject(); } - private void releaseClient(Client client) { + public void releaseClient(Client client) { releaseClient(client, false); } - private void releaseClient(Client client, boolean broken) { + public void releaseClient(Client client, boolean broken) { if (broken) { releaseBrokenClient(client); } else { @@ -93,7 +108,7 @@ private void releaseClient(Client client, boolean broken) { } } - private void releaseBrokenClient(Client client) { + public void releaseBrokenClient(Client client) { try { clientPool.invalidateObject(client); } catch (Exception e) { @@ -101,6 +116,90 @@ private void releaseBrokenClient(Client client) { } } + public int dereference() { + synchronized (referenceCount) { + int r = referenceCount.decrementAndGet(); + if (r == 0) { + logger.info("shutdown interpreter process"); + remoteInterpreterEventPoller.shutdown(); + + // first try shutdown + Client client = null; + try { + client = getClient(); + client.shutdown(); + } catch (Exception e) { + // safely ignore exception while client.shutdown() may terminates remote process + logger.info("Exception in RemoteInterpreterProcess while synchronized dereference, can " + + "safely ignore exception while client.shutdown() may terminates remote process"); + logger.debug(e.getMessage(), e); + } finally { + if (client != null) { + // no longer used + releaseBrokenClient(client); + } + } + + clientPool.clear(); + clientPool.close(); + + // wait for some time (connectTimeout) and force kill + // remote process server.serve() loop is not always finishing gracefully + long startTime = System.currentTimeMillis(); + while (System.currentTimeMillis() - startTime < connectTimeout) { + if (this.isRunning()) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + logger.error("Exception in RemoteInterpreterProcess while synchronized dereference " + + "Thread.sleep", e); + } + } else { + break; + } + } + } + return r; + } + } + + public int referenceCount() { + synchronized (referenceCount) { + return referenceCount.get(); + } + } + + public int getNumActiveClient() { + if (clientPool == null) { + return 0; + } else { + return clientPool.getNumActive(); + } + } + + public int getNumIdleClient() { + if (clientPool == null) { + return 0; + } else { + return clientPool.getNumIdle(); + } + } + + public void setMaxPoolSize(int size) { + if (clientPool != null) { + //Size + 2 for progress poller , cancel operation + clientPool.setMaxTotal(size + 2); + } + } + + public int getMaxPoolSize() { + if (clientPool != null) { + return clientPool.getMaxTotal(); + } else { + return 0; + } + } + /** * Called when angular object is updated in client side to propagate * change to the remote process @@ -140,33 +239,4 @@ public void updateRemoteAngularObject(String name, String noteId, String paragra public InterpreterContextRunnerPool getInterpreterContextRunnerPool() { return interpreterContextRunnerPool; } - - public T callRemoteFunction(RemoteFunction func) { - Client client = null; - boolean broken = false; - try { - client = getClient(); - if (client != null) { - return func.call(client); - } - } catch (TException e) { - broken = true; - throw new InterpreterException(e); - } catch (Exception e1) { - throw new InterpreterException(e1); - } finally { - if (client != null) { - releaseClient(client, broken); - } - } - return null; - } - - /** - * - * @param - */ - public interface RemoteFunction { - T call(Client client) throws Exception; - } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java index 3d8123efddf..38534682b34 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java @@ -106,7 +106,6 @@ public void run() { @Override public void shutdown() throws TException { - logger.info("Shutting down..."); eventClient.waitForEventQueueBecomesEmpty(DEFAULT_SHUTDOWN_TIMEOUT); if (interpreterGroup != null) { interpreterGroup.close(); @@ -160,7 +159,7 @@ public static void main(String[] args) } @Override - public void createInterpreter(String interpreterGroupId, String sessionId, String + public void createInterpreter(String interpreterGroupId, String sessionKey, String className, Map properties, String userName) throws TException { if (interpreterGroup == null) { interpreterGroup = new InterpreterGroup(interpreterGroupId); @@ -191,11 +190,20 @@ public void createInterpreter(String interpreterGroupId, String sessionId, Strin replClass.getConstructor(new Class[] {Properties.class}); Interpreter repl = constructor.newInstance(p); repl.setClassloaderUrls(new URL[]{}); + + synchronized (interpreterGroup) { + List interpreters = interpreterGroup.get(sessionKey); + if (interpreters == null) { + interpreters = new LinkedList<>(); + interpreterGroup.put(sessionKey, interpreters); + } + + interpreters.add(new LazyOpenInterpreter(repl)); + } + logger.info("Instantiate interpreter {}", className); repl.setInterpreterGroup(interpreterGroup); repl.setUserName(userName); - - interpreterGroup.addInterpreterToSession(new LazyOpenInterpreter(repl), sessionId); } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { @@ -229,13 +237,13 @@ private void setSystemProperty(Properties properties) { } } - protected Interpreter getInterpreter(String sessionId, String className) throws TException { + protected Interpreter getInterpreter(String sessionKey, String className) throws TException { if (interpreterGroup == null) { throw new TException( new InterpreterException("Interpreter instance " + className + " not created")); } synchronized (interpreterGroup) { - List interpreters = interpreterGroup.get(sessionId); + List interpreters = interpreterGroup.get(sessionKey); if (interpreters == null) { throw new TException( new InterpreterException("Interpreter " + className + " not initialized")); @@ -251,20 +259,19 @@ protected Interpreter getInterpreter(String sessionId, String className) throws } @Override - public void open(String sessionId, String className) throws TException { - logger.info(String.format("Open Interpreter %s for session %s ", className, sessionId)); - Interpreter intp = getInterpreter(sessionId, className); + public void open(String noteId, String className) throws TException { + Interpreter intp = getInterpreter(noteId, className); intp.open(); } @Override - public void close(String sessionId, String className) throws TException { + public void close(String sessionKey, String className) throws TException { // unload all applications for (String appId : runningApplications.keySet()) { RunningApplication appInfo = runningApplications.get(appId); // see NoteInterpreterLoader.SHARED_SESSION - if (appInfo.noteId.equals(sessionId) || sessionId.equals("shared_session")) { + if (appInfo.noteId.equals(sessionKey) || sessionKey.equals("shared_session")) { try { logger.info("Unload App {} ", appInfo.pkg.getName()); appInfo.app.unload(); @@ -279,7 +286,7 @@ public void close(String sessionId, String className) throws TException { // close interpreters List interpreters; synchronized (interpreterGroup) { - interpreters = interpreterGroup.get(sessionId); + interpreters = interpreterGroup.get(sessionKey); } if (interpreters != null) { Iterator it = interpreters.iterator(); @@ -315,6 +322,7 @@ public RemoteInterpreterResult interpret(String noteId, String className, String intp, st, context); + scheduler.submit(job); while (!job.isTerminated()) { @@ -558,34 +566,30 @@ public void cancel(String noteId, String className, RemoteInterpreterContext int } @Override - public int getProgress(String sessionId, String className, + public int getProgress(String noteId, String className, RemoteInterpreterContext interpreterContext) throws TException { Integer manuallyProvidedProgress = progressMap.get(interpreterContext.getParagraphId()); if (manuallyProvidedProgress != null) { return manuallyProvidedProgress; } else { - Interpreter intp = getInterpreter(sessionId, className); - if (intp == null) { - throw new TException("No interpreter {} existed for session {}".format( - className, sessionId)); - } + Interpreter intp = getInterpreter(noteId, className); return intp.getProgress(convert(interpreterContext, null)); } } @Override - public String getFormType(String sessionId, String className) throws TException { - Interpreter intp = getInterpreter(sessionId, className); + public String getFormType(String noteId, String className) throws TException { + Interpreter intp = getInterpreter(noteId, className); return intp.getFormType().toString(); } @Override - public List completion(String sessionId, + public List completion(String noteId, String className, String buf, int cursor, RemoteInterpreterContext remoteInterpreterContext) throws TException { - Interpreter intp = getInterpreter(sessionId, className); + Interpreter intp = getInterpreter(noteId, className); List completion = intp.completion(buf, cursor, convert(remoteInterpreterContext, null)); return completion; } @@ -762,16 +766,16 @@ private RemoteInterpreterResult convert(InterpreterResult result, } @Override - public String getStatus(String sessionId, String jobId) + public String getStatus(String sessionKey, String jobId) throws TException { if (interpreterGroup == null) { - return Status.UNKNOWN.name(); + return "Unknown"; } synchronized (interpreterGroup) { - List interpreters = interpreterGroup.get(sessionId); + List interpreters = interpreterGroup.get(sessionKey); if (interpreters == null) { - return Status.UNKNOWN.name(); + return "Unknown"; } for (Interpreter intp : interpreters) { @@ -788,7 +792,7 @@ public String getStatus(String sessionId, String jobId) } } } - return Status.UNKNOWN.name(); + return "Unknown"; } 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 new file mode 100644 index 00000000000..b26995ada2d --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/resource/ResourcePoolUtils.java @@ -0,0 +1,138 @@ +/* + * 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.resource; + +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; +import org.slf4j.Logger; + +import java.util.List; + +/** + * Utilities for ResourcePool + */ +public class ResourcePoolUtils { + static Logger logger = org.slf4j.LoggerFactory.getLogger(ResourcePoolUtils.class); + + public static ResourceSet getAllResources() { + return getAllResourcesExcept(null); + } + + public static ResourceSet getAllResourcesExcept(String interpreterGroupExcludsion) { + ResourceSet resourceSet = new ResourceSet(); + for (InterpreterGroup intpGroup : InterpreterGroup.getAll()) { + if (interpreterGroupExcludsion != null && + intpGroup.getId().equals(interpreterGroupExcludsion)) { + continue; + } + + RemoteInterpreterProcess remoteInterpreterProcess = intpGroup.getRemoteInterpreterProcess(); + if (remoteInterpreterProcess == null) { + ResourcePool localPool = intpGroup.getResourcePool(); + if (localPool != null) { + resourceSet.addAll(localPool.getAll()); + } + } else if (remoteInterpreterProcess.isRunning()) { + RemoteInterpreterService.Client client = null; + boolean broken = false; + try { + client = remoteInterpreterProcess.getClient(); + if (client == null) { + // remote interpreter may not started yet or terminated. + continue; + } + List resourceList = client.resourcePoolGetAll(); + for (String res : resourceList) { + resourceSet.add(Resource.fromJson(res)); + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + broken = true; + } finally { + if (client != null) { + intpGroup.getRemoteInterpreterProcess().releaseClient(client, broken); + } + } + } + } + return resourceSet; + } + + public static void removeResourcesBelongsToNote(String noteId) { + removeResourcesBelongsToParagraph(noteId, null); + } + + public static void removeResourcesBelongsToParagraph(String noteId, String paragraphId) { + for (InterpreterGroup intpGroup : InterpreterGroup.getAll()) { + ResourceSet resourceSet = new ResourceSet(); + RemoteInterpreterProcess remoteInterpreterProcess = intpGroup.getRemoteInterpreterProcess(); + if (remoteInterpreterProcess == null) { + ResourcePool localPool = intpGroup.getResourcePool(); + if (localPool != null) { + resourceSet.addAll(localPool.getAll()); + } + if (noteId != null) { + resourceSet = resourceSet.filterByNoteId(noteId); + } + if (paragraphId != null) { + resourceSet = resourceSet.filterByParagraphId(paragraphId); + } + + for (Resource r : resourceSet) { + localPool.remove( + r.getResourceId().getNoteId(), + r.getResourceId().getParagraphId(), + r.getResourceId().getName()); + } + } else if (remoteInterpreterProcess.isRunning()) { + RemoteInterpreterService.Client client = null; + boolean broken = false; + try { + client = remoteInterpreterProcess.getClient(); + List resourceList = client.resourcePoolGetAll(); + for (String res : resourceList) { + resourceSet.add(Resource.fromJson(res)); + } + + if (noteId != null) { + resourceSet = resourceSet.filterByNoteId(noteId); + } + if (paragraphId != null) { + resourceSet = resourceSet.filterByParagraphId(paragraphId); + } + + for (Resource r : resourceSet) { + client.resourceRemove( + r.getResourceId().getNoteId(), + r.getResourceId().getParagraphId(), + r.getResourceId().getName()); + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + broken = true; + } finally { + if (client != null) { + intpGroup.getRemoteInterpreterProcess().releaseClient(client, broken); + } + } + } + } + } +} + diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/Job.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/Job.java index 191902a1c32..d0025d86b9f 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/Job.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/Job.java @@ -41,7 +41,6 @@ public abstract class Job { /** * Job status. * - * UNKNOWN - Job is not found in remote * READY - Job is not running, ready to run. * PENDING - Job is submitted to scheduler. but not running yet * RUNNING - Job is running. @@ -49,8 +48,8 @@ public abstract class Job { * ERROR - Job finished run. with error * ABORT - Job finished by abort */ - public enum Status { - UNKNOWN, READY, PENDING, RUNNING, FINISHED, ERROR, ABORT; + public static enum Status { + READY, PENDING, RUNNING, FINISHED, ERROR, ABORT; public boolean isReady() { return this == READY; @@ -71,14 +70,14 @@ public boolean isPending() { Date dateCreated; Date dateStarted; Date dateFinished; - volatile Status status; + Status status; static Logger LOGGER = LoggerFactory.getLogger(Job.class); transient boolean aborted = false; - private volatile String errorMessage; - private transient volatile Throwable exception; + private String errorMessage; + private transient Throwable exception; private transient JobListener listener; private long progressUpdateIntervalMs; 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 e41540bfee7..f9ddc4e99c2 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 @@ -17,9 +17,11 @@ 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.RemoteInterpreter; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; import org.apache.zeppelin.scheduler.Job.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,7 +34,6 @@ /** * RemoteScheduler runs in ZeppelinServer and proxies Scheduler running on RemoteInterpreter - * */ public class RemoteScheduler implements Scheduler { Logger logger = LoggerFactory.getLogger(RemoteScheduler.class); @@ -44,17 +45,17 @@ public class RemoteScheduler implements Scheduler { boolean terminate = false; private String name; private int maxConcurrency; - private final String sessionId; - private RemoteInterpreter remoteInterpreter; + private final String noteId; + private RemoteInterpreterProcess interpreterProcess; - public RemoteScheduler(String name, ExecutorService executor, String sessionId, - RemoteInterpreter remoteInterpreter, SchedulerListener listener, + public RemoteScheduler(String name, ExecutorService executor, String noteId, + RemoteInterpreterProcess interpreterProcess, SchedulerListener listener, int maxConcurrency) { this.name = name; this.executor = executor; this.listener = listener; - this.sessionId = sessionId; - this.remoteInterpreter = remoteInterpreter; + this.noteId = noteId; + this.interpreterProcess = interpreterProcess; this.maxConcurrency = maxConcurrency; } @@ -166,15 +167,14 @@ private class JobStatusPoller extends Thread { private long initialPeriodMsec; private long initialPeriodCheckIntervalMsec; private long checkIntervalMsec; - private volatile boolean terminate; + private boolean terminate; private JobListener listener; private Job job; - volatile Status lastStatus; + Status lastStatus; public JobStatusPoller(long initialPeriodMsec, long initialPeriodCheckIntervalMsec, long checkIntervalMsec, Job job, JobListener listener) { - setName("JobStatusPoller-" + job.getId()); this.initialPeriodMsec = initialPeriodMsec; this.initialPeriodCheckIntervalMsec = initialPeriodCheckIntervalMsec; this.checkIntervalMsec = checkIntervalMsec; @@ -209,7 +209,7 @@ public void run() { } Status newStatus = getStatus(); - if (newStatus == Status.UNKNOWN) { // unknown + if (newStatus == null) { // unknown continue; } @@ -231,9 +231,7 @@ public void shutdown() { private Status getLastStatus() { if (terminate == true) { - if (job.getErrorMessage() != null) { - return Status.ERROR; - } else if (lastStatus != Status.FINISHED && + if (lastStatus != Status.FINISHED && lastStatus != Status.ERROR && lastStatus != Status.ABORT) { return Status.FINISHED; @@ -241,35 +239,58 @@ private Status getLastStatus() { return (lastStatus == null) ? Status.FINISHED : lastStatus; } } else { - return (lastStatus == null) ? Status.UNKNOWN : lastStatus; + return (lastStatus == null) ? Status.FINISHED : lastStatus; } } public synchronized Job.Status getStatus() { - if (!remoteInterpreter.isOpened()) { + if (interpreterProcess.referenceCount() <= 0) { return getLastStatus(); } - Status status = Status.valueOf(remoteInterpreter.getStatus(job.getId())); - if (status == Status.UNKNOWN) { - // not found this job in the remote schedulers. - // maybe not submitted, maybe already finished - //Status status = getLastStatus(); - listener.afterStatusChange(job, null, null); - return job.getStatus(); + + Client client; + try { + client = interpreterProcess.getClient(); + } catch (Exception e) { + logger.error("Can't get status information", e); + lastStatus = Status.ERROR; + return Status.ERROR; + } + + boolean broken = false; + try { + String statusStr = client.getStatus(noteId, job.getId()); + if ("Unknown".equals(statusStr)) { + // not found this job in the remote schedulers. + // maybe not submitted, maybe already finished + //Status status = getLastStatus(); + listener.afterStatusChange(job, null, null); + return job.getStatus(); + } + Status status = Status.valueOf(statusStr); + lastStatus = status; + listener.afterStatusChange(job, null, status); + return status; + } catch (TException e) { + broken = true; + logger.error("Can't get status information", e); + lastStatus = Status.ERROR; + return Status.ERROR; + } catch (Exception e) { + logger.error("Unknown status", e); + lastStatus = Status.ERROR; + return Status.ERROR; + } finally { + interpreterProcess.releaseClient(client, broken); } - lastStatus = status; - listener.afterStatusChange(job, null, status); - return status; } } - //TODO(zjffdu) need to refactor the schdule module which is too complicated private class JobRunner implements Runnable, JobListener { - private final Logger logger = LoggerFactory.getLogger(JobRunner.class); private Scheduler scheduler; private Job job; - private volatile boolean jobExecuted; - volatile boolean jobSubmittedRemotely; + private boolean jobExecuted; + boolean jobSubmittedRemotely; public JobRunner(Scheduler scheduler, Job job) { this.scheduler = scheduler; @@ -317,22 +338,20 @@ public void run() { } // set job status based on result. + Status lastStatus = jobStatusPoller.getStatus(); Object jobResult = job.getReturn(); - if (job.isAborted()) { - job.setStatus(Status.ABORT); - } else if (job.getException() != null) { -// logger.info("Job ABORT, " + job.getId()); - job.setStatus(Status.ERROR); - } else if (jobResult != null && jobResult instanceof InterpreterResult - && ((InterpreterResult) jobResult).code() == Code.ERROR) { -// logger.info("Job Error, " + job.getId()); - job.setStatus(Status.ERROR); - } else { -// logger.info("Job Finished, " + job.getId()); - job.setStatus(Status.FINISHED); + if (jobResult != null && jobResult instanceof InterpreterResult) { + if (((InterpreterResult) jobResult).code() == Code.ERROR) { + lastStatus = Status.ERROR; + } + } + if (job.getException() != null) { + lastStatus = Status.ERROR; } synchronized (queue) { + job.setStatus(lastStatus); + if (listener != null) { listener.jobFinished(scheduler, job); } @@ -355,6 +374,25 @@ public void beforeStatusChange(Job job, Status before, Status after) { @Override public void afterStatusChange(Job job, Status before, Status after) { + if (after == null) { // unknown. maybe before sumitted remotely, maybe already finished. + if (jobExecuted) { + jobSubmittedRemotely = true; + Object jobResult = job.getReturn(); + if (job.isAborted()) { + job.setStatus(Status.ABORT); + } else if (job.getException() != null) { + job.setStatus(Status.ERROR); + } else if (jobResult != null && jobResult instanceof InterpreterResult + && ((InterpreterResult) jobResult).code() == Code.ERROR) { + job.setStatus(Status.ERROR); + } else { + job.setStatus(Status.FINISHED); + } + } + return; + } + + // Update remoteStatus if (jobExecuted == false) { if (after == Status.FINISHED || after == Status.ABORT @@ -364,18 +402,14 @@ public void afterStatusChange(Job job, Status before, Status after) { return; } else if (after == Status.RUNNING) { jobSubmittedRemotely = true; - job.setStatus(Status.RUNNING); -// logger.info("Job RUNNING, " + job.getId()); } } else { jobSubmittedRemotely = true; } - // only set status when it is RUNNING - // We would set other status based on the interpret result - if (after == Status.RUNNING) { -// logger.info("Job RUNNING, " + job.getId()); - job.setStatus(Status.RUNNING); + // status polled by status poller + if (job.getStatus() != after) { + job.setStatus(after); } } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/SchedulerFactory.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/SchedulerFactory.java index 5871ca5befc..af52dec345b 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/SchedulerFactory.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/SchedulerFactory.java @@ -24,18 +24,17 @@ import java.util.Map; import java.util.concurrent.ExecutorService; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Factory class for creating schedulers - * + * TODO(moon) : add description. */ public class SchedulerFactory implements SchedulerListener { private static final Logger logger = LoggerFactory.getLogger(SchedulerFactory.class); - private ExecutorService executor; - private Map schedulers = new LinkedHashMap<>(); + ExecutorService executor; + Map schedulers = new LinkedHashMap<>(); private static SchedulerFactory singleton; private static Long singletonLock = new Long(0); @@ -55,17 +54,17 @@ public static SchedulerFactory singleton() { return singleton; } - SchedulerFactory() throws Exception { - executor = ExecutorFactory.singleton().createOrGet("SchedulerFactory", 100); + public SchedulerFactory() throws Exception { + executor = ExecutorFactory.singleton().createOrGet("schedulerFactory", 100); } public void destroy() { - ExecutorFactory.singleton().shutdown("SchedulerFactory"); + ExecutorFactory.singleton().shutdown("schedulerFactory"); } public Scheduler createOrGetFIFOScheduler(String name) { synchronized (schedulers) { - if (!schedulers.containsKey(name)) { + if (schedulers.containsKey(name) == false) { Scheduler s = new FIFOScheduler(name, executor, this); schedulers.put(name, s); executor.execute(s); @@ -76,7 +75,7 @@ public Scheduler createOrGetFIFOScheduler(String name) { public Scheduler createOrGetParallelScheduler(String name, int maxConcurrency) { synchronized (schedulers) { - if (!schedulers.containsKey(name)) { + if (schedulers.containsKey(name) == false) { Scheduler s = new ParallelScheduler(name, executor, this, maxConcurrency); schedulers.put(name, s); executor.execute(s); @@ -87,17 +86,17 @@ public Scheduler createOrGetParallelScheduler(String name, int maxConcurrency) { public Scheduler createOrGetRemoteScheduler( String name, - String sessionId, - RemoteInterpreter remoteInterpreter, + String noteId, + RemoteInterpreterProcess interpreterProcess, int maxConcurrency) { synchronized (schedulers) { - if (!schedulers.containsKey(name)) { + if (schedulers.containsKey(name) == false) { Scheduler s = new RemoteScheduler( name, executor, - sessionId, - remoteInterpreter, + noteId, + interpreterProcess, this, maxConcurrency); schedulers.put(name, s); @@ -107,24 +106,38 @@ public Scheduler createOrGetRemoteScheduler( } } - public void removeScheduler(String name) { + public Scheduler removeScheduler(String name) { synchronized (schedulers) { Scheduler s = schedulers.remove(name); if (s != null) { s.stop(); } } + return null; + } + + public Collection listScheduler(String name) { + List s = new LinkedList<>(); + synchronized (schedulers) { + for (Scheduler ss : schedulers.values()) { + s.add(ss); + } + } + return s; } @Override public void jobStarted(Scheduler scheduler, Job job) { - logger.info("Job " + job.getId() + " started by scheduler " + scheduler.getName()); + logger.info("Job " + job.getJobName() + " started by scheduler " + scheduler.getName()); } @Override public void jobFinished(Scheduler scheduler, Job job) { - logger.info("Job " + job.getId() + " finished by scheduler " + scheduler.getName()); + logger.info("Job " + job.getJobName() + " finished by scheduler " + scheduler.getName()); } + + + } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/TableDataProxy.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/TableDataProxy.java index 19265287ae5..8673476ed42 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/TableDataProxy.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/TableDataProxy.java @@ -17,6 +17,7 @@ package org.apache.zeppelin.tabledata; import org.apache.zeppelin.resource.Resource; +import org.apache.zeppelin.resource.ResourcePoolUtils; import java.util.Iterator; diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/IdHashes.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/IdHashes.java deleted file mode 100644 index 14c03a11cf0..00000000000 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/IdHashes.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.util; - -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - -/** - * Generate Tiny ID. - */ -public class IdHashes { - private static final char[] DICTIONARY = new char[] {'1', '2', '3', '4', '5', '6', '7', '8', '9', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', - 'W', 'X', 'Y', 'Z'}; - - /** - * encodes the given string into the base of the dictionary provided in the constructor. - * - * @param value the number to encode. - * @return the encoded string. - */ - private static String encode(Long value) { - - List result = new ArrayList<>(); - BigInteger base = new BigInteger("" + DICTIONARY.length); - int exponent = 1; - BigInteger remaining = new BigInteger(value.toString()); - while (true) { - BigInteger a = base.pow(exponent); // 16^1 = 16 - BigInteger b = remaining.mod(a); // 119 % 16 = 7 | 112 % 256 = 112 - BigInteger c = base.pow(exponent - 1); - BigInteger d = b.divide(c); - - // if d > dictionary.length, we have a problem. but BigInteger doesnt have - // a greater than method :-( hope for the best. theoretically, d is always - // an index of the dictionary! - result.add(DICTIONARY[d.intValue()]); - remaining = remaining.subtract(b); // 119 - 7 = 112 | 112 - 112 = 0 - - // finished? - if (remaining.equals(BigInteger.ZERO)) { - break; - } - - exponent++; - } - - // need to reverse it, since the start of the list contains the least significant values - StringBuffer sb = new StringBuffer(); - for (int i = result.size() - 1; i >= 0; i--) { - sb.append(result.get(i)); - } - return sb.toString(); - } - - public static String generateId() { - return encode(System.currentTimeMillis() + new Random().nextInt()); - } -} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java deleted file mode 100644 index 21d75262a05..00000000000 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.apache.zeppelin.interpreter; - -import org.apache.commons.io.FileUtils; -import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.display.AngularObjectRegistryListener; -import org.apache.zeppelin.helium.ApplicationEventListener; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; -import org.junit.After; -import org.junit.Before; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; - -import static org.mockito.Mockito.mock; - - -/** - * This class will load configuration files under - * src/test/resources/interpreter - * src/test/resources/conf - * - * to construct InterpreterSettingManager and InterpreterFactory properly - * - */ -public abstract class AbstractInterpreterTest { - protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractInterpreterTest.class); - private static final String INTERPRETER_SCRIPT = - System.getProperty("os.name").startsWith("Windows") ? - "../bin/interpreter.cmd" : - "../bin/interpreter.sh"; - - protected InterpreterSettingManager interpreterSettingManager; - protected InterpreterFactory interpreterFactory; - protected File testRootDir; - protected File interpreterDir; - protected File confDir; - protected File notebookDir; - protected ZeppelinConfiguration conf; - - @Before - public void setUp() throws Exception { - // copy the resources files to a temp folder - testRootDir = new File(System.getProperty("java.io.tmpdir") + "/Zeppelin_Test_" + System.currentTimeMillis()); - testRootDir.mkdirs(); - LOGGER.info("Create tmp directory: {} as root folder of ZEPPELIN_INTERPRETER_DIR & ZEPPELIN_CONF_DIR", testRootDir.getAbsolutePath()); - interpreterDir = new File(testRootDir, "interpreter"); - confDir = new File(testRootDir, "conf"); - notebookDir = new File(testRootDir, "notebook"); - - interpreterDir.mkdirs(); - confDir.mkdirs(); - notebookDir.mkdirs(); - - FileUtils.copyDirectory(new File("src/test/resources/interpreter"), interpreterDir); - FileUtils.copyDirectory(new File("src/test/resources/conf"), confDir); - - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONF_DIR.getVarName(), confDir.getAbsolutePath()); - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_DIR.getVarName(), interpreterDir.getAbsolutePath()); - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), notebookDir.getAbsolutePath()); - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_REMOTE_RUNNER.getVarName(), INTERPRETER_SCRIPT); - - conf = new ZeppelinConfiguration(); - interpreterSettingManager = new InterpreterSettingManager(conf, - mock(AngularObjectRegistryListener.class), mock(RemoteInterpreterProcessListener.class), mock(ApplicationEventListener.class)); - interpreterFactory = new InterpreterFactory(interpreterSettingManager); - } - - @After - public void tearDown() throws Exception { - interpreterSettingManager.close(); - FileUtils.deleteDirectory(testRootDir); - } -} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DummyInterpreter.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DummyInterpreter.java new file mode 100644 index 00000000000..a7a6eb9b715 --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DummyInterpreter.java @@ -0,0 +1,43 @@ +package org.apache.zeppelin.interpreter; + +import java.util.Properties; + +/** + * + */ +public class DummyInterpreter extends Interpreter { + + public DummyInterpreter(Properties property) { + super(property); + } + + @Override + public void open() { + + } + + @Override + public void close() { + + } + + @Override + public InterpreterResult interpret(String st, InterpreterContext context) { + return null; + } + + @Override + public void cancel(InterpreterContext context) { + + } + + @Override + public FormType getFormType() { + return null; + } + + @Override + public int getProgress(InterpreterContext context) { + return 0; + } +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java deleted file mode 100644 index f3137d9c895..00000000000 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.interpreter; - -import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; -import org.junit.Test; - -import java.io.IOException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -public class InterpreterFactoryTest extends AbstractInterpreterTest { - - @Test - public void testGetFactory() throws IOException { - // no default interpreter because there's no interpreter setting binded to this note - assertNull(interpreterFactory.getInterpreter("user1", "note1", "")); - - interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); - assertTrue(interpreterFactory.getInterpreter("user1", "note1", "") instanceof RemoteInterpreter); - RemoteInterpreter remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", ""); - // EchoInterpreter is the default interpreter (see zeppelin-interpreter/src/test/resources/conf/interpreter.json) - assertEquals(EchoInterpreter.class.getName(), remoteInterpreter.getClassName()); - - assertTrue(interpreterFactory.getInterpreter("user1", "note1", "test") instanceof RemoteInterpreter); - remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test"); - assertEquals(EchoInterpreter.class.getName(), remoteInterpreter.getClassName()); - - assertTrue(interpreterFactory.getInterpreter("user1", "note1", "echo") instanceof RemoteInterpreter); - remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "echo"); - assertEquals(EchoInterpreter.class.getName(), remoteInterpreter.getClassName()); - - assertTrue(interpreterFactory.getInterpreter("user1", "note1", "double_echo") instanceof RemoteInterpreter); - remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "double_echo"); - assertEquals(DoubleEchoInterpreter.class.getName(), remoteInterpreter.getClassName()); - } - - @Test(expected = InterpreterException.class) - public void testUnknownRepl1() throws IOException { - interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); - interpreterFactory.getInterpreter("user1", "note1", "test.unknown_repl"); - } - - @Test - public void testUnknownRepl2() throws IOException { - interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); - assertNull(interpreterFactory.getInterpreter("user1", "note1", "unknown_repl")); - } -} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterGroupTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterGroupTest.java deleted file mode 100644 index 11607bb9e80..00000000000 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterGroupTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.interpreter; - -import org.junit.Before; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonatype.aether.RepositoryException; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -import static org.junit.Assert.assertEquals; - - -public class InterpreterGroupTest { - - private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterGroupTest.class); - - private InterpreterSetting interpreterSetting; - - @Before - public void setUp() throws IOException, RepositoryException { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerUser(InterpreterOption.SCOPED); - interpreterOption.setRemote(false); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); - InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - interpreterInfos.add(interpreterInfo2); - interpreterSetting = new InterpreterSetting.Builder() - .setId("id") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .create(); - } - - @Test - public void testInterpreterGroup() { - InterpreterGroup interpreterGroup = new InterpreterGroup("group_1", interpreterSetting); - assertEquals(0, interpreterGroup.getSessionNum()); - - // create session_1 - List interpreters = interpreterGroup.getOrCreateSession("user1", "session_1"); - assertEquals(2, interpreters.size()); - assertEquals(EchoInterpreter.class.getName(), interpreters.get(0).getClassName()); - assertEquals(DoubleEchoInterpreter.class.getName(), interpreters.get(1).getClassName()); - assertEquals(1, interpreterGroup.getSessionNum()); - - // get the same interpreters when interpreterGroup.getOrCreateSession is invoked again - assertEquals(interpreters, interpreterGroup.getOrCreateSession("user1", "session_1")); - assertEquals(1, interpreterGroup.getSessionNum()); - - // create session_2 - List interpreters2 = interpreterGroup.getOrCreateSession("user1", "session_2"); - assertEquals(2, interpreters2.size()); - assertEquals(EchoInterpreter.class.getName(), interpreters2.get(0).getClassName()); - assertEquals(DoubleEchoInterpreter.class.getName(), interpreters2.get(1).getClassName()); - assertEquals(2, interpreterGroup.getSessionNum()); - - // close session_1 - interpreterGroup.close("session_1"); - assertEquals(1, interpreterGroup.getSessionNum()); - - // close InterpreterGroup - interpreterGroup.close(); - assertEquals(0, interpreterGroup.getSessionNum()); - } -} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterOutputChangeWatcherTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterOutputChangeWatcherTest.java index f3a30fbd274..e37680905bb 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterOutputChangeWatcherTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterOutputChangeWatcherTest.java @@ -21,7 +21,6 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; import org.junit.Before; @@ -30,7 +29,7 @@ public class InterpreterOutputChangeWatcherTest implements InterpreterOutputChangeListener { private File tmpDir; private File fileChanged; - private AtomicInteger numChanged; + private int numChanged; private InterpreterOutputChangeWatcher watcher; @Before @@ -41,7 +40,7 @@ public void setUp() throws Exception { tmpDir = new File(System.getProperty("java.io.tmpdir")+"/ZeppelinLTest_"+System.currentTimeMillis()); tmpDir.mkdirs(); fileChanged = null; - numChanged = new AtomicInteger(0); + numChanged = 0; } @After @@ -67,7 +66,7 @@ else if(file.isDirectory()){ @Test public void test() throws IOException, InterruptedException { assertNull(fileChanged); - assertEquals(0, numChanged.get()); + assertEquals(0, numChanged); Thread.sleep(1000); // create new file @@ -93,14 +92,14 @@ public void test() throws IOException, InterruptedException { } assertNotNull(fileChanged); - assertEquals(1, numChanged.get()); + assertEquals(1, numChanged); } @Override public void fileChanged(File file) { fileChanged = file; - numChanged.incrementAndGet(); + numChanged++; synchronized(this) { notify(); diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java deleted file mode 100644 index c74760ff106..00000000000 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java +++ /dev/null @@ -1,270 +0,0 @@ -/* - * 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.interpreter; - -import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.dep.Dependency; -import org.apache.zeppelin.display.AngularObjectRegistryListener; -import org.apache.zeppelin.helium.ApplicationEventListener; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; -import org.junit.Test; -import org.sonatype.aether.RepositoryException; -import org.sonatype.aether.repository.RemoteRepository; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; - - -public class InterpreterSettingManagerTest extends AbstractInterpreterTest { - - @Test - public void testInitInterpreterSettingManager() throws IOException, RepositoryException { - assertEquals(2, interpreterSettingManager.get().size()); - InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("test"); - assertEquals("test", interpreterSetting.getName()); - assertEquals("test", interpreterSetting.getGroup()); - assertEquals(2, interpreterSetting.getInterpreterInfos().size()); - // 3 other builtin properties: - // * zeppelin.interpeter.output.limit - // * zeppelin.interpreter.localRepo - // * zeppelin.interpreter.max.poolsize - assertEquals(6, interpreterSetting.getJavaProperties().size()); - assertEquals("value_1", interpreterSetting.getJavaProperties().getProperty("property_1")); - assertEquals("new_value_2", interpreterSetting.getJavaProperties().getProperty("property_2")); - assertEquals("value_3", interpreterSetting.getJavaProperties().getProperty("property_3")); - assertEquals("shared", interpreterSetting.getOption().perNote); - assertEquals("shared", interpreterSetting.getOption().perUser); - assertEquals(0, interpreterSetting.getDependencies().size()); - assertNotNull(interpreterSetting.getAngularObjectRegistryListener()); - assertNotNull(interpreterSetting.getRemoteInterpreterProcessListener()); - assertNotNull(interpreterSetting.getAppEventListener()); - assertNotNull(interpreterSetting.getDependencyResolver()); - assertNotNull(interpreterSetting.getInterpreterSettingManager()); - - List repositories = interpreterSettingManager.getRepositories(); - assertEquals(2, repositories.size()); - assertEquals("central", repositories.get(0).getId()); - - // Load it again - InterpreterSettingManager interpreterSettingManager2 = new InterpreterSettingManager(conf, - mock(AngularObjectRegistryListener.class), mock(RemoteInterpreterProcessListener.class), mock(ApplicationEventListener.class)); - assertEquals(2, interpreterSettingManager2.get().size()); - interpreterSetting = interpreterSettingManager2.getByName("test"); - assertEquals("test", interpreterSetting.getName()); - assertEquals("test", interpreterSetting.getGroup()); - assertEquals(2, interpreterSetting.getInterpreterInfos().size()); - assertEquals(6, interpreterSetting.getJavaProperties().size()); - assertEquals("value_1", interpreterSetting.getJavaProperties().getProperty("property_1")); - assertEquals("new_value_2", interpreterSetting.getJavaProperties().getProperty("property_2")); - assertEquals("value_3", interpreterSetting.getJavaProperties().getProperty("property_3")); - assertEquals("shared", interpreterSetting.getOption().perNote); - assertEquals("shared", interpreterSetting.getOption().perUser); - assertEquals(0, interpreterSetting.getDependencies().size()); - - repositories = interpreterSettingManager2.getRepositories(); - assertEquals(2, repositories.size()); - assertEquals("central", repositories.get(0).getId()); - - } - - @Test - public void testCreateUpdateRemoveSetting() throws IOException { - // create new interpreter setting - InterpreterOption option = new InterpreterOption(); - option.setPerNote("scoped"); - option.setPerUser("scoped"); - Map properties = new HashMap<>(); - properties.put("property_4", new InterpreterProperty("property_4","value_4")); - - try { - interpreterSettingManager.createNewSetting("test2", "test", new ArrayList(), option, properties); - fail("Should fail due to interpreter already existed"); - } catch (IOException e) { - assertTrue(e.getMessage().contains("already existed")); - } - - interpreterSettingManager.createNewSetting("test3", "test", new ArrayList(), option, properties); - assertEquals(3, interpreterSettingManager.get().size()); - InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("test3"); - assertEquals("test3", interpreterSetting.getName()); - assertEquals("test", interpreterSetting.getGroup()); - // 3 other builtin properties: - // * zeppelin.interpeter.output.limit - // * zeppelin.interpreter.localRepo - // * zeppelin.interpreter.max.poolsize - assertEquals(4, interpreterSetting.getJavaProperties().size()); - assertEquals("value_4", interpreterSetting.getJavaProperties().getProperty("property_4")); - assertEquals("scoped", interpreterSetting.getOption().perNote); - assertEquals("scoped", interpreterSetting.getOption().perUser); - assertEquals(0, interpreterSetting.getDependencies().size()); - assertNotNull(interpreterSetting.getAngularObjectRegistryListener()); - assertNotNull(interpreterSetting.getRemoteInterpreterProcessListener()); - assertNotNull(interpreterSetting.getAppEventListener()); - assertNotNull(interpreterSetting.getDependencyResolver()); - assertNotNull(interpreterSetting.getInterpreterSettingManager()); - - // load it again, it should be saved in interpreter-setting.json. So we can restore it properly - InterpreterSettingManager interpreterSettingManager2 = new InterpreterSettingManager(conf, - mock(AngularObjectRegistryListener.class), mock(RemoteInterpreterProcessListener.class), mock(ApplicationEventListener.class)); - assertEquals(3, interpreterSettingManager2.get().size()); - interpreterSetting = interpreterSettingManager2.getByName("test3"); - assertEquals("test3", interpreterSetting.getName()); - assertEquals("test", interpreterSetting.getGroup()); - assertEquals(6, interpreterSetting.getJavaProperties().size()); - assertEquals("value_4", interpreterSetting.getJavaProperties().getProperty("property_4")); - assertEquals("scoped", interpreterSetting.getOption().perNote); - assertEquals("scoped", interpreterSetting.getOption().perUser); - assertEquals(0, interpreterSetting.getDependencies().size()); - - // update interpreter setting - InterpreterOption newOption = new InterpreterOption(); - newOption.setPerNote("scoped"); - newOption.setPerUser("isolated"); - Map newProperties = new HashMap<>(properties); - newProperties.put("property_4", new InterpreterProperty("property_4", "new_value_4")); - List newDependencies = new ArrayList<>(); - newDependencies.add(new Dependency("com.databricks:spark-avro_2.11:3.1.0")); - interpreterSettingManager.setPropertyAndRestart(interpreterSetting.getId(), newOption, newProperties, newDependencies); - interpreterSetting = interpreterSettingManager.get(interpreterSetting.getId()); - assertEquals("test3", interpreterSetting.getName()); - assertEquals("test", interpreterSetting.getGroup()); - assertEquals(4, interpreterSetting.getJavaProperties().size()); - assertEquals("new_value_4", interpreterSetting.getJavaProperties().getProperty("property_4")); - assertEquals("scoped", interpreterSetting.getOption().perNote); - assertEquals("isolated", interpreterSetting.getOption().perUser); - assertEquals(1, interpreterSetting.getDependencies().size()); - assertNotNull(interpreterSetting.getAngularObjectRegistryListener()); - assertNotNull(interpreterSetting.getRemoteInterpreterProcessListener()); - assertNotNull(interpreterSetting.getAppEventListener()); - assertNotNull(interpreterSetting.getDependencyResolver()); - assertNotNull(interpreterSetting.getInterpreterSettingManager()); - - // restart in note page - interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); - interpreterSettingManager.setInterpreterBinding("user2", "note2", interpreterSettingManager.getSettingIds()); - interpreterSettingManager.setInterpreterBinding("user3", "note3", interpreterSettingManager.getSettingIds()); - // create 3 sessions as it is scoped mode - interpreterSetting.getOption().setPerUser("scoped"); - interpreterSetting.getDefaultInterpreter("user1", "note1"); - interpreterSetting.getDefaultInterpreter("user2", "note2"); - interpreterSetting.getDefaultInterpreter("user3", "note3"); - InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); - assertEquals(3, interpreterGroup.getSessionNum()); - // only close user1's session - interpreterSettingManager.restart(interpreterSetting.getId(), "note1", "user1"); - assertEquals(2, interpreterGroup.getSessionNum()); - // close all the sessions - interpreterSettingManager.restart(interpreterSetting.getId(), "note1", "anonymous"); - assertEquals(0, interpreterGroup.getSessionNum()); - - // remove interpreter setting - interpreterSettingManager.remove(interpreterSetting.getId()); - assertEquals(2, interpreterSettingManager.get().size()); - - // load it again - InterpreterSettingManager interpreterSettingManager3 = new InterpreterSettingManager(new ZeppelinConfiguration(), - mock(AngularObjectRegistryListener.class), mock(RemoteInterpreterProcessListener.class), mock(ApplicationEventListener.class)); - assertEquals(2, interpreterSettingManager3.get().size()); - - } - - @Test - public void testInterpreterBinding() throws IOException { - assertNull(interpreterSettingManager.getInterpreterBinding("note1")); - interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getInterpreterSettingIds()); - assertEquals(interpreterSettingManager.getInterpreterSettingIds(), interpreterSettingManager.getInterpreterBinding("note1")); - } - - @Test - public void testUpdateInterpreterBinding_PerNoteShared() throws IOException { - InterpreterSetting defaultInterpreterSetting = interpreterSettingManager.get().get(0); - defaultInterpreterSetting.getOption().setPerNote("shared"); - - interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getInterpreterSettingIds()); - // create interpreter of the first binded interpreter setting - interpreterFactory.getInterpreter("user1", "note1", ""); - assertEquals(1, defaultInterpreterSetting.getAllInterpreterGroups().size()); - - // choose the first setting - List newSettingIds = new ArrayList<>(); - newSettingIds.add(interpreterSettingManager.getInterpreterSettingIds().get(1)); - - interpreterSettingManager.setInterpreterBinding("user1", "note1", newSettingIds); - assertEquals(newSettingIds, interpreterSettingManager.getInterpreterBinding("note1")); - // InterpreterGroup will still be alive as it is shared - assertEquals(1, defaultInterpreterSetting.getAllInterpreterGroups().size()); - } - - @Test - public void testUpdateInterpreterBinding_PerNoteIsolated() throws IOException { - InterpreterSetting defaultInterpreterSetting = interpreterSettingManager.get().get(0); - defaultInterpreterSetting.getOption().setPerNote("isolated"); - - interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getInterpreterSettingIds()); - // create interpreter of the first binded interpreter setting - interpreterFactory.getInterpreter("user1", "note1", ""); - assertEquals(1, defaultInterpreterSetting.getAllInterpreterGroups().size()); - - // choose the first setting - List newSettingIds = new ArrayList<>(); - newSettingIds.add(interpreterSettingManager.getInterpreterSettingIds().get(1)); - - interpreterSettingManager.setInterpreterBinding("user1", "note1", newSettingIds); - assertEquals(newSettingIds, interpreterSettingManager.getInterpreterBinding("note1")); - // InterpreterGroup will be closed as it is only belong to this note - assertEquals(0, defaultInterpreterSetting.getAllInterpreterGroups().size()); - - } - - @Test - public void testUpdateInterpreterBinding_PerNoteScoped() throws IOException { - InterpreterSetting defaultInterpreterSetting = interpreterSettingManager.get().get(0); - defaultInterpreterSetting.getOption().setPerNote("scoped"); - - interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getInterpreterSettingIds()); - interpreterSettingManager.setInterpreterBinding("user1", "note2", interpreterSettingManager.getInterpreterSettingIds()); - // create 2 interpreter of the first binded interpreter setting for note1 and note2 - interpreterFactory.getInterpreter("user1", "note1", ""); - interpreterFactory.getInterpreter("user1", "note2", ""); - assertEquals(1, defaultInterpreterSetting.getAllInterpreterGroups().size()); - assertEquals(2, defaultInterpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - // choose the first setting - List newSettingIds = new ArrayList<>(); - newSettingIds.add(interpreterSettingManager.getInterpreterSettingIds().get(1)); - - interpreterSettingManager.setInterpreterBinding("user1", "note1", newSettingIds); - assertEquals(newSettingIds, interpreterSettingManager.getInterpreterBinding("note1")); - // InterpreterGroup will be still alive but session belong to note1 will be closed - assertEquals(1, defaultInterpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, defaultInterpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - } -} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java deleted file mode 100644 index 3c061a99a4c..00000000000 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java +++ /dev/null @@ -1,411 +0,0 @@ -/* - * 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.interpreter; - -import org.junit.Test; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -public class InterpreterSettingTest { - - @Test - public void testCreateInterpreters() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerUser(InterpreterOption.SHARED); - interpreterOption.setRemote(false); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); - InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - interpreterInfos.add(interpreterInfo2); - InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() - .setId("id") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .create(); - - // create default interpreter for user1 and note1 - assertEquals(EchoInterpreter.class.getName(), interpreterSetting.getDefaultInterpreter("user1", "note1").getClassName()); - - // create interpreter echo for user1 and note1 - assertEquals(EchoInterpreter.class.getName(), interpreterSetting.getInterpreter("user1", "note1", "echo").getClassName()); - assertEquals(interpreterSetting.getDefaultInterpreter("user1", "note1"), interpreterSetting.getInterpreter("user1", "note1", "echo")); - - // create interpreter double_echo for user1 and note1 - assertEquals(DoubleEchoInterpreter.class.getName(), interpreterSetting.getInterpreter("user1", "note1", "double_echo").getClassName()); - - // create non-existed interpreter - assertNull(interpreterSetting.getInterpreter("user1", "note1", "invalid_echo")); - } - - @Test - public void testSharedMode() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerUser(InterpreterOption.SHARED); - interpreterOption.setRemote(false); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); - InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - interpreterInfos.add(interpreterInfo2); - InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() - .setId("id") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .create(); - - // create default interpreter for user1 and note1 - interpreterSetting.getDefaultInterpreter("user1", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - - // create default interpreter for user2 and note1 - interpreterSetting.getDefaultInterpreter("user2", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - - // create default interpreter user1 and note2 - interpreterSetting.getDefaultInterpreter("user1", "note2"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - - // only 1 session is created, this session is shared across users and notes - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - interpreterSetting.closeInterpreters("note1", "user1"); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - } - - @Test - public void testPerUserScopedMode() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerUser(InterpreterOption.SCOPED); - interpreterOption.setRemote(true); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); - InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - interpreterInfos.add(interpreterInfo2); - InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() - .setId("id") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .create(); - - // create interpreter for user1 and note1 - interpreterSetting.getDefaultInterpreter("user1", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - // create interpreter for user2 and note1 - interpreterSetting.getDefaultInterpreter("user2", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - interpreterSetting.closeInterpreters("user1", "note1"); - // InterpreterGroup is still there, but one session is removed - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - interpreterSetting.closeInterpreters("user2", "note1"); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - } - - @Test - public void testPerNoteScopedMode() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerNote(InterpreterOption.SCOPED); - interpreterOption.setRemote(true); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); - InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - interpreterInfos.add(interpreterInfo2); - InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() - .setId("id") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .create(); - - // create interpreter for user1 and note1 - interpreterSetting.getDefaultInterpreter("user1", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - // create interpreter for user1 and note2 - interpreterSetting.getDefaultInterpreter("user1", "note2"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - interpreterSetting.closeInterpreters("user1", "note1"); - // InterpreterGroup is still there, but one session is removed - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - interpreterSetting.closeInterpreters("user1", "note2"); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - } - - @Test - public void testPerUserIsolatedMode() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerUser(InterpreterOption.ISOLATED); - interpreterOption.setRemote(true); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); - InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - interpreterInfos.add(interpreterInfo2); - InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() - .setId("id") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .create(); - - // create interpreter for user1 and note1 - interpreterSetting.getDefaultInterpreter("user1", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - // create interpreter for user2 and note1 - interpreterSetting.getDefaultInterpreter("user2", "note1"); - assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); - - // Each user own one InterpreterGroup and one session per InterpreterGroup - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(1).getSessionNum()); - - interpreterSetting.closeInterpreters("user1", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - interpreterSetting.closeInterpreters("user2", "note1"); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - } - - @Test - public void testPerNoteIsolatedMode() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerNote(InterpreterOption.ISOLATED); - interpreterOption.setRemote(true); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); - InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - interpreterInfos.add(interpreterInfo2); - InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() - .setId("id") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .create(); - - // create interpreter for user1 and note1 - interpreterSetting.getDefaultInterpreter("user1", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - // create interpreter for user2 and note2 - interpreterSetting.getDefaultInterpreter("user1", "note2"); - assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); - // Each user own one InterpreterGroup and one session per InterpreterGroup - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(1).getSessionNum()); - - interpreterSetting.closeInterpreters("user1", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - interpreterSetting.closeInterpreters("user1", "note2"); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - } - - @Test - public void testPerUserIsolatedPerNoteScopedMode() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerUser(InterpreterOption.ISOLATED); - interpreterOption.setPerNote(InterpreterOption.SCOPED); - interpreterOption.setRemote(true); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); - InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - interpreterInfos.add(interpreterInfo2); - InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() - .setId("id") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .create(); - - // create interpreter for user1 and note1 - interpreterSetting.getDefaultInterpreter("user1", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - interpreterSetting.getDefaultInterpreter("user1", "note2"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - // create interpreter for user2 and note1 - interpreterSetting.getDefaultInterpreter("user2", "note1"); - assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); - - // group1 for user1 has 2 sessions, and group2 for user2 has 1 session - assertEquals(interpreterSetting.getInterpreterGroup("user1", "note1"), interpreterSetting.getInterpreterGroup("user1", "note2")); - assertEquals(2, interpreterSetting.getInterpreterGroup("user1", "note1").getSessionNum()); - assertEquals(2, interpreterSetting.getInterpreterGroup("user1", "note2").getSessionNum()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user2", "note1").getSessionNum()); - - // close one session for user1 - interpreterSetting.closeInterpreters("user1", "note1"); - assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").getSessionNum()); - - // close another session for user1 - interpreterSetting.closeInterpreters("user1", "note2"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - - // close session for user2 - interpreterSetting.closeInterpreters("user2", "note1"); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - } - - @Test - public void testPerUserIsolatedPerNoteIsolatedMode() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerUser(InterpreterOption.ISOLATED); - interpreterOption.setPerNote(InterpreterOption.ISOLATED); - interpreterOption.setRemote(true); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); - InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - interpreterInfos.add(interpreterInfo2); - InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() - .setId("id") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .create(); - - // create interpreter for user1 and note1 - interpreterSetting.getDefaultInterpreter("user1", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - - // create interpreter for user1 and note2 - interpreterSetting.getDefaultInterpreter("user1", "note2"); - assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); - - // create interpreter for user2 and note1 - interpreterSetting.getDefaultInterpreter("user2", "note1"); - assertEquals(3, interpreterSetting.getAllInterpreterGroups().size()); - - // create interpreter for user2 and note2 - interpreterSetting.getDefaultInterpreter("user2", "note2"); - assertEquals(4, interpreterSetting.getAllInterpreterGroups().size()); - - for (InterpreterGroup interpreterGroup : interpreterSetting.getAllInterpreterGroups()) { - // each InterpreterGroup has one session - assertEquals(1, interpreterGroup.getSessionNum()); - } - - // close one session for user1 and note1 - interpreterSetting.closeInterpreters("user1", "note1"); - assertEquals(3, interpreterSetting.getAllInterpreterGroups().size()); - - // close one session for user1 and note2 - interpreterSetting.closeInterpreters("user1", "note2"); - assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); - - // close one session for user2 and note1 - interpreterSetting.closeInterpreters("user2", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - - // close one session for user2 and note2 - interpreterSetting.closeInterpreters("user2", "note2"); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - } - - @Test - public void testPerUserScopedPerNoteScopedMode() { - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setPerUser(InterpreterOption.SCOPED); - interpreterOption.setPerNote(InterpreterOption.SCOPED); - interpreterOption.setRemote(true); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); - InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - interpreterInfos.add(interpreterInfo2); - InterpreterSetting interpreterSetting = new InterpreterSetting.Builder() - .setId("id") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .create(); - - // create interpreter for user1 and note1 - interpreterSetting.getDefaultInterpreter("user1", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - // create interpreter for user1 and note2 - interpreterSetting.getDefaultInterpreter("user1", "note2"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - // create interpreter for user2 and note1 - interpreterSetting.getDefaultInterpreter("user2", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(3, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - // create interpreter for user2 and note2 - interpreterSetting.getDefaultInterpreter("user2", "note2"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(4, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - // close one session for user1 and note1 - interpreterSetting.closeInterpreters("user1", "note1"); - assertEquals(3, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - // close one session for user1 and note2 - interpreterSetting.closeInterpreters("user1", "note2"); - assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - // close one session for user2 and note1 - interpreterSetting.closeInterpreters("user2", "note1"); - assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - - // close one session for user2 and note2 - interpreterSetting.closeInterpreters("user2", "note2"); - assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); - } -} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterTest.java index d46eaa710d1..305268c89df 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterTest.java @@ -24,14 +24,13 @@ import static org.junit.Assert.assertEquals; -//TODO(zjffdu) add more test for Interpreter which is a very important class public class InterpreterTest { @Test public void testDefaultProperty() { Properties p = new Properties(); p.put("p1", "v1"); - Interpreter intp = new EchoInterpreter(p); + Interpreter intp = new DummyInterpreter(p); assertEquals(1, intp.getProperty().size()); assertEquals("v1", intp.getProperty().get("p1")); @@ -42,7 +41,7 @@ public void testDefaultProperty() { public void testOverriddenProperty() { Properties p = new Properties(); p.put("p1", "v1"); - Interpreter intp = new EchoInterpreter(p); + Interpreter intp = new DummyInterpreter(p); Properties overriddenProperty = new Properties(); overriddenProperty.put("p1", "v2"); intp.setProperty(overriddenProperty); @@ -74,7 +73,7 @@ public void testPropertyWithReplacedContextFields() { Properties p = new Properties(); p.put("p1", "replName #{noteId}, #{paragraphTitle}, #{paragraphId}, #{paragraphText}, #{replName}, #{noteId}, #{user}," + " #{authenticationInfo}"); - Interpreter intp = new EchoInterpreter(p); + Interpreter intp = new DummyInterpreter(p); intp.setUserName(user); String actual = intp.getProperty("p1"); InterpreterContext.remove(); diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/SleepInterpreter.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/SleepInterpreter.java deleted file mode 100644 index 9deafcfa00c..00000000000 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/SleepInterpreter.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.apache.zeppelin.interpreter; - -import org.apache.zeppelin.scheduler.Scheduler; -import org.apache.zeppelin.scheduler.SchedulerFactory; - -import java.util.Properties; - -/** - * Interpreter that only accept long value and sleep for such period - */ -public class SleepInterpreter extends Interpreter { - - public SleepInterpreter(Properties property) { - super(property); - } - - @Override - public void open() { - - } - - @Override - public void close() { - - } - - @Override - public InterpreterResult interpret(String st, InterpreterContext context) { - try { - Thread.sleep(Long.parseLong(st)); - return new InterpreterResult(InterpreterResult.Code.SUCCESS); - } catch (Exception e) { - return new InterpreterResult(InterpreterResult.Code.ERROR, e.getMessage()); - } - } - - @Override - public void cancel(InterpreterContext context) { - - } - - @Override - public FormType getFormType() { - return FormType.NATIVE; - } - - @Override - public Scheduler getScheduler() { - if (Boolean.parseBoolean(property.getProperty("zeppelin.SleepInterpreter.parallel", "false"))) { - return SchedulerFactory.singleton().createOrGetParallelScheduler( - "Parallel-" + SleepInterpreter.class.getName(), 10); - } - return super.getScheduler(); - } - - @Override - public int getProgress(InterpreterContext context) { - return 0; - } -} 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 deleted file mode 100644 index ae98dc386e5..00000000000 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java +++ /dev/null @@ -1,520 +0,0 @@ -/* - * 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.interpreter.remote; - -import org.apache.thrift.transport.TTransportException; -import org.apache.zeppelin.display.GUI; -import org.apache.zeppelin.interpreter.*; -import org.apache.zeppelin.interpreter.InterpreterResult.Code; -import org.apache.zeppelin.interpreter.remote.mock.GetEnvPropertyInterpreter; -import org.apache.zeppelin.user.AuthenticationInfo; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; - -public class RemoteInterpreterTest { - - - private static final String INTERPRETER_SCRIPT = - System.getProperty("os.name").startsWith("Windows") ? - "../bin/interpreter.cmd" : - "../bin/interpreter.sh"; - - private InterpreterSetting interpreterSetting; - - @Before - public void setUp() throws Exception { - InterpreterOption interpreterOption = new InterpreterOption(); - - interpreterOption.setRemote(true); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(EchoInterpreter.class.getName(), "echo", true, new HashMap()); - InterpreterInfo interpreterInfo2 = new InterpreterInfo(DoubleEchoInterpreter.class.getName(), "double_echo", false, new HashMap()); - InterpreterInfo interpreterInfo3 = new InterpreterInfo(SleepInterpreter.class.getName(), "sleep", false, new HashMap()); - InterpreterInfo interpreterInfo4 = new InterpreterInfo(GetEnvPropertyInterpreter.class.getName(), "get", false, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - interpreterInfos.add(interpreterInfo2); - interpreterInfos.add(interpreterInfo3); - interpreterInfos.add(interpreterInfo4); - InterpreterRunner runner = new InterpreterRunner(INTERPRETER_SCRIPT, INTERPRETER_SCRIPT); - interpreterSetting = new InterpreterSetting.Builder() - .setId("test") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .setRunner(runner) - .setInterpreterDir("../interpeters/test") - .create(); - } - - @After - public void tearDown() throws Exception { - interpreterSetting.close(); - } - - @Test - public void testSharedMode() { - interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); - - Interpreter interpreter1 = interpreterSetting.getDefaultInterpreter("user1", "note1"); - Interpreter interpreter2 = interpreterSetting.getDefaultInterpreter("user2", "note1"); - assertTrue(interpreter1 instanceof RemoteInterpreter); - RemoteInterpreter remoteInterpreter1 = (RemoteInterpreter) interpreter1; - assertTrue(interpreter2 instanceof RemoteInterpreter); - RemoteInterpreter remoteInterpreter2 = (RemoteInterpreter) interpreter2; - - InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), - null, null, new ArrayList(), null); - assertEquals("hello", remoteInterpreter1.interpret("hello", context1).message().get(0).getData()); - assertEquals(Interpreter.FormType.NATIVE, interpreter1.getFormType()); - assertEquals(0, remoteInterpreter1.getProgress(context1)); - assertNotNull(remoteInterpreter1.getOrCreateInterpreterProcess()); - assertTrue(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess().isRunning()); - - assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); - assertEquals(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess(), - remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess()); - - // Call InterpreterGroup.close instead of Interpreter.close, otherwise we will have the - // RemoteInterpreterProcess leakage. - remoteInterpreter1.getInterpreterGroup().close(remoteInterpreter1.getSessionId()); - assertNull(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess()); - try { - assertEquals("hello", remoteInterpreter1.interpret("hello", context1).message().get(0).getData()); - fail("Should not be able to call interpret after interpreter is closed"); - } catch (Exception e) { - e.printStackTrace(); - } - - try { - assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); - fail("Should not be able to call getProgress after RemoterInterpreterProcess is stoped"); - } catch (Exception e) { - e.printStackTrace(); - } - } - - @Test - public void testScopedMode() { - interpreterSetting.getOption().setPerUser(InterpreterOption.SCOPED); - - Interpreter interpreter1 = interpreterSetting.getDefaultInterpreter("user1", "note1"); - Interpreter interpreter2 = interpreterSetting.getDefaultInterpreter("user2", "note1"); - assertTrue(interpreter1 instanceof RemoteInterpreter); - RemoteInterpreter remoteInterpreter1 = (RemoteInterpreter) interpreter1; - assertTrue(interpreter2 instanceof RemoteInterpreter); - RemoteInterpreter remoteInterpreter2 = (RemoteInterpreter) interpreter2; - - InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), - null, null, new ArrayList(), null); - assertEquals("hello", remoteInterpreter1.interpret("hello", context1).message().get(0).getData()); - assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); - assertEquals(Interpreter.FormType.NATIVE, interpreter1.getFormType()); - assertEquals(0, remoteInterpreter1.getProgress(context1)); - - assertNotNull(remoteInterpreter1.getOrCreateInterpreterProcess()); - assertTrue(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess().isRunning()); - - assertEquals(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess(), - remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess()); - // Call InterpreterGroup.close instead of Interpreter.close, otherwise we will have the - // RemoteInterpreterProcess leakage. - remoteInterpreter1.getInterpreterGroup().close(remoteInterpreter1.getSessionId()); - try { - assertEquals("hello", remoteInterpreter1.interpret("hello", context1).message().get(0).getData()); - fail("Should not be able to call interpret after interpreter is closed"); - } catch (Exception e) { - e.printStackTrace(); - } - - assertTrue(remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess().isRunning()); - assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); - remoteInterpreter2.getInterpreterGroup().close(remoteInterpreter2.getSessionId()); - try { - assertEquals("hello", remoteInterpreter2.interpret("hello", context1)); - fail("Should not be able to call interpret after interpreter is closed"); - } catch (Exception e) { - e.printStackTrace(); - } - assertNull(remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess()); - } - - @Test - public void testIsolatedMode() { - interpreterSetting.getOption().setPerUser(InterpreterOption.ISOLATED); - - Interpreter interpreter1 = interpreterSetting.getDefaultInterpreter("user1", "note1"); - Interpreter interpreter2 = interpreterSetting.getDefaultInterpreter("user2", "note1"); - assertTrue(interpreter1 instanceof RemoteInterpreter); - RemoteInterpreter remoteInterpreter1 = (RemoteInterpreter) interpreter1; - assertTrue(interpreter2 instanceof RemoteInterpreter); - RemoteInterpreter remoteInterpreter2 = (RemoteInterpreter) interpreter2; - - InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), - null, null, new ArrayList(), null); - assertEquals("hello", remoteInterpreter1.interpret("hello", context1).message().get(0).getData()); - assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); - assertEquals(Interpreter.FormType.NATIVE, interpreter1.getFormType()); - assertEquals(0, remoteInterpreter1.getProgress(context1)); - assertNotNull(remoteInterpreter1.getOrCreateInterpreterProcess()); - assertTrue(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess().isRunning()); - - assertNotEquals(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess(), - remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess()); - // Call InterpreterGroup.close instead of Interpreter.close, otherwise we will have the - // RemoteInterpreterProcess leakage. - remoteInterpreter1.getInterpreterGroup().close(remoteInterpreter1.getSessionId()); - assertNull(remoteInterpreter1.getInterpreterGroup().getRemoteInterpreterProcess()); - assertTrue(remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess().isRunning()); - try { - remoteInterpreter1.interpret("hello", context1); - fail("Should not be able to call getProgress after interpreter is closed"); - } catch (Exception e) { - e.printStackTrace(); - } - - assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); - remoteInterpreter2.getInterpreterGroup().close(remoteInterpreter2.getSessionId()); - try { - assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); - fail("Should not be able to call interpret after interpreter is closed"); - } catch (Exception e) { - e.printStackTrace(); - } - assertNull(remoteInterpreter2.getInterpreterGroup().getRemoteInterpreterProcess()); - - } - -// @Test -// public void testExecuteIncorrectPrecode() throws TTransportException, IOException { -// interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); -// interpreterSetting.getProperties().setProperty("zeppelin.SleepInterpreter.precode", "fail test"); -// -// Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); -// InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", -// "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), -// null, null, new ArrayList(), null); -// assertEquals(Code.ERROR, interpreter1.interpret("10", context1).code()); -// } -// -// @Test -// public void testExecuteCorrectPrecode() throws TTransportException, IOException { -// interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); -// interpreterSetting.getProperties().setProperty("zeppelin.SleepInterpreter.precode", "1"); -// -// Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); -// InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", -// "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), -// null, null, new ArrayList(), null); -// assertEquals(Code.SUCCESS, interpreter1.interpret("10", context1).code()); -// } - - @Test - public void testRemoteInterperterErrorStatus() throws TTransportException, IOException { - interpreterSetting.setProperty("zeppelin.interpreter.echo.fail", "true"); - interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); - - Interpreter interpreter1 = interpreterSetting.getDefaultInterpreter("user1", "note1"); - assertTrue(interpreter1 instanceof RemoteInterpreter); - RemoteInterpreter remoteInterpreter1 = (RemoteInterpreter) interpreter1; - - InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), - null, null, new ArrayList(), null); - assertEquals(Code.ERROR, remoteInterpreter1.interpret("hello", context1).code()); - } - - @Test - public void testFIFOScheduler() throws InterruptedException { - interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); - // by default SleepInterpreter would use FIFOScheduler - - final Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); - final InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), - null, null, new ArrayList(), null); - // run this dummy interpret method first to launch the RemoteInterpreterProcess to avoid the - // time overhead of launching the process. - interpreter1.interpret("1", context1); - Thread thread1 = new Thread() { - @Override - public void run() { - assertEquals(Code.SUCCESS, interpreter1.interpret("100", context1).code()); - } - }; - Thread thread2 = new Thread() { - @Override - public void run() { - assertEquals(Code.SUCCESS, interpreter1.interpret("100", context1).code()); - } - }; - long start = System.currentTimeMillis(); - thread1.start(); - thread2.start(); - thread1.join(); - thread2.join(); - long end = System.currentTimeMillis(); - assertTrue((end - start) >= 200); - } - - @Test - public void testParallelScheduler() throws InterruptedException { - interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); - interpreterSetting.setProperty("zeppelin.SleepInterpreter.parallel", "true"); - - final Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); - final InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), - null, null, new ArrayList(), null); - - // run this dummy interpret method first to launch the RemoteInterpreterProcess to avoid the - // time overhead of launching the process. - interpreter1.interpret("1", context1); - Thread thread1 = new Thread() { - @Override - public void run() { - assertEquals(Code.SUCCESS, interpreter1.interpret("100", context1).code()); - } - }; - Thread thread2 = new Thread() { - @Override - public void run() { - assertEquals(Code.SUCCESS, interpreter1.interpret("100", context1).code()); - } - }; - long start = System.currentTimeMillis(); - thread1.start(); - thread2.start(); - thread1.join(); - thread2.join(); - long end = System.currentTimeMillis(); - assertTrue((end - start) <= 200); - } - -// @Test -// public void testRunOrderPreserved() throws InterruptedException { -// Properties p = new Properties(); -// intpGroup.put("note", new LinkedList()); -// -// final RemoteInterpreter intpA = createMockInterpreterA(p); -// -// intpGroup.get("note").add(intpA); -// intpA.setInterpreterGroup(intpGroup); -// -// intpA.open(); -// -// int concurrency = 3; -// final List results = new LinkedList<>(); -// -// Scheduler scheduler = intpA.getScheduler(); -// for (int i = 0; i < concurrency; i++) { -// final String jobId = Integer.toString(i); -// scheduler.submit(new Job(jobId, Integer.toString(i), null, 200) { -// private Object r; -// -// @Override -// public Object getReturn() { -// return r; -// } -// -// @Override -// public void setResult(Object results) { -// this.r = results; -// } -// -// @Override -// public int progress() { -// return 0; -// } -// -// @Override -// public Map info() { -// return null; -// } -// -// @Override -// protected Object jobRun() throws Throwable { -// InterpreterResult ret = intpA.interpret(getJobName(), new InterpreterContext( -// "note", -// jobId, -// null, -// "title", -// "text", -// new AuthenticationInfo(), -// new HashMap(), -// new GUI(), -// new AngularObjectRegistry(intpGroup.getId(), null), -// new LocalResourcePool("pool1"), -// new LinkedList(), null)); -// -// synchronized (results) { -// results.addAll(ret.message()); -// results.notify(); -// } -// return null; -// } -// -// @Override -// protected boolean jobAbort() { -// return false; -// } -// -// }); -// } -// -// // wait for job finished -// synchronized (results) { -// while (results.size() != concurrency) { -// results.wait(300); -// } -// } -// -// int i = 0; -// for (InterpreterResultMessage result : results) { -// assertEquals(Integer.toString(i++), result.getData()); -// } -// assertEquals(concurrency, i); -// -// intpA.close(); -// } - - -// @Test -// public void testRemoteInterpreterSharesTheSameSchedulerInstanceInTheSameGroup() { -// Properties p = new Properties(); -// intpGroup.put("note", new LinkedList()); -// -// RemoteInterpreter intpA = createMockInterpreterA(p); -// -// intpGroup.get("note").add(intpA); -// intpA.setInterpreterGroup(intpGroup); -// -// RemoteInterpreter intpB = createMockInterpreterB(p); -// -// intpGroup.get("note").add(intpB); -// intpB.setInterpreterGroup(intpGroup); -// -// intpA.open(); -// intpB.open(); -// -// assertEquals(intpA.getScheduler(), intpB.getScheduler()); -// } - -// @Test -// public void testMultiInterpreterSession() { -// Properties p = new Properties(); -// intpGroup.put("sessionA", new LinkedList()); -// intpGroup.put("sessionB", new LinkedList()); -// -// RemoteInterpreter intpAsessionA = createMockInterpreterA(p, "sessionA"); -// intpGroup.get("sessionA").add(intpAsessionA); -// intpAsessionA.setInterpreterGroup(intpGroup); -// -// RemoteInterpreter intpBsessionA = createMockInterpreterB(p, "sessionA"); -// intpGroup.get("sessionA").add(intpBsessionA); -// intpBsessionA.setInterpreterGroup(intpGroup); -// -// intpAsessionA.open(); -// intpBsessionA.open(); -// -// assertEquals(intpAsessionA.getScheduler(), intpBsessionA.getScheduler()); -// -// RemoteInterpreter intpAsessionB = createMockInterpreterA(p, "sessionB"); -// intpGroup.get("sessionB").add(intpAsessionB); -// intpAsessionB.setInterpreterGroup(intpGroup); -// -// RemoteInterpreter intpBsessionB = createMockInterpreterB(p, "sessionB"); -// intpGroup.get("sessionB").add(intpBsessionB); -// intpBsessionB.setInterpreterGroup(intpGroup); -// -// intpAsessionB.open(); -// intpBsessionB.open(); -// -// assertEquals(intpAsessionB.getScheduler(), intpBsessionB.getScheduler()); -// assertNotEquals(intpAsessionA.getScheduler(), intpAsessionB.getScheduler()); -// } - -// @Test -// public void should_push_local_angular_repo_to_remote() throws Exception { -// //Given -// final Client client = mock(Client.class); -// final RemoteInterpreter intr = null; -//// new RemoteInterpreter(new Properties(), "noteId", -//// MockInterpreterA.class.getName(), "runner", "path", "localRepo", env, 10 * 1000, null, -//// null, "anonymous", false); -// final AngularObjectRegistry registry = new AngularObjectRegistry("spark", null); -// registry.add("name", "DuyHai DOAN", "nodeId", "paragraphId"); -// final InterpreterGroup interpreterGroup = new InterpreterGroup("groupId"); -// interpreterGroup.setAngularObjectRegistry(registry); -// intr.setInterpreterGroup(interpreterGroup); -// -// final java.lang.reflect.Type registryType = new TypeToken>>() { -// }.getType(); -// final Gson gson = new Gson(); -// final String expected = gson.toJson(registry.getRegistry(), registryType); -// -// //When -//// intr.pushAngularObjectRegistryToRemote(client); -// -// //Then -// Mockito.verify(client).angularRegistryPush(expected); -// } - - @Test - public void testEnvStringPattern() { - assertFalse(RemoteInterpreterUtils.isEnvString(null)); - assertFalse(RemoteInterpreterUtils.isEnvString("")); - assertFalse(RemoteInterpreterUtils.isEnvString("abcDEF")); - assertFalse(RemoteInterpreterUtils.isEnvString("ABC-DEF")); - assertTrue(RemoteInterpreterUtils.isEnvString("ABCDEF")); - assertTrue(RemoteInterpreterUtils.isEnvString("ABC_DEF")); - assertTrue(RemoteInterpreterUtils.isEnvString("ABC_DEF123")); - } - - @Test - public void testEnvironmentAndProperty() { - interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); - interpreterSetting.setProperty("ENV_1", "VALUE_1"); - interpreterSetting.setProperty("property_1", "value_1"); - - final Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "get"); - final InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), - null, null, new ArrayList(), null); - - assertEquals("VALUE_1", interpreter1.interpret("getEnv ENV_1", context1).message().get(0).getData()); - assertEquals("null", interpreter1.interpret("getEnv ENV_2", context1).message().get(0).getData()); - - assertEquals("value_1", interpreter1.interpret("getProperty property_1", context1).message().get(0).getData()); - assertEquals("null", interpreter1.interpret("getProperty property_2", context1).message().get(0).getData()); - } - -} diff --git a/zeppelin-interpreter/src/test/resources/conf/interpreter.json b/zeppelin-interpreter/src/test/resources/conf/interpreter.json deleted file mode 100644 index 45e1d601fd3..00000000000 --- a/zeppelin-interpreter/src/test/resources/conf/interpreter.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "interpreterSettings": { - "2C3RWCVAG": { - "id": "2C3RWCVAG", - "name": "test", - "group": "test", - "properties": { - "property_1": "value_1", - "property_2": "new_value_2", - "property_3": "value_3" - }, - "status": "READY", - "interpreterGroup": [ - { - "name": "echo", - "class": "org.apache.zeppelin.interpreter.EchoInterpreter", - "defaultInterpreter": true, - "editor": { - "language": "java", - "editOnDblClick": false - } - } - ], - "dependencies": [], - "option": { - "remote": true, - "port": -1, - "perNote": "shared", - "perUser": "shared", - "isExistingProcess": false, - "setPermission": false, - "users": [], - "isUserImpersonate": false - } - }, - - "2CKWE7B19": { - "id": "2CKWE7B19", - "name": "test2", - "group": "test", - "properties": { - "property_1": "value_1", - "property_2": "new_value_2", - "property_3": "value_3" - }, - "status": "READY", - "interpreterGroup": [ - { - "name": "echo", - "class": "org.apache.zeppelin.interpreter.EchoInterpreter", - "defaultInterpreter": true, - "editor": { - "language": "java", - "editOnDblClick": false - } - } - ], - "dependencies": [], - "option": { - "remote": true, - "port": -1, - "perNote": "shared", - "perUser": "shared", - "isExistingProcess": false, - "setPermission": false, - "users": [], - "isUserImpersonate": false - } - } - }, - "interpreterBindings": { - "2C6793KRV": [ - "2C48Y7FSJ", - "2C63XW4XE", - "2C66GE1VB", - "2C5VH924X", - "2C4BJDRRZ", - "2C3SQSB7V", - "2C4HKDCQW", - "2C3DR183X", - "2C66Z9XPQ", - "2C3PTPMUH", - "2C69WE69N", - "2C5SRRXHM", - "2C4ZD49PF", - "2C6V3D44K", - "2C4UB1UZA", - "2C5S1R21W", - "2C5DCRVGM", - "2C686X8ZH", - "2C3RWCVAG", - "2C3JKFMJU", - "2C3VECEG2" - ] - }, - "interpreterRepositories": [ - { - "id": "central", - "type": "default", - "url": "http://repo1.maven.org/maven2/", - "releasePolicy": { - "enabled": true, - "updatePolicy": "daily", - "checksumPolicy": "warn" - }, - "snapshotPolicy": { - "enabled": true, - "updatePolicy": "daily", - "checksumPolicy": "warn" - }, - "mirroredRepositories": [], - "repositoryManager": false - } - ] -} \ No newline at end of file diff --git a/zeppelin-interpreter/src/test/resources/interpreter/test/interpreter-setting.json b/zeppelin-interpreter/src/test/resources/interpreter/test/interpreter-setting.json deleted file mode 100644 index 1ba1b94b397..00000000000 --- a/zeppelin-interpreter/src/test/resources/interpreter/test/interpreter-setting.json +++ /dev/null @@ -1,42 +0,0 @@ -[ - { - "group": "test", - "name": "double_echo", - "className": "org.apache.zeppelin.interpreter.DoubleEchoInterpreter", - "properties": { - "property_1": { - "envName": "PROPERTY_1", - "propertyName": "property_1", - "defaultValue": "value_1", - "description": "desc_1" - }, - "property_2": { - "envName": "PROPERTY_2", - "propertyName": "property_2", - "defaultValue": "value_2", - "description": "desc_2" - } - } - }, - - { - "group": "test", - "name": "echo", - "defaultInterpreter": true, - "className": "org.apache.zeppelin.interpreter.EchoInterpreter", - "properties": { - "property_1": { - "envName": "PROPERTY_1", - "propertyName": "property_1", - "defaultValue": "value_1", - "description": "desc_1" - }, - "property_2": { - "envName": "PROPERTY_2", - "propertyName": "property_2", - "defaultValue": "value_2", - "description": "desc_2" - } - } - } -] diff --git a/zeppelin-interpreter/src/test/resources/log4j.properties b/zeppelin-interpreter/src/test/resources/log4j.properties index 6f346916cc9..d8a783974e3 100644 --- a/zeppelin-interpreter/src/test/resources/log4j.properties +++ b/zeppelin-interpreter/src/test/resources/log4j.properties @@ -26,6 +26,4 @@ log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c:%L - %m%n # # Root logger option -log4j.rootLogger=INFO, stdout -log4j.logger.org.apache.zeppelin.interpreter=DEBUG -log4j.logger.org.apache.zeppelin.scheduler=DEBUG \ No newline at end of file +log4j.rootLogger=INFO, stdout \ No newline at end of file diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/InterpreterRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/InterpreterRestApi.java index c1dba5c417b..cd0210e4f28 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/InterpreterRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/InterpreterRestApi.java @@ -185,7 +185,7 @@ public Response restartSetting(String message, @PathParam("settingId") String se String noteId = request == null ? null : request.getNoteId(); if (null == noteId) { - interpreterSettingManager.close(settingId); + interpreterSettingManager.close(setting); } else { interpreterSettingManager.restart(settingId, noteId, SecurityUtils.getPrincipal()); } @@ -208,7 +208,7 @@ public Response restartSetting(String message, @PathParam("settingId") String se @GET @ZeppelinApi public Response listInterpreter(String message) { - Map m = interpreterSettingManager.getInterpreterSettingTemplates(); + Map m = interpreterSettingManager.getAvailableInterpreterSettings(); return new JsonResponse<>(Status.OK, "", m).build(); } 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 53ee1146107..745347048a2 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 @@ -93,11 +93,13 @@ public class ZeppelinServer extends Application { private NotebookRepoSync notebookRepo; private NotebookAuthorization notebookAuthorization; private Credentials credentials; + private DependencyResolver depResolver; public ZeppelinServer() throws Exception { ZeppelinConfiguration conf = ZeppelinConfiguration.create(); - + this.depResolver = new DependencyResolver( + conf.getString(ConfVars.ZEPPELIN_INTERPRETER_LOCALREPO)); InterpreterOutput.limit = conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_OUTPUT_LIMIT); @@ -127,26 +129,13 @@ public ZeppelinServer() throws Exception { new File(conf.getRelativeDir("zeppelin-web/src/app/spell"))); } - this.schedulerFactory = SchedulerFactory.singleton(); - this.interpreterSettingManager = new InterpreterSettingManager(conf, notebookWsServer, - notebookWsServer, notebookWsServer); - this.replFactory = new InterpreterFactory(interpreterSettingManager); - this.notebookRepo = new NotebookRepoSync(conf); - this.noteSearchService = new LuceneSearch(); - this.notebookAuthorization = NotebookAuthorization.init(conf); - this.credentials = new Credentials(conf.credentialsPersist(), conf.getCredentialsPath()); - notebook = new Notebook(conf, - notebookRepo, schedulerFactory, replFactory, interpreterSettingManager, notebookWsServer, - noteSearchService, notebookAuthorization, credentials); - ZeppelinServer.helium = new Helium( conf.getHeliumConfPath(), conf.getHeliumRegistry(), new File(conf.getRelativeDir(ConfVars.ZEPPELIN_DEP_LOCALREPO), "helium-registry-cache"), heliumBundleFactory, - heliumApplicationFactory, - interpreterSettingManager); + heliumApplicationFactory); // create bundle try { @@ -155,6 +144,20 @@ public ZeppelinServer() throws Exception { LOG.error(e.getMessage(), e); } + this.schedulerFactory = new SchedulerFactory(); + this.interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, + new InterpreterOption(true)); + this.replFactory = new InterpreterFactory(conf, notebookWsServer, + notebookWsServer, heliumApplicationFactory, depResolver, SecurityUtils.isAuthenticated(), + interpreterSettingManager); + this.notebookRepo = new NotebookRepoSync(conf); + this.noteSearchService = new LuceneSearch(); + this.notebookAuthorization = NotebookAuthorization.init(conf); + this.credentials = new Credentials(conf.credentialsPersist(), conf.getCredentialsPath()); + notebook = new Notebook(conf, + notebookRepo, schedulerFactory, replFactory, interpreterSettingManager, notebookWsServer, + noteSearchService, notebookAuthorization, credentials); + // to update notebook from application event from remote process. heliumApplicationFactory.setNotebook(notebook); // to update fire websocket event on application event. @@ -203,7 +206,7 @@ public static void main(String[] args) throws InterruptedException { LOG.info("Shutting down Zeppelin Server ... "); try { jettyWebServer.stop(); - notebook.getInterpreterSettingManager().close(); + notebook.getInterpreterSettingManager().shutdown(); notebook.close(); Thread.sleep(3000); } catch (Exception e) { 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 102ca1aa343..3ddeec034e2 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 @@ -41,7 +41,12 @@ import org.apache.zeppelin.display.Input; import org.apache.zeppelin.helium.ApplicationEventListener; import org.apache.zeppelin.helium.HeliumPackage; -import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContextRunner; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResultMessage; +import org.apache.zeppelin.interpreter.InterpreterSetting; import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; @@ -458,8 +463,7 @@ private void broadcastToNoteBindedInterpreter(String interpreterGroupId, Message Notebook notebook = notebook(); List notes = notebook.getAllNotes(); for (Note note : notes) { - List ids = notebook.getInterpreterSettingManager() - .getInterpreterBinding(note.getId()); + List ids = notebook.getInterpreterSettingManager().getInterpreters(note.getId()); for (String id : ids) { if (id.equals(interpreterGroupId)) { broadcast(note.getId(), m); @@ -999,7 +1003,7 @@ private void createNote(NotebookSocket conn, HashSet userAndRoles, Noteb List interpreterSettingIds = new LinkedList<>(); interpreterSettingIds.add(defaultInterpreterId); for (String interpreterSettingId : notebook.getInterpreterSettingManager(). - getInterpreterSettingIds()) { + getDefaultInterpreterSettingList()) { if (!interpreterSettingId.equals(defaultInterpreterId)) { interpreterSettingIds.add(interpreterSettingId); } @@ -1359,13 +1363,12 @@ private void angularObjectUpdated(NotebookSocket conn, HashSet userAndRo List settings = notebook.getInterpreterSettingManager().getInterpreterSettings(note.getId()); for (InterpreterSetting setting : settings) { - if (setting.getOrCreateInterpreterGroup(user, note.getId()) == null) { + if (setting.getInterpreterGroup(user, note.getId()) == null) { continue; } - if (interpreterGroupId.equals(setting.getOrCreateInterpreterGroup(user, note.getId()) - .getId())) { + if (interpreterGroupId.equals(setting.getInterpreterGroup(user, note.getId()).getId())) { AngularObjectRegistry angularObjectRegistry = - setting.getOrCreateInterpreterGroup(user, note.getId()).getAngularObjectRegistry(); + setting.getInterpreterGroup(user, note.getId()).getAngularObjectRegistry(); // first trying to get local registry ao = angularObjectRegistry.get(varName, noteId, paragraphId); @@ -1402,13 +1405,12 @@ private void angularObjectUpdated(NotebookSocket conn, HashSet userAndRo List settings = notebook.getInterpreterSettingManager().getInterpreterSettings(note.getId()); for (InterpreterSetting setting : settings) { - if (setting.getOrCreateInterpreterGroup(user, n.getId()) == null) { + if (setting.getInterpreterGroup(user, n.getId()) == null) { continue; } - if (interpreterGroupId.equals(setting.getOrCreateInterpreterGroup(user, n.getId()) - .getId())) { + if (interpreterGroupId.equals(setting.getInterpreterGroup(user, n.getId()).getId())) { AngularObjectRegistry angularObjectRegistry = - setting.getOrCreateInterpreterGroup(user, n.getId()).getAngularObjectRegistry(); + setting.getInterpreterGroup(user, n.getId()).getAngularObjectRegistry(); this.broadcastExcept(n.getId(), new Message(OP.ANGULAR_OBJECT_UPDATE).put("angularObject", ao) .put("interpreterGroupId", interpreterGroupId).put("noteId", n.getId()) @@ -2281,13 +2283,13 @@ private void sendAllAngularObjects(Note note, String user, NotebookSocket conn) for (InterpreterSetting intpSetting : settings) { AngularObjectRegistry registry = - intpSetting.getOrCreateInterpreterGroup(user, note.getId()).getAngularObjectRegistry(); + intpSetting.getInterpreterGroup(user, note.getId()).getAngularObjectRegistry(); List objects = registry.getAllWithGlobal(note.getId()); for (AngularObject object : objects) { conn.send(serializeMessage( new Message(OP.ANGULAR_OBJECT_UPDATE).put("angularObject", object) .put("interpreterGroupId", - intpSetting.getOrCreateInterpreterGroup(user, note.getId()).getId()) + intpSetting.getInterpreterGroup(user, note.getId()).getId()) .put("noteId", note.getId()).put("paragraphId", object.getParagraphId()))); } } @@ -2333,7 +2335,7 @@ public void onRemove(String interpreterGroupId, String name, String noteId, Stri } List settingIds = - notebook.getInterpreterSettingManager().getInterpreterBinding(note.getId()); + notebook.getInterpreterSettingManager().getInterpreters(note.getId()); for (String id : settingIds) { if (interpreterGroupId.contains(id)) { broadcast(note.getId(), diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DoubleEchoInterpreter.java b/zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java similarity index 58% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DoubleEchoInterpreter.java rename to zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java index 8eea4b25a5f..1b1306a78f0 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/DoubleEchoInterpreter.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java @@ -14,48 +14,62 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.apache.zeppelin.interpreter.mock; - -package org.apache.zeppelin.interpreter; - +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Properties; -/** - * - */ -public class DoubleEchoInterpreter extends Interpreter { +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.apache.zeppelin.scheduler.Scheduler; +import org.apache.zeppelin.scheduler.SchedulerFactory; - public DoubleEchoInterpreter(Properties property) { +public class MockInterpreter1 extends Interpreter{ + Map vars = new HashMap<>(); + + public MockInterpreter1(Properties property) { super(property); } @Override public void open() { - } @Override public void close() { - } @Override public InterpreterResult interpret(String st, InterpreterContext context) { - return new InterpreterResult(InterpreterResult.Code.SUCCESS, st + "," + st); + return new InterpreterResult(InterpreterResult.Code.SUCCESS, "repl1: "+st); } @Override public void cancel(InterpreterContext context) { - } @Override public FormType getFormType() { - return null; + return FormType.SIMPLE; } @Override public int getProgress(InterpreterContext context) { return 0; } + + @Override + public Scheduler getScheduler() { + return SchedulerFactory.singleton().createOrGetFIFOScheduler("test_"+this.hashCode()); + } + + @Override + public List completion(String buf, int cursor, + InterpreterContext interpreterContext) { + return null; + } } 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 e2f171f9424..a7907db286b 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 @@ -307,9 +307,10 @@ private static boolean isActiveSparkHome(File dir) { protected static void shutDown() throws Exception { if (!wasRunning) { // restart interpreter to stop all interpreter processes - List settingList = ZeppelinServer.notebook.getInterpreterSettingManager().get(); - for (InterpreterSetting setting : settingList) { - ZeppelinServer.notebook.getInterpreterSettingManager().restart(setting.getId()); + List settingList = ZeppelinServer.notebook.getInterpreterSettingManager() + .getDefaultInterpreterSettingList(); + for (String setting : settingList) { + ZeppelinServer.notebook.getInterpreterSettingManager().restart(setting); } if (shiroIni != null) { FileUtils.deleteQuietly(shiroIni); diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/InterpreterRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/InterpreterRestApiTest.java index 72dd8a7dce9..28541bd3fe9 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/InterpreterRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/InterpreterRestApiTest.java @@ -80,7 +80,7 @@ public void getAvailableInterpreters() throws IOException { // then assertThat(get, isAllowed()); - assertEquals(ZeppelinServer.notebook.getInterpreterSettingManager().getInterpreterSettingTemplates().size(), + assertEquals(ZeppelinServer.notebook.getInterpreterSettingManager().getAvailableInterpreterSettings().size(), body.entrySet().size()); get.releaseConnection(); } @@ -110,7 +110,7 @@ public void testGetNonExistInterpreterSetting() throws IOException { @Test public void testSettingsCRUD() throws IOException { // when: call create setting API - String rawRequest = "{\"name\":\"md3\",\"group\":\"md\"," + + String rawRequest = "{\"name\":\"md2\",\"group\":\"md\"," + "\"properties\":{\"propname\": {\"value\": \"propvalue\", \"name\": \"propname\", \"type\": \"textarea\"}}," + "\"interpreterGroup\":[{\"class\":\"org.apache.zeppelin.markdown.Markdown\",\"name\":\"md\"}]," + "\"dependencies\":[]," + @@ -367,7 +367,7 @@ public void testAddDeleteRepository() throws IOException { @Test public void testGetMetadataInfo() throws IOException { - String jsonRequest = "{\"name\":\"spark_new\",\"group\":\"spark\"," + + String jsonRequest = "{\"name\":\"spark\",\"group\":\"spark\"," + "\"properties\":{\"propname\": {\"value\": \"propvalue\", \"name\": \"propname\", \"type\": \"textarea\"}}," + "\"interpreterGroup\":[{\"class\":\"org.apache.zeppelin.markdown.Markdown\",\"name\":\"md\"}]," + "\"dependencies\":[]," + diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java index 10d77b2dc8e..8da36a61bfa 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java @@ -30,7 +30,6 @@ import org.apache.zeppelin.notebook.socket.Message; import org.apache.zeppelin.notebook.socket.Message.OP; import org.apache.zeppelin.rest.AbstractTestRestApi; -import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.server.ZeppelinServer; import org.apache.zeppelin.user.AuthenticationInfo; import org.junit.AfterClass; @@ -96,7 +95,7 @@ public void checkInvalidOrigin(){ } @Test - public void testMakeSureNoAngularObjectBroadcastToWebsocketWhoFireTheEvent() throws IOException, InterruptedException { + public void testMakeSureNoAngularObjectBroadcastToWebsocketWhoFireTheEvent() throws IOException { // create a notebook Note note1 = notebook.createNote(anonymous); @@ -105,7 +104,7 @@ public void testMakeSureNoAngularObjectBroadcastToWebsocketWhoFireTheEvent() thr List settings = notebook.getInterpreterSettingManager().getInterpreterSettings(note1.getId()); for (InterpreterSetting setting : settings) { if (setting.getName().equals("md")) { - interpreterGroup = setting.getOrCreateInterpreterGroup("anonymous", "sharedProcess"); + interpreterGroup = setting.getInterpreterGroup("anonymous", "sharedProcess"); break; } } @@ -116,14 +115,6 @@ public void testMakeSureNoAngularObjectBroadcastToWebsocketWhoFireTheEvent() thr p1.setAuthenticationInfo(anonymous); note1.run(p1.getId()); - // wait for paragraph finished - while(true) { - if (p1.getStatus() == Job.Status.FINISHED) { - break; - } - Thread.sleep(100); - } - // add angularObject interpreterGroup.getAngularObjectRegistry().add("object1", "value1", note1.getId(), null); diff --git a/zeppelin-server/src/test/resources/log4j.properties b/zeppelin-server/src/test/resources/log4j.properties index 83689930c1a..b0d1067bc46 100644 --- a/zeppelin-server/src/test/resources/log4j.properties +++ b/zeppelin-server/src/test/resources/log4j.properties @@ -33,6 +33,7 @@ log4j.logger.org.apache.hadoop.mapred=WARN log4j.logger.org.apache.hadoop.hive.ql=WARN log4j.logger.org.apache.hadoop.hive.metastore=WARN log4j.logger.org.apache.haadoop.hive.service.HiveServer=WARN +log4j.logger.org.apache.zeppelin.scheduler=WARN log4j.logger.org.quartz=WARN log4j.logger.DataNucleus=WARN diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index 3ae382afc9d..b3d5c63b4fc 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -60,14 +60,6 @@ 0.8.0-SNAPSHOT - - ${project.groupId} - zeppelin-interpreter - ${project.version} - tests - test - - org.slf4j slf4j-api @@ -78,6 +70,11 @@ slf4j-log4j12 + + commons-configuration + commons-configuration + + commons-io commons-io diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java similarity index 99% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java index 03cc0699552..f00fe9346f3 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java @@ -17,21 +17,22 @@ package org.apache.zeppelin.conf; +import java.io.File; +import java.net.URL; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.XMLConfiguration; import org.apache.commons.configuration.tree.ConfigurationNode; import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.notebook.repo.GitNotebookRepo; import org.apache.zeppelin.util.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.net.URL; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** * Zeppelin configuration. * @@ -527,7 +528,7 @@ public Map dumpConfigurations(ZeppelinConfiguration conf, ConfigurationKeyPredicate predicate) { Map configurations = new HashMap<>(); - for (ConfVars v : ConfVars.values()) { + for (ZeppelinConfiguration.ConfVars v : ZeppelinConfiguration.ConfVars.values()) { String key = v.getVarName(); if (!predicate.apply(key)) { @@ -652,8 +653,7 @@ public static enum ConfVars { ZEPPELIN_NOTEBOOK_MONGO_COLLECTION("zeppelin.notebook.mongo.collection", "notes"), ZEPPELIN_NOTEBOOK_MONGO_URI("zeppelin.notebook.mongo.uri", "mongodb://localhost"), ZEPPELIN_NOTEBOOK_MONGO_AUTOIMPORT("zeppelin.notebook.mongo.autoimport", false), - ZEPPELIN_NOTEBOOK_STORAGE("zeppelin.notebook.storage", - "org.apache.zeppelin.notebook.repo.GitNotebookRepo"), + ZEPPELIN_NOTEBOOK_STORAGE("zeppelin.notebook.storage", GitNotebookRepo.class.getName()), ZEPPELIN_NOTEBOOK_ONE_WAY_SYNC("zeppelin.notebook.one.way.sync", false), // whether by default note is public or private ZEPPELIN_NOTEBOOK_PUBLIC("zeppelin.notebook.public", true), 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 index 5eecd6b1cd3..17a3529d915 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java @@ -19,12 +19,11 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.zeppelin.interpreter.Interpreter; -import org.apache.zeppelin.interpreter.InterpreterGroup; -import org.apache.zeppelin.interpreter.InterpreterSettingManager; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; import org.apache.zeppelin.notebook.Paragraph; -import org.apache.zeppelin.resource.*; +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; @@ -48,22 +47,19 @@ public class Helium { private final HeliumBundleFactory bundleFactory; private final HeliumApplicationFactory applicationFactory; - private final InterpreterSettingManager interpreterSettingManager; public Helium( String heliumConfPath, String registryPaths, File registryCacheDir, HeliumBundleFactory bundleFactory, - HeliumApplicationFactory applicationFactory, - InterpreterSettingManager interpreterSettingManager) + HeliumApplicationFactory applicationFactory) throws IOException { this.heliumConfPath = heliumConfPath; this.registryPaths = registryPaths; this.registryCacheDir = registryCacheDir; this.bundleFactory = bundleFactory; this.applicationFactory = applicationFactory; - this.interpreterSettingManager = interpreterSettingManager; heliumConf = loadConf(heliumConfPath); allPackages = getAllPackageInfo(); } @@ -354,7 +350,7 @@ public HeliumPackageSuggestion suggestApp(Paragraph paragraph) { allResources = resourcePool.getAll(); } } else { - allResources = interpreterSettingManager.getAllResources(); + allResources = ResourcePoolUtils.getAllResources(); } for (List pkgs : allPackages.values()) { @@ -482,39 +478,4 @@ private static Map> createMixedConfig(Map resourceList = remoteInterpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction>() { - @Override - public List call(RemoteInterpreterService.Client client) throws Exception { - return client.resourcePoolGetAll(); - } - } - ); - for (String res : resourceList) { - resourceSet.add(Resource.fromJson(res)); - } - } - } - return resourceSet; - } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumApplicationFactory.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumApplicationFactory.java index 5f5330c17a5..84368a76b47 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumApplicationFactory.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumApplicationFactory.java @@ -105,33 +105,38 @@ public void run() { private void load(RemoteInterpreterProcess intpProcess, ApplicationState appState) throws Exception { + RemoteInterpreterService.Client client = null; + synchronized (appState) { if (appState.getStatus() == ApplicationState.Status.LOADED) { // already loaded return; } - appStatusChange(paragraph, appState.getId(), ApplicationState.Status.LOADING); - final String pkgInfo = pkg.toJson(); - final String appId = appState.getId(); - - RemoteApplicationResult ret = intpProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public RemoteApplicationResult call(RemoteInterpreterService.Client client) - throws Exception { - return client.loadApplication( - appId, - pkgInfo, - paragraph.getNote().getId(), - paragraph.getId()); - } - } - ); - if (ret.isSuccess()) { - appStatusChange(paragraph, appState.getId(), ApplicationState.Status.LOADED); - } else { - throw new ApplicationException(ret.getMsg()); + try { + appStatusChange(paragraph, appState.getId(), ApplicationState.Status.LOADING); + String pkgInfo = pkg.toJson(); + String appId = appState.getId(); + + client = intpProcess.getClient(); + RemoteApplicationResult ret = client.loadApplication( + appId, + pkgInfo, + paragraph.getNote().getId(), + paragraph.getId()); + + if (ret.isSuccess()) { + appStatusChange(paragraph, appState.getId(), ApplicationState.Status.LOADED); + } else { + throw new ApplicationException(ret.getMsg()); + } + } catch (TException e) { + intpProcess.releaseBrokenClient(client); + throw e; + } finally { + if (client != null) { + intpProcess.releaseClient(client); + } } } } @@ -194,7 +199,7 @@ public void run() { } } - private void unload(final ApplicationState appsToUnload) throws ApplicationException { + private void unload(ApplicationState appsToUnload) throws ApplicationException { synchronized (appsToUnload) { if (appsToUnload.getStatus() != ApplicationState.Status.LOADED) { throw new ApplicationException( @@ -212,19 +217,26 @@ private void unload(final ApplicationState appsToUnload) throws ApplicationExcep throw new ApplicationException("Target interpreter process is not running"); } - RemoteApplicationResult ret = intpProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public RemoteApplicationResult call(RemoteInterpreterService.Client client) - throws Exception { - return client.unloadApplication(appsToUnload.getId()); - } - } - ); - if (ret.isSuccess()) { - appStatusChange(paragraph, appsToUnload.getId(), ApplicationState.Status.UNLOADED); - } else { - throw new ApplicationException(ret.getMsg()); + RemoteInterpreterService.Client client; + try { + client = intpProcess.getClient(); + } catch (Exception e) { + throw new ApplicationException(e); + } + + try { + RemoteApplicationResult ret = client.unloadApplication(appsToUnload.getId()); + + if (ret.isSuccess()) { + appStatusChange(paragraph, appsToUnload.getId(), ApplicationState.Status.UNLOADED); + } else { + throw new ApplicationException(ret.getMsg()); + } + } catch (TException e) { + intpProcess.releaseBrokenClient(client); + throw new ApplicationException(e); + } finally { + intpProcess.releaseClient(client); } } } @@ -274,7 +286,7 @@ public void run() { } } - private void run(final ApplicationState app) throws ApplicationException { + private void run(ApplicationState app) throws ApplicationException { synchronized (app) { if (app.getStatus() != ApplicationState.Status.LOADED) { throw new ApplicationException( @@ -291,19 +303,29 @@ private void run(final ApplicationState app) throws ApplicationException { if (intpProcess == null) { throw new ApplicationException("Target interpreter process is not running"); } - RemoteApplicationResult ret = intpProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public RemoteApplicationResult call(RemoteInterpreterService.Client client) - throws Exception { - return client.runApplication(app.getId()); - } - } - ); - if (ret.isSuccess()) { - // success - } else { - throw new ApplicationException(ret.getMsg()); + RemoteInterpreterService.Client client = null; + try { + client = intpProcess.getClient(); + } catch (Exception e) { + throw new ApplicationException(e); + } + + try { + RemoteApplicationResult ret = client.runApplication(app.getId()); + + if (ret.isSuccess()) { + // success + } else { + throw new ApplicationException(ret.getMsg()); + } + } catch (TException e) { + intpProcess.releaseBrokenClient(client); + client = null; + throw new ApplicationException(e); + } finally { + if (client != null) { + intpProcess.releaseClient(client); + } } } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java new file mode 100644 index 00000000000..9403b4f34e2 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java @@ -0,0 +1,423 @@ +/* + * 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.interpreter; + +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import org.apache.commons.lang.NullArgumentException; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; +import org.apache.zeppelin.dep.DependencyResolver; +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.display.AngularObjectRegistryListener; +import org.apache.zeppelin.helium.ApplicationEventListener; +import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonatype.aether.RepositoryException; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * Manage interpreters. + */ +public class InterpreterFactory implements InterpreterGroupFactory { + private static final Logger logger = LoggerFactory.getLogger(InterpreterFactory.class); + + private Map cleanCl = + Collections.synchronizedMap(new HashMap()); + + private ZeppelinConfiguration conf; + + private final InterpreterSettingManager interpreterSettingManager; + private AngularObjectRegistryListener angularObjectRegistryListener; + private final RemoteInterpreterProcessListener remoteInterpreterProcessListener; + private final ApplicationEventListener appEventListener; + + private boolean shiroEnabled; + + private Map env = new HashMap<>(); + + private Interpreter devInterpreter; + + public InterpreterFactory(ZeppelinConfiguration conf, + AngularObjectRegistryListener angularObjectRegistryListener, + RemoteInterpreterProcessListener remoteInterpreterProcessListener, + ApplicationEventListener appEventListener, DependencyResolver depResolver, + boolean shiroEnabled, InterpreterSettingManager interpreterSettingManager) + throws InterpreterException, IOException, RepositoryException { + this.conf = conf; + this.angularObjectRegistryListener = angularObjectRegistryListener; + this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; + this.appEventListener = appEventListener; + this.shiroEnabled = shiroEnabled; + + this.interpreterSettingManager = interpreterSettingManager; + //TODO(jl): Fix it not to use InterpreterGroupFactory + interpreterSettingManager.setInterpreterGroupFactory(this); + + logger.info("shiroEnabled: {}", shiroEnabled); + } + + /** + * @param id interpreterGroup id. Combination of interpreterSettingId + noteId/userId/shared + * depends on interpreter mode + */ + @Override + public InterpreterGroup createInterpreterGroup(String id, InterpreterOption option) + throws InterpreterException, NullArgumentException { + + //When called from REST API without option we receive NPE + if (option == null) { + throw new NullArgumentException("option"); + } + + AngularObjectRegistry angularObjectRegistry; + + InterpreterGroup interpreterGroup = new InterpreterGroup(id); + if (option.isRemote()) { + angularObjectRegistry = + new RemoteAngularObjectRegistry(id, angularObjectRegistryListener, interpreterGroup); + } else { + angularObjectRegistry = new AngularObjectRegistry(id, angularObjectRegistryListener); + + // TODO(moon) : create distributed resource pool for local interpreters and set + } + + interpreterGroup.setAngularObjectRegistry(angularObjectRegistry); + return interpreterGroup; + } + + public void createInterpretersForNote(InterpreterSetting interpreterSetting, String user, + String noteId, String interpreterSessionKey) { + InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup(user, noteId); + InterpreterOption option = interpreterSetting.getOption(); + Properties properties = interpreterSetting.getFlatProperties(); + // if interpreters are already there, wait until they're being removed + synchronized (interpreterGroup) { + long interpreterRemovalWaitStart = System.nanoTime(); + // interpreter process supposed to be terminated by RemoteInterpreterProcess.dereference() + // in ZEPPELIN_INTERPRETER_CONNECT_TIMEOUT msec. However, if termination of the process and + // removal from interpreter group take too long, throw an error. + long minTimeout = 10L * 1000 * 1000000; // 10 sec + long interpreterRemovalWaitTimeout = Math.max(minTimeout, + conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_CONNECT_TIMEOUT) * 1000000L * 2); + while (interpreterGroup.containsKey(interpreterSessionKey)) { + if (System.nanoTime() - interpreterRemovalWaitStart > interpreterRemovalWaitTimeout) { + throw new InterpreterException("Can not create interpreter"); + } + try { + interpreterGroup.wait(1000); + } catch (InterruptedException e) { + logger.debug(e.getMessage(), e); + } + } + } + + logger.info("Create interpreter instance {} for note {}", interpreterSetting.getName(), noteId); + + List interpreterInfos = interpreterSetting.getInterpreterInfos(); + String path = interpreterSetting.getPath(); + InterpreterRunner runner = interpreterSetting.getInterpreterRunner(); + Interpreter interpreter; + for (InterpreterInfo info : interpreterInfos) { + if (option.isRemote()) { + if (option.isExistingProcess()) { + interpreter = + connectToRemoteRepl(interpreterSessionKey, info.getClassName(), option.getHost(), + option.getPort(), properties, interpreterSetting.getId(), user, + option.isUserImpersonate); + } else { + interpreter = createRemoteRepl(path, interpreterSessionKey, info.getClassName(), + properties, interpreterSetting.getId(), user, option.isUserImpersonate(), runner); + } + } else { + interpreter = createRepl(interpreterSetting.getPath(), info.getClassName(), properties); + } + + synchronized (interpreterGroup) { + List interpreters = interpreterGroup.get(interpreterSessionKey); + if (null == interpreters) { + interpreters = new ArrayList<>(); + interpreterGroup.put(interpreterSessionKey, interpreters); + } + if (info.isDefaultInterpreter()) { + interpreters.add(0, interpreter); + } else { + interpreters.add(interpreter); + } + } + logger.info("Interpreter {} {} created", interpreter.getClassName(), interpreter.hashCode()); + interpreter.setInterpreterGroup(interpreterGroup); + } + } + + private Interpreter createRepl(String dirName, String className, Properties property) + throws InterpreterException { + logger.info("Create repl {} from {}", className, dirName); + + ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); + try { + + URLClassLoader ccl = cleanCl.get(dirName); + if (ccl == null) { + // classloader fallback + ccl = URLClassLoader.newInstance(new URL[]{}, oldcl); + } + + boolean separateCL = true; + try { // check if server's classloader has driver already. + Class cls = this.getClass().forName(className); + if (cls != null) { + separateCL = false; + } + } catch (Exception e) { + logger.error("exception checking server classloader driver", e); + } + + URLClassLoader cl; + + if (separateCL == true) { + cl = URLClassLoader.newInstance(new URL[]{}, ccl); + } else { + cl = ccl; + } + Thread.currentThread().setContextClassLoader(cl); + + Class replClass = (Class) cl.loadClass(className); + Constructor constructor = + replClass.getConstructor(new Class[]{Properties.class}); + Interpreter repl = constructor.newInstance(property); + repl.setClassloaderUrls(ccl.getURLs()); + LazyOpenInterpreter intp = new LazyOpenInterpreter(new ClassloaderInterpreter(repl, cl)); + return intp; + } catch (SecurityException e) { + throw new InterpreterException(e); + } catch (NoSuchMethodException e) { + throw new InterpreterException(e); + } catch (IllegalArgumentException e) { + throw new InterpreterException(e); + } catch (InstantiationException e) { + throw new InterpreterException(e); + } catch (IllegalAccessException e) { + throw new InterpreterException(e); + } catch (InvocationTargetException e) { + throw new InterpreterException(e); + } catch (ClassNotFoundException e) { + throw new InterpreterException(e); + } finally { + Thread.currentThread().setContextClassLoader(oldcl); + } + } + + private Interpreter connectToRemoteRepl(String interpreterSessionKey, String className, + String host, int port, Properties property, String interpreterSettingId, String userName, + Boolean isUserImpersonate) { + int connectTimeout = conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_CONNECT_TIMEOUT); + int maxPoolSize = conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_MAX_POOL_SIZE); + String localRepoPath = conf.getInterpreterLocalRepoPath() + "/" + interpreterSettingId; + LazyOpenInterpreter intp = new LazyOpenInterpreter( + new RemoteInterpreter(property, interpreterSessionKey, className, host, port, localRepoPath, + connectTimeout, maxPoolSize, remoteInterpreterProcessListener, appEventListener, + userName, isUserImpersonate, conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_OUTPUT_LIMIT))); + return intp; + } + + Interpreter createRemoteRepl(String interpreterPath, String interpreterSessionKey, + String className, Properties property, String interpreterSettingId, + String userName, Boolean isUserImpersonate, InterpreterRunner interpreterRunner) { + int connectTimeout = conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_CONNECT_TIMEOUT); + String localRepoPath = conf.getInterpreterLocalRepoPath() + "/" + interpreterSettingId; + int maxPoolSize = conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_MAX_POOL_SIZE); + String interpreterRunnerPath; + String interpreterGroupName = interpreterSettingManager.get(interpreterSettingId).getName(); + if (null != interpreterRunner) { + interpreterRunnerPath = interpreterRunner.getPath(); + Path p = Paths.get(interpreterRunnerPath); + if (!p.isAbsolute()) { + interpreterRunnerPath = Joiner.on(File.separator) + .join(interpreterPath, interpreterRunnerPath); + } + } else { + interpreterRunnerPath = conf.getInterpreterRemoteRunnerPath(); + } + + RemoteInterpreter remoteInterpreter = + new RemoteInterpreter(property, interpreterSessionKey, className, + interpreterRunnerPath, interpreterPath, localRepoPath, connectTimeout, maxPoolSize, + remoteInterpreterProcessListener, appEventListener, userName, isUserImpersonate, + conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_OUTPUT_LIMIT), interpreterGroupName); + remoteInterpreter.addEnv(env); + + return new LazyOpenInterpreter(remoteInterpreter); + } + + private List createOrGetInterpreterList(String user, String noteId, + InterpreterSetting setting) { + InterpreterGroup interpreterGroup = setting.getInterpreterGroup(user, noteId); + synchronized (interpreterGroup) { + String interpreterSessionKey = + interpreterSettingManager.getInterpreterSessionKey(user, noteId, setting); + if (!interpreterGroup.containsKey(interpreterSessionKey)) { + createInterpretersForNote(setting, user, noteId, interpreterSessionKey); + } + return interpreterGroup.get(interpreterSessionKey); + } + } + + private InterpreterSetting getInterpreterSettingByGroup(List settings, + String group) { + Preconditions.checkNotNull(group, "group should be not null"); + + for (InterpreterSetting setting : settings) { + if (group.equals(setting.getName())) { + return setting; + } + } + return null; + } + + private String getInterpreterClassFromInterpreterSetting(InterpreterSetting setting, + String name) { + Preconditions.checkNotNull(name, "name should be not null"); + + for (InterpreterInfo info : setting.getInterpreterInfos()) { + String infoName = info.getName(); + if (null != info.getName() && name.equals(infoName)) { + return info.getClassName(); + } + } + return null; + } + + private Interpreter getInterpreter(String user, String noteId, InterpreterSetting setting, + String name) { + Preconditions.checkNotNull(noteId, "noteId should be not null"); + Preconditions.checkNotNull(setting, "setting should be not null"); + Preconditions.checkNotNull(name, "name should be not null"); + + String className; + if (null != (className = getInterpreterClassFromInterpreterSetting(setting, name))) { + List interpreterGroup = createOrGetInterpreterList(user, noteId, setting); + for (Interpreter interpreter : interpreterGroup) { + if (className.equals(interpreter.getClassName())) { + return interpreter; + } + } + } + return null; + } + + public Interpreter getInterpreter(String user, String noteId, String replName) { + List settings = interpreterSettingManager.getInterpreterSettings(noteId); + InterpreterSetting setting; + Interpreter interpreter; + + if (settings == null || settings.size() == 0) { + return null; + } + + if (replName == null || replName.trim().length() == 0) { + // get default settings (first available) + // TODO(jl): Fix it in case of returning null + InterpreterSetting defaultSettings = interpreterSettingManager + .getDefaultInterpreterSetting(settings); + return createOrGetInterpreterList(user, noteId, defaultSettings).get(0); + } + + String[] replNameSplit = replName.split("\\."); + if (replNameSplit.length == 2) { + String group = null; + String name = null; + group = replNameSplit[0]; + name = replNameSplit[1]; + + setting = getInterpreterSettingByGroup(settings, group); + + if (null != setting) { + interpreter = getInterpreter(user, noteId, setting, name); + + if (null != interpreter) { + return interpreter; + } + } + + throw new InterpreterException(replName + " interpreter not found"); + + } else { + // first assume replName is 'name' of interpreter. ('groupName' is ommitted) + // search 'name' from first (default) interpreter group + // TODO(jl): Handle with noteId to support defaultInterpreter per note. + setting = interpreterSettingManager.getDefaultInterpreterSetting(settings); + + interpreter = getInterpreter(user, noteId, setting, replName); + + if (null != interpreter) { + return interpreter; + } + + // next, assume replName is 'group' of interpreter ('name' is ommitted) + // search interpreter group and return first interpreter. + setting = getInterpreterSettingByGroup(settings, replName); + + if (null != setting) { + List interpreters = createOrGetInterpreterList(user, noteId, setting); + if (null != interpreters) { + return interpreters.get(0); + } + } + + // Support the legacy way to use it + for (InterpreterSetting s : settings) { + if (s.getGroup().equals(replName)) { + List interpreters = createOrGetInterpreterList(user, noteId, s); + if (null != interpreters) { + return interpreters.get(0); + } + } + } + } + + return null; + } + + public Map getEnv() { + return env; + } + + public void setEnv(Map env) { + this.env = env; + } + + +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroupFactory.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroupFactory.java new file mode 100644 index 00000000000..3b9be400dd2 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroupFactory.java @@ -0,0 +1,26 @@ +/* + * 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.interpreter; + +import org.apache.commons.lang.NullArgumentException; + +/** + * Created InterpreterGroup + */ +public interface InterpreterGroupFactory { + InterpreterGroup createInterpreterGroup(String interpreterGroupId, InterpreterOption option); +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfo.java similarity index 100% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfo.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfo.java diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/EchoInterpreter.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java similarity index 50% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/EchoInterpreter.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java index e7a04f3b5f9..ca688dcb6e7 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/EchoInterpreter.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java @@ -15,51 +15,32 @@ * limitations under the License. */ - package org.apache.zeppelin.interpreter; -import java.util.Properties; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.zeppelin.common.JsonSerializable; +import org.sonatype.aether.repository.RemoteRepository; + +import java.util.List; +import java.util.Map; /** - * Just return the received statement back + * */ -public class EchoInterpreter extends Interpreter { - - public EchoInterpreter(Properties property) { - super(property); - } - - @Override - public void open() { - - } - - @Override - public void close() { +public class InterpreterInfoSaving implements JsonSerializable { - } + private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); - @Override - public InterpreterResult interpret(String st, InterpreterContext context) { - if (Boolean.parseBoolean(property.getProperty("zeppelin.interpreter.echo.fail", "false"))) { - return new InterpreterResult(InterpreterResult.Code.ERROR); - } else { - return new InterpreterResult(InterpreterResult.Code.SUCCESS, st); - } - } - - @Override - public void cancel(InterpreterContext context) { - - } + public Map interpreterSettings; + public Map> interpreterBindings; + public List interpreterRepositories; - @Override - public FormType getFormType() { - return FormType.NATIVE; + public String toJson() { + return gson.toJson(this); } - @Override - public int getProgress(InterpreterContext context) { - return 0; + public static InterpreterInfoSaving fromJson(String json) { + return gson.fromJson(json, InterpreterInfoSaving.class); } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java new file mode 100644 index 00000000000..752b4e28824 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java @@ -0,0 +1,459 @@ +/* + * 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.interpreter; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.zeppelin.dep.Dependency; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; +import com.google.gson.internal.StringMap; + +import static org.apache.zeppelin.notebook.utility.IdHashes.generateId; + +/** + * Interpreter settings + */ +public class InterpreterSetting { + + private static final Logger logger = LoggerFactory.getLogger(InterpreterSetting.class); + private static final String SHARED_PROCESS = "shared_process"; + private String id; + private String name; + // always be null in case of InterpreterSettingRef + private String group; + private transient Map infos; + + // Map of the note and paragraphs which has runtime infos generated by this interpreter setting. + // This map is used to clear the infos in paragraph when the interpretersetting is restarted + private transient Map> runtimeInfosToBeCleared; + + /** + * properties can be either Map or + * Map + * properties should be: + * - Map when Interpreter instances are saved to + * `conf/interpreter.json` file + * - Map when Interpreters are registered + * : this is needed after https://github.com/apache/zeppelin/pull/1145 + * which changed the way of getting default interpreter setting AKA interpreterSettingsRef + */ + private Object properties; + private Status status; + private String errorReason; + + @SerializedName("interpreterGroup") + private List interpreterInfos; + private final transient Map interpreterGroupRef = new HashMap<>(); + private List dependencies = new LinkedList<>(); + private InterpreterOption option; + private transient String path; + + @SerializedName("runner") + private InterpreterRunner interpreterRunner; + + @Deprecated + private transient InterpreterGroupFactory interpreterGroupFactory; + + private final transient ReentrantReadWriteLock.ReadLock interpreterGroupReadLock; + private final transient ReentrantReadWriteLock.WriteLock interpreterGroupWriteLock; + + public InterpreterSetting() { + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + interpreterGroupReadLock = lock.readLock(); + interpreterGroupWriteLock = lock.writeLock(); + } + + public InterpreterSetting(String id, String name, String group, + List interpreterInfos, Object properties, List dependencies, + InterpreterOption option, String path, InterpreterRunner runner) { + this(); + this.id = id; + this.name = name; + this.group = group; + this.interpreterInfos = interpreterInfos; + this.properties = properties; + this.dependencies = dependencies; + this.option = option; + this.path = path; + this.status = Status.READY; + this.interpreterRunner = runner; + } + + public InterpreterSetting(String name, String group, List interpreterInfos, + Object properties, List dependencies, InterpreterOption option, String path, + InterpreterRunner runner) { + this(generateId(), name, group, interpreterInfos, properties, dependencies, option, path, + runner); + } + + /** + * Create interpreter from interpreterSettingRef + * + * @param o interpreterSetting from interpreterSettingRef + */ + public InterpreterSetting(InterpreterSetting o) { + this(generateId(), o.getName(), o.getGroup(), o.getInterpreterInfos(), o.getProperties(), + o.getDependencies(), o.getOption(), o.getPath(), o.getInterpreterRunner()); + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getGroup() { + return group; + } + + private String getInterpreterProcessKey(String user, String noteId) { + InterpreterOption option = getOption(); + String key; + if (getOption().isExistingProcess) { + key = Constants.EXISTING_PROCESS; + } else if (getOption().isProcess()) { + key = (option.perUserIsolated() ? user : "") + ":" + (option.perNoteIsolated() ? noteId : ""); + } else { + key = SHARED_PROCESS; + } + + //logger.debug("getInterpreterProcessKey: {} for InterpreterSetting Id: {}, Name: {}", + // key, getId(), getName()); + return key; + } + + private boolean isEqualInterpreterKeyProcessKey(String refKey, String processKey) { + InterpreterOption option = getOption(); + int validCount = 0; + if (getOption().isProcess() + && !(option.perUserIsolated() == true && option.perNoteIsolated() == true)) { + + List processList = Arrays.asList(processKey.split(":")); + List refList = Arrays.asList(refKey.split(":")); + + if (refList.size() <= 1 || processList.size() <= 1) { + return refKey.equals(processKey); + } + + if (processList.get(0).equals("") || processList.get(0).equals(refList.get(0))) { + validCount = validCount + 1; + } + + if (processList.get(1).equals("") || processList.get(1).equals(refList.get(1))) { + validCount = validCount + 1; + } + + return (validCount >= 2); + } else { + return refKey.equals(processKey); + } + } + + String getInterpreterSessionKey(String user, String noteId) { + InterpreterOption option = getOption(); + String key; + if (option.isExistingProcess()) { + key = Constants.EXISTING_PROCESS; + } else if (option.perNoteScoped() && option.perUserScoped()) { + key = user + ":" + noteId; + } else if (option.perUserScoped()) { + key = user; + } else if (option.perNoteScoped()) { + key = noteId; + } else { + key = "shared_session"; + } + + logger.debug("Interpreter session key: {}, for note: {}, user: {}, InterpreterSetting Name: " + + "{}", key, noteId, user, getName()); + return key; + } + + public InterpreterGroup getInterpreterGroup(String user, String noteId) { + String key = getInterpreterProcessKey(user, noteId); + if (!interpreterGroupRef.containsKey(key)) { + String interpreterGroupId = getId() + ":" + key; + InterpreterGroup intpGroup = + interpreterGroupFactory.createInterpreterGroup(interpreterGroupId, getOption()); + + interpreterGroupWriteLock.lock(); + logger.debug("create interpreter group with groupId:" + interpreterGroupId); + interpreterGroupRef.put(key, intpGroup); + interpreterGroupWriteLock.unlock(); + } + try { + interpreterGroupReadLock.lock(); + return interpreterGroupRef.get(key); + } finally { + interpreterGroupReadLock.unlock(); + } + } + + public Collection getAllInterpreterGroups() { + try { + interpreterGroupReadLock.lock(); + return new LinkedList<>(interpreterGroupRef.values()); + } finally { + interpreterGroupReadLock.unlock(); + } + } + + void closeAndRemoveInterpreterGroup(String noteId, String user) { + if (user.equals("anonymous")) { + user = ""; + } + String processKey = getInterpreterProcessKey(user, noteId); + String sessionKey = getInterpreterSessionKey(user, noteId); + List groupToRemove = new LinkedList<>(); + InterpreterGroup groupItem; + for (String intpKey : new HashSet<>(interpreterGroupRef.keySet())) { + if (isEqualInterpreterKeyProcessKey(intpKey, processKey)) { + interpreterGroupWriteLock.lock(); + // TODO(jl): interpreterGroup has two or more sessionKeys inside it. thus we should not + // remove interpreterGroup if it has two or more values. + groupItem = interpreterGroupRef.get(intpKey); + interpreterGroupWriteLock.unlock(); + groupToRemove.add(groupItem); + } + for (InterpreterGroup groupToClose : groupToRemove) { + // TODO(jl): Fix the logic removing session. Now, it's handled into groupToClose.clsose() + groupToClose.close(interpreterGroupRef, intpKey, sessionKey); + } + groupToRemove.clear(); + } + + //Remove session because all interpreters in this session are closed + //TODO(jl): Change all code to handle interpreter one by one or all at once + + } + + void closeAndRemoveAllInterpreterGroups() { + for (String processKey : new HashSet<>(interpreterGroupRef.keySet())) { + InterpreterGroup interpreterGroup = interpreterGroupRef.get(processKey); + for (String sessionKey : new HashSet<>(interpreterGroup.keySet())) { + interpreterGroup.close(interpreterGroupRef, processKey, sessionKey); + } + } + } + + void shutdownAndRemoveAllInterpreterGroups() { + for (InterpreterGroup interpreterGroup : interpreterGroupRef.values()) { + interpreterGroup.shutdown(); + } + } + + public Object getProperties() { + return properties; + } + + public Properties getFlatProperties() { + Properties p = new Properties(); + if (properties != null) { + Map propertyMap = (Map) properties; + for (String key : propertyMap.keySet()) { + InterpreterProperty tmp = propertyMap.get(key); + p.put(tmp.getName() != null ? tmp.getName() : key, + tmp.getValue() != null ? tmp.getValue().toString() : null); + } + } + return p; + } + + public List getDependencies() { + if (dependencies == null) { + return new LinkedList<>(); + } + return dependencies; + } + + public void setDependencies(List dependencies) { + this.dependencies = dependencies; + } + + public InterpreterOption getOption() { + if (option == null) { + option = new InterpreterOption(); + } + + return option; + } + + public void setOption(InterpreterOption option) { + this.option = option; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public List getInterpreterInfos() { + return interpreterInfos; + } + + void setInterpreterGroupFactory(InterpreterGroupFactory interpreterGroupFactory) { + this.interpreterGroupFactory = interpreterGroupFactory; + } + + void appendDependencies(List dependencies) { + for (Dependency dependency : dependencies) { + if (!this.dependencies.contains(dependency)) { + this.dependencies.add(dependency); + } + } + } + + void setInterpreterOption(InterpreterOption interpreterOption) { + this.option = interpreterOption; + } + + public void setProperties(Map p) { + this.properties = p; + } + + void setGroup(String group) { + this.group = group; + } + + void setName(String name) { + this.name = name; + } + + /*** + * Interpreter status + */ + public enum Status { + DOWNLOADING_DEPENDENCIES, + ERROR, + READY + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getErrorReason() { + return errorReason; + } + + public void setErrorReason(String errorReason) { + this.errorReason = errorReason; + } + + public void setInfos(Map infos) { + this.infos = infos; + } + + public Map getInfos() { + return infos; + } + + public InterpreterRunner getInterpreterRunner() { + return interpreterRunner; + } + + public void setInterpreterRunner(InterpreterRunner interpreterRunner) { + this.interpreterRunner = interpreterRunner; + } + + public void addNoteToPara(String noteId, String paraId) { + if (runtimeInfosToBeCleared == null) { + runtimeInfosToBeCleared = new HashMap<>(); + } + Set paraIdSet = runtimeInfosToBeCleared.get(noteId); + if (paraIdSet == null) { + paraIdSet = new HashSet<>(); + runtimeInfosToBeCleared.put(noteId, paraIdSet); + } + paraIdSet.add(paraId); + } + + public Map> getNoteIdAndParaMap() { + return runtimeInfosToBeCleared; + } + + public void clearNoteIdAndParaMap() { + runtimeInfosToBeCleared = null; + } + + // For backward compatibility of interpreter.json format after ZEPPELIN-2654 + public void convertPermissionsFromUsersToOwners(JsonObject jsonObject) { + if (jsonObject != null) { + JsonObject option = jsonObject.getAsJsonObject("option"); + if (option != null) { + JsonArray users = option.getAsJsonArray("users"); + if (users != null) { + if (this.option.getOwners() == null) { + this.option.owners = new LinkedList<>(); + } + for (JsonElement user : users) { + this.option.getOwners().add(user.getAsString()); + } + } + } + } + } + + // For backward compatibility of interpreter.json format after ZEPPELIN-2403 + public void convertFlatPropertiesToPropertiesWithWidgets() { + StringMap newProperties = new StringMap(); + if (properties != null && properties instanceof StringMap) { + StringMap p = (StringMap) properties; + + for (Object o : p.entrySet()) { + Map.Entry entry = (Map.Entry) o; + if (!(entry.getValue() instanceof StringMap)) { + StringMap newProperty = new StringMap(); + newProperty.put("name", entry.getKey()); + newProperty.put("value", entry.getValue()); + newProperty.put("type", InterpreterPropertyType.TEXTAREA.getValue()); + newProperties.put(entry.getKey().toString(), newProperty); + } else { + // already converted + return; + } + } + + this.properties = newProperties; + } + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java new file mode 100644 index 00000000000..12545d6ba69 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java @@ -0,0 +1,1136 @@ +/* + * 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.interpreter; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.lang.reflect.Type; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream.Filter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; +import org.apache.zeppelin.dep.Dependency; +import org.apache.zeppelin.dep.DependencyResolver; +import org.apache.zeppelin.interpreter.Interpreter.RegisteredInterpreter; +import org.apache.zeppelin.scheduler.Job; +import org.apache.zeppelin.scheduler.Job.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonatype.aether.RepositoryException; +import org.sonatype.aether.repository.Authentication; +import org.sonatype.aether.repository.Proxy; +import org.sonatype.aether.repository.RemoteRepository; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.internal.StringMap; +import com.google.gson.reflect.TypeToken; + +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; + +/** + * TBD + */ +public class InterpreterSettingManager { + + private static final Logger logger = LoggerFactory.getLogger(InterpreterSettingManager.class); + private static final String SHARED_SESSION = "shared_session"; + private static final Map DEFAULT_EDITOR = ImmutableMap.of( + "language", (Object) "text", + "editOnDblClick", false); + + private final ZeppelinConfiguration zeppelinConfiguration; + private final Path interpreterDirPath; + private final Path interpreterBindingPath; + + /** + * This is only references with default settings, name and properties + * key: InterpreterSetting.name + */ + private final Map interpreterSettingsRef; + /** + * This is used by creating and running Interpreters + * key: InterpreterSetting.id <- This is becuase backward compatibility + */ + private final Map interpreterSettings; + private final Map> interpreterBindings; + + private final DependencyResolver dependencyResolver; + private final List interpreterRepositories; + + private final InterpreterOption defaultOption; + + private final Map cleanCl; + + @Deprecated + private String[] interpreterClassList; + private String[] interpreterGroupOrderList; + private InterpreterGroupFactory interpreterGroupFactory; + + private final Gson gson; + + public InterpreterSettingManager(ZeppelinConfiguration zeppelinConfiguration, + DependencyResolver dependencyResolver, InterpreterOption interpreterOption) + throws IOException, RepositoryException { + this.zeppelinConfiguration = zeppelinConfiguration; + this.interpreterDirPath = Paths.get(zeppelinConfiguration.getInterpreterDir()); + logger.debug("InterpreterRootPath: {}", interpreterDirPath); + this.interpreterBindingPath = Paths.get(zeppelinConfiguration.getInterpreterSettingPath()); + logger.debug("InterpreterBindingPath: {}", interpreterBindingPath); + + this.interpreterSettingsRef = Maps.newConcurrentMap(); + this.interpreterSettings = Maps.newConcurrentMap(); + this.interpreterBindings = Maps.newConcurrentMap(); + + this.dependencyResolver = dependencyResolver; + this.interpreterRepositories = dependencyResolver.getRepos(); + + this.defaultOption = interpreterOption; + + this.cleanCl = Collections.synchronizedMap(new HashMap()); + + String replsConf = zeppelinConfiguration.getString(ConfVars.ZEPPELIN_INTERPRETERS); + this.interpreterClassList = replsConf.split(","); + String groupOrder = zeppelinConfiguration.getString(ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER); + this.interpreterGroupOrderList = groupOrder.split(","); + + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.setPrettyPrinting(); + this.gson = gsonBuilder.create(); + + init(); + } + + /** + * Remember this method doesn't keep current connections after being called + */ + private void loadFromFile() { + if (!Files.exists(interpreterBindingPath)) { + // nothing to read + return; + } + InterpreterInfoSaving infoSaving; + try (BufferedReader jsonReader = + Files.newBufferedReader(interpreterBindingPath, StandardCharsets.UTF_8)) { + JsonParser jsonParser = new JsonParser(); + JsonObject jsonObject = jsonParser.parse(jsonReader).getAsJsonObject(); + infoSaving = gson.fromJson(jsonObject.toString(), InterpreterInfoSaving.class); + + for (String k : infoSaving.interpreterSettings.keySet()) { + InterpreterSetting setting = infoSaving.interpreterSettings.get(k); + + setting.convertFlatPropertiesToPropertiesWithWidgets(); + + List infos = setting.getInterpreterInfos(); + + // Convert json StringMap to Properties + StringMap p = (StringMap) setting.getProperties(); + Map properties = new HashMap(); + for (String key : p.keySet()) { + StringMap fields = (StringMap) p.get(key); + String type = InterpreterPropertyType.TEXTAREA.getValue(); + try { + type = InterpreterPropertyType.byValue(fields.get("type")).getValue(); + } catch (Exception e) { + logger.warn("Incorrect type of property {} in settings {}", key, + setting.getId()); + } + properties.put(key, new InterpreterProperty(key, fields.get("value"), type)); + } + setting.setProperties(properties); + + // Always use separate interpreter process + // While we decided to turn this feature on always (without providing + // enable/disable option on GUI). + // previously created setting should turn this feature on here. + setting.getOption().setRemote(true); + + setting.convertPermissionsFromUsersToOwners( + jsonObject.getAsJsonObject("interpreterSettings").getAsJsonObject(setting.getId())); + + // Update transient information from InterpreterSettingRef + InterpreterSetting interpreterSettingObject = + interpreterSettingsRef.get(setting.getGroup()); + if (interpreterSettingObject == null) { + logger.warn("can't get InterpreterSetting " + + "Information From loaded Interpreter Setting Ref - {} ", setting.getGroup()); + continue; + } + String depClassPath = interpreterSettingObject.getPath(); + setting.setPath(depClassPath); + + for (InterpreterInfo info : infos) { + if (info.getEditor() == null) { + Map editor = getEditorFromSettingByClassName(interpreterSettingObject, + info.getClassName()); + info.setEditor(editor); + } + } + + setting.setInterpreterGroupFactory(interpreterGroupFactory); + + loadInterpreterDependencies(setting); + interpreterSettings.put(k, setting); + } + + interpreterBindings.putAll(infoSaving.interpreterBindings); + + if (infoSaving.interpreterRepositories != null) { + for (RemoteRepository repo : infoSaving.interpreterRepositories) { + if (!dependencyResolver.getRepos().contains(repo)) { + this.interpreterRepositories.add(repo); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void saveToFile() throws IOException { + String jsonString; + + synchronized (interpreterSettings) { + InterpreterInfoSaving info = new InterpreterInfoSaving(); + info.interpreterBindings = interpreterBindings; + info.interpreterSettings = interpreterSettings; + info.interpreterRepositories = interpreterRepositories; + + jsonString = info.toJson(); + } + + if (!Files.exists(interpreterBindingPath)) { + Files.createFile(interpreterBindingPath); + + try { + Set permissions = EnumSet.of(OWNER_READ, OWNER_WRITE); + Files.setPosixFilePermissions(interpreterBindingPath, permissions); + } catch (UnsupportedOperationException e) { + // File system does not support Posix file permissions (likely windows) - continue anyway. + logger.warn("unable to setPosixFilePermissions on '{}'.", interpreterBindingPath); + } + } + + FileOutputStream fos = new FileOutputStream(interpreterBindingPath.toFile(), false); + OutputStreamWriter out = new OutputStreamWriter(fos); + out.append(jsonString); + out.close(); + fos.close(); + } + + //TODO(jl): Fix it to remove InterpreterGroupFactory + public void setInterpreterGroupFactory(InterpreterGroupFactory interpreterGroupFactory) { + for (InterpreterSetting setting : interpreterSettings.values()) { + setting.setInterpreterGroupFactory(interpreterGroupFactory); + } + this.interpreterGroupFactory = interpreterGroupFactory; + } + + private void init() throws InterpreterException, IOException, RepositoryException { + String interpreterJson = zeppelinConfiguration.getInterpreterJson(); + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + if (Files.exists(interpreterDirPath)) { + for (Path interpreterDir : Files + .newDirectoryStream(interpreterDirPath, new Filter() { + @Override + public boolean accept(Path entry) throws IOException { + return Files.exists(entry) && Files.isDirectory(entry); + } + })) { + String interpreterDirString = interpreterDir.toString(); + + /** + * Register interpreter by the following ordering + * 1. Register it from path {ZEPPELIN_HOME}/interpreter/{interpreter_name}/ + * interpreter-setting.json + * 2. Register it from interpreter-setting.json in classpath + * {ZEPPELIN_HOME}/interpreter/{interpreter_name} + * 3. Register it by Interpreter.register + */ + if (!registerInterpreterFromPath(interpreterDirString, interpreterJson)) { + if (!registerInterpreterFromResource(cl, interpreterDirString, interpreterJson)) { + /* + * TODO(jongyoul) + * - Remove these codes below because of legacy code + * - Support ThreadInterpreter + */ + URLClassLoader ccl = new URLClassLoader( + recursiveBuildLibList(interpreterDir.toFile()), cl); + for (String className : interpreterClassList) { + try { + // Load classes + Class.forName(className, true, ccl); + Set interpreterKeys = Interpreter.registeredInterpreters.keySet(); + for (String interpreterKey : interpreterKeys) { + if (className + .equals(Interpreter.registeredInterpreters.get(interpreterKey) + .getClassName())) { + Interpreter.registeredInterpreters.get(interpreterKey) + .setPath(interpreterDirString); + logger.info("Interpreter " + interpreterKey + " found. class=" + className); + cleanCl.put(interpreterDirString, ccl); + } + } + } catch (Throwable t) { + // nothing to do + } + } + } + } + } + } + + for (RegisteredInterpreter registeredInterpreter : Interpreter.registeredInterpreters + .values()) { + logger + .debug("Registered: {} -> {}. Properties: {}", registeredInterpreter.getInterpreterKey(), + registeredInterpreter.getClassName(), registeredInterpreter.getProperties()); + } + + // RegisteredInterpreters -> interpreterSettingRef + InterpreterInfo interpreterInfo; + for (RegisteredInterpreter r : Interpreter.registeredInterpreters.values()) { + interpreterInfo = + new InterpreterInfo(r.getClassName(), r.getName(), r.isDefaultInterpreter(), + r.getEditor()); + add(r.getGroup(), interpreterInfo, r.getProperties(), defaultOption, r.getPath(), + r.getRunner()); + } + + for (String settingId : interpreterSettingsRef.keySet()) { + InterpreterSetting setting = interpreterSettingsRef.get(settingId); + logger.info("InterpreterSettingRef name {}", setting.getName()); + } + + loadFromFile(); + + // if no interpreter settings are loaded, create default set + if (0 == interpreterSettings.size()) { + Map temp = new HashMap<>(); + InterpreterSetting interpreterSetting; + for (InterpreterSetting setting : interpreterSettingsRef.values()) { + interpreterSetting = createFromInterpreterSettingRef(setting); + temp.put(setting.getName(), interpreterSetting); + } + + for (String group : interpreterGroupOrderList) { + if (null != (interpreterSetting = temp.remove(group))) { + interpreterSettings.put(interpreterSetting.getId(), interpreterSetting); + } + } + + for (InterpreterSetting setting : temp.values()) { + interpreterSettings.put(setting.getId(), setting); + } + + saveToFile(); + } + + for (String settingId : interpreterSettings.keySet()) { + InterpreterSetting setting = interpreterSettings.get(settingId); + logger.info("InterpreterSetting group {} : id={}, name={}", setting.getGroup(), settingId, + setting.getName()); + } + } + + private boolean registerInterpreterFromResource(ClassLoader cl, String interpreterDir, + String interpreterJson) throws IOException, RepositoryException { + URL[] urls = recursiveBuildLibList(new File(interpreterDir)); + ClassLoader tempClassLoader = new URLClassLoader(urls, cl); + + Enumeration interpreterSettings = tempClassLoader.getResources(interpreterJson); + if (!interpreterSettings.hasMoreElements()) { + return false; + } + for (URL url : Collections.list(interpreterSettings)) { + try (InputStream inputStream = url.openStream()) { + logger.debug("Reading {} from {}", interpreterJson, url); + List registeredInterpreterList = + getInterpreterListFromJson(inputStream); + registerInterpreters(registeredInterpreterList, interpreterDir); + } + } + return true; + } + + private boolean registerInterpreterFromPath(String interpreterDir, String interpreterJson) + throws IOException, RepositoryException { + + Path interpreterJsonPath = Paths.get(interpreterDir, interpreterJson); + if (Files.exists(interpreterJsonPath)) { + logger.debug("Reading {}", interpreterJsonPath); + List registeredInterpreterList = + getInterpreterListFromJson(interpreterJsonPath); + registerInterpreters(registeredInterpreterList, interpreterDir); + return true; + } + return false; + } + + private List getInterpreterListFromJson(Path filename) + throws FileNotFoundException { + return getInterpreterListFromJson(new FileInputStream(filename.toFile())); + } + + private List getInterpreterListFromJson(InputStream stream) { + Type registeredInterpreterListType = new TypeToken>() { + }.getType(); + return gson.fromJson(new InputStreamReader(stream), registeredInterpreterListType); + } + + private void registerInterpreters(List registeredInterpreters, + String absolutePath) throws IOException, RepositoryException { + + for (RegisteredInterpreter registeredInterpreter : registeredInterpreters) { + InterpreterInfo interpreterInfo = + new InterpreterInfo(registeredInterpreter.getClassName(), registeredInterpreter.getName(), + registeredInterpreter.isDefaultInterpreter(), registeredInterpreter.getEditor()); + // use defaultOption if it is not specified in interpreter-setting.json + InterpreterOption option = registeredInterpreter.getOption() == null ? defaultOption : + registeredInterpreter.getOption(); + add(registeredInterpreter.getGroup(), interpreterInfo, registeredInterpreter.getProperties(), + option, absolutePath, registeredInterpreter.getRunner()); + } + + } + + public InterpreterSetting getDefaultInterpreterSetting(List settings) { + if (settings == null || settings.isEmpty()) { + return null; + } + return settings.get(0); + } + + public InterpreterSetting getDefaultInterpreterSetting(String noteId) { + return getDefaultInterpreterSetting(getInterpreterSettings(noteId)); + } + + public List getInterpreterSettings(String noteId) { + List interpreterSettingIds = getNoteInterpreterSettingBinding(noteId); + LinkedList settings = new LinkedList<>(); + + Iterator iter = interpreterSettingIds.iterator(); + while (iter.hasNext()) { + String id = iter.next(); + InterpreterSetting setting = get(id); + if (setting == null) { + // interpreter setting is removed from factory. remove id from here, too + iter.remove(); + } else { + settings.add(setting); + } + } + return settings; + } + + private List getNoteInterpreterSettingBinding(String noteId) { + LinkedList bindings = new LinkedList<>(); + List settingIds = interpreterBindings.get(noteId); + if (settingIds != null) { + bindings.addAll(settingIds); + } + return bindings; + } + + private InterpreterSetting createFromInterpreterSettingRef(String name) { + Preconditions.checkNotNull(name, "reference name should be not null"); + InterpreterSetting settingRef = interpreterSettingsRef.get(name); + return createFromInterpreterSettingRef(settingRef); + } + + private InterpreterSetting createFromInterpreterSettingRef(InterpreterSetting o) { + // should return immutable objects + List infos = (null == o.getInterpreterInfos()) ? + new ArrayList() : new ArrayList<>(o.getInterpreterInfos()); + List deps = (null == o.getDependencies()) ? + new ArrayList() : new ArrayList<>(o.getDependencies()); + Map props = + convertInterpreterProperties((Map) o.getProperties()); + InterpreterOption option = InterpreterOption.fromInterpreterOption(o.getOption()); + + InterpreterSetting setting = new InterpreterSetting(o.getName(), o.getName(), + infos, props, deps, option, o.getPath(), o.getInterpreterRunner()); + setting.setInterpreterGroupFactory(interpreterGroupFactory); + return setting; + } + + private Map convertInterpreterProperties( + Map defaultProperties) { + Map properties = new HashMap<>(); + + for (String key : defaultProperties.keySet()) { + DefaultInterpreterProperty defaultInterpreterProperty = defaultProperties.get(key); + properties.put(key, new InterpreterProperty(key, defaultInterpreterProperty.getValue(), + defaultInterpreterProperty.getType())); + } + return properties; + } + + public Map getEditorSetting(Interpreter interpreter, String user, String noteId, + String replName) { + Map editor = DEFAULT_EDITOR; + String group = StringUtils.EMPTY; + try { + String defaultSettingName = getDefaultInterpreterSetting(noteId).getName(); + List intpSettings = getInterpreterSettings(noteId); + for (InterpreterSetting intpSetting : intpSettings) { + String[] replNameSplit = replName.split("\\."); + if (replNameSplit.length == 2) { + group = replNameSplit[0]; + } + // when replName is 'name' of interpreter + if (defaultSettingName.equals(intpSetting.getName())) { + editor = getEditorFromSettingByClassName(intpSetting, interpreter.getClassName()); + } + // when replName is 'alias name' of interpreter or 'group' of interpreter + if (replName.equals(intpSetting.getName()) || group.equals(intpSetting.getName())) { + editor = getEditorFromSettingByClassName(intpSetting, interpreter.getClassName()); + break; + } + } + } catch (NullPointerException e) { + // Use `debug` level because this log occurs frequently + logger.debug("Couldn't get interpreter editor setting"); + } + return editor; + } + + public Map getEditorFromSettingByClassName(InterpreterSetting intpSetting, + String className) { + List intpInfos = intpSetting.getInterpreterInfos(); + for (InterpreterInfo intpInfo : intpInfos) { + + if (className.equals(intpInfo.getClassName())) { + if (intpInfo.getEditor() == null) { + break; + } + return intpInfo.getEditor(); + } + } + return DEFAULT_EDITOR; + } + + private void loadInterpreterDependencies(final InterpreterSetting setting) { + setting.setStatus(InterpreterSetting.Status.DOWNLOADING_DEPENDENCIES); + setting.setErrorReason(null); + interpreterSettings.put(setting.getId(), setting); + synchronized (interpreterSettings) { + final Thread t = new Thread() { + public void run() { + try { + // dependencies to prevent library conflict + File localRepoDir = new File(zeppelinConfiguration.getInterpreterLocalRepoPath() + "/" + + setting.getId()); + if (localRepoDir.exists()) { + try { + FileUtils.forceDelete(localRepoDir); + } catch (FileNotFoundException e) { + logger.info("A file that does not exist cannot be deleted, nothing to worry", e); + } + } + + // load dependencies + List deps = setting.getDependencies(); + if (deps != null) { + for (Dependency d : deps) { + File destDir = new File( + zeppelinConfiguration.getRelativeDir(ConfVars.ZEPPELIN_DEP_LOCALREPO)); + + if (d.getExclusions() != null) { + dependencyResolver.load(d.getGroupArtifactVersion(), d.getExclusions(), + new File(destDir, setting.getId())); + } else { + dependencyResolver + .load(d.getGroupArtifactVersion(), new File(destDir, setting.getId())); + } + } + } + + setting.setStatus(InterpreterSetting.Status.READY); + setting.setErrorReason(null); + } catch (Exception e) { + logger.error(String.format("Error while downloading repos for interpreter group : %s," + + " go to interpreter setting page click on edit and save it again to make " + + "this interpreter work properly. : %s", + setting.getGroup(), e.getLocalizedMessage()), e); + setting.setErrorReason(e.getLocalizedMessage()); + setting.setStatus(InterpreterSetting.Status.ERROR); + } finally { + interpreterSettings.put(setting.getId(), setting); + } + } + }; + t.start(); + } + } + + /** + * Overwrite dependency jar under local-repo/{interpreterId} + * if jar file in original path is changed + */ + private void copyDependenciesFromLocalPath(final InterpreterSetting setting) { + setting.setStatus(InterpreterSetting.Status.DOWNLOADING_DEPENDENCIES); + interpreterSettings.put(setting.getId(), setting); + synchronized (interpreterSettings) { + final Thread t = new Thread() { + public void run() { + try { + List deps = setting.getDependencies(); + if (deps != null) { + for (Dependency d : deps) { + File destDir = new File( + zeppelinConfiguration.getRelativeDir(ConfVars.ZEPPELIN_DEP_LOCALREPO)); + + int numSplits = d.getGroupArtifactVersion().split(":").length; + if (!(numSplits >= 3 && numSplits <= 6)) { + dependencyResolver.copyLocalDependency(d.getGroupArtifactVersion(), + new File(destDir, setting.getId())); + } + } + } + setting.setStatus(InterpreterSetting.Status.READY); + } catch (Exception e) { + logger.error(String.format("Error while copying deps for interpreter group : %s," + + " go to interpreter setting page click on edit and save it again to make " + + "this interpreter work properly.", + setting.getGroup()), e); + setting.setErrorReason(e.getLocalizedMessage()); + setting.setStatus(InterpreterSetting.Status.ERROR); + } finally { + interpreterSettings.put(setting.getId(), setting); + } + } + }; + t.start(); + } + } + + /** + * Return ordered interpreter setting list. + * The list does not contain more than one setting from the same interpreter class. + * Order by InterpreterClass (order defined by ZEPPELIN_INTERPRETERS), Interpreter setting name + */ + public List getDefaultInterpreterSettingList() { + // this list will contain default interpreter setting list + List defaultSettings = new LinkedList<>(); + + // to ignore the same interpreter group + Map interpreterGroupCheck = new HashMap<>(); + + List sortedSettings = get(); + + for (InterpreterSetting setting : sortedSettings) { + if (defaultSettings.contains(setting.getId())) { + continue; + } + + if (!interpreterGroupCheck.containsKey(setting.getName())) { + defaultSettings.add(setting.getId()); + interpreterGroupCheck.put(setting.getName(), true); + } + } + return defaultSettings; + } + + List getRegisteredInterpreterList() { + return new ArrayList<>(Interpreter.registeredInterpreters.values()); + } + + + private boolean findDefaultInterpreter(List infos) { + for (InterpreterInfo interpreterInfo : infos) { + if (interpreterInfo.isDefaultInterpreter()) { + return true; + } + } + return false; + } + + public InterpreterSetting createNewSetting(String name, String group, + List dependencies, InterpreterOption option, Map p) + throws IOException { + if (name.indexOf(".") >= 0) { + throw new IOException("'.' is invalid for InterpreterSetting name."); + } + InterpreterSetting setting = createFromInterpreterSettingRef(group); + setting.setName(name); + setting.setGroup(group); + setting.appendDependencies(dependencies); + setting.setInterpreterOption(option); + setting.setProperties(p); + setting.setInterpreterGroupFactory(interpreterGroupFactory); + interpreterSettings.put(setting.getId(), setting); + loadInterpreterDependencies(setting); + saveToFile(); + return setting; + } + + private InterpreterSetting add(String group, InterpreterInfo interpreterInfo, + Map interpreterProperties, InterpreterOption option, + String path, InterpreterRunner runner) + throws InterpreterException, IOException, RepositoryException { + ArrayList infos = new ArrayList<>(); + infos.add(interpreterInfo); + return add(group, infos, new ArrayList(), option, interpreterProperties, path, + runner); + } + + /** + * @param group InterpreterSetting reference name + */ + public InterpreterSetting add(String group, ArrayList interpreterInfos, + List dependencies, InterpreterOption option, + Map interpreterProperties, String path, + InterpreterRunner runner) { + Preconditions.checkNotNull(group, "name should not be null"); + Preconditions.checkNotNull(interpreterInfos, "interpreterInfos should not be null"); + Preconditions.checkNotNull(dependencies, "dependencies should not be null"); + Preconditions.checkNotNull(option, "option should not be null"); + Preconditions.checkNotNull(interpreterProperties, "properties should not be null"); + + InterpreterSetting interpreterSetting; + + synchronized (interpreterSettingsRef) { + if (interpreterSettingsRef.containsKey(group)) { + interpreterSetting = interpreterSettingsRef.get(group); + + // Append InterpreterInfo + List infos = interpreterSetting.getInterpreterInfos(); + boolean hasDefaultInterpreter = findDefaultInterpreter(infos); + for (InterpreterInfo interpreterInfo : interpreterInfos) { + if (!infos.contains(interpreterInfo)) { + if (!hasDefaultInterpreter && interpreterInfo.isDefaultInterpreter()) { + hasDefaultInterpreter = true; + infos.add(0, interpreterInfo); + } else { + infos.add(interpreterInfo); + } + } + } + + // Append dependencies + List dependencyList = interpreterSetting.getDependencies(); + for (Dependency dependency : dependencies) { + if (!dependencyList.contains(dependency)) { + dependencyList.add(dependency); + } + } + + // Append properties + Map properties = + (Map) interpreterSetting.getProperties(); + for (String key : interpreterProperties.keySet()) { + if (!properties.containsKey(key)) { + properties.put(key, interpreterProperties.get(key)); + } + } + + } else { + interpreterSetting = + new InterpreterSetting(group, null, interpreterInfos, interpreterProperties, + dependencies, option, path, runner); + interpreterSettingsRef.put(group, interpreterSetting); + } + } + + if (dependencies.size() > 0) { + loadInterpreterDependencies(interpreterSetting); + } + + interpreterSetting.setInterpreterGroupFactory(interpreterGroupFactory); + return interpreterSetting; + } + + /** + * map interpreter ids into noteId + * + * @param noteId note id + * @param ids InterpreterSetting id list + */ + public void setInterpreters(String user, String noteId, List ids) throws IOException { + putNoteInterpreterSettingBinding(user, noteId, ids); + } + + private void putNoteInterpreterSettingBinding(String user, String noteId, + List settingList) throws IOException { + List unBindedSettings = new LinkedList<>(); + + synchronized (interpreterSettings) { + List oldSettings = interpreterBindings.get(noteId); + if (oldSettings != null) { + for (String oldSettingId : oldSettings) { + if (!settingList.contains(oldSettingId)) { + unBindedSettings.add(oldSettingId); + } + } + } + interpreterBindings.put(noteId, settingList); + saveToFile(); + + for (String settingId : unBindedSettings) { + InterpreterSetting setting = get(settingId); + removeInterpretersForNote(setting, user, noteId); + } + } + } + + public void removeInterpretersForNote(InterpreterSetting interpreterSetting, String user, + String noteId) { + //TODO(jl): This is only for hotfix. You should fix it as a beautiful way + InterpreterOption interpreterOption = interpreterSetting.getOption(); + if (!(InterpreterOption.SHARED.equals(interpreterOption.perNote) + && InterpreterOption.SHARED.equals(interpreterOption.perUser))) { + interpreterSetting.closeAndRemoveInterpreterGroup(noteId, ""); + } + } + + public String getInterpreterSessionKey(String user, String noteId, InterpreterSetting setting) { + InterpreterOption option = setting.getOption(); + String key; + if (option.isExistingProcess()) { + key = Constants.EXISTING_PROCESS; + } else if (option.perNoteScoped() && option.perUserScoped()) { + key = user + ":" + noteId; + } else if (option.perUserScoped()) { + key = user; + } else if (option.perNoteScoped()) { + key = noteId; + } else { + key = SHARED_SESSION; + } + + logger.debug("Interpreter session key: {}, for note: {}, user: {}, InterpreterSetting Name: " + + "{}", key, noteId, user, setting.getName()); + return key; + } + + + public List getInterpreters(String noteId) { + return getNoteInterpreterSettingBinding(noteId); + } + + public void closeNote(String user, String noteId) { + // close interpreters in this note session + List settings = getInterpreterSettings(noteId); + if (settings == null || settings.size() == 0) { + return; + } + + logger.info("closeNote: {}", noteId); + for (InterpreterSetting setting : settings) { + removeInterpretersForNote(setting, user, noteId); + } + } + + public Map getAvailableInterpreterSettings() { + return interpreterSettingsRef; + } + + private URL[] recursiveBuildLibList(File path) throws MalformedURLException { + URL[] urls = new URL[0]; + if (path == null || !path.exists()) { + return urls; + } else if (path.getName().startsWith(".")) { + return urls; + } else if (path.isDirectory()) { + File[] files = path.listFiles(); + if (files != null) { + for (File f : files) { + urls = (URL[]) ArrayUtils.addAll(urls, recursiveBuildLibList(f)); + } + } + return urls; + } else { + return new URL[]{path.toURI().toURL()}; + } + } + + public List getRepositories() { + return this.interpreterRepositories; + } + + public void addRepository(String id, String url, boolean snapshot, Authentication auth, + Proxy proxy) throws IOException { + dependencyResolver.addRepo(id, url, snapshot, auth, proxy); + saveToFile(); + } + + public void removeRepository(String id) throws IOException { + dependencyResolver.delRepo(id); + saveToFile(); + } + + public void removeNoteInterpreterSettingBinding(String user, String noteId) throws IOException { + List settingIds = interpreterBindings.remove(noteId); + if (settingIds != null) { + for (String settingId : settingIds) { + InterpreterSetting setting = get(settingId); + if (setting != null) { + this.removeInterpretersForNote(setting, user, noteId); + } + } + } + saveToFile(); + } + + /** + * Change interpreter property and restart + */ + public void setPropertyAndRestart(String id, InterpreterOption option, + Map properties, + List dependencies) throws IOException { + synchronized (interpreterSettings) { + InterpreterSetting intpSetting = interpreterSettings.get(id); + if (intpSetting != null) { + try { + stopJobAllInterpreter(intpSetting); + + intpSetting.closeAndRemoveAllInterpreterGroups(); + intpSetting.setOption(option); + intpSetting.setProperties(properties); + intpSetting.setDependencies(dependencies); + loadInterpreterDependencies(intpSetting); + + saveToFile(); + } catch (Exception e) { + loadFromFile(); + throw e; + } + } else { + throw new InterpreterException("Interpreter setting id " + id + " not found"); + } + } + } + + public void restart(String settingId, String noteId, String user) { + InterpreterSetting intpSetting = interpreterSettings.get(settingId); + Preconditions.checkNotNull(intpSetting); + synchronized (interpreterSettings) { + intpSetting = interpreterSettings.get(settingId); + // Check if dependency in specified path is changed + // If it did, overwrite old dependency jar with new one + if (intpSetting != null) { + //clean up metaInfos + intpSetting.setInfos(null); + copyDependenciesFromLocalPath(intpSetting); + + stopJobAllInterpreter(intpSetting); + if (user.equals("anonymous")) { + intpSetting.closeAndRemoveAllInterpreterGroups(); + } else { + intpSetting.closeAndRemoveInterpreterGroup(noteId, user); + } + + } else { + throw new InterpreterException("Interpreter setting id " + settingId + " not found"); + } + } + } + + public void restart(String id) { + restart(id, "", "anonymous"); + } + + private void stopJobAllInterpreter(InterpreterSetting intpSetting) { + if (intpSetting != null) { + for (InterpreterGroup intpGroup : intpSetting.getAllInterpreterGroups()) { + for (List interpreters : intpGroup.values()) { + for (Interpreter intp : interpreters) { + for (Job job : intp.getScheduler().getJobsRunning()) { + job.abort(); + job.setStatus(Status.ABORT); + logger.info("Job " + job.getJobName() + " aborted "); + } + for (Job job : intp.getScheduler().getJobsWaiting()) { + job.abort(); + job.setStatus(Status.ABORT); + logger.info("Job " + job.getJobName() + " aborted "); + } + } + } + } + } + } + + public InterpreterSetting get(String name) { + synchronized (interpreterSettings) { + return interpreterSettings.get(name); + } + } + + public void remove(String id) throws IOException { + synchronized (interpreterSettings) { + if (interpreterSettings.containsKey(id)) { + InterpreterSetting intp = interpreterSettings.get(id); + intp.closeAndRemoveAllInterpreterGroups(); + + interpreterSettings.remove(id); + for (List settings : interpreterBindings.values()) { + Iterator it = settings.iterator(); + while (it.hasNext()) { + String settingId = it.next(); + if (settingId.equals(id)) { + it.remove(); + } + } + } + saveToFile(); + } + } + + File localRepoDir = new File(zeppelinConfiguration.getInterpreterLocalRepoPath() + "/" + id); + FileUtils.deleteDirectory(localRepoDir); + } + + /** + * Get interpreter settings + */ + public List get() { + synchronized (interpreterSettings) { + List orderedSettings = new LinkedList<>(); + + Map> nameInterpreterSettingMap = new HashMap<>(); + for (InterpreterSetting interpreterSetting : interpreterSettings.values()) { + String group = interpreterSetting.getGroup(); + if (!nameInterpreterSettingMap.containsKey(group)) { + nameInterpreterSettingMap.put(group, new ArrayList()); + } + nameInterpreterSettingMap.get(group).add(interpreterSetting); + } + + for (String groupName : interpreterGroupOrderList) { + List interpreterSettingList = + nameInterpreterSettingMap.remove(groupName); + if (null != interpreterSettingList) { + for (InterpreterSetting interpreterSetting : interpreterSettingList) { + orderedSettings.add(interpreterSetting); + } + } + } + + List settings = new ArrayList<>(); + + for (List interpreterSettingList : nameInterpreterSettingMap.values()) { + for (InterpreterSetting interpreterSetting : interpreterSettingList) { + settings.add(interpreterSetting); + } + } + + Collections.sort(settings, new Comparator() { + @Override + public int compare(InterpreterSetting o1, InterpreterSetting o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + + orderedSettings.addAll(settings); + + return orderedSettings; + } + } + + public void close(InterpreterSetting interpreterSetting) { + interpreterSetting.closeAndRemoveAllInterpreterGroups(); + } + + public void close() { + List closeThreads = new LinkedList<>(); + synchronized (interpreterSettings) { + Collection intpSettings = interpreterSettings.values(); + for (final InterpreterSetting intpSetting : intpSettings) { + Thread t = new Thread() { + public void run() { + intpSetting.closeAndRemoveAllInterpreterGroups(); + } + }; + t.start(); + closeThreads.add(t); + } + } + + for (Thread t : closeThreads) { + try { + t.join(); + } catch (InterruptedException e) { + logger.error("Can't close interpreterGroup", e); + } + } + } + + public void shutdown() { + List closeThreads = new LinkedList<>(); + synchronized (interpreterSettings) { + Collection intpSettings = interpreterSettings.values(); + for (final InterpreterSetting intpSetting : intpSettings) { + Thread t = new Thread() { + public void run() { + intpSetting.shutdownAndRemoveAllInterpreterGroups(); + } + }; + t.start(); + closeThreads.add(t); + } + } + + for (Thread t : closeThreads) { + try { + t.join(); + } catch (InterruptedException e) { + logger.error("Can't close interpreterGroup", e); + } + } + } +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java similarity index 98% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java index 08175959b85..3838f63b20d 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java @@ -17,17 +17,19 @@ package org.apache.zeppelin.interpreter.install; import org.apache.commons.io.FileUtils; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.Logger; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.dep.DependencyResolver; import org.apache.zeppelin.util.Util; import org.sonatype.aether.RepositoryException; - import java.io.File; import java.io.IOException; import java.net.URL; import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java similarity index 71% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java index 74a2da2aad3..0ac71165348 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java @@ -17,18 +17,18 @@ package org.apache.zeppelin.interpreter.remote; -import com.google.gson.Gson; +import java.util.List; + import org.apache.thrift.TException; import org.apache.zeppelin.display.AngularObject; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.AngularObjectRegistryListener; import org.apache.zeppelin.interpreter.InterpreterGroup; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.List; +import com.google.gson.Gson; /** * Proxy for AngularObjectRegistry that exists in remote interpreter process @@ -56,29 +56,31 @@ private RemoteInterpreterProcess getRemoteInterpreterProcess() { * @param noteId * @return */ - public AngularObject addAndNotifyRemoteProcess(final String name, - final Object o, - final String noteId, - final String paragraphId) { - + public AngularObject addAndNotifyRemoteProcess(String name, Object o, String noteId, String + paragraphId) { + Gson gson = new Gson(); RemoteInterpreterProcess remoteInterpreterProcess = getRemoteInterpreterProcess(); if (!remoteInterpreterProcess.isRunning()) { return super.add(name, o, noteId, paragraphId, true); } - remoteInterpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public Void call(Client client) throws Exception { - Gson gson = new Gson(); - client.angularObjectAdd(name, noteId, paragraphId, gson.toJson(o)); - return null; - } - } - ); - - return super.add(name, o, noteId, paragraphId, true); - + Client client = null; + boolean broken = false; + try { + client = remoteInterpreterProcess.getClient(); + client.angularObjectAdd(name, noteId, paragraphId, gson.toJson(o)); + return super.add(name, o, noteId, paragraphId, true); + } catch (TException e) { + broken = true; + logger.error("Error", e); + } catch (Exception e) { + logger.error("Error", e); + } finally { + if (client != null) { + remoteInterpreterProcess.releaseClient(client, broken); + } + } + return null; } /** @@ -89,24 +91,30 @@ public Void call(Client client) throws Exception { * @param paragraphId * @return */ - public AngularObject removeAndNotifyRemoteProcess(final String name, - final String noteId, - final String paragraphId) { + public AngularObject removeAndNotifyRemoteProcess(String name, String noteId, String + paragraphId) { RemoteInterpreterProcess remoteInterpreterProcess = getRemoteInterpreterProcess(); if (remoteInterpreterProcess == null || !remoteInterpreterProcess.isRunning()) { return super.remove(name, noteId, paragraphId); } - remoteInterpreterProcess.callRemoteFunction( - new RemoteInterpreterProcess.RemoteFunction() { - @Override - public Void call(Client client) throws Exception { - client.angularObjectRemove(name, noteId, paragraphId); - return null; - } - } - ); - return super.remove(name, noteId, paragraphId); + Client client = null; + boolean broken = false; + try { + client = remoteInterpreterProcess.getClient(); + client.angularObjectRemove(name, noteId, paragraphId); + return super.remove(name, noteId, paragraphId); + } catch (TException e) { + broken = true; + logger.error("Error", e); + } catch (Exception e) { + logger.error("Error", e); + } finally { + if (client != null) { + remoteInterpreterProcess.releaseClient(client, broken); + } + } + return null; } public void removeAllAndNotifyRemoteProcess(String noteId, String paragraphId) { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java new file mode 100644 index 00000000000..12e0caa435d --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java @@ -0,0 +1,597 @@ +/* + * 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.interpreter.remote; + +import java.util.*; + +import org.apache.commons.lang3.StringUtils; +import org.apache.thrift.TException; +import org.apache.zeppelin.display.AngularObject; +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.helium.ApplicationEventListener; +import org.apache.zeppelin.display.Input; +import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterContext; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterResult; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterResultMessage; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; +import org.apache.zeppelin.scheduler.Scheduler; +import org.apache.zeppelin.scheduler.SchedulerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +/** + * Proxy for Interpreter instance that runs on separate process + */ +public class RemoteInterpreter extends Interpreter { + private static final Logger logger = LoggerFactory.getLogger(RemoteInterpreter.class); + + private final RemoteInterpreterProcessListener remoteInterpreterProcessListener; + private final ApplicationEventListener applicationEventListener; + private Gson gson = new Gson(); + private String interpreterRunner; + private String interpreterPath; + private String localRepoPath; + private String className; + private String sessionKey; + private FormType formType; + private boolean initialized; + private Map env; + private int connectTimeout; + private int maxPoolSize; + private String host; + private int port; + private String userName; + private Boolean isUserImpersonate; + private int outputLimit = Constants.ZEPPELIN_INTERPRETER_OUTPUT_LIMIT; + private String interpreterGroupName; + + /** + * Remote interpreter and manage interpreter process + */ + public RemoteInterpreter(Properties property, String sessionKey, String className, + String interpreterRunner, String interpreterPath, String localRepoPath, int connectTimeout, + int maxPoolSize, RemoteInterpreterProcessListener remoteInterpreterProcessListener, + ApplicationEventListener appListener, String userName, Boolean isUserImpersonate, + int outputLimit, String interpreterGroupName) { + super(property); + this.sessionKey = sessionKey; + this.className = className; + initialized = false; + this.interpreterRunner = interpreterRunner; + this.interpreterPath = interpreterPath; + this.localRepoPath = localRepoPath; + env = getEnvFromInterpreterProperty(property); + this.connectTimeout = connectTimeout; + this.maxPoolSize = maxPoolSize; + this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; + this.applicationEventListener = appListener; + this.userName = userName; + this.isUserImpersonate = isUserImpersonate; + this.outputLimit = outputLimit; + this.interpreterGroupName = interpreterGroupName; + } + + + /** + * Connect to existing process + */ + public RemoteInterpreter(Properties property, String sessionKey, String className, String host, + int port, String localRepoPath, int connectTimeout, int maxPoolSize, + RemoteInterpreterProcessListener remoteInterpreterProcessListener, + ApplicationEventListener appListener, String userName, Boolean isUserImpersonate, + int outputLimit) { + super(property); + this.sessionKey = sessionKey; + this.className = className; + initialized = false; + this.host = host; + this.port = port; + this.localRepoPath = localRepoPath; + this.connectTimeout = connectTimeout; + this.maxPoolSize = maxPoolSize; + this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; + this.applicationEventListener = appListener; + this.userName = userName; + this.isUserImpersonate = isUserImpersonate; + this.outputLimit = outputLimit; + } + + + // VisibleForTesting + public RemoteInterpreter(Properties property, String sessionKey, String className, + String interpreterRunner, String interpreterPath, String localRepoPath, + Map env, int connectTimeout, + RemoteInterpreterProcessListener remoteInterpreterProcessListener, + ApplicationEventListener appListener, String userName, Boolean isUserImpersonate) { + super(property); + this.className = className; + this.sessionKey = sessionKey; + this.interpreterRunner = interpreterRunner; + this.interpreterPath = interpreterPath; + this.localRepoPath = localRepoPath; + env.putAll(getEnvFromInterpreterProperty(property)); + this.env = env; + this.connectTimeout = connectTimeout; + this.maxPoolSize = 10; + this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; + this.applicationEventListener = appListener; + this.userName = userName; + this.isUserImpersonate = isUserImpersonate; + } + + private Map getEnvFromInterpreterProperty(Properties property) { + Map env = new HashMap(); + StringBuilder sparkConfBuilder = new StringBuilder(); + for (String key : property.stringPropertyNames()) { + if (RemoteInterpreterUtils.isEnvString(key)) { + env.put(key, property.getProperty(key)); + } + if (key.equals("master")) { + sparkConfBuilder.append(" --master " + property.getProperty("master")); + } + if (isSparkConf(key, property.getProperty(key))) { + sparkConfBuilder.append(" --conf " + key + "=" + + toShellFormat(property.getProperty(key))); + } + } + env.put("ZEPPELIN_SPARK_CONF", sparkConfBuilder.toString()); + return env; + } + + private String toShellFormat(String value) { + if (value.contains("\'") && value.contains("\"")) { + throw new RuntimeException("Spark property value could not contain both \" and '"); + } else if (value.contains("\'")) { + return "\"" + value + "\""; + } else { + return "\'" + value + "\'"; + } + } + + static boolean isSparkConf(String key, String value) { + return !StringUtils.isEmpty(key) && key.startsWith("spark.") && !StringUtils.isEmpty(value); + } + + @Override + public String getClassName() { + return className; + } + + private boolean connectToExistingProcess() { + return host != null && port > 0; + } + + public RemoteInterpreterProcess getInterpreterProcess() { + InterpreterGroup intpGroup = getInterpreterGroup(); + if (intpGroup == null) { + return null; + } + + synchronized (intpGroup) { + if (intpGroup.getRemoteInterpreterProcess() == null) { + RemoteInterpreterProcess remoteProcess; + if (connectToExistingProcess()) { + remoteProcess = new RemoteInterpreterRunningProcess( + connectTimeout, + remoteInterpreterProcessListener, + applicationEventListener, + host, + port); + } else { + // create new remote process + remoteProcess = new RemoteInterpreterManagedProcess( + interpreterRunner, interpreterPath, localRepoPath, env, connectTimeout, + remoteInterpreterProcessListener, applicationEventListener, interpreterGroupName); + } + + intpGroup.setRemoteInterpreterProcess(remoteProcess); + } + + return intpGroup.getRemoteInterpreterProcess(); + } + } + + public synchronized void init() { + if (initialized == true) { + return; + } + + RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); + + final InterpreterGroup interpreterGroup = getInterpreterGroup(); + + interpreterProcess.setMaxPoolSize( + Math.max(this.maxPoolSize, interpreterProcess.getMaxPoolSize())); + String groupId = interpreterGroup.getId(); + + synchronized (interpreterProcess) { + Client client = null; + try { + client = interpreterProcess.getClient(); + } catch (Exception e1) { + throw new InterpreterException(e1); + } + + boolean broken = false; + try { + logger.info("Create remote interpreter {}", getClassName()); + if (localRepoPath != null) { + property.put("zeppelin.interpreter.localRepo", localRepoPath); + } + + property.put("zeppelin.interpreter.output.limit", Integer.toString(outputLimit)); + client.createInterpreter(groupId, sessionKey, + getClassName(), (Map) property, userName); + // Push angular object loaded from JSON file to remote interpreter + if (!interpreterGroup.isAngularRegistryPushed()) { + pushAngularObjectRegistryToRemote(client); + interpreterGroup.setAngularRegistryPushed(true); + } + + } catch (TException e) { + logger.error("Failed to create interpreter: {}", getClassName()); + throw new InterpreterException(e); + } finally { + // TODO(jongyoul): Fixed it when not all of interpreter in same interpreter group are broken + interpreterProcess.releaseClient(client, broken); + } + } + initialized = true; + } + + + @Override + public void open() { + InterpreterGroup interpreterGroup = getInterpreterGroup(); + + synchronized (interpreterGroup) { + // initialize all interpreters in this interpreter group + List interpreters = interpreterGroup.get(sessionKey); + // TODO(jl): this open method is called by LazyOpenInterpreter.open(). It, however, + // initializes all of interpreters with same sessionKey. But LazyOpenInterpreter assumes if it + // doesn't call open method, it's not open. It causes problem while running intp.close() + // In case of Spark, this method initializes all of interpreters and init() method increases + // reference count of RemoteInterpreterProcess. But while closing this interpreter group, all + // other interpreters doesn't do anything because those LazyInterpreters aren't open. + // But for now, we have to initialise all of interpreters for some reasons. + // See Interpreter.getInterpreterInTheSameSessionByClassName(String) + RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); + if (!initialized) { + // reference per session + interpreterProcess.reference(interpreterGroup, userName, isUserImpersonate); + } + for (Interpreter intp : new ArrayList<>(interpreters)) { + Interpreter p = intp; + while (p instanceof WrappedInterpreter) { + p = ((WrappedInterpreter) p).getInnerInterpreter(); + } + try { + ((RemoteInterpreter) p).init(); + } catch (InterpreterException e) { + logger.error("Failed to initialize interpreter: {}. Remove it from interpreterGroup", + p.getClassName()); + interpreters.remove(p); + } + } + } + } + + @Override + public void close() { + InterpreterGroup interpreterGroup = getInterpreterGroup(); + synchronized (interpreterGroup) { + // close all interpreters in this session + List interpreters = interpreterGroup.get(sessionKey); + // TODO(jl): this open method is called by LazyOpenInterpreter.open(). It, however, + // initializes all of interpreters with same sessionKey. But LazyOpenInterpreter assumes if it + // doesn't call open method, it's not open. It causes problem while running intp.close() + // In case of Spark, this method initializes all of interpreters and init() method increases + // reference count of RemoteInterpreterProcess. But while closing this interpreter group, all + // other interpreters doesn't do anything because those LazyInterpreters aren't open. + // But for now, we have to initialise all of interpreters for some reasons. + // See Interpreter.getInterpreterInTheSameSessionByClassName(String) + if (initialized) { + // dereference per session + getInterpreterProcess().dereference(); + } + for (Interpreter intp : new ArrayList<>(interpreters)) { + Interpreter p = intp; + while (p instanceof WrappedInterpreter) { + p = ((WrappedInterpreter) p).getInnerInterpreter(); + } + try { + ((RemoteInterpreter) p).closeInterpreter(); + } catch (InterpreterException e) { + logger.error("Failed to initialize interpreter: {}. Remove it from interpreterGroup", + p.getClassName()); + interpreters.remove(p); + } + } + } + } + + public void closeInterpreter() { + if (this.initialized == false) { + return; + } + RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); + Client client = null; + boolean broken = false; + try { + client = interpreterProcess.getClient(); + if (client != null) { + client.close(sessionKey, className); + } + } catch (TException e) { + broken = true; + throw new InterpreterException(e); + } catch (Exception e1) { + throw new InterpreterException(e1); + } finally { + if (client != null) { + interpreterProcess.releaseClient(client, broken); + } + this.initialized = false; + } + } + + @Override + public InterpreterResult interpret(String st, InterpreterContext context) { + if (logger.isDebugEnabled()) { + logger.debug("st:\n{}", st); + } + + FormType form = getFormType(); + RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); + Client client = null; + try { + client = interpreterProcess.getClient(); + } catch (Exception e1) { + throw new InterpreterException(e1); + } + + InterpreterContextRunnerPool interpreterContextRunnerPool = interpreterProcess + .getInterpreterContextRunnerPool(); + + List runners = context.getRunners(); + if (runners != null && runners.size() != 0) { + // assume all runners in this InterpreterContext have the same note id + String noteId = runners.get(0).getNoteId(); + + interpreterContextRunnerPool.clear(noteId); + interpreterContextRunnerPool.addAll(noteId, runners); + } + + boolean broken = false; + try { + + final GUI currentGUI = context.getGui(); + RemoteInterpreterResult remoteResult = client.interpret( + sessionKey, className, st, convert(context)); + + Map remoteConfig = (Map) gson.fromJson( + remoteResult.getConfig(), new TypeToken>() { + }.getType()); + context.getConfig().clear(); + context.getConfig().putAll(remoteConfig); + + if (form == FormType.NATIVE) { + GUI remoteGui = GUI.fromJson(remoteResult.getGui()); + currentGUI.clear(); + currentGUI.setParams(remoteGui.getParams()); + currentGUI.setForms(remoteGui.getForms()); + } else if (form == FormType.SIMPLE) { + final Map currentForms = currentGUI.getForms(); + final Map currentParams = currentGUI.getParams(); + final GUI remoteGUI = GUI.fromJson(remoteResult.getGui()); + final Map remoteForms = remoteGUI.getForms(); + final Map remoteParams = remoteGUI.getParams(); + currentForms.putAll(remoteForms); + currentParams.putAll(remoteParams); + } + + InterpreterResult result = convert(remoteResult); + return result; + } catch (TException e) { + broken = true; + throw new InterpreterException(e); + } finally { + interpreterProcess.releaseClient(client, broken); + } + } + + @Override + public void cancel(InterpreterContext context) { + RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); + Client client = null; + try { + client = interpreterProcess.getClient(); + } catch (Exception e1) { + throw new InterpreterException(e1); + } + + boolean broken = false; + try { + client.cancel(sessionKey, className, convert(context)); + } catch (TException e) { + broken = true; + throw new InterpreterException(e); + } finally { + interpreterProcess.releaseClient(client, broken); + } + } + + @Override + public FormType getFormType() { + open(); + + if (formType != null) { + return formType; + } + + RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); + Client client = null; + try { + client = interpreterProcess.getClient(); + } catch (Exception e1) { + throw new InterpreterException(e1); + } + + boolean broken = false; + try { + formType = FormType.valueOf(client.getFormType(sessionKey, className)); + return formType; + } catch (TException e) { + broken = true; + throw new InterpreterException(e); + } finally { + interpreterProcess.releaseClient(client, broken); + } + } + + @Override + public int getProgress(InterpreterContext context) { + RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); + if (interpreterProcess == null || !interpreterProcess.isRunning()) { + return 0; + } + + Client client = null; + try { + client = interpreterProcess.getClient(); + } catch (Exception e1) { + throw new InterpreterException(e1); + } + + boolean broken = false; + try { + return client.getProgress(sessionKey, className, convert(context)); + } catch (TException e) { + broken = true; + throw new InterpreterException(e); + } finally { + interpreterProcess.releaseClient(client, broken); + } + } + + + @Override + public List completion(String buf, int cursor, + InterpreterContext interpreterContext) { + RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); + Client client = null; + try { + client = interpreterProcess.getClient(); + } catch (Exception e1) { + throw new InterpreterException(e1); + } + + boolean broken = false; + try { + List completion = client.completion(sessionKey, className, buf, cursor, + convert(interpreterContext)); + return completion; + } catch (TException e) { + broken = true; + throw new InterpreterException(e); + } finally { + interpreterProcess.releaseClient(client, broken); + } + } + + @Override + public Scheduler getScheduler() { + int maxConcurrency = maxPoolSize; + RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); + if (interpreterProcess == null) { + return null; + } else { + return SchedulerFactory.singleton().createOrGetRemoteScheduler( + RemoteInterpreter.class.getName() + sessionKey + interpreterProcess.hashCode(), + sessionKey, interpreterProcess, maxConcurrency); + } + } + + private String getInterpreterGroupKey(InterpreterGroup interpreterGroup) { + return interpreterGroup.getId(); + } + + private RemoteInterpreterContext convert(InterpreterContext ic) { + return new RemoteInterpreterContext(ic.getNoteId(), ic.getParagraphId(), ic.getReplName(), + ic.getParagraphTitle(), ic.getParagraphText(), ic.getAuthenticationInfo().toJson(), + gson.toJson(ic.getConfig()), ic.getGui().toJson(), gson.toJson(ic.getRunners())); + } + + private InterpreterResult convert(RemoteInterpreterResult result) { + InterpreterResult r = new InterpreterResult( + InterpreterResult.Code.valueOf(result.getCode())); + + for (RemoteInterpreterResultMessage m : result.getMsg()) { + r.add(InterpreterResult.Type.valueOf(m.getType()), m.getData()); + } + + return r; + } + + /** + * Push local angular object registry to + * remote interpreter. This method should be + * call ONLY inside the init() method + */ + void pushAngularObjectRegistryToRemote(Client client) throws TException { + final AngularObjectRegistry angularObjectRegistry = this.getInterpreterGroup() + .getAngularObjectRegistry(); + + if (angularObjectRegistry != null && angularObjectRegistry.getRegistry() != null) { + final Map> registry = angularObjectRegistry + .getRegistry(); + + logger.info("Push local angular object registry from ZeppelinServer to" + + " remote interpreter group {}", this.getInterpreterGroup().getId()); + + final java.lang.reflect.Type registryType = new TypeToken>>() { + }.getType(); + + Gson gson = new Gson(); + client.angularRegistryPush(gson.toJson(registry, registryType)); + } + } + + public Map getEnv() { + return env; + } + + public void addEnv(Map env) { + if (this.env == null) { + this.env = new HashMap<>(); + } + this.env.putAll(env); + } + + //Only for test + public String getInterpreterRunner() { + return interpreterRunner; + } +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java similarity index 93% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java index 19356fb16ed..1fb9b90771c 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java @@ -21,7 +21,6 @@ import org.apache.commons.exec.environment.EnvironmentUtils; import org.apache.zeppelin.helium.ApplicationEventListener; import org.apache.zeppelin.interpreter.InterpreterException; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -98,7 +97,6 @@ public void start(String userName, Boolean isUserImpersonate) { // start server process try { port = RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces(); - logger.info("Choose port {} for RemoteInterpreterProcess", port); } catch (IOException e1) { throw new InterpreterException(e1); } @@ -174,17 +172,6 @@ public void start(String userName, Boolean isUserImpersonate) { public void stop() { if (isRunning()) { logger.info("kill interpreter process"); - try { - callRemoteFunction(new RemoteFunction() { - @Override - public Void call(RemoteInterpreterService.Client client) throws Exception { - client.shutdown(); - return null; - } - }); - } catch (Exception e) { - logger.warn("ignore the exception when shutting down"); - } watchdog.destroyProcess(); } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java similarity index 100% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/ApplicationState.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/ApplicationState.java index bc71d893221..1505db9ada3 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/ApplicationState.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/ApplicationState.java @@ -17,6 +17,7 @@ package org.apache.zeppelin.notebook; import org.apache.zeppelin.helium.HeliumPackage; +import org.apache.zeppelin.interpreter.InterpreterGroup; /** * Current state of application diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java index 4a93d08011b..198e278e04b 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java @@ -41,6 +41,7 @@ import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.notebook.repo.NotebookRepo; import org.apache.zeppelin.notebook.utility.IdHashes; +import org.apache.zeppelin.resource.ResourcePoolUtils; import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.scheduler.Job.Status; import org.apache.zeppelin.search.SearchService; @@ -125,6 +126,11 @@ private void generateId() { id = IdHashes.generateId(); } + private String getDefaultInterpreterName() { + InterpreterSetting setting = interpreterSettingManager.getDefaultInterpreterSetting(getId()); + return null != setting ? setting.getName() : StringUtils.EMPTY; + } + public boolean isPersonalizedMode() { Object v = getConfig().get("personalizedMode"); return null != v && "true".equals(v); @@ -379,7 +385,7 @@ public void insertParagraph(Paragraph paragraph, int index) { */ public Paragraph removeParagraph(String user, String paragraphId) { removeAllAngularObjectInParagraph(user, paragraphId); - interpreterSettingManager.removeResourcesBelongsToParagraph(getId(), paragraphId); + ResourcePoolUtils.removeResourcesBelongsToParagraph(getId(), paragraphId); synchronized (paragraphs) { Iterator i = paragraphs.iterator(); while (i.hasNext()) { @@ -684,7 +690,7 @@ private void snapshotAngularObjectRegistry(String user) { } for (InterpreterSetting setting : settings) { - InterpreterGroup intpGroup = setting.getOrCreateInterpreterGroup(user, id); + InterpreterGroup intpGroup = setting.getInterpreterGroup(user, id); AngularObjectRegistry registry = intpGroup.getAngularObjectRegistry(); angularObjects.put(intpGroup.getId(), registry.getAllWithGlobal(id)); } @@ -699,7 +705,7 @@ private void removeAllAngularObjectInParagraph(String user, String paragraphId) } for (InterpreterSetting setting : settings) { - InterpreterGroup intpGroup = setting.getOrCreateInterpreterGroup(user, id); + InterpreterGroup intpGroup = setting.getInterpreterGroup(user, id); AngularObjectRegistry registry = intpGroup.getAngularObjectRegistry(); if (registry instanceof RemoteAngularObjectRegistry) { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java index fd3111b6c8d..a0c1dff8059 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java @@ -60,6 +60,7 @@ import org.apache.zeppelin.notebook.repo.NotebookRepo; import org.apache.zeppelin.notebook.repo.NotebookRepo.Revision; import org.apache.zeppelin.notebook.repo.NotebookRepoSync; +import org.apache.zeppelin.resource.ResourcePoolUtils; import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.scheduler.SchedulerFactory; import org.apache.zeppelin.search.SearchService; @@ -139,7 +140,7 @@ public Note createNote(AuthenticationInfo subject) throws IOException { Preconditions.checkNotNull(subject, "AuthenticationInfo should not be null"); Note note; if (conf.getBoolean(ConfVars.ZEPPELIN_NOTEBOOK_AUTO_INTERPRETER_BINDING)) { - note = createNote(interpreterSettingManager.getInterpreterSettingIds(), subject); + note = createNote(interpreterSettingManager.getDefaultInterpreterSettingList(), subject); } else { note = createNote(null, subject); } @@ -269,8 +270,8 @@ public void bindInterpretersToNote(String user, String id, List interpre } } - interpreterSettingManager.setInterpreterBinding(user, note.getId(), interpreterSettingIds); - // comment out while note.getNoteReplLoader().setInterpreterBinding(...) do the same + interpreterSettingManager.setInterpreters(user, note.getId(), interpreterSettingIds); + // comment out while note.getNoteReplLoader().setInterpreters(...) do the same // replFactory.putNoteInterpreterSettingBinding(id, interpreterSettingIds); } } @@ -278,7 +279,7 @@ public void bindInterpretersToNote(String user, String id, List interpre List getBindedInterpreterSettingsIds(String id) { Note note = getNote(id); if (note != null) { - return interpreterSettingManager.getInterpreterBinding(note.getId()); + return interpreterSettingManager.getInterpreters(note.getId()); } else { return new LinkedList<>(); } @@ -312,10 +313,9 @@ public boolean hasFolder(String folderId) { } public void moveNoteToTrash(String noteId) { - try { - interpreterSettingManager.setInterpreterBinding("", noteId, new ArrayList()); - } catch (IOException e) { - e.printStackTrace(); + for (InterpreterSetting interpreterSetting : interpreterSettingManager + .getInterpreterSettings(noteId)) { + interpreterSettingManager.removeInterpretersForNote(interpreterSetting, "", noteId); } } @@ -339,7 +339,7 @@ public void removeNote(String id, AuthenticationInfo subject) { // remove from all interpreter instance's angular object registry for (InterpreterSetting settings : interpreterSettingManager.get()) { AngularObjectRegistry registry = - settings.getOrCreateInterpreterGroup(subject.getUser(), id).getAngularObjectRegistry(); + settings.getInterpreterGroup(subject.getUser(), id).getAngularObjectRegistry(); if (registry instanceof RemoteAngularObjectRegistry) { // remove paragraph scope object for (Paragraph p : note.getParagraphs()) { @@ -374,7 +374,7 @@ public void removeNote(String id, AuthenticationInfo subject) { } } - interpreterSettingManager.removeResourcesBelongsToNote(id); + ResourcePoolUtils.removeResourcesBelongsToNote(id); fireNoteRemoveEvent(note); @@ -521,8 +521,7 @@ public Note loadNoteFromRepo(String id, AuthenticationInfo subject) { SnapshotAngularObject snapshot = angularObjectSnapshot.get(name); List settings = interpreterSettingManager.get(); for (InterpreterSetting setting : settings) { - InterpreterGroup intpGroup = setting.getOrCreateInterpreterGroup(subject.getUser(), - note.getId()); + InterpreterGroup intpGroup = setting.getInterpreterGroup(subject.getUser(), note.getId()); if (intpGroup.getId().equals(snapshot.getIntpGroupId())) { AngularObjectRegistry registry = intpGroup.getAngularObjectRegistry(); String noteId = snapshot.getAngularObject().getNoteId(); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java index bfe45662f4a..37138e63c42 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java @@ -93,10 +93,10 @@ public class Paragraph extends Job implements Cloneable, JsonSerializable { // since zeppelin-0.7.0, zeppelin stores multiple results of the paragraph // see ZEPPELIN-212 - volatile Object results; + Object results; // For backward compatibility of note.json format after ZEPPELIN-212 - volatile Object result; + Object result; private Map runtimeInfos; /** @@ -157,7 +157,7 @@ public Paragraph getUserParagraph(String user) { } @Override - public synchronized void setResult(Object results) { + public void setResult(Object results) { this.results = results; } @@ -354,7 +354,7 @@ public InterpreterResult getResult() { } @Override - public synchronized Object getReturn() { + public Object getReturn() { return results; } @@ -401,7 +401,6 @@ protected Object jobRun() throws Throwable { logger.error("Can not find interpreter name " + repl); throw new RuntimeException("Can not find interpreter for " + getRequiredReplName()); } - //TODO(zjffdu) check interpreter setting status in interpreter setting itself InterpreterSetting intp = getInterpreterSettingById(repl.getInterpreterGroup().getId()); while (intp.getStatus().equals( org.apache.zeppelin.interpreter.InterpreterSetting.Status.DOWNLOADING_DEPENDENCIES)) { @@ -561,10 +560,8 @@ private InterpreterContext getInterpreterContextWithoutRunner(InterpreterOutput if (!interpreterSettingManager.getInterpreterSettings(note.getId()).isEmpty()) { InterpreterSetting intpGroup = interpreterSettingManager.getInterpreterSettings(note.getId()).get(0); - registry = intpGroup.getOrCreateInterpreterGroup(getUser(), note.getId()) - .getAngularObjectRegistry(); - resourcePool = intpGroup.getOrCreateInterpreterGroup(getUser(), note.getId()) - .getResourcePool(); + registry = intpGroup.getInterpreterGroup(getUser(), note.getId()).getAngularObjectRegistry(); + resourcePool = intpGroup.getInterpreterGroup(getUser(), note.getId()).getResourcePool(); } List runners = new LinkedList<>(); @@ -594,10 +591,8 @@ private InterpreterContext getInterpreterContext(InterpreterOutput output) { if (!interpreterSettingManager.getInterpreterSettings(note.getId()).isEmpty()) { InterpreterSetting intpGroup = interpreterSettingManager.getInterpreterSettings(note.getId()).get(0); - registry = intpGroup.getOrCreateInterpreterGroup(getUser(), note.getId()) - .getAngularObjectRegistry(); - resourcePool = intpGroup.getOrCreateInterpreterGroup(getUser(), note.getId()) - .getResourcePool(); + registry = intpGroup.getInterpreterGroup(getUser(), note.getId()).getAngularObjectRegistry(); + resourcePool = intpGroup.getInterpreterGroup(getUser(), note.getId()).getResourcePool(); } List runners = new LinkedList<>(); diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/Util.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/util/Util.java similarity index 98% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/Util.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/util/Util.java index 6153f499bd6..be45b9ef097 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/util/Util.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/util/Util.java @@ -17,7 +17,7 @@ package org.apache.zeppelin.util; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import java.io.IOException; import java.util.Properties; diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumApplicationFactoryTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumApplicationFactoryTest.java index c204711f573..305258afa4a 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumApplicationFactoryTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumApplicationFactoryTest.java @@ -16,14 +16,14 @@ */ package org.apache.zeppelin.helium; +import com.google.common.collect.Maps; import org.apache.commons.io.FileUtils; import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.dep.Dependency; import org.apache.zeppelin.dep.DependencyResolver; -import org.apache.zeppelin.display.AngularObjectRegistryListener; import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.mock.MockInterpreter1; import org.apache.zeppelin.interpreter.mock.MockInterpreter2; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; import org.apache.zeppelin.notebook.*; import org.apache.zeppelin.notebook.repo.VFSNotebookRepo; import org.apache.zeppelin.scheduler.Job; @@ -45,9 +45,14 @@ import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; -public class HeliumApplicationFactoryTest extends AbstractInterpreterTest implements JobListenerFactory { - +public class HeliumApplicationFactoryTest implements JobListenerFactory { + private File tmpDir; + private File notebookDir; + private ZeppelinConfiguration conf; private SchedulerFactory schedulerFactory; + private DependencyResolver depResolver; + private InterpreterFactory factory; + private InterpreterSettingManager interpreterSettingManager; private VFSNotebookRepo notebookRepo; private Notebook notebook; private HeliumApplicationFactory heliumAppFactory; @@ -55,15 +60,46 @@ public class HeliumApplicationFactoryTest extends AbstractInterpreterTest implem @Before public void setUp() throws Exception { - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER.getVarName(), "mock1,mock2"); - super.setUp(); + tmpDir = new File(System.getProperty("java.io.tmpdir")+"/ZepelinLTest_"+System.currentTimeMillis()); + tmpDir.mkdirs(); + File confDir = new File(tmpDir, "conf"); + confDir.mkdirs(); + notebookDir = new File(tmpDir + "/notebook"); + notebookDir.mkdirs(); + + File home = new File(getClass().getClassLoader().getResource("note").getFile()) // zeppelin/zeppelin-zengine/target/test-classes/note + .getParentFile() // zeppelin/zeppelin-zengine/target/test-classes + .getParentFile() // zeppelin/zeppelin-zengine/target + .getParentFile() // zeppelin/zeppelin-zengine + .getParentFile(); // zeppelin + + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName(), home.getAbsolutePath()); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONF_DIR.getVarName(), tmpDir.getAbsolutePath() + "/conf"); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), notebookDir.getAbsolutePath()); + + conf = new ZeppelinConfiguration(); + + this.schedulerFactory = new SchedulerFactory(); - this.schedulerFactory = SchedulerFactory.singleton(); heliumAppFactory = new HeliumApplicationFactory(); - // set AppEventListener properly - for (InterpreterSetting interpreterSetting : interpreterSettingManager.get()) { - interpreterSetting.setAppEventListener(heliumAppFactory); - } + depResolver = new DependencyResolver(tmpDir.getAbsolutePath() + "/local-repo"); + interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); + factory = new InterpreterFactory(conf, null, null, heliumAppFactory, depResolver, false, interpreterSettingManager); + HashMap env = new HashMap<>(); + env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); + factory.setEnv(env); + + ArrayList interpreterInfos = new ArrayList<>(); + interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); + interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), + Maps.newHashMap(), "mock1", null); + interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(true), new HashMap()); + + ArrayList interpreterInfos2 = new ArrayList<>(); + interpreterInfos2.add(new InterpreterInfo(MockInterpreter2.class.getName(), "mock2", true, new HashMap())); + interpreterSettingManager.add("mock2", interpreterInfos2, new ArrayList(), new InterpreterOption(), + Maps.newHashMap(), "mock2", null); + interpreterSettingManager.createNewSetting("mock2", "mock2", new ArrayList(), new InterpreterOption(), new HashMap()); SearchService search = mock(SearchService.class); notebookRepo = new VFSNotebookRepo(conf); @@ -72,7 +108,7 @@ public void setUp() throws Exception { conf, notebookRepo, schedulerFactory, - interpreterFactory, + factory, interpreterSettingManager, this, search, @@ -88,7 +124,16 @@ public void setUp() throws Exception { @After public void tearDown() throws Exception { - super.tearDown(); + List settings = interpreterSettingManager.get(); + for (InterpreterSetting setting : settings) { + for (InterpreterGroup intpGroup : setting.getAllInterpreterGroups()) { + intpGroup.close(); + } + } + + FileUtils.deleteDirectory(tmpDir); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONF_DIR.getVarName(), + ZeppelinConfiguration.ConfVars.ZEPPELIN_CONF_DIR.getStringValue()); } @@ -105,7 +150,7 @@ public void testLoadRunUnloadApplication() "", ""); Note note1 = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding("user", note1.getId(),interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters("user", note1.getId(),interpreterSettingManager.getDefaultInterpreterSettingList()); Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -151,7 +196,7 @@ public void testUnloadOnParagraphRemove() throws IOException { "", ""); Note note1 = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding("user", note1.getId(), interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters("user", note1.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -191,7 +236,7 @@ public void testUnloadOnInterpreterUnbind() throws IOException { "", ""); Note note1 = notebook.createNote(anonymous); - notebook.bindInterpretersToNote("user", note1.getId(), interpreterSettingManager.getInterpreterSettingIds()); + notebook.bindInterpretersToNote("user", note1.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -252,7 +297,7 @@ public void testUnloadOnInterpreterRestart() throws IOException { "", ""); Note note1 = notebook.createNote(anonymous); - notebook.bindInterpretersToNote("user", note1.getId(), interpreterSettingManager.getInterpreterSettingIds()); + notebook.bindInterpretersToNote("user", note1.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); String mock1IntpSettingId = null; for (InterpreterSetting setting : notebook.getBindedInterpreterSettings(note1.getId())) { if (setting.getName().equals("mock1")) { diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumTest.java index bdd639e3bb6..6b4932d0594 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumTest.java @@ -52,7 +52,7 @@ public void testSaveLoadConf() throws IOException, URISyntaxException, TaskRunne // given File heliumConf = new File(tmpDir, "helium.conf"); Helium helium = new Helium(heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), - null, null, null, null); + null, null, null); assertFalse(heliumConf.exists()); // when @@ -63,14 +63,14 @@ public void testSaveLoadConf() throws IOException, URISyntaxException, TaskRunne // then load without exception Helium heliumRestored = new Helium( - heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), null, null, null, null); + heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), null, null, null); } @Test public void testRestoreRegistryInstances() throws IOException, URISyntaxException, TaskRunnerException { File heliumConf = new File(tmpDir, "helium.conf"); Helium helium = new Helium( - heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), null, null, null, null); + heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), null, null, null); HeliumTestRegistry registry1 = new HeliumTestRegistry("r1", "r1"); HeliumTestRegistry registry2 = new HeliumTestRegistry("r2", "r2"); helium.addRegistry(registry1); @@ -105,7 +105,7 @@ public void testRestoreRegistryInstances() throws IOException, URISyntaxExceptio public void testRefresh() throws IOException, URISyntaxException, TaskRunnerException { File heliumConf = new File(tmpDir, "helium.conf"); Helium helium = new Helium( - heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), null, null, null, null); + heliumConf.getAbsolutePath(), localRegistryPath.getAbsolutePath(), null, null, null); HeliumTestRegistry registry1 = new HeliumTestRegistry("r1", "r1"); helium.addRegistry(registry1); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java new file mode 100644 index 00000000000..aaa8864e8cd --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java @@ -0,0 +1,497 @@ +/* + * 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.interpreter; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.NullArgumentException; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; +import org.apache.zeppelin.dep.Dependency; +import org.apache.zeppelin.dep.DependencyResolver; +import org.apache.zeppelin.interpreter.mock.MockInterpreter1; +import org.apache.zeppelin.interpreter.mock.MockInterpreter2; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; +import org.apache.zeppelin.notebook.JobListenerFactory; +import org.apache.zeppelin.notebook.Note; +import org.apache.zeppelin.notebook.Notebook; +import org.apache.zeppelin.notebook.NotebookAuthorization; +import org.apache.zeppelin.notebook.repo.NotebookRepo; +import org.apache.zeppelin.notebook.repo.VFSNotebookRepo; +import org.apache.zeppelin.scheduler.SchedulerFactory; +import org.apache.zeppelin.search.SearchService; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.quartz.SchedulerException; +import org.sonatype.aether.RepositoryException; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class InterpreterFactoryTest { + + private InterpreterFactory factory; + private InterpreterSettingManager interpreterSettingManager; + private File tmpDir; + private ZeppelinConfiguration conf; + private InterpreterContext context; + private Notebook notebook; + private NotebookRepo notebookRepo; + private DependencyResolver depResolver; + private SchedulerFactory schedulerFactory; + private NotebookAuthorization notebookAuthorization; + @Mock + private JobListenerFactory jobListenerFactory; + + @Before + public void setUp() throws Exception { + tmpDir = new File(System.getProperty("java.io.tmpdir")+"/ZeppelinLTest_"+System.currentTimeMillis()); + tmpDir.mkdirs(); + new File(tmpDir, "conf").mkdirs(); + FileUtils.copyDirectory(new File("src/test/resources/interpreter"), new File(tmpDir, "interpreter")); + + System.setProperty(ConfVars.ZEPPELIN_HOME.getVarName(), tmpDir.getAbsolutePath()); + System.setProperty(ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER.getVarName(), + "mock1,mock2,mock11,dev"); + conf = new ZeppelinConfiguration(); + schedulerFactory = new SchedulerFactory(); + depResolver = new DependencyResolver(tmpDir.getAbsolutePath() + "/local-repo"); + interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); + factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); + context = new InterpreterContext("note", "id", null, "title", "text", null, null, null, null, null, null, null); + + ArrayList interpreterInfos = new ArrayList<>(); + interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); + interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), + Maps.newHashMap(), "mock1", null); + Map intp1Properties = new HashMap(); + intp1Properties.put("PROPERTY_1", + new InterpreterProperty("PROPERTY_1", "VALUE_1")); + intp1Properties.put("property_2", + new InterpreterProperty("property_2", "value_2")); + interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(true), intp1Properties); + + ArrayList interpreterInfos2 = new ArrayList<>(); + interpreterInfos2.add(new InterpreterInfo(MockInterpreter2.class.getName(), "mock2", true, new HashMap())); + interpreterSettingManager.add("mock2", interpreterInfos2, new ArrayList(), new InterpreterOption(), + Maps.newHashMap(), "mock2", null); + interpreterSettingManager.createNewSetting("mock2", "mock2", new ArrayList(), new InterpreterOption(), new HashMap()); + + SearchService search = mock(SearchService.class); + notebookRepo = new VFSNotebookRepo(conf); + notebookAuthorization = NotebookAuthorization.init(conf); + notebook = new Notebook(conf, notebookRepo, schedulerFactory, factory, interpreterSettingManager, jobListenerFactory, search, + notebookAuthorization, null); + } + + @After + public void tearDown() throws Exception { + FileUtils.deleteDirectory(tmpDir); + } + + @Test + public void testBasic() { + List all = interpreterSettingManager.get(); + InterpreterSetting mock1Setting = null; + for (InterpreterSetting setting : all) { + if (setting.getName().equals("mock1")) { + mock1Setting = setting; + break; + } + } + +// mock1Setting = factory.createNewSetting("mock11", "mock1", new ArrayList(), new InterpreterOption(false), new Properties()); + + InterpreterGroup interpreterGroup = mock1Setting.getInterpreterGroup("user", "sharedProcess"); + factory.createInterpretersForNote(mock1Setting, "user", "sharedProcess", "session"); + + // get interpreter + assertNotNull("get Interpreter", interpreterGroup.get("session").get(0)); + + // try to get unavailable interpreter + assertNull(interpreterSettingManager.get("unknown")); + + // restart interpreter + interpreterSettingManager.restart(mock1Setting.getId()); + assertNull(mock1Setting.getInterpreterGroup("user", "sharedProcess").get("session")); + } + + @Test + public void testRemoteRepl() throws Exception { + interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); + ArrayList interpreterInfos = new ArrayList<>(); + interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); + interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), + Maps.newHashMap(), "mock1", null); + Map intp1Properties = new HashMap(); + intp1Properties.put("PROPERTY_1", + new InterpreterProperty("PROPERTY_1", "VALUE_1")); + intp1Properties.put("property_2", new InterpreterProperty("property_2", "value_2")); + interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(true), intp1Properties); + factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); + List all = interpreterSettingManager.get(); + InterpreterSetting mock1Setting = null; + for (InterpreterSetting setting : all) { + if (setting.getName().equals("mock1")) { + mock1Setting = setting; + break; + } + } + InterpreterGroup interpreterGroup = mock1Setting.getInterpreterGroup("user", "sharedProcess"); + factory.createInterpretersForNote(mock1Setting, "user", "sharedProcess", "session"); + // get interpreter + assertNotNull("get Interpreter", interpreterGroup.get("session").get(0)); + assertTrue(interpreterGroup.get("session").get(0) instanceof LazyOpenInterpreter); + LazyOpenInterpreter lazyInterpreter = (LazyOpenInterpreter)(interpreterGroup.get("session").get(0)); + assertTrue(lazyInterpreter.getInnerInterpreter() instanceof RemoteInterpreter); + RemoteInterpreter remoteInterpreter = (RemoteInterpreter) lazyInterpreter.getInnerInterpreter(); + assertEquals("VALUE_1", remoteInterpreter.getEnv().get("PROPERTY_1")); + assertEquals("value_2", remoteInterpreter.getProperty("property_2")); + } + + /** + * 2 users' interpreters in scoped mode. Each user has one session. Restarting user1's interpreter + * won't affect user2's interpreter + * @throws Exception + */ + @Test + public void testRestartInterpreterInScopedMode() throws Exception { + interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); + ArrayList interpreterInfos = new ArrayList<>(); + interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); + interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), + Maps.newHashMap(), "mock1", null); + Map intp1Properties = new HashMap(); + intp1Properties.put("PROPERTY_1", + new InterpreterProperty("PROPERTY_1", "VALUE_1")); + intp1Properties.put("property_2", + new InterpreterProperty("property_2", "value_2")); + interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(true), intp1Properties); + factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); + List all = interpreterSettingManager.get(); + InterpreterSetting mock1Setting = null; + for (InterpreterSetting setting : all) { + if (setting.getName().equals("mock1")) { + mock1Setting = setting; + break; + } + } + mock1Setting.getOption().setPerUser("scoped"); + mock1Setting.getOption().setPerNote("shared"); + // set remote as false so that we won't create new remote interpreter process + mock1Setting.getOption().setRemote(false); + mock1Setting.getOption().setHost("localhost"); + mock1Setting.getOption().setPort(2222); + InterpreterGroup interpreterGroup = mock1Setting.getInterpreterGroup("user1", "sharedProcess"); + factory.createInterpretersForNote(mock1Setting, "user1", "sharedProcess", "user1"); + factory.createInterpretersForNote(mock1Setting, "user2", "sharedProcess", "user2"); + + LazyOpenInterpreter interpreter1 = (LazyOpenInterpreter)interpreterGroup.get("user1").get(0); + interpreter1.open(); + LazyOpenInterpreter interpreter2 = (LazyOpenInterpreter)interpreterGroup.get("user2").get(0); + interpreter2.open(); + + mock1Setting.closeAndRemoveInterpreterGroup("sharedProcess", "user1"); + assertFalse(interpreter1.isOpen()); + assertTrue(interpreter2.isOpen()); + } + + /** + * 2 users' interpreters in isolated mode. Each user has one interpreterGroup. Restarting user1's interpreter + * won't affect user2's interpreter + * @throws Exception + */ + @Test + public void testRestartInterpreterInIsolatedMode() throws Exception { + interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); + ArrayList interpreterInfos = new ArrayList<>(); + interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); + interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), + Maps.newHashMap(), "mock1", null); + Map intp1Properties = new HashMap(); + intp1Properties.put("PROPERTY_1", + new InterpreterProperty("PROPERTY_1", "VALUE_1")); + intp1Properties.put("property_2", + new InterpreterProperty("property_2", "value_2")); + interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(true), intp1Properties); + factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); + List all = interpreterSettingManager.get(); + InterpreterSetting mock1Setting = null; + for (InterpreterSetting setting : all) { + if (setting.getName().equals("mock1")) { + mock1Setting = setting; + break; + } + } + mock1Setting.getOption().setPerUser("isolated"); + mock1Setting.getOption().setPerNote("shared"); + // set remote as false so that we won't create new remote interpreter process + mock1Setting.getOption().setRemote(false); + mock1Setting.getOption().setHost("localhost"); + mock1Setting.getOption().setPort(2222); + InterpreterGroup interpreterGroup1 = mock1Setting.getInterpreterGroup("user1", "note1"); + InterpreterGroup interpreterGroup2 = mock1Setting.getInterpreterGroup("user2", "note2"); + factory.createInterpretersForNote(mock1Setting, "user1", "note1", "shared_session"); + factory.createInterpretersForNote(mock1Setting, "user2", "note2", "shared_session"); + + LazyOpenInterpreter interpreter1 = (LazyOpenInterpreter)interpreterGroup1.get("shared_session").get(0); + interpreter1.open(); + LazyOpenInterpreter interpreter2 = (LazyOpenInterpreter)interpreterGroup2.get("shared_session").get(0); + interpreter2.open(); + + mock1Setting.closeAndRemoveInterpreterGroup("note1", "user1"); + assertFalse(interpreter1.isOpen()); + assertTrue(interpreter2.isOpen()); + } + + @Test + public void testFactoryDefaultList() throws IOException, RepositoryException { + // get default settings + List all = interpreterSettingManager.getDefaultInterpreterSettingList(); + assertTrue(interpreterSettingManager.get().size() >= all.size()); + } + + @Test + public void testExceptions() throws InterpreterException, IOException, RepositoryException { + List all = interpreterSettingManager.getDefaultInterpreterSettingList(); + // add setting with null option & properties expected nullArgumentException.class + try { + interpreterSettingManager.add("mock2", new ArrayList(), new LinkedList(), new InterpreterOption(false), Collections.EMPTY_MAP, "", null); + } catch(NullArgumentException e) { + assertEquals("Test null option" , e.getMessage(),new NullArgumentException("option").getMessage()); + } + try { + interpreterSettingManager.add("mock2", new ArrayList(), new LinkedList(), new InterpreterOption(false), Collections.EMPTY_MAP, "", null); + } catch (NullArgumentException e){ + assertEquals("Test null properties" , e.getMessage(),new NullArgumentException("properties").getMessage()); + } + } + + + @Test + public void testSaveLoad() throws IOException, RepositoryException { + // interpreter settings + int numInterpreters = interpreterSettingManager.get().size(); + + // check if file saved + assertTrue(new File(conf.getInterpreterSettingPath()).exists()); + + interpreterSettingManager.createNewSetting("new-mock1", "mock1", new LinkedList(), new InterpreterOption(false), new HashMap()); + assertEquals(numInterpreters + 1, interpreterSettingManager.get().size()); + + interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); + + /* + Current situation, if InterpreterSettinfRef doesn't have the key of InterpreterSetting, it would be ignored. + Thus even though interpreter.json have several interpreterSetting in that file, it would be ignored and would not be initialized from loadFromFile. + In this case, only "mock11" would be referenced from file under interpreter/mock, and "mock11" group would be initialized. + */ + // TODO(jl): Decide how to handle the know referenced interpreterSetting. + assertEquals(1, interpreterSettingManager.get().size()); + } + + @Test + public void testInterpreterSettingPropertyClass() throws IOException, RepositoryException { + // check if default interpreter reference's property type is map + Map interpreterSettingRefs = interpreterSettingManager.getAvailableInterpreterSettings(); + InterpreterSetting intpSetting = interpreterSettingRefs.get("mock1"); + Map intpProperties = + (Map) intpSetting.getProperties(); + assertTrue(intpProperties instanceof Map); + + // check if interpreter instance is saved as Properties in conf/interpreter.json file + Map properties = new HashMap(); + properties.put("key1", new InterpreterProperty("key1", "value1", "type1")); + properties.put("key2", new InterpreterProperty("key2", "value2", "type2")); + + interpreterSettingManager.createNewSetting("newMock", "mock1", new LinkedList(), new InterpreterOption(false), properties); + + String confFilePath = conf.getInterpreterSettingPath(); + byte[] encoded = Files.readAllBytes(Paths.get(confFilePath)); + String json = new String(encoded, "UTF-8"); + + InterpreterInfoSaving infoSaving = InterpreterInfoSaving.fromJson(json); + Map interpreterSettings = infoSaving.interpreterSettings; + for (String key : interpreterSettings.keySet()) { + InterpreterSetting setting = interpreterSettings.get(key); + if (setting.getName().equals("newMock")) { + assertEquals(setting.getProperties().toString(), properties.toString()); + } + } + } + + @Test + public void testInterpreterAliases() throws IOException, RepositoryException { + interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); + factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); + final InterpreterInfo info1 = new InterpreterInfo("className1", "name1", true, null); + final InterpreterInfo info2 = new InterpreterInfo("className2", "name1", true, null); + interpreterSettingManager.add("group1", new ArrayList() {{ + add(info1); + }}, new ArrayList(), new InterpreterOption(true), Collections.EMPTY_MAP, "/path1", null); + interpreterSettingManager.add("group2", new ArrayList(){{ + add(info2); + }}, new ArrayList(), new InterpreterOption(true), Collections.EMPTY_MAP, "/path2", null); + + final InterpreterSetting setting1 = interpreterSettingManager.createNewSetting("test-group1", "group1", new ArrayList(), new InterpreterOption(true), new HashMap()); + final InterpreterSetting setting2 = interpreterSettingManager.createNewSetting("test-group2", "group1", new ArrayList(), new InterpreterOption(true), new HashMap()); + + interpreterSettingManager.setInterpreters("user", "note", new ArrayList() {{ + add(setting1.getId()); + add(setting2.getId()); + }}); + + assertEquals("className1", factory.getInterpreter("user1", "note", "test-group1").getClassName()); + assertEquals("className1", factory.getInterpreter("user1", "note", "group1").getClassName()); + } + + @Test + public void testMultiUser() throws IOException, RepositoryException { + interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); + factory = new InterpreterFactory(conf, null, null, null, depResolver, true, interpreterSettingManager); + final InterpreterInfo info1 = new InterpreterInfo("className1", "name1", true, null); + interpreterSettingManager.add("group1", new ArrayList(){{ + add(info1); + }}, new ArrayList(), new InterpreterOption(true), Collections.EMPTY_MAP, "/path1", null); + + InterpreterOption perUserInterpreterOption = new InterpreterOption(true, InterpreterOption.ISOLATED, InterpreterOption.SHARED); + final InterpreterSetting setting1 = interpreterSettingManager.createNewSetting("test-group1", "group1", new ArrayList(), perUserInterpreterOption, new HashMap()); + + interpreterSettingManager.setInterpreters("user1", "note", new ArrayList() {{ + add(setting1.getId()); + }}); + + interpreterSettingManager.setInterpreters("user2", "note", new ArrayList() {{ + add(setting1.getId()); + }}); + + assertNotEquals(factory.getInterpreter("user1", "note", "test-group1"), factory.getInterpreter("user2", "note", "test-group1")); + } + + + @Test + public void testInvalidInterpreterSettingName() { + try { + interpreterSettingManager.createNewSetting("new.mock1", "mock1", new LinkedList(), new InterpreterOption(false), new HashMap()); + fail("expect fail because of invalid InterpreterSetting Name"); + } catch (IOException e) { + assertEquals("'.' is invalid for InterpreterSetting name.", e.getMessage()); + } + } + + + @Test + public void getEditorSetting() throws IOException, RepositoryException, SchedulerException { + List intpIds = new ArrayList<>(); + for(InterpreterSetting intpSetting: interpreterSettingManager.get()) { + if (intpSetting.getName().startsWith("mock1")) { + intpIds.add(intpSetting.getId()); + } + } + Note note = notebook.createNote(intpIds, new AuthenticationInfo("anonymous")); + + Interpreter interpreter = factory.getInterpreter("user1", note.getId(), "mock11"); + // get editor setting from interpreter-setting.json + Map editor = interpreterSettingManager.getEditorSetting(interpreter, "user1", note.getId(), "mock11"); + assertEquals("java", editor.get("language")); + + // when interpreter is not loaded via interpreter-setting.json + // or editor setting doesn't exit + editor = interpreterSettingManager.getEditorSetting(factory.getInterpreter("user1", note.getId(), "mock1"),"user1", note.getId(), "mock1"); + assertEquals(null, editor.get("language")); + + // when interpreter is not bound to note + editor = interpreterSettingManager.getEditorSetting(factory.getInterpreter("user1", note.getId(), "mock11"),"user1", note.getId(), "mock2"); + assertEquals("text", editor.get("language")); + } + + @Test + public void registerCustomInterpreterRunner() throws IOException { + InterpreterSettingManager spyInterpreterSettingManager = spy(interpreterSettingManager); + + doNothing().when(spyInterpreterSettingManager).saveToFile(); + + ArrayList interpreterInfos1 = new ArrayList<>(); + interpreterInfos1.add(new InterpreterInfo("name1.class", "name1", true, Maps.newHashMap())); + + spyInterpreterSettingManager.add("normalGroup1", interpreterInfos1, Lists.newArrayList(), new InterpreterOption(true), Maps.newHashMap(), "/normalGroup1", null); + + spyInterpreterSettingManager.createNewSetting("normalGroup1", "normalGroup1", Lists.newArrayList(), new InterpreterOption(true), new HashMap()); + + ArrayList interpreterInfos2 = new ArrayList<>(); + interpreterInfos2.add(new InterpreterInfo("name1.class", "name1", true, Maps.newHashMap())); + + InterpreterRunner mockInterpreterRunner = mock(InterpreterRunner.class); + + when(mockInterpreterRunner.getPath()).thenReturn("custom-linux-path.sh"); + + spyInterpreterSettingManager.add("customGroup1", interpreterInfos2, Lists.newArrayList(), new InterpreterOption(true), Maps.newHashMap(), "/customGroup1", mockInterpreterRunner); + + spyInterpreterSettingManager.createNewSetting("customGroup1", "customGroup1", Lists.newArrayList(), new InterpreterOption(true), new HashMap()); + + spyInterpreterSettingManager.setInterpreters("anonymous", "noteCustome", spyInterpreterSettingManager.getDefaultInterpreterSettingList()); + + factory.getInterpreter("anonymous", "noteCustome", "customGroup1"); + + verify(mockInterpreterRunner, times(1)).getPath(); + } + + @Test + public void interpreterRunnerTest() { + InterpreterRunner mockInterpreterRunner = mock(InterpreterRunner.class); + String testInterpreterRunner = "relativePath.sh"; + when(mockInterpreterRunner.getPath()).thenReturn(testInterpreterRunner); // This test only for Linux + Interpreter i = factory.createRemoteRepl("path1", "sessionKey", "className", new Properties(), interpreterSettingManager.get().get(0).getId(), "userName", false, mockInterpreterRunner); + String interpreterRunner = ((RemoteInterpreter) ((LazyOpenInterpreter) i).getInnerInterpreter()).getInterpreterRunner(); + assertNotEquals(interpreterRunner, testInterpreterRunner); + + testInterpreterRunner = "/AbsolutePath.sh"; + when(mockInterpreterRunner.getPath()).thenReturn(testInterpreterRunner); + i = factory.createRemoteRepl("path1", "sessionKey", "className", new Properties(), interpreterSettingManager.get().get(0).getId(), "userName", false, mockInterpreterRunner); + interpreterRunner = ((RemoteInterpreter) ((LazyOpenInterpreter) i).getInnerInterpreter()).getInterpreterRunner(); + assertEquals(interpreterRunner, testInterpreterRunner); + } +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java new file mode 100644 index 00000000000..1aab7572758 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java @@ -0,0 +1,327 @@ +package org.apache.zeppelin.interpreter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.junit.Test; + +import org.apache.zeppelin.dep.Dependency; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +public class InterpreterSettingTest { + + @Test + public void sharedModeCloseandRemoveInterpreterGroupTest() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.SHARED); + InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); + + interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { + @Override + public InterpreterGroup createInterpreterGroup(String interpreterGroupId, + InterpreterOption option) { + return new InterpreterGroup(interpreterGroupId); + } + }); + + Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); + List interpreterList1 = new ArrayList<>(); + interpreterList1.add(mockInterpreter1); + InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); + + // This won't effect anything + Interpreter mockInterpreter2 = mock(RemoteInterpreter.class); + List interpreterList2 = new ArrayList<>(); + interpreterList2.add(mockInterpreter2); + interpreterGroup = interpreterSetting.getInterpreterGroup("user2", "note1"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user2", "note1"), interpreterList2); + + assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); + + interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user2"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void perUserScopedModeCloseAndRemoveInterpreterGroupTest() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.SCOPED); + InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); + + interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { + @Override + public InterpreterGroup createInterpreterGroup(String interpreterGroupId, + InterpreterOption option) { + return new InterpreterGroup(interpreterGroupId); + } + }); + + Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); + List interpreterList1 = new ArrayList<>(); + interpreterList1.add(mockInterpreter1); + InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); + + Interpreter mockInterpreter2 = mock(RemoteInterpreter.class); + List interpreterList2 = new ArrayList<>(); + interpreterList2.add(mockInterpreter2); + interpreterGroup = interpreterSetting.getInterpreterGroup("user2", "note1"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user2", "note1"), interpreterList2); + + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(2, interpreterSetting.getInterpreterGroup("user1", "note1").size()); + assertEquals(2, interpreterSetting.getInterpreterGroup("user2", "note1").size()); + + interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); + assertEquals(1, interpreterSetting.getInterpreterGroup("user2","note1").size()); + + // Check if non-existed key works or not + interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); + assertEquals(1, interpreterSetting.getInterpreterGroup("user2","note1").size()); + + interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user2"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void perUserIsolatedModeCloseAndRemoveInterpreterGroupTest() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.ISOLATED); + InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); + + interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { + @Override + public InterpreterGroup createInterpreterGroup(String interpreterGroupId, + InterpreterOption option) { + return new InterpreterGroup(interpreterGroupId); + } + }); + + Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); + List interpreterList1 = new ArrayList<>(); + interpreterList1.add(mockInterpreter1); + InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); + + Interpreter mockInterpreter2 = mock(RemoteInterpreter.class); + List interpreterList2 = new ArrayList<>(); + interpreterList2.add(mockInterpreter2); + interpreterGroup = interpreterSetting.getInterpreterGroup("user2", "note1"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user2", "note1"), interpreterList2); + + assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); + assertEquals(1, interpreterSetting.getInterpreterGroup("user2", "note1").size()); + + interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); + assertEquals(1, interpreterSetting.getInterpreterGroup("user2","note1").size()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + + interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user2"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void perNoteScopedModeCloseAndRemoveInterpreterGroupTest() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerNote(InterpreterOption.SCOPED); + InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); + + interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { + @Override + public InterpreterGroup createInterpreterGroup(String interpreterGroupId, + InterpreterOption option) { + return new InterpreterGroup(interpreterGroupId); + } + }); + + Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); + List interpreterList1 = new ArrayList<>(); + interpreterList1.add(mockInterpreter1); + InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); + + Interpreter mockInterpreter2 = mock(RemoteInterpreter.class); + List interpreterList2 = new ArrayList<>(); + interpreterList2.add(mockInterpreter2); + interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note2"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note2"), interpreterList2); + + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(2, interpreterSetting.getInterpreterGroup("user1", "note1").size()); + assertEquals(2, interpreterSetting.getInterpreterGroup("user1", "note2").size()); + + interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); + assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note2").size()); + + // Check if non-existed key works or not + interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); + assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note2").size()); + + interpreterSetting.closeAndRemoveInterpreterGroup("note2", "user1"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void perNoteIsolatedModeCloseAndRemoveInterpreterGroupTest() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerNote(InterpreterOption.ISOLATED); + InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); + + interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { + @Override + public InterpreterGroup createInterpreterGroup(String interpreterGroupId, + InterpreterOption option) { + return new InterpreterGroup(interpreterGroupId); + } + }); + + Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); + List interpreterList1 = new ArrayList<>(); + interpreterList1.add(mockInterpreter1); + InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); + + Interpreter mockInterpreter2 = mock(RemoteInterpreter.class); + List interpreterList2 = new ArrayList<>(); + interpreterList2.add(mockInterpreter2); + interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note2"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note2"), interpreterList2); + + assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); + assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note2").size()); + + interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); + assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note2").size()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + + interpreterSetting.closeAndRemoveInterpreterGroup("note2", "user1"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void perNoteScopedModeRemoveInterpreterGroupWhenNoteIsRemoved() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerNote(InterpreterOption.SCOPED); + InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); + + interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { + @Override + public InterpreterGroup createInterpreterGroup(String interpreterGroupId, + InterpreterOption option) { + return new InterpreterGroup(interpreterGroupId); + } + }); + + Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); + List interpreterList1 = new ArrayList<>(); + interpreterList1.add(mockInterpreter1); + InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); + + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); + + // This method will be called when remove note + interpreterSetting.closeAndRemoveInterpreterGroup("note1",""); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + // Be careful that getInterpreterGroup makes interpreterGroup if it doesn't exist + assertEquals(0, interpreterSetting.getInterpreterGroup("user1","note1").size()); + } + + @Test + public void perNoteIsolatedModeRemoveInterpreterGroupWhenNoteIsRemoved() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerNote(InterpreterOption.ISOLATED); + InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); + + interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { + @Override + public InterpreterGroup createInterpreterGroup(String interpreterGroupId, + InterpreterOption option) { + return new InterpreterGroup(interpreterGroupId); + } + }); + + Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); + List interpreterList1 = new ArrayList<>(); + interpreterList1.add(mockInterpreter1); + InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); + + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); + + // This method will be called when remove note + interpreterSetting.closeAndRemoveInterpreterGroup("note1",""); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + // Be careful that getInterpreterGroup makes interpreterGroup if it doesn't exist + assertEquals(0, interpreterSetting.getInterpreterGroup("user1","note1").size()); + } + + @Test + public void perUserScopedModeNeverRemoveInterpreterGroupWhenNoteIsRemoved() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.SCOPED); + InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); + + interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { + @Override + public InterpreterGroup createInterpreterGroup(String interpreterGroupId, + InterpreterOption option) { + return new InterpreterGroup(interpreterGroupId); + } + }); + + Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); + List interpreterList1 = new ArrayList<>(); + interpreterList1.add(mockInterpreter1); + InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); + + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); + + // This method will be called when remove note + interpreterSetting.closeAndRemoveInterpreterGroup("note1",""); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + // Be careful that getInterpreterGroup makes interpreterGroup if it doesn't exist + assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note1").size()); + } + + @Test + public void perUserIsolatedModeNeverRemoveInterpreterGroupWhenNoteIsRemoved() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.ISOLATED); + InterpreterSetting interpreterSetting = new InterpreterSetting("", "", "", new ArrayList(), new Properties(), new ArrayList(), interpreterOption, "", null); + + interpreterSetting.setInterpreterGroupFactory(new InterpreterGroupFactory() { + @Override + public InterpreterGroup createInterpreterGroup(String interpreterGroupId, + InterpreterOption option) { + return new InterpreterGroup(interpreterGroupId); + } + }); + + Interpreter mockInterpreter1 = mock(RemoteInterpreter.class); + List interpreterList1 = new ArrayList<>(); + interpreterList1.add(mockInterpreter1); + InterpreterGroup interpreterGroup = interpreterSetting.getInterpreterGroup("user1", "note1"); + interpreterGroup.put(interpreterSetting.getInterpreterSessionKey("user1", "note1"), interpreterList1); + + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); + + // This method will be called when remove note + interpreterSetting.closeAndRemoveInterpreterGroup("note1",""); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + // Be careful that getInterpreterGroup makes interpreterGroup if it doesn't exist + assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note1").size()); + } +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/install/InstallInterpreterTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/install/InstallInterpreterTest.java similarity index 100% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/install/InstallInterpreterTest.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/install/InstallInterpreterTest.java diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java similarity index 96% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java index a533c12913e..b16e9371bf8 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java @@ -17,6 +17,11 @@ package org.apache.zeppelin.interpreter.mock; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterResult; @@ -24,14 +29,8 @@ import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -public class MockInterpreter1 extends Interpreter { - - Map vars = new HashMap<>(); +public class MockInterpreter1 extends Interpreter{ +Map vars = new HashMap<>(); public MockInterpreter1(Properties property) { super(property); diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java similarity index 91% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java index d53716f56c7..5b9e8022145 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java @@ -17,6 +17,10 @@ package org.apache.zeppelin.interpreter.mock; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; @@ -25,18 +29,12 @@ import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -public class MockInterpreter11 extends Interpreter { +public class MockInterpreter11 extends Interpreter{ Map vars = new HashMap<>(); public MockInterpreter11(Properties property) { super(property); } - boolean open; @Override @@ -55,7 +53,7 @@ public boolean isOpen() { @Override public InterpreterResult interpret(String st, InterpreterContext context) { - return new InterpreterResult(InterpreterResult.Code.SUCCESS, "repl11: " + st); + return new InterpreterResult(InterpreterResult.Code.SUCCESS, "repl11: "+st); } @Override @@ -74,12 +72,12 @@ public int getProgress(InterpreterContext context) { @Override public Scheduler getScheduler() { - return SchedulerFactory.singleton().createOrGetFIFOScheduler("test_" + this.hashCode()); + return SchedulerFactory.singleton().createOrGetFIFOScheduler("test_"+this.hashCode()); } @Override public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { + InterpreterContext interpreterContext) { return null; } -} \ No newline at end of file +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java similarity index 100% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java index f36df56b57d..7a52f7d36c7 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java @@ -17,6 +17,11 @@ package org.apache.zeppelin.interpreter.mock; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterResult; @@ -24,11 +29,6 @@ import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - public class MockInterpreter2 extends Interpreter{ Map vars = new HashMap<>(); diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java similarity index 97% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java index c9dc5c042f9..c8c64eac254 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java @@ -17,14 +17,15 @@ package org.apache.zeppelin.interpreter.remote; -import org.apache.log4j.AppenderSkeleton; -import org.apache.log4j.Level; -import org.apache.log4j.Logger; -import org.apache.log4j.spi.LoggingEvent; -import org.junit.After; -import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import java.util.ArrayList; import java.util.List; @@ -33,11 +34,14 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Mockito.*; +import org.apache.log4j.AppenderSkeleton; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.apache.log4j.spi.LoggingEvent; +import org.junit.After; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; public class AppendOutputRunnerTest { diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java similarity index 78% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java index 61e4ef06c0b..f7404e35cb0 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java @@ -17,10 +17,15 @@ package org.apache.zeppelin.interpreter.remote; -import org.apache.zeppelin.display.AngularObject; -import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.display.AngularObjectRegistryListener; -import org.apache.zeppelin.display.GUI; +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.zeppelin.display.*; import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterAngular; import org.apache.zeppelin.resource.LocalResourcePool; @@ -29,25 +34,17 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; - public class RemoteAngularObjectTest implements AngularObjectRegistryListener { private static final String INTERPRETER_SCRIPT = - System.getProperty("os.name").startsWith("Windows") ? - "../bin/interpreter.cmd" : - "../bin/interpreter.sh"; + System.getProperty("os.name").startsWith("Windows") ? + "../bin/interpreter.cmd" : + "../bin/interpreter.sh"; + private InterpreterGroup intpGroup; + private HashMap env; private RemoteInterpreter intp; private InterpreterContext context; private RemoteAngularObjectRegistry localRegistry; - private InterpreterSetting interpreterSetting; private AtomicInteger onAdd; private AtomicInteger onUpdate; @@ -59,24 +56,32 @@ public void setUp() throws Exception { onUpdate = new AtomicInteger(0); onRemove = new AtomicInteger(0); - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setRemote(true); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(MockInterpreterAngular.class.getName(), "mock", true, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - InterpreterRunner runner = new InterpreterRunner(INTERPRETER_SCRIPT, INTERPRETER_SCRIPT); - interpreterSetting = new InterpreterSetting.Builder() - .setId("test") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .setRunner(runner) - .setInterpreterDir("../interpeters/test") - .create(); - - intp = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); - localRegistry = (RemoteAngularObjectRegistry) intp.getInterpreterGroup().getAngularObjectRegistry(); + intpGroup = new InterpreterGroup("intpId"); + localRegistry = new RemoteAngularObjectRegistry("intpId", this, intpGroup); + intpGroup.setAngularObjectRegistry(localRegistry); + env = new HashMap<>(); + env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); + + Properties p = new Properties(); + + intp = new RemoteInterpreter( + p, + "note", + MockInterpreterAngular.class.getName(), + new File(INTERPRETER_SCRIPT).getAbsolutePath(), + "fake", + "fakeRepo", + env, + 10 * 1000, + null, + null, + "anonymous", + false + ); + + intpGroup.put("note", new LinkedList()); + intpGroup.get("note").add(intp); + intp.setInterpreterGroup(intpGroup); context = new InterpreterContext( "note", @@ -87,17 +92,17 @@ public void setUp() throws Exception { new AuthenticationInfo(), new HashMap(), new GUI(), - new AngularObjectRegistry(intp.getInterpreterGroup().getId(), null), + new AngularObjectRegistry(intpGroup.getId(), null), new LocalResourcePool("pool1"), new LinkedList(), null); intp.open(); - } @After public void tearDown() throws Exception { - interpreterSetting.close(); + intp.close(); + intpGroup.close(); } @Test @@ -142,7 +147,7 @@ public void testAngularObjectRemovalOnZeppelinServerSide() throws InterruptedExc Thread.sleep(500); // waitFor eventpoller pool event String[] result = ret.message().get(0).getData().split(" "); assertEquals("0", result[0]); // size of registry - + // create object ret = intp.interpret("add n1 v1", context); Thread.sleep(500); @@ -167,11 +172,11 @@ public void testAngularObjectAddOnZeppelinServerSide() throws InterruptedExcepti Thread.sleep(500); // waitFor eventpoller pool event String[] result = ret.message().get(0).getData().split(" "); assertEquals("0", result[0]); // size of registry - + // create object localRegistry.addAndNotifyRemoteProcess("n1", "v1", "note", null); - - // get from remote registry + + // get from remote registry ret = intp.interpret("get", context); Thread.sleep(500); // waitFor eventpoller pool event result = ret.message().get(0).getData().split(" "); diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPollerTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPollerTest.java similarity index 100% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPollerTest.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPollerTest.java diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java similarity index 79% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java index 1687060d6d2..3f865cb370d 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java @@ -17,18 +17,22 @@ package org.apache.zeppelin.interpreter.remote; +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.user.AuthenticationInfo; import org.apache.zeppelin.display.GUI; import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterOutputStream; -import org.apache.zeppelin.user.AuthenticationInfo; import org.junit.After; import org.junit.Before; import org.junit.Test; -import java.util.*; +import java.io.File; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Properties; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; /** @@ -39,32 +43,41 @@ public class RemoteInterpreterOutputTestStream implements RemoteInterpreterProce System.getProperty("os.name").startsWith("Windows") ? "../bin/interpreter.cmd" : "../bin/interpreter.sh"; - - private InterpreterSetting interpreterSetting; + private InterpreterGroup intpGroup; + private HashMap env; @Before public void setUp() throws Exception { - InterpreterOption interpreterOption = new InterpreterOption(); - - interpreterOption.setRemote(true); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(MockInterpreterOutputStream.class.getName(), "mock", true, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - InterpreterRunner runner = new InterpreterRunner(INTERPRETER_SCRIPT, INTERPRETER_SCRIPT); - interpreterSetting = new InterpreterSetting.Builder() - .setId("test") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .setRunner(runner) - .setInterpreterDir("../interpeters/test") - .create(); + intpGroup = new InterpreterGroup(); + intpGroup.put("note", new LinkedList()); + + env = new HashMap<>(); + env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); } @After public void tearDown() throws Exception { - interpreterSetting.close(); + intpGroup.close(); + } + + private RemoteInterpreter createMockInterpreter() { + RemoteInterpreter intp = new RemoteInterpreter( + new Properties(), + "note", + MockInterpreterOutputStream.class.getName(), + new File(INTERPRETER_SCRIPT).getAbsolutePath(), + "fake", + "fakeRepo", + env, + 10 * 1000, + this, + null, + "anonymous", + false); + + intpGroup.get("note").add(intp); + intp.setInterpreterGroup(intpGroup); + return intp; } private InterpreterContext createInterpreterContext() { @@ -77,14 +90,14 @@ private InterpreterContext createInterpreterContext() { new AuthenticationInfo(), new HashMap(), new GUI(), - null, + new AngularObjectRegistry(intpGroup.getId(), null), null, new LinkedList(), null); } @Test public void testInterpreterResultOnly() { - RemoteInterpreter intp = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); + RemoteInterpreter intp = createMockInterpreter(); InterpreterResult ret = intp.interpret("SUCCESS::staticresult", createInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, ret.code()); assertEquals("staticresult", ret.message().get(0).getData()); @@ -100,7 +113,7 @@ public void testInterpreterResultOnly() { @Test public void testInterpreterOutputStreamOnly() { - RemoteInterpreter intp = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); + RemoteInterpreter intp = createMockInterpreter(); InterpreterResult ret = intp.interpret("SUCCESS:streamresult:", createInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, ret.code()); assertEquals("streamresult", ret.message().get(0).getData()); @@ -112,7 +125,7 @@ public void testInterpreterOutputStreamOnly() { @Test public void testInterpreterResultOutputStreamMixed() { - RemoteInterpreter intp = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); + RemoteInterpreter intp = createMockInterpreter(); InterpreterResult ret = intp.interpret("SUCCESS:stream:static", createInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, ret.code()); assertEquals("stream", ret.message().get(0).getData()); @@ -121,7 +134,7 @@ public void testInterpreterResultOutputStreamMixed() { @Test public void testOutputType() { - RemoteInterpreter intp = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); + RemoteInterpreter intp = createMockInterpreter(); InterpreterResult ret = intp.interpret("SUCCESS:%html hello:", createInterpreterContext()); assertEquals(InterpreterResult.Type.HTML, ret.message().get(0).getType()); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessTest.java new file mode 100644 index 00000000000..b85d7ef2fb0 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessTest.java @@ -0,0 +1,131 @@ +/* + * 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.interpreter.remote; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.*; + +import java.util.HashMap; +import java.util.Properties; + +import org.apache.thrift.TException; +import org.apache.thrift.transport.TTransportException; +import org.apache.zeppelin.interpreter.Constants; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; +import org.junit.Test; + +public class RemoteInterpreterProcessTest { + private static final String INTERPRETER_SCRIPT = + System.getProperty("os.name").startsWith("Windows") ? + "../bin/interpreter.cmd" : + "../bin/interpreter.sh"; + private static final int DUMMY_PORT=3678; + + @Test + public void testStartStop() { + InterpreterGroup intpGroup = new InterpreterGroup(); + RemoteInterpreterManagedProcess rip = new RemoteInterpreterManagedProcess( + INTERPRETER_SCRIPT, "nonexists", "fakeRepo", new HashMap(), + 10 * 1000, null, null,"fakeName"); + assertFalse(rip.isRunning()); + assertEquals(0, rip.referenceCount()); + assertEquals(1, rip.reference(intpGroup, "anonymous", false)); + assertEquals(2, rip.reference(intpGroup, "anonymous", false)); + assertEquals(true, rip.isRunning()); + assertEquals(1, rip.dereference()); + assertEquals(true, rip.isRunning()); + assertEquals(0, rip.dereference()); + assertEquals(false, rip.isRunning()); + } + + @Test + public void testClientFactory() throws Exception { + InterpreterGroup intpGroup = new InterpreterGroup(); + RemoteInterpreterManagedProcess rip = new RemoteInterpreterManagedProcess( + INTERPRETER_SCRIPT, "nonexists", "fakeRepo", new HashMap(), + mock(RemoteInterpreterEventPoller.class), 10 * 1000, "fakeName"); + rip.reference(intpGroup, "anonymous", false); + assertEquals(0, rip.getNumActiveClient()); + assertEquals(0, rip.getNumIdleClient()); + + Client client = rip.getClient(); + assertEquals(1, rip.getNumActiveClient()); + assertEquals(0, rip.getNumIdleClient()); + + rip.releaseClient(client); + assertEquals(0, rip.getNumActiveClient()); + assertEquals(1, rip.getNumIdleClient()); + + rip.dereference(); + } + + @Test + public void testStartStopRemoteInterpreter() throws TException, InterruptedException { + RemoteInterpreterServer server = new RemoteInterpreterServer(3678); + server.start(); + boolean running = false; + long startTime = System.currentTimeMillis(); + while (System.currentTimeMillis() - startTime < 10 * 1000) { + if (server.isRunning()) { + running = true; + break; + } else { + Thread.sleep(200); + } + } + Properties properties = new Properties(); + properties.setProperty(Constants.ZEPPELIN_INTERPRETER_PORT, "3678"); + properties.setProperty(Constants.ZEPPELIN_INTERPRETER_HOST, "localhost"); + InterpreterGroup intpGroup = mock(InterpreterGroup.class); + when(intpGroup.getProperty()).thenReturn(properties); + when(intpGroup.containsKey(Constants.EXISTING_PROCESS)).thenReturn(true); + + RemoteInterpreterProcess rip = new RemoteInterpreterManagedProcess( + INTERPRETER_SCRIPT, + "nonexists", + "fakeRepo", + new HashMap(), + mock(RemoteInterpreterEventPoller.class) + , 10 * 1000, + "fakeName"); + assertFalse(rip.isRunning()); + assertEquals(0, rip.referenceCount()); + assertEquals(1, rip.reference(intpGroup, "anonymous", false)); + assertEquals(true, rip.isRunning()); + } + + + @Test + public void testPropagateError() throws TException, InterruptedException { + InterpreterGroup intpGroup = new InterpreterGroup(); + RemoteInterpreterManagedProcess rip = new RemoteInterpreterManagedProcess( + "echo hello_world", "nonexists", "fakeRepo", new HashMap(), + 10 * 1000, null, null, "fakeName"); + assertFalse(rip.isRunning()); + assertEquals(0, rip.referenceCount()); + try { + assertEquals(1, rip.reference(intpGroup, "anonymous", false)); + } catch (InterpreterException e) { + e.getMessage().contains("hello_world"); + } + assertEquals(0, rip.referenceCount()); + } +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java new file mode 100644 index 00000000000..95235e51a97 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java @@ -0,0 +1,975 @@ +/* + * 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.interpreter.remote; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.apache.thrift.transport.TTransportException; +import org.apache.zeppelin.display.AngularObject; +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterEnv; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterResultMessage; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.InterpreterResult.Code; +import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterA; +import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterB; +import org.apache.zeppelin.resource.LocalResourcePool; +import org.apache.zeppelin.scheduler.Job; +import org.apache.zeppelin.scheduler.Job.Status; +import org.apache.zeppelin.scheduler.Scheduler; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +public class RemoteInterpreterTest { + + + private static final String INTERPRETER_SCRIPT = + System.getProperty("os.name").startsWith("Windows") ? + "../bin/interpreter.cmd" : + "../bin/interpreter.sh"; + + private InterpreterGroup intpGroup; + private HashMap env; + + @Before + public void setUp() throws Exception { + intpGroup = new InterpreterGroup(); + env = new HashMap<>(); + env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); + } + + @After + public void tearDown() throws Exception { + intpGroup.close(); + } + + private RemoteInterpreter createMockInterpreterA(Properties p) { + return createMockInterpreterA(p, "note"); + } + + private RemoteInterpreter createMockInterpreterA(Properties p, String noteId) { + return new RemoteInterpreter( + p, + noteId, + MockInterpreterA.class.getName(), + new File(INTERPRETER_SCRIPT).getAbsolutePath(), + "fake", + "fakeRepo", + env, + 10 * 1000, + null, + null, + "anonymous", + false); + } + + private RemoteInterpreter createMockInterpreterB(Properties p) { + return createMockInterpreterB(p, "note"); + } + + private RemoteInterpreter createMockInterpreterB(Properties p, String noteId) { + return new RemoteInterpreter( + p, + noteId, + MockInterpreterB.class.getName(), + new File(INTERPRETER_SCRIPT).getAbsolutePath(), + "fake", + "fakeRepo", + env, + 10 * 1000, + null, + null, + "anonymous", + false); + } + + @Test + public void testRemoteInterperterCall() throws TTransportException, IOException { + Properties p = new Properties(); + intpGroup.put("note", new LinkedList()); + + RemoteInterpreter intpA = createMockInterpreterA(p); + + intpGroup.get("note").add(intpA); + + intpA.setInterpreterGroup(intpGroup); + + RemoteInterpreter intpB = createMockInterpreterB(p); + + intpGroup.get("note").add(intpB); + intpB.setInterpreterGroup(intpGroup); + + + RemoteInterpreterProcess process = intpA.getInterpreterProcess(); + process.equals(intpB.getInterpreterProcess()); + + assertFalse(process.isRunning()); + assertEquals(0, process.getNumIdleClient()); + assertEquals(0, process.referenceCount()); + + intpA.open(); // initializa all interpreters in the same group + assertTrue(process.isRunning()); + assertEquals(1, process.getNumIdleClient()); + assertEquals(1, process.referenceCount()); + + intpA.interpret("1", + new InterpreterContext( + "note", + "id", + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null)); + + intpB.open(); + assertEquals(1, process.referenceCount()); + + intpA.close(); + assertEquals(0, process.referenceCount()); + intpB.close(); + assertEquals(0, process.referenceCount()); + + assertFalse(process.isRunning()); + + } + + @Test + public void testExecuteIncorrectPrecode() throws TTransportException, IOException { + Properties p = new Properties(); + p.put("zeppelin.MockInterpreterA.precode", "fail test"); + intpGroup.put("note", new LinkedList()); + + RemoteInterpreter intpA = createMockInterpreterA(p); + + intpGroup.get("note").add(intpA); + + intpA.setInterpreterGroup(intpGroup); + + RemoteInterpreterProcess process = intpA.getInterpreterProcess(); + + intpA.open(); + + InterpreterResult result = intpA.interpret("1", + new InterpreterContext( + "note", + "id", + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null)); + + + + intpA.close(); + assertEquals(Code.ERROR, result.code()); + } + + @Test + public void testExecuteCorrectPrecode() throws TTransportException, IOException { + Properties p = new Properties(); + p.put("zeppelin.MockInterpreterA.precode", "2"); + intpGroup.put("note", new LinkedList()); + + RemoteInterpreter intpA = createMockInterpreterA(p); + + intpGroup.get("note").add(intpA); + + intpA.setInterpreterGroup(intpGroup); + + RemoteInterpreterProcess process = intpA.getInterpreterProcess(); + + intpA.open(); + + InterpreterResult result = intpA.interpret("1", + new InterpreterContext( + "note", + "id", + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null)); + + + + intpA.close(); + assertEquals(Code.SUCCESS, result.code()); + assertEquals("1", result.message().get(0).getData()); + } + + @Test + public void testRemoteInterperterErrorStatus() throws TTransportException, IOException { + Properties p = new Properties(); + + RemoteInterpreter intpA = createMockInterpreterA(p); + + intpGroup.put("note", new LinkedList()); + intpGroup.get("note").add(intpA); + intpA.setInterpreterGroup(intpGroup); + + intpA.open(); + InterpreterResult ret = intpA.interpret("non numeric value", + new InterpreterContext( + "noteId", + "id", + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null)); + + assertEquals(Code.ERROR, ret.code()); + } + + @Test + public void testRemoteSchedulerSharing() throws TTransportException, IOException { + Properties p = new Properties(); + intpGroup.put("note", new LinkedList()); + + RemoteInterpreter intpA = new RemoteInterpreter( + p, + "note", + MockInterpreterA.class.getName(), + new File(INTERPRETER_SCRIPT).getAbsolutePath(), + "fake", + "fakeRepo", + env, + 10 * 1000, + null, + null, + "anonymous", + false); + + intpGroup.get("note").add(intpA); + intpA.setInterpreterGroup(intpGroup); + + RemoteInterpreter intpB = new RemoteInterpreter( + p, + "note", + MockInterpreterB.class.getName(), + new File(INTERPRETER_SCRIPT).getAbsolutePath(), + "fake", + "fakeRepo", + env, + 10 * 1000, + null, + null, + "anonymous", + false); + + intpGroup.get("note").add(intpB); + intpB.setInterpreterGroup(intpGroup); + + intpA.open(); + intpB.open(); + + long start = System.currentTimeMillis(); + InterpreterResult ret = intpA.interpret("500", + new InterpreterContext( + "note", + "id", + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null)); + assertEquals("500", ret.message().get(0).getData()); + + ret = intpB.interpret("500", + new InterpreterContext( + "note", + "id", + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null)); + assertEquals("1000", ret.message().get(0).getData()); + long end = System.currentTimeMillis(); + assertTrue(end - start >= 1000); + + + intpA.close(); + intpB.close(); + } + + @Test + public void testRemoteSchedulerSharingSubmit() throws TTransportException, IOException, InterruptedException { + Properties p = new Properties(); + intpGroup.put("note", new LinkedList()); + + final RemoteInterpreter intpA = createMockInterpreterA(p); + + intpGroup.get("note").add(intpA); + intpA.setInterpreterGroup(intpGroup); + + final RemoteInterpreter intpB = createMockInterpreterB(p); + + intpGroup.get("note").add(intpB); + intpB.setInterpreterGroup(intpGroup); + + intpA.open(); + intpB.open(); + + long start = System.currentTimeMillis(); + Job jobA = new Job("jobA", null) { + private Object r; + + @Override + public Object getReturn() { + return r; + } + + @Override + public void setResult(Object results) { + this.r = results; + } + + @Override + public int progress() { + return 0; + } + + @Override + public Map info() { + return null; + } + + @Override + protected Object jobRun() throws Throwable { + return intpA.interpret("500", + new InterpreterContext( + "note", + "jobA", + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null)); + } + + @Override + protected boolean jobAbort() { + return false; + } + + }; + intpA.getScheduler().submit(jobA); + + Job jobB = new Job("jobB", null) { + + private Object r; + + @Override + public Object getReturn() { + return r; + } + + @Override + public void setResult(Object results) { + this.r = results; + } + + @Override + public int progress() { + return 0; + } + + @Override + public Map info() { + return null; + } + + @Override + protected Object jobRun() throws Throwable { + return intpB.interpret("500", + new InterpreterContext( + "note", + "jobB", + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null)); + } + + @Override + protected boolean jobAbort() { + return false; + } + + }; + intpB.getScheduler().submit(jobB); + // wait until both job finished + while (jobA.getStatus() != Status.FINISHED || + jobB.getStatus() != Status.FINISHED) { + Thread.sleep(100); + } + long end = System.currentTimeMillis(); + assertTrue(end - start >= 1000); + + assertEquals("1000", ((InterpreterResult) jobB.getReturn()).message().get(0).getData()); + + intpA.close(); + intpB.close(); + } + + @Test + public void testRunOrderPreserved() throws InterruptedException { + Properties p = new Properties(); + intpGroup.put("note", new LinkedList()); + + final RemoteInterpreter intpA = createMockInterpreterA(p); + + intpGroup.get("note").add(intpA); + intpA.setInterpreterGroup(intpGroup); + + intpA.open(); + + int concurrency = 3; + final List results = new LinkedList<>(); + + Scheduler scheduler = intpA.getScheduler(); + for (int i = 0; i < concurrency; i++) { + final String jobId = Integer.toString(i); + scheduler.submit(new Job(jobId, Integer.toString(i), null, 200) { + private Object r; + + @Override + public Object getReturn() { + return r; + } + + @Override + public void setResult(Object results) { + this.r = results; + } + + @Override + public int progress() { + return 0; + } + + @Override + public Map info() { + return null; + } + + @Override + protected Object jobRun() throws Throwable { + InterpreterResult ret = intpA.interpret(getJobName(), new InterpreterContext( + "note", + jobId, + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null)); + + synchronized (results) { + results.addAll(ret.message()); + results.notify(); + } + return null; + } + + @Override + protected boolean jobAbort() { + return false; + } + + }); + } + + // wait for job finished + synchronized (results) { + while (results.size() != concurrency) { + results.wait(300); + } + } + + int i = 0; + for (InterpreterResultMessage result : results) { + assertEquals(Integer.toString(i++), result.getData()); + } + assertEquals(concurrency, i); + + intpA.close(); + } + + + @Test + public void testRunParallel() throws InterruptedException { + Properties p = new Properties(); + p.put("parallel", "true"); + intpGroup.put("note", new LinkedList()); + + final RemoteInterpreter intpA = createMockInterpreterA(p); + + intpGroup.get("note").add(intpA); + intpA.setInterpreterGroup(intpGroup); + + intpA.open(); + + int concurrency = 4; + final int timeToSleep = 1000; + final List results = new LinkedList<>(); + long start = System.currentTimeMillis(); + + Scheduler scheduler = intpA.getScheduler(); + for (int i = 0; i < concurrency; i++) { + final String jobId = Integer.toString(i); + scheduler.submit(new Job(jobId, Integer.toString(i), null, 300) { + private Object r; + + @Override + public Object getReturn() { + return r; + } + + @Override + public void setResult(Object results) { + this.r = results; + } + + @Override + public int progress() { + return 0; + } + + @Override + public Map info() { + return null; + } + + @Override + protected Object jobRun() throws Throwable { + String stmt = Integer.toString(timeToSleep); + InterpreterResult ret = intpA.interpret(stmt, new InterpreterContext( + "note", + jobId, + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null)); + + synchronized (results) { + results.addAll(ret.message()); + results.notify(); + } + return stmt; + } + + @Override + protected boolean jobAbort() { + return false; + } + + }); + } + + // wait for job finished + synchronized (results) { + while (results.size() != concurrency) { + results.wait(300); + } + } + + long end = System.currentTimeMillis(); + + assertTrue(end - start < timeToSleep * concurrency); + + intpA.close(); + } + + @Test + public void testInterpreterGroupResetBeforeProcessStarts() { + Properties p = new Properties(); + + RemoteInterpreter intpA = createMockInterpreterA(p); + + intpA.setInterpreterGroup(intpGroup); + RemoteInterpreterProcess processA = intpA.getInterpreterProcess(); + + intpA.setInterpreterGroup(new InterpreterGroup(intpA.getInterpreterGroup().getId())); + RemoteInterpreterProcess processB = intpA.getInterpreterProcess(); + + assertNotSame(processA.hashCode(), processB.hashCode()); + } + + @Test + public void testInterpreterGroupResetAfterProcessFinished() { + Properties p = new Properties(); + intpGroup.put("note", new LinkedList()); + + RemoteInterpreter intpA = createMockInterpreterA(p); + + intpA.setInterpreterGroup(intpGroup); + RemoteInterpreterProcess processA = intpA.getInterpreterProcess(); + intpA.open(); + + processA.dereference(); // intpA.close(); + + intpA.setInterpreterGroup(new InterpreterGroup(intpA.getInterpreterGroup().getId())); + RemoteInterpreterProcess processB = intpA.getInterpreterProcess(); + + assertNotSame(processA.hashCode(), processB.hashCode()); + } + + @Test + public void testInterpreterGroupResetDuringProcessRunning() throws InterruptedException { + Properties p = new Properties(); + intpGroup.put("note", new LinkedList()); + + final RemoteInterpreter intpA = createMockInterpreterA(p); + + intpGroup.get("note").add(intpA); + intpA.setInterpreterGroup(intpGroup); + + intpA.open(); + + Job jobA = new Job("jobA", null) { + private Object r; + + @Override + public Object getReturn() { + return r; + } + + @Override + public void setResult(Object results) { + this.r = results; + } + + @Override + public int progress() { + return 0; + } + + @Override + public Map info() { + return null; + } + + @Override + protected Object jobRun() throws Throwable { + return intpA.interpret("2000", + new InterpreterContext( + "note", + "jobA", + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null)); + } + + @Override + protected boolean jobAbort() { + return false; + } + + }; + intpA.getScheduler().submit(jobA); + + // wait for job started + while (intpA.getScheduler().getJobsRunning().size() == 0) { + Thread.sleep(100); + } + + // restart interpreter + RemoteInterpreterProcess processA = intpA.getInterpreterProcess(); + intpA.close(); + + InterpreterGroup newInterpreterGroup = + new InterpreterGroup(intpA.getInterpreterGroup().getId()); + newInterpreterGroup.put("note", new LinkedList()); + + intpA.setInterpreterGroup(newInterpreterGroup); + intpA.open(); + RemoteInterpreterProcess processB = intpA.getInterpreterProcess(); + + assertNotSame(processA.hashCode(), processB.hashCode()); + + } + + @Test + public void testRemoteInterpreterSharesTheSameSchedulerInstanceInTheSameGroup() { + Properties p = new Properties(); + intpGroup.put("note", new LinkedList()); + + RemoteInterpreter intpA = createMockInterpreterA(p); + + intpGroup.get("note").add(intpA); + intpA.setInterpreterGroup(intpGroup); + + RemoteInterpreter intpB = createMockInterpreterB(p); + + intpGroup.get("note").add(intpB); + intpB.setInterpreterGroup(intpGroup); + + intpA.open(); + intpB.open(); + + assertEquals(intpA.getScheduler(), intpB.getScheduler()); + } + + @Test + public void testMultiInterpreterSession() { + Properties p = new Properties(); + intpGroup.put("sessionA", new LinkedList()); + intpGroup.put("sessionB", new LinkedList()); + + RemoteInterpreter intpAsessionA = createMockInterpreterA(p, "sessionA"); + intpGroup.get("sessionA").add(intpAsessionA); + intpAsessionA.setInterpreterGroup(intpGroup); + + RemoteInterpreter intpBsessionA = createMockInterpreterB(p, "sessionA"); + intpGroup.get("sessionA").add(intpBsessionA); + intpBsessionA.setInterpreterGroup(intpGroup); + + intpAsessionA.open(); + intpBsessionA.open(); + + assertEquals(intpAsessionA.getScheduler(), intpBsessionA.getScheduler()); + + RemoteInterpreter intpAsessionB = createMockInterpreterA(p, "sessionB"); + intpGroup.get("sessionB").add(intpAsessionB); + intpAsessionB.setInterpreterGroup(intpGroup); + + RemoteInterpreter intpBsessionB = createMockInterpreterB(p, "sessionB"); + intpGroup.get("sessionB").add(intpBsessionB); + intpBsessionB.setInterpreterGroup(intpGroup); + + intpAsessionB.open(); + intpBsessionB.open(); + + assertEquals(intpAsessionB.getScheduler(), intpBsessionB.getScheduler()); + assertNotEquals(intpAsessionA.getScheduler(), intpAsessionB.getScheduler()); + } + + @Test + public void should_push_local_angular_repo_to_remote() throws Exception { + //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, + null, "anonymous", false); + final AngularObjectRegistry registry = new AngularObjectRegistry("spark", null); + registry.add("name", "DuyHai DOAN", "nodeId", "paragraphId"); + final InterpreterGroup interpreterGroup = new InterpreterGroup("groupId"); + interpreterGroup.setAngularObjectRegistry(registry); + intr.setInterpreterGroup(interpreterGroup); + + final java.lang.reflect.Type registryType = new TypeToken>>() {}.getType(); + final Gson gson = new Gson(); + final String expected = gson.toJson(registry.getRegistry(), registryType); + + //When + intr.pushAngularObjectRegistryToRemote(client); + + //Then + Mockito.verify(client).angularRegistryPush(expected); + } + + @Test + public void testEnvStringPattern() { + assertFalse(RemoteInterpreterUtils.isEnvString(null)); + assertFalse(RemoteInterpreterUtils.isEnvString("")); + assertFalse(RemoteInterpreterUtils.isEnvString("abcDEF")); + assertFalse(RemoteInterpreterUtils.isEnvString("ABC-DEF")); + assertTrue(RemoteInterpreterUtils.isEnvString("ABCDEF")); + assertTrue(RemoteInterpreterUtils.isEnvString("ABC_DEF")); + assertTrue(RemoteInterpreterUtils.isEnvString("ABC_DEF123")); + } + + @Test + public void testEnvronmentAndPropertySet() { + Properties p = new Properties(); + p.setProperty("MY_ENV1", "env value 1"); + p.setProperty("my.property.1", "property value 1"); + + RemoteInterpreter intp = new RemoteInterpreter( + p, + "note", + MockInterpreterEnv.class.getName(), + new File(INTERPRETER_SCRIPT).getAbsolutePath(), + "fake", + "fakeRepo", + env, + 10 * 1000, + null, + null, + "anonymous", + false); + + intpGroup.put("note", new LinkedList()); + intpGroup.get("note").add(intp); + intp.setInterpreterGroup(intpGroup); + + intp.open(); + + InterpreterContext context = new InterpreterContext( + "noteId", + "id", + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null); + + + assertEquals("env value 1", intp.interpret("getEnv MY_ENV1", context).message().get(0).getData()); + assertEquals(Code.ERROR, intp.interpret("getProperty MY_ENV1", context).code()); + assertEquals(Code.ERROR, intp.interpret("getEnv my.property.1", context).code()); + assertEquals("property value 1", intp.interpret("getProperty my.property.1", context).message().get(0).getData()); + + intp.close(); + } + + @Test + public void testSetProgress() throws InterruptedException { + // given MockInterpreterA set progress through InterpreterContext + Properties p = new Properties(); + p.setProperty("progress", "50"); + final RemoteInterpreter intpA = createMockInterpreterA(p); + + intpGroup.put("note", new LinkedList()); + intpGroup.get("note").add(intpA); + intpA.setInterpreterGroup(intpGroup); + + intpA.open(); + + final InterpreterContext context1 = new InterpreterContext( + "noteId", + "id1", + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null); + + InterpreterContext context2 = new InterpreterContext( + "noteId", + "id2", + null, + "title", + "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("pool1"), + new LinkedList(), null); + + + assertEquals(0, intpA.getProgress(context1)); + assertEquals(0, intpA.getProgress(context2)); + + // when interpreter update progress through InterpreterContext + Thread t = new Thread() { + public void run() { + InterpreterResult ret = intpA.interpret("1000", context1); + } + }; + t.start(); + + // then progress need to be updated in given context + while(intpA.getProgress(context1) == 0) Thread.yield(); + assertEquals(50, intpA.getProgress(context1)); + assertEquals(0, intpA.getProgress(context2)); + + t.join(); + assertEquals(0, intpA.getProgress(context1)); + assertEquals(0, intpA.getProgress(context2)); + } + +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java similarity index 94% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java index 5f7426a5fbb..975d6ea3c76 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java @@ -17,11 +17,12 @@ package org.apache.zeppelin.interpreter.remote; -import org.junit.Test; +import static org.junit.Assert.assertTrue; import java.io.IOException; -import static org.junit.Assert.assertTrue; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterUtils; +import org.junit.Test; public class RemoteInterpreterUtilsTest { diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java similarity index 97% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java index 5a3e57c06f5..50d988875bb 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java @@ -17,18 +17,19 @@ package org.apache.zeppelin.interpreter.remote.mock; +import java.util.List; +import java.util.Properties; + import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterPropertyBuilder; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResult.Code; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; -import java.util.List; -import java.util.Properties; - public class MockInterpreterA extends Interpreter { private String lastSt; diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java similarity index 98% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java index ec89241136e..d4b26ad186d 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterAngular.java @@ -17,18 +17,19 @@ package org.apache.zeppelin.interpreter.remote.mock; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.AngularObjectWatcher; 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.thrift.InterpreterCompletion; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; - public class MockInterpreterAngular extends Interpreter { AtomicInteger numWatch = new AtomicInteger(0); diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java similarity index 89% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java index ff3ff9f0872..7103335ac33 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java @@ -17,14 +17,20 @@ package org.apache.zeppelin.interpreter.remote.mock; -import org.apache.zeppelin.interpreter.*; +import java.util.List; +import java.util.Properties; + +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterPropertyBuilder; +import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResult.Code; +import org.apache.zeppelin.interpreter.WrappedInterpreter; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.scheduler.Scheduler; -import java.util.List; -import java.util.Properties; - public class MockInterpreterB extends Interpreter { public MockInterpreterB(Properties property) { diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/GetEnvPropertyInterpreter.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterEnv.java similarity index 83% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/GetEnvPropertyInterpreter.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterEnv.java index a039a59861b..12e11f77b20 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/GetEnvPropertyInterpreter.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterEnv.java @@ -16,9 +16,7 @@ */ package org.apache.zeppelin.interpreter.remote.mock; -import org.apache.zeppelin.interpreter.Interpreter; -import org.apache.zeppelin.interpreter.InterpreterContext; -import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; @@ -27,9 +25,9 @@ import java.util.Properties; -public class GetEnvPropertyInterpreter extends Interpreter { +public class MockInterpreterEnv extends Interpreter { - public GetEnvPropertyInterpreter(Properties property) { + public MockInterpreterEnv(Properties property) { super(property); } @@ -45,9 +43,9 @@ public void close() { public InterpreterResult interpret(String st, InterpreterContext context) { String[] cmd = st.split(" "); if (cmd[0].equals("getEnv")) { - return new InterpreterResult(InterpreterResult.Code.SUCCESS, System.getenv(cmd[1]) == null ? "null" : System.getenv(cmd[1])); + return new InterpreterResult(InterpreterResult.Code.SUCCESS, System.getenv(cmd[1])); } else if (cmd[0].equals("getProperty")){ - return new InterpreterResult(InterpreterResult.Code.SUCCESS, System.getProperty(cmd[1]) == null ? "null" : System.getProperty(cmd[1])); + return new InterpreterResult(InterpreterResult.Code.SUCCESS, System.getProperty(cmd[1])); } else { return new InterpreterResult(InterpreterResult.Code.ERROR, cmd[0]); } diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java similarity index 91% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java index 1890cbc94e5..349315c8ed1 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterOutputStream.java @@ -16,10 +16,7 @@ */ package org.apache.zeppelin.interpreter.remote.mock; -import org.apache.zeppelin.interpreter.Interpreter; -import org.apache.zeppelin.interpreter.InterpreterContext; -import org.apache.zeppelin.interpreter.InterpreterException; -import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java similarity index 95% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java index ee9f15cb787..c4ff6abf6f4 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterResourcePool.java @@ -17,19 +17,22 @@ package org.apache.zeppelin.interpreter.remote.mock; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + import com.google.gson.Gson; +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.display.AngularObjectWatcher; 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.thrift.InterpreterCompletion; import org.apache.zeppelin.resource.Resource; import org.apache.zeppelin.resource.ResourcePool; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; - public class MockInterpreterResourcePool extends Interpreter { AtomicInteger numWatch = new AtomicInteger(0); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteInterpreterLoaderTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteInterpreterLoaderTest.java new file mode 100644 index 00000000000..56325136309 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteInterpreterLoaderTest.java @@ -0,0 +1,243 @@ +/* + * 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.notebook; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; +import org.apache.zeppelin.dep.Dependency; +import org.apache.zeppelin.dep.DependencyResolver; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterFactory; +import org.apache.zeppelin.interpreter.InterpreterInfo; +import org.apache.zeppelin.interpreter.InterpreterOption; +import org.apache.zeppelin.interpreter.DefaultInterpreterProperty; +import org.apache.zeppelin.interpreter.InterpreterProperty; +import org.apache.zeppelin.interpreter.InterpreterSettingManager; +import org.apache.zeppelin.interpreter.LazyOpenInterpreter; +import org.apache.zeppelin.interpreter.mock.MockInterpreter1; +import org.apache.zeppelin.interpreter.mock.MockInterpreter11; +import org.apache.zeppelin.interpreter.mock.MockInterpreter2; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class NoteInterpreterLoaderTest { + + private File tmpDir; + private ZeppelinConfiguration conf; + private InterpreterFactory factory; + private InterpreterSettingManager interpreterSettingManager; + private DependencyResolver depResolver; + + @Before + public void setUp() throws Exception { + tmpDir = new File(System.getProperty("java.io.tmpdir")+"/ZeppelinLTest_"+System.currentTimeMillis()); + tmpDir.mkdirs(); + new File(tmpDir, "conf").mkdirs(); + + System.setProperty(ConfVars.ZEPPELIN_HOME.getVarName(), tmpDir.getAbsolutePath()); + + conf = ZeppelinConfiguration.create(); + + depResolver = new DependencyResolver(tmpDir.getAbsolutePath() + "/local-repo"); + interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); + factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); + + ArrayList interpreterInfos = new ArrayList<>(); + interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, Maps.newHashMap())); + interpreterInfos.add(new InterpreterInfo(MockInterpreter11.class.getName(), "mock11", false, Maps.newHashMap())); + ArrayList interpreterInfos2 = new ArrayList<>(); + interpreterInfos2.add(new InterpreterInfo(MockInterpreter2.class.getName(), "mock2", true, Maps.newHashMap())); + + interpreterSettingManager.add("group1", interpreterInfos, Lists.newArrayList(), new InterpreterOption(), Maps.newHashMap(), "mock", null); + interpreterSettingManager.add("group2", interpreterInfos2, Lists.newArrayList(), new InterpreterOption(), Maps.newHashMap(), "mock", null); + + interpreterSettingManager.createNewSetting("group1", "group1", Lists.newArrayList(), new InterpreterOption(), new HashMap()); + interpreterSettingManager.createNewSetting("group2", "group2", Lists.newArrayList(), new InterpreterOption(), new HashMap()); + + + } + + @After + public void tearDown() throws Exception { + delete(tmpDir); + Interpreter.registeredInterpreters.clear(); + } + + @Test + public void testGetInterpreter() throws IOException { + interpreterSettingManager.setInterpreters("user", "note", interpreterSettingManager.getDefaultInterpreterSettingList()); + + // when there're no interpreter selection directive + assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter1", factory.getInterpreter("user", "note", null).getClassName()); + assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter1", factory.getInterpreter("user", "note", "").getClassName()); + assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter1", factory.getInterpreter("user", "note", " ").getClassName()); + + // when group name is omitted + assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter11", factory.getInterpreter("user", "note", "mock11").getClassName()); + + // when 'name' is ommitted + assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter1", factory.getInterpreter("user", "note", "group1").getClassName()); + assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter2", factory.getInterpreter("user", "note", "group2").getClassName()); + + // when nothing is ommitted + assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter1", factory.getInterpreter("user", "note", "group1.mock1").getClassName()); + assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter11", factory.getInterpreter("user", "note", "group1.mock11").getClassName()); + assertEquals("org.apache.zeppelin.interpreter.mock.MockInterpreter2", factory.getInterpreter("user", "note", "group2.mock2").getClassName()); + + interpreterSettingManager.closeNote("user", "note"); + } + + @Test + public void testNoteSession() throws IOException { + interpreterSettingManager.setInterpreters("user", "noteA", interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.getInterpreterSettings("noteA").get(0).getOption().setPerNote(InterpreterOption.SCOPED); + + interpreterSettingManager.setInterpreters("user", "noteB", interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.getInterpreterSettings("noteB").get(0).getOption().setPerNote(InterpreterOption.SCOPED); + + // interpreters are not created before accessing it + assertNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "noteA").get("noteA")); + assertNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "noteB").get("noteB")); + + factory.getInterpreter("user", "noteA", null).open(); + factory.getInterpreter("user", "noteB", null).open(); + + assertTrue( + factory.getInterpreter("user", "noteA", null).getInterpreterGroup().getId().equals( + factory.getInterpreter("user", "noteB", null).getInterpreterGroup().getId())); + + // interpreters are created after accessing it + assertNotNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "noteA").get("noteA")); + assertNotNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "noteB").get("noteB")); + + // invalid close + interpreterSettingManager.closeNote("user", "note"); + assertNotNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "shared_process").get("noteA")); + assertNotNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "shared_process").get("noteB")); + + // when + interpreterSettingManager.closeNote("user", "noteA"); + interpreterSettingManager.closeNote("user", "noteB"); + + // interpreters are destroyed after close + assertNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "shared_process").get("noteA")); + assertNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "shared_process").get("noteB")); + + } + + @Test + public void testNotePerInterpreterProcess() throws IOException { + interpreterSettingManager.setInterpreters("user", "noteA", interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.getInterpreterSettings("noteA").get(0).getOption().setPerNote(InterpreterOption.ISOLATED); + + interpreterSettingManager.setInterpreters("user", "noteB", interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.getInterpreterSettings("noteB").get(0).getOption().setPerNote(InterpreterOption.ISOLATED); + + // interpreters are not created before accessing it + assertNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "noteA").get("shared_session")); + assertNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "noteB").get("shared_session")); + + factory.getInterpreter("user", "noteA", null).open(); + factory.getInterpreter("user", "noteB", null).open(); + + // per note interpreter process + assertFalse( + factory.getInterpreter("user", "noteA", null).getInterpreterGroup().getId().equals( + factory.getInterpreter("user", "noteB", null).getInterpreterGroup().getId())); + + // interpreters are created after accessing it + assertNotNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "noteA").get("shared_session")); + assertNotNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "noteB").get("shared_session")); + + // when + interpreterSettingManager.closeNote("user", "noteA"); + interpreterSettingManager.closeNote("user", "noteB"); + + // interpreters are destroyed after close + assertNull(interpreterSettingManager.getInterpreterSettings("noteA").get(0).getInterpreterGroup("user", "noteA").get("shared_session")); + assertNull(interpreterSettingManager.getInterpreterSettings("noteB").get(0).getInterpreterGroup("user", "noteB").get("shared_session")); + } + + @Test + public void testNoteInterpreterCloseForAll() throws IOException { + interpreterSettingManager.setInterpreters("user", "FitstNote", interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.getInterpreterSettings("FitstNote").get(0).getOption().setPerNote(InterpreterOption.SCOPED); + + interpreterSettingManager.setInterpreters("user", "yourFirstNote", interpreterSettingManager.getDefaultInterpreterSettingList()); + interpreterSettingManager.getInterpreterSettings("yourFirstNote").get(0).getOption().setPerNote(InterpreterOption.ISOLATED); + + // interpreters are not created before accessing it + assertNull(interpreterSettingManager.getInterpreterSettings("FitstNote").get(0).getInterpreterGroup("user", "FitstNote").get("FitstNote")); + assertNull(interpreterSettingManager.getInterpreterSettings("yourFirstNote").get(0).getInterpreterGroup("user", "yourFirstNote").get("yourFirstNote")); + + Interpreter firstNoteIntp = factory.getInterpreter("user", "FitstNote", "group1.mock1"); + Interpreter yourFirstNoteIntp = factory.getInterpreter("user", "yourFirstNote", "group1.mock1"); + + firstNoteIntp.open(); + yourFirstNoteIntp.open(); + + assertTrue(((LazyOpenInterpreter)firstNoteIntp).isOpen()); + assertTrue(((LazyOpenInterpreter)yourFirstNoteIntp).isOpen()); + + interpreterSettingManager.closeNote("user", "FitstNote"); + + assertFalse(((LazyOpenInterpreter)firstNoteIntp).isOpen()); + assertTrue(((LazyOpenInterpreter)yourFirstNoteIntp).isOpen()); + + //reopen + firstNoteIntp.open(); + + assertTrue(((LazyOpenInterpreter)firstNoteIntp).isOpen()); + assertTrue(((LazyOpenInterpreter)yourFirstNoteIntp).isOpen()); + + // invalid check + interpreterSettingManager.closeNote("invalid", "Note"); + + assertTrue(((LazyOpenInterpreter)firstNoteIntp).isOpen()); + assertTrue(((LazyOpenInterpreter)yourFirstNoteIntp).isOpen()); + + // invalid contains value check + interpreterSettingManager.closeNote("u", "Note"); + + assertTrue(((LazyOpenInterpreter)firstNoteIntp).isOpen()); + assertTrue(((LazyOpenInterpreter)yourFirstNoteIntp).isOpen()); + } + + + private void delete(File file){ + if(file.isFile()) file.delete(); + else if(file.isDirectory()){ + File [] files = file.listFiles(); + if(files!=null && files.length>0){ + for(File f : files){ + delete(f); + } + } + file.delete(); + } + } +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java index 603be2e007e..e1a20b543b1 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java @@ -17,27 +17,31 @@ package org.apache.zeppelin.notebook; +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; + +import com.google.common.collect.Maps; +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + import com.google.common.collect.Sets; import org.apache.commons.io.FileUtils; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; +import org.apache.zeppelin.dep.Dependency; +import org.apache.zeppelin.dep.DependencyResolver; import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.interpreter.AbstractInterpreterTest; -import org.apache.zeppelin.interpreter.ClassloaderInterpreter; -import org.apache.zeppelin.interpreter.InterpreterException; -import org.apache.zeppelin.interpreter.InterpreterFactory; -import org.apache.zeppelin.interpreter.InterpreterGroup; -import org.apache.zeppelin.interpreter.InterpreterOption; -import org.apache.zeppelin.interpreter.InterpreterResult; -import org.apache.zeppelin.interpreter.InterpreterResultMessage; -import org.apache.zeppelin.interpreter.InterpreterSetting; -import org.apache.zeppelin.interpreter.LazyOpenInterpreter; +import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.mock.MockInterpreter1; import org.apache.zeppelin.interpreter.mock.MockInterpreter2; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; import org.apache.zeppelin.notebook.repo.NotebookRepo; import org.apache.zeppelin.notebook.repo.VFSNotebookRepo; import org.apache.zeppelin.resource.LocalResourcePool; +import org.apache.zeppelin.resource.ResourcePoolUtils; import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.scheduler.Job.Status; import org.apache.zeppelin.scheduler.SchedulerFactory; @@ -52,35 +56,18 @@ import org.slf4j.LoggerFactory; import org.sonatype.aether.RepositoryException; -import java.io.File; -import java.io.IOException; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; - -public class NotebookTest extends AbstractInterpreterTest implements JobListenerFactory { +public class NotebookTest implements JobListenerFactory{ private static final Logger logger = LoggerFactory.getLogger(NotebookTest.class); + private File tmpDir; + private ZeppelinConfiguration conf; private SchedulerFactory schedulerFactory; + private File notebookDir; private Notebook notebook; private NotebookRepo notebookRepo; + private InterpreterFactory factory; + private InterpreterSettingManager interpreterSettingManager; + private DependencyResolver depResolver; private NotebookAuthorization notebookAuthorization; private Credentials credentials; private AuthenticationInfo anonymous = AuthenticationInfo.ANONYMOUS; @@ -88,30 +75,57 @@ public class NotebookTest extends AbstractInterpreterTest implements JobListener @Before public void setUp() throws Exception { - System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_PUBLIC.getVarName(), "true"); - System.setProperty(ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER.getVarName(), "mock1,mock2"); - super.setUp(); - schedulerFactory = SchedulerFactory.singleton(); + tmpDir = new File(System.getProperty("java.io.tmpdir")+"/ZeppelinLTest_"+System.currentTimeMillis()); + tmpDir.mkdirs(); + new File(tmpDir, "conf").mkdirs(); + notebookDir = new File(tmpDir + "/notebook"); + notebookDir.mkdirs(); + + System.setProperty(ConfVars.ZEPPELIN_CONF_DIR.getVarName(), tmpDir.toString() + "/conf"); + System.setProperty(ConfVars.ZEPPELIN_HOME.getVarName(), tmpDir.getAbsolutePath()); + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), notebookDir.getAbsolutePath()); + + conf = ZeppelinConfiguration.create(); + + this.schedulerFactory = new SchedulerFactory(); + + depResolver = new DependencyResolver(tmpDir.getAbsolutePath() + "/local-repo"); + interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(false)); + factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); + + ArrayList interpreterInfos = new ArrayList<>(); + interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); + interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), + Maps.newHashMap(), "mock1", null); + interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(), new HashMap()); + + ArrayList interpreterInfos2 = new ArrayList<>(); + interpreterInfos2.add(new InterpreterInfo(MockInterpreter2.class.getName(), "mock2", true, new HashMap())); + interpreterSettingManager.add("mock2", interpreterInfos2, new ArrayList(), new InterpreterOption(), + Maps.newHashMap(), "mock2", null); + interpreterSettingManager.createNewSetting("mock2", "mock2", new ArrayList(), new InterpreterOption(), new HashMap()); + SearchService search = mock(SearchService.class); notebookRepo = new VFSNotebookRepo(conf); notebookAuthorization = NotebookAuthorization.init(conf); credentials = new Credentials(conf.credentialsPersist(), conf.getCredentialsPath()); - notebook = new Notebook(conf, notebookRepo, schedulerFactory, interpreterFactory, interpreterSettingManager, this, search, + notebook = new Notebook(conf, notebookRepo, schedulerFactory, factory, interpreterSettingManager, this, search, notebookAuthorization, credentials); + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_PUBLIC.getVarName(), "true"); } @After public void tearDown() throws Exception { - super.tearDown(); + delete(tmpDir); } @Test public void testSelectingReplImplementation() throws IOException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); // run with default repl Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -245,7 +259,7 @@ public void testPersist() throws IOException, SchedulerException, RepositoryExce Notebook notebook2 = new Notebook( conf, notebookRepo, schedulerFactory, - new InterpreterFactory(interpreterSettingManager), + new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager), interpreterSettingManager, null, null, null, null); assertEquals(1, notebook2.getAllNotes().size()); @@ -302,7 +316,7 @@ public void testRunBlankParagraph() throws IOException, SchedulerException, Inte @Test public void testRunAll() throws IOException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding("user", note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters("user", note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); // p1 Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -341,7 +355,7 @@ public void testRunAll() throws IOException { public void testSchedule() throws InterruptedException, IOException { // create a note and a paragraph Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding("user", note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters("user", note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); Map config = new HashMap<>(); @@ -414,8 +428,8 @@ private void terminateScheduledNote(Note note) { public void testAutoRestartInterpreterAfterSchedule() throws InterruptedException, IOException{ // create a note and a paragraph Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); - + interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); Map config = new HashMap<>(); p.setConfig(config); @@ -435,11 +449,11 @@ public void testAutoRestartInterpreterAfterSchedule() throws InterruptedExceptio MockInterpreter1 mock1 = ((MockInterpreter1) (((ClassloaderInterpreter) - ((LazyOpenInterpreter) interpreterFactory.getInterpreter(anonymous.getUser(), note.getId(), "mock1")).getInnerInterpreter()) + ((LazyOpenInterpreter) factory.getInterpreter(anonymous.getUser(), note.getId(), "mock1")).getInnerInterpreter()) .getInnerInterpreter())); MockInterpreter2 mock2 = ((MockInterpreter2) (((ClassloaderInterpreter) - ((LazyOpenInterpreter) interpreterFactory.getInterpreter(anonymous.getUser(), note.getId(), "mock2")).getInnerInterpreter()) + ((LazyOpenInterpreter) factory.getInterpreter(anonymous.getUser(), note.getId(), "mock2")).getInnerInterpreter()) .getInnerInterpreter())); // wait until interpreters are started @@ -453,9 +467,9 @@ public void testAutoRestartInterpreterAfterSchedule() throws InterruptedExceptio } // remove cron scheduler. -// config.put("cron", null); -// note.setConfig(config); -// notebook.refreshCron(note.getId()); + config.put("cron", null); + note.setConfig(config); + notebook.refreshCron(note.getId()); // make sure all paragraph has been executed assertNotNull(p.getDateFinished()); @@ -467,7 +481,7 @@ public void testAutoRestartInterpreterAfterSchedule() throws InterruptedExceptio public void testExportAndImportNote() throws IOException, CloneNotSupportedException, InterruptedException, InterpreterException, SchedulerException, RepositoryException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding("user", note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters("user", note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); final Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); String simpleText = "hello world"; @@ -506,7 +520,7 @@ public void testExportAndImportNote() throws IOException, CloneNotSupportedExcep public void testCloneNote() throws IOException, CloneNotSupportedException, InterruptedException, InterpreterException, SchedulerException, RepositoryException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding("user", note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters("user", note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); final Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); p.setText("hello world"); @@ -540,7 +554,7 @@ public void testCloneNote() throws IOException, CloneNotSupportedException, public void testCloneNoteWithNoName() throws IOException, CloneNotSupportedException, InterruptedException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); Note cloneNote = notebook.cloneNote(note.getId(), null, anonymous); assertEquals(cloneNote.getName(), "Note " + cloneNote.getId()); @@ -552,7 +566,7 @@ public void testCloneNoteWithNoName() throws IOException, CloneNotSupportedExcep public void testCloneNoteWithExceptionResult() throws IOException, CloneNotSupportedException, InterruptedException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); final Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); p.setText("hello world"); @@ -577,28 +591,28 @@ public void testCloneNoteWithExceptionResult() throws IOException, CloneNotSuppo @Test public void testResourceRemovealOnParagraphNoteRemove() throws IOException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); - + interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); + for (InterpreterGroup intpGroup : InterpreterGroup.getAll()) { + intpGroup.setResourcePool(new LocalResourcePool(intpGroup.getId())); + } Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); p1.setText("hello"); Paragraph p2 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); p2.setText("%mock2 world"); - for (InterpreterGroup intpGroup : interpreterSettingManager.getAllInterpreterGroup()) { - intpGroup.setResourcePool(new LocalResourcePool(intpGroup.getId())); - } + note.runAll(); while (p1.isTerminated() == false || p1.getResult() == null) Thread.yield(); while (p2.isTerminated() == false || p2.getResult() == null) Thread.yield(); - assertEquals(2, interpreterSettingManager.getAllResources().size()); + assertEquals(2, ResourcePoolUtils.getAllResources().size()); // remove a paragraph note.removeParagraph(anonymous.getUser(), p1.getId()); - assertEquals(1, interpreterSettingManager.getAllResources().size()); + assertEquals(1, ResourcePoolUtils.getAllResources().size()); // remove note notebook.removeNote(note.getId(), anonymous); - assertEquals(0, interpreterSettingManager.getAllResources().size()); + assertEquals(0, ResourcePoolUtils.getAllResources().size()); } @Test @@ -606,10 +620,10 @@ public void testAngularObjectRemovalOnNotebookRemove() throws InterruptedExcepti IOException { // create a note and a paragraph Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); AngularObjectRegistry registry = interpreterSettingManager - .getInterpreterSettings(note.getId()).get(0).getOrCreateInterpreterGroup(anonymous.getUser(), "sharedProcess") + .getInterpreterSettings(note.getId()).get(0).getInterpreterGroup(anonymous.getUser(), "sharedProcess") .getAngularObjectRegistry(); Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -639,10 +653,10 @@ public void testAngularObjectRemovalOnParagraphRemove() throws InterruptedExcept IOException { // create a note and a paragraph Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); AngularObjectRegistry registry = interpreterSettingManager - .getInterpreterSettings(note.getId()).get(0).getOrCreateInterpreterGroup(anonymous.getUser(), "sharedProcess") + .getInterpreterSettings(note.getId()).get(0).getInterpreterGroup(anonymous.getUser(), "sharedProcess") .getAngularObjectRegistry(); Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -673,10 +687,10 @@ public void testAngularObjectRemovalOnInterpreterRestart() throws InterruptedExc IOException { // create a note and a paragraph Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); AngularObjectRegistry registry = interpreterSettingManager - .getInterpreterSettings(note.getId()).get(0).getOrCreateInterpreterGroup(anonymous.getUser(), "sharedProcess") + .getInterpreterSettings(note.getId()).get(0).getInterpreterGroup(anonymous.getUser(), "sharedProcess") .getAngularObjectRegistry(); // add local scope object @@ -686,13 +700,14 @@ public void testAngularObjectRemovalOnInterpreterRestart() throws InterruptedExc // restart interpreter interpreterSettingManager.restart(interpreterSettingManager.getInterpreterSettings(note.getId()).get(0).getId()); - registry = interpreterSettingManager.getInterpreterSettings(note.getId()).get(0) - .getOrCreateInterpreterGroup(anonymous.getUser(), "sharedProcess") - .getAngularObjectRegistry(); - - // New InterpreterGroup will be created and its AngularObjectRegistry will be created - assertNull(registry.get("o1", note.getId(), null)); - assertNull(registry.get("o2", null, null)); + registry = interpreterSettingManager.getInterpreterSettings(note.getId()).get(0).getInterpreterGroup(anonymous.getUser(), "sharedProcess") + .getAngularObjectRegistry(); + + // local and global scope object should be removed + // But InterpreterGroup does not implement angularObjectRegistry per session (scoped, isolated) + // So for now, does not have good way to remove all objects in particular session on restart. + assertNotNull(registry.get("o1", note.getId(), null)); + assertNotNull(registry.get("o2", null, null)); notebook.removeNote(note.getId(), anonymous); } @@ -787,7 +802,7 @@ public void testAuthorizationRoles() throws IOException { public void testAbortParagraphStatusOnInterpreterRestart() throws InterruptedException, IOException { Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters(anonymous.getUser(), note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); // create three paragraphs Paragraph p1 = note.addNewParagraph(anonymous); @@ -811,11 +826,11 @@ public void testAbortParagraphStatusOnInterpreterRestart() throws InterruptedExc interpreterSettingManager.restart(interpreterSettingManager.getInterpreterSettings(note.getId()).get(0).getId()); // make sure three differnt status aborted well. -// assertEquals(Status.FINISHED, p1.getStatus()); -// assertEquals(Status.ABORT, p2.getStatus()); -// assertEquals(Status.ABORT, p3.getStatus()); -// -// notebook.removeNote(note.getId(), anonymous); + assertEquals(Status.FINISHED, p1.getStatus()); + assertEquals(Status.ABORT, p2.getStatus()); + assertEquals(Status.ABORT, p3.getStatus()); + + notebook.removeNote(note.getId(), anonymous); } @Test diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java index 7c85778d8db..7bd6819c39a 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java @@ -21,7 +21,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; -import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; @@ -40,7 +39,7 @@ import org.apache.zeppelin.display.AngularObjectBuilder; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.Input; -import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.Interpreter.FormType; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterGroup; @@ -48,7 +47,10 @@ 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.InterpreterResultMessage; +import org.apache.zeppelin.interpreter.InterpreterSetting; import org.apache.zeppelin.interpreter.InterpreterSetting.Status; +import org.apache.zeppelin.interpreter.InterpreterSettingManager; import org.apache.zeppelin.resource.ResourcePool; import org.apache.zeppelin.user.AuthenticationInfo; import org.apache.zeppelin.user.Credentials; @@ -56,7 +58,6 @@ import java.util.HashMap; import java.util.Map; - import org.mockito.Mockito; public class ParagraphTest { @@ -185,7 +186,7 @@ public void returnUnchangedResultsWithDifferentUser() throws Throwable { when(mockInterpreterOption.permissionIsSet()).thenReturn(false); when(mockInterpreterSetting.getStatus()).thenReturn(Status.READY); when(mockInterpreterSetting.getId()).thenReturn("mock_id_1"); - when(mockInterpreterSetting.getOrCreateInterpreterGroup(anyString(), anyString())).thenReturn(mockInterpreterGroup); + when(mockInterpreterSetting.getInterpreterGroup(anyString(), anyString())).thenReturn(mockInterpreterGroup); spyInterpreterSettingList.add(mockInterpreterSetting); when(mockNote.getId()).thenReturn("any_id"); when(mockInterpreterSettingManager.getInterpreterSettings(anyString())).thenReturn(spyInterpreterSettingList); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java index 14c8789a27c..803912e61a7 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java @@ -33,13 +33,10 @@ import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; import org.apache.zeppelin.dep.DependencyResolver; -import org.apache.zeppelin.display.AngularObjectRegistryListener; -import org.apache.zeppelin.helium.ApplicationEventListener; import org.apache.zeppelin.interpreter.InterpreterFactory; import org.apache.zeppelin.interpreter.InterpreterOption; import org.apache.zeppelin.interpreter.InterpreterResultMessage; import org.apache.zeppelin.interpreter.InterpreterSettingManager; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; import org.apache.zeppelin.notebook.*; import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.scheduler.Job.Status; @@ -72,7 +69,7 @@ public class NotebookRepoSyncTest implements JobListenerFactory { private Credentials credentials; private AuthenticationInfo anonymous; private static final Logger LOG = LoggerFactory.getLogger(NotebookRepoSyncTest.class); - + @Before public void setUp() throws Exception { String zpath = System.getProperty("java.io.tmpdir")+"/ZeppelinLTest_"+System.currentTimeMillis(); @@ -94,13 +91,12 @@ public void setUp() throws Exception { LOG.info("secondary note dir : " + secNotePath); conf = ZeppelinConfiguration.create(); - this.schedulerFactory = SchedulerFactory.singleton(); + this.schedulerFactory = new SchedulerFactory(); depResolver = new DependencyResolver(mainZepDir.getAbsolutePath() + "/local-repo"); - interpreterSettingManager = new InterpreterSettingManager(conf, - mock(AngularObjectRegistryListener.class), mock(RemoteInterpreterProcessListener.class), mock(ApplicationEventListener.class)); - factory = new InterpreterFactory(interpreterSettingManager); - + interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); + factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); + search = mock(SearchService.class); notebookRepoSync = new NotebookRepoSync(conf); notebookAuthorization = NotebookAuthorization.init(conf); @@ -114,19 +110,19 @@ public void setUp() throws Exception { public void tearDown() throws Exception { delete(mainZepDir); } - + @Test public void testRepoCount() throws IOException { assertTrue(notebookRepoSync.getMaxRepoNum() >= notebookRepoSync.getRepoCount()); } - + @Test public void testSyncOnCreate() throws IOException { /* check that both storage systems are empty */ assertTrue(notebookRepoSync.getRepoCount() > 1); assertEquals(0, notebookRepoSync.list(0, anonymous).size()); assertEquals(0, notebookRepoSync.list(1, anonymous).size()); - + /* create note */ Note note = notebookSync.createNote(anonymous); @@ -134,7 +130,7 @@ public void testSyncOnCreate() throws IOException { assertEquals(1, notebookRepoSync.list(0, anonymous).size()); assertEquals(1, notebookRepoSync.list(1, anonymous).size()); assertEquals(notebookRepoSync.list(0, anonymous).get(0).getId(),notebookRepoSync.list(1, anonymous).get(0).getId()); - + notebookSync.removeNote(notebookRepoSync.list(0, null).get(0).getId(), anonymous); } @@ -144,26 +140,26 @@ public void testSyncOnDelete() throws IOException { assertTrue(notebookRepoSync.getRepoCount() > 1); assertEquals(0, notebookRepoSync.list(0, anonymous).size()); assertEquals(0, notebookRepoSync.list(1, anonymous).size()); - + Note note = notebookSync.createNote(anonymous); /* check that created in both storage systems */ assertEquals(1, notebookRepoSync.list(0, anonymous).size()); assertEquals(1, notebookRepoSync.list(1, anonymous).size()); assertEquals(notebookRepoSync.list(0, anonymous).get(0).getId(),notebookRepoSync.list(1, anonymous).get(0).getId()); - + /* remove Note */ notebookSync.removeNote(notebookRepoSync.list(0, anonymous).get(0).getId(), anonymous); - + /* check that deleted in both storages */ assertEquals(0, notebookRepoSync.list(0, anonymous).size()); assertEquals(0, notebookRepoSync.list(1, anonymous).size()); - + } - + @Test public void testSyncUpdateMain() throws IOException { - + /* create note */ Note note = notebookSync.createNote(anonymous); Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -171,19 +167,19 @@ public void testSyncUpdateMain() throws IOException { config.put("enabled", true); p1.setConfig(config); p1.setText("hello world"); - + /* new paragraph exists in note instance */ assertEquals(1, note.getParagraphs().size()); - + /* new paragraph not yet saved into storages */ assertEquals(0, notebookRepoSync.get(0, notebookRepoSync.list(0, anonymous).get(0).getId(), anonymous).getParagraphs().size()); assertEquals(0, notebookRepoSync.get(1, notebookRepoSync.list(1, anonymous).get(0).getId(), anonymous).getParagraphs().size()); - - /* save to storage under index 0 (first storage) */ + + /* save to storage under index 0 (first storage) */ notebookRepoSync.save(0, note, anonymous); - + /* check paragraph saved to first storage */ assertEquals(1, notebookRepoSync.get(0, notebookRepoSync.list(0, anonymous).get(0).getId(), anonymous).getParagraphs().size()); @@ -288,45 +284,45 @@ public void testCheckpointOneStorage() throws IOException, SchedulerException { // one git versioned storage initialized assertThat(vRepoSync.getRepoCount()).isEqualTo(1); assertThat(vRepoSync.getRepo(0)).isInstanceOf(GitNotebookRepo.class); - + GitNotebookRepo gitRepo = (GitNotebookRepo) vRepoSync.getRepo(0); - + // no notes assertThat(vRepoSync.list(anonymous).size()).isEqualTo(0); // create note Note note = vNotebookSync.createNote(anonymous); assertThat(vRepoSync.list(anonymous).size()).isEqualTo(1); - + String noteId = vRepoSync.list(anonymous).get(0).getId(); // first checkpoint vRepoSync.checkpoint(noteId, "checkpoint message", anonymous); int vCount = gitRepo.revisionHistory(noteId, anonymous).size(); assertThat(vCount).isEqualTo(1); - + Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); Map config = p.getConfig(); config.put("enabled", true); p.setConfig(config); p.setText("%md checkpoint test"); - + // save and checkpoint again vRepoSync.save(note, anonymous); vRepoSync.checkpoint(noteId, "checkpoint message 2", anonymous); assertThat(gitRepo.revisionHistory(noteId, anonymous).size()).isEqualTo(vCount + 1); notebookRepoSync.remove(note.getId(), anonymous); } - + @Test public void testSyncWithAcl() throws IOException { /* scenario 1 - note exists with acl on main storage */ AuthenticationInfo user1 = new AuthenticationInfo("user1"); Note note = notebookSync.createNote(user1); assertEquals(0, note.getParagraphs().size()); - + // saved on both storages assertEquals(1, notebookRepoSync.list(0, null).size()); assertEquals(1, notebookRepoSync.list(1, null).size()); - + /* check that user1 is the only owner */ NotebookAuthorization authInfo = NotebookAuthorization.getInstance(); Set entity = new HashSet(); @@ -335,23 +331,23 @@ public void testSyncWithAcl() throws IOException { assertEquals(1, authInfo.getOwners(note.getId()).size()); assertEquals(0, authInfo.getReaders(note.getId()).size()); assertEquals(0, authInfo.getWriters(note.getId()).size()); - + /* update note and save on secondary storage */ Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); p1.setText("hello world"); assertEquals(1, note.getParagraphs().size()); notebookRepoSync.save(1, note, null); - + /* check paragraph isn't saved into first storage */ assertEquals(0, notebookRepoSync.get(0, notebookRepoSync.list(0, null).get(0).getId(), null).getParagraphs().size()); /* check paragraph is saved into second storage */ assertEquals(1, notebookRepoSync.get(1, notebookRepoSync.list(1, null).get(0).getId(), null).getParagraphs().size()); - + /* now sync by user1 */ notebookRepoSync.sync(user1); - + /* check that note updated and acl are same on main storage*/ assertEquals(1, notebookRepoSync.get(0, notebookRepoSync.list(0, null).get(0).getId(), null).getParagraphs().size()); @@ -359,7 +355,7 @@ public void testSyncWithAcl() throws IOException { assertEquals(1, authInfo.getOwners(note.getId()).size()); assertEquals(0, authInfo.getReaders(note.getId()).size()); assertEquals(0, authInfo.getWriters(note.getId()).size()); - + /* scenario 2 - note doesn't exist on main storage */ /* remove from main storage */ notebookRepoSync.remove(0, note.getId(), user1); @@ -369,7 +365,7 @@ public void testSyncWithAcl() throws IOException { assertEquals(0, authInfo.getOwners(note.getId()).size()); assertEquals(0, authInfo.getReaders(note.getId()).size()); assertEquals(0, authInfo.getWriters(note.getId()).size()); - + /* now sync - should bring note from secondary storage with added acl */ notebookRepoSync.sync(user1); assertEquals(1, notebookRepoSync.list(0, null).size()); @@ -427,5 +423,5 @@ public void afterStatusChange(Job job, Status before, Status after) { } }; } - + } diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java index b3935890b1f..6f85bf62d31 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java @@ -22,15 +22,18 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Properties; +import com.google.common.collect.Maps; import org.apache.commons.io.FileUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; - import org.apache.zeppelin.dep.Dependency; import org.apache.zeppelin.dep.DependencyResolver; -import org.apache.zeppelin.interpreter.AbstractInterpreterTest; import org.apache.zeppelin.interpreter.InterpreterFactory; import org.apache.zeppelin.interpreter.InterpreterInfo; import org.apache.zeppelin.interpreter.InterpreterOption; @@ -38,7 +41,6 @@ import org.apache.zeppelin.interpreter.InterpreterProperty; import org.apache.zeppelin.interpreter.InterpreterSettingManager; import org.apache.zeppelin.interpreter.mock.MockInterpreter1; - import org.apache.zeppelin.notebook.JobListenerFactory; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.Notebook; @@ -56,34 +58,59 @@ import com.google.common.collect.ImmutableMap; -public class VFSNotebookRepoTest extends AbstractInterpreterTest implements JobListenerFactory { - +public class VFSNotebookRepoTest implements JobListenerFactory { private static final Logger LOG = LoggerFactory.getLogger(VFSNotebookRepoTest.class); - + private ZeppelinConfiguration conf; private SchedulerFactory schedulerFactory; private Notebook notebook; private NotebookRepo notebookRepo; + private InterpreterSettingManager interpreterSettingManager; + private InterpreterFactory factory; + private DependencyResolver depResolver; private NotebookAuthorization notebookAuthorization; + private File mainZepDir; + private File mainNotebookDir; + @Before public void setUp() throws Exception { - System.setProperty(ConfVars.ZEPPELIN_INTERPRETER_GROUP_ORDER.getVarName(), "mock1,mock2"); + String zpath = System.getProperty("java.io.tmpdir") + "/ZeppelinLTest_" + System.currentTimeMillis(); + mainZepDir = new File(zpath); + mainZepDir.mkdirs(); + new File(mainZepDir, "conf").mkdirs(); + String mainNotePath = zpath + "/notebook"; + mainNotebookDir = new File(mainNotePath); + mainNotebookDir.mkdirs(); + + System.setProperty(ConfVars.ZEPPELIN_HOME.getVarName(), mainZepDir.getAbsolutePath()); + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), mainNotebookDir.getAbsolutePath()); System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_STORAGE.getVarName(), "org.apache.zeppelin.notebook.repo.VFSNotebookRepo"); + conf = ZeppelinConfiguration.create(); + + this.schedulerFactory = new SchedulerFactory(); + + this.schedulerFactory = new SchedulerFactory(); + depResolver = new DependencyResolver(mainZepDir.getAbsolutePath() + "/local-repo"); + interpreterSettingManager = new InterpreterSettingManager(conf, depResolver, new InterpreterOption(true)); + factory = new InterpreterFactory(conf, null, null, null, depResolver, false, interpreterSettingManager); - super.setUp(); + ArrayList interpreterInfos = new ArrayList<>(); + interpreterInfos.add(new InterpreterInfo(MockInterpreter1.class.getName(), "mock1", true, new HashMap())); + interpreterSettingManager.add("mock1", interpreterInfos, new ArrayList(), new InterpreterOption(), + Maps.newHashMap(), "mock1", null); + interpreterSettingManager.createNewSetting("mock1", "mock1", new ArrayList(), new InterpreterOption(), new HashMap()); - this.schedulerFactory = SchedulerFactory.singleton(); SearchService search = mock(SearchService.class); notebookRepo = new VFSNotebookRepo(conf); notebookAuthorization = NotebookAuthorization.init(conf); - notebook = new Notebook(conf, notebookRepo, schedulerFactory, interpreterFactory, interpreterSettingManager, this, search, + notebook = new Notebook(conf, notebookRepo, schedulerFactory, factory, interpreterSettingManager, this, search, notebookAuthorization, null); } @After public void tearDown() throws Exception { - if (!FileUtils.deleteQuietly(testRootDir)) { - LOG.error("Failed to delete {} ", testRootDir.getName()); + if (!FileUtils.deleteQuietly(mainZepDir)) { + LOG.error("Failed to delete {} ", mainZepDir.getName()); } } @@ -93,7 +120,7 @@ public void testInvalidJsonFile() throws IOException { int numNotes = notebookRepo.list(null).size(); // when create invalid json file - File testNoteDir = new File(notebookDir, "test"); + File testNoteDir = new File(mainNotebookDir, "test"); testNoteDir.mkdir(); FileUtils.writeStringToFile(new File(testNoteDir, "note.json"), ""); @@ -105,7 +132,7 @@ public void testInvalidJsonFile() throws IOException { public void testSaveNotebook() throws IOException, InterruptedException { AuthenticationInfo anonymous = new AuthenticationInfo("anonymous"); Note note = notebook.createNote(anonymous); - interpreterSettingManager.setInterpreterBinding("user", note.getId(), interpreterSettingManager.getInterpreterSettingIds()); + interpreterSettingManager.setInterpreters("user", note.getId(), interpreterSettingManager.getDefaultInterpreterSettingList()); Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); Map config = p1.getConfig(); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java index 72231099c02..46134e5461f 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java @@ -17,34 +17,35 @@ package org.apache.zeppelin.resource; import com.google.gson.Gson; -import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.user.AuthenticationInfo; import org.apache.zeppelin.display.GUI; import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterEventPoller; import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterResourcePool; -import org.apache.zeppelin.user.AuthenticationInfo; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.File; -import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; -import java.util.List; +import java.util.Properties; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * Unittest for DistributedResourcePool */ -public class DistributedResourcePoolTest extends AbstractInterpreterTest { - +public class DistributedResourcePoolTest { + private static final String INTERPRETER_SCRIPT = + System.getProperty("os.name").startsWith("Windows") ? + "../bin/interpreter.cmd" : + "../bin/interpreter.sh"; + private InterpreterGroup intpGroup1; + private InterpreterGroup intpGroup2; + private HashMap env; private RemoteInterpreter intp1; private RemoteInterpreter intp2; private InterpreterContext context; @@ -54,10 +55,50 @@ public class DistributedResourcePoolTest extends AbstractInterpreterTest { @Before public void setUp() throws Exception { - super.setUp(); - InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("mock_resource_pool"); - intp1 = (RemoteInterpreter) interpreterSetting.getInterpreter("user1", "note1", "mock_resource_pool"); - intp2 = (RemoteInterpreter) interpreterSetting.getInterpreter("user2", "note1", "mock_resource_pool"); + env = new HashMap<>(); + env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); + + Properties p = new Properties(); + + intp1 = new RemoteInterpreter( + p, + "note", + MockInterpreterResourcePool.class.getName(), + new File(INTERPRETER_SCRIPT).getAbsolutePath(), + "fake", + "fakeRepo", + env, + 10 * 1000, + null, + null, + "anonymous", + false + ); + + intpGroup1 = new InterpreterGroup("intpGroup1"); + intpGroup1.put("note", new LinkedList()); + intpGroup1.get("note").add(intp1); + intp1.setInterpreterGroup(intpGroup1); + + intp2 = new RemoteInterpreter( + p, + "note", + MockInterpreterResourcePool.class.getName(), + new File(INTERPRETER_SCRIPT).getAbsolutePath(), + "fake", + "fakeRepo", + env, + 10 * 1000, + null, + null, + "anonymous", + false + ); + + intpGroup2 = new InterpreterGroup("intpGroup2"); + intpGroup2.put("note", new LinkedList()); + intpGroup2.get("note").add(intp2); + intp2.setInterpreterGroup(intpGroup2); context = new InterpreterContext( "note", @@ -76,13 +117,26 @@ public void setUp() throws Exception { intp1.open(); intp2.open(); - eventPoller1 = intp1.getInterpreterGroup().getRemoteInterpreterProcess().getRemoteInterpreterEventPoller(); - eventPoller2 = intp1.getInterpreterGroup().getRemoteInterpreterProcess().getRemoteInterpreterEventPoller(); + eventPoller1 = new RemoteInterpreterEventPoller(null, null); + eventPoller1.setInterpreterGroup(intpGroup1); + eventPoller1.setInterpreterProcess(intpGroup1.getRemoteInterpreterProcess()); + + eventPoller2 = new RemoteInterpreterEventPoller(null, null); + eventPoller2.setInterpreterGroup(intpGroup2); + eventPoller2.setInterpreterProcess(intpGroup2.getRemoteInterpreterProcess()); + + eventPoller1.start(); + eventPoller2.start(); } @After public void tearDown() throws Exception { - interpreterSettingManager.close(); + eventPoller1.shutdown(); + intp1.close(); + intpGroup1.close(); + eventPoller2.shutdown(); + intp2.close(); + intpGroup2.close(); } @Test @@ -181,13 +235,13 @@ public void testResourcePoolUtils() { // then get all resources. - assertEquals(4, interpreterSettingManager.getAllResources().size()); + assertEquals(4, ResourcePoolUtils.getAllResources().size()); // when remove all resources from note1 - interpreterSettingManager.removeResourcesBelongsToNote("note1"); + ResourcePoolUtils.removeResourcesBelongsToNote("note1"); // then resources should be removed. - assertEquals(2, interpreterSettingManager.getAllResources().size()); + assertEquals(2, ResourcePoolUtils.getAllResources().size()); assertEquals("", gson.fromJson( intp1.interpret("get note1:paragraph1:key1", context).message().get(0).getData(), String.class)); @@ -197,10 +251,10 @@ public void testResourcePoolUtils() { // when remove all resources from note2:paragraph1 - interpreterSettingManager.removeResourcesBelongsToParagraph("note2", "paragraph1"); + ResourcePoolUtils.removeResourcesBelongsToParagraph("note2", "paragraph1"); // then 1 - assertEquals(1, interpreterSettingManager.getAllResources().size()); + assertEquals(1, ResourcePoolUtils.getAllResources().size()); assertEquals("value2", gson.fromJson( intp1.interpret("get note2:paragraph2:key2", context).message().get(0).getData(), String.class)); diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java similarity index 77% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java index a1afe0e9d6a..ebb51004285 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java @@ -17,86 +17,83 @@ package org.apache.zeppelin.scheduler; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Properties; + +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.user.AuthenticationInfo; import org.apache.zeppelin.display.GUI; -import org.apache.zeppelin.interpreter.InterpreterContext; -import org.apache.zeppelin.interpreter.InterpreterContextRunner; -import org.apache.zeppelin.interpreter.InterpreterInfo; -import org.apache.zeppelin.interpreter.InterpreterOption; -import org.apache.zeppelin.interpreter.InterpreterResult; -import org.apache.zeppelin.interpreter.InterpreterRunner; -import org.apache.zeppelin.interpreter.InterpreterSetting; import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; import org.apache.zeppelin.interpreter.remote.mock.MockInterpreterA; import org.apache.zeppelin.resource.LocalResourcePool; import org.apache.zeppelin.scheduler.Job.Status; -import org.apache.zeppelin.user.AuthenticationInfo; import org.junit.After; import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - public class RemoteSchedulerTest implements RemoteInterpreterProcessListener { private static final String INTERPRETER_SCRIPT = - System.getProperty("os.name").startsWith("Windows") ? - "../bin/interpreter.cmd" : - "../bin/interpreter.sh"; - - private InterpreterSetting interpreterSetting; + System.getProperty("os.name").startsWith("Windows") ? + "../bin/interpreter.cmd" : + "../bin/interpreter.sh"; private SchedulerFactory schedulerSvc; private static final int TICK_WAIT = 100; private static final int MAX_WAIT_CYCLES = 100; @Before - public void setUp() throws Exception { + public void setUp() throws Exception{ schedulerSvc = new SchedulerFactory(); - - InterpreterOption interpreterOption = new InterpreterOption(); - interpreterOption.setRemote(true); - InterpreterInfo interpreterInfo1 = new InterpreterInfo(MockInterpreterA.class.getName(), "mock", true, new HashMap()); - List interpreterInfos = new ArrayList<>(); - interpreterInfos.add(interpreterInfo1); - InterpreterRunner runner = new InterpreterRunner(INTERPRETER_SCRIPT, INTERPRETER_SCRIPT); - interpreterSetting = new InterpreterSetting.Builder() - .setId("test") - .setName("test") - .setGroup("test") - .setInterpreterInfos(interpreterInfos) - .setOption(interpreterOption) - .setRunner(runner) - .setInterpreterDir("../interpeters/test") - .create(); } @After - public void tearDown() { - interpreterSetting.close(); + public void tearDown(){ + } @Test public void test() throws Exception { - final RemoteInterpreter intpA = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); + Properties p = new Properties(); + final InterpreterGroup intpGroup = new InterpreterGroup(); + Map env = new HashMap<>(); + env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); + + final RemoteInterpreter intpA = new RemoteInterpreter( + p, + "note", + MockInterpreterA.class.getName(), + new File(INTERPRETER_SCRIPT).getAbsolutePath(), + "fake", + "fakeRepo", + env, + 10 * 1000, + this, + null, + "anonymous", + false); + + intpGroup.put("note", new LinkedList()); + intpGroup.get("note").add(intpA); + intpA.setInterpreterGroup(intpGroup); intpA.open(); Scheduler scheduler = schedulerSvc.createOrGetRemoteScheduler("test", "note", - intpA, + intpA.getInterpreterProcess(), 10); Job job = new Job("jobId", "jobName", null, 200) { Object results; - @Override public Object getReturn() { return results; @@ -123,7 +120,7 @@ protected Object jobRun() throws Throwable { new AuthenticationInfo(), new HashMap(), new GUI(), - null, + new AngularObjectRegistry(intpGroup.getId(), null), new LocalResourcePool("pool1"), new LinkedList(), null)); return "1000"; @@ -148,7 +145,7 @@ public void setResult(Object results) { } assertTrue(job.isRunning()); - Thread.sleep(5 * TICK_WAIT); + Thread.sleep(5*TICK_WAIT); assertEquals(0, scheduler.getJobsWaiting().size()); assertEquals(1, scheduler.getJobsRunning().size()); @@ -168,10 +165,34 @@ public void setResult(Object results) { @Test public void testAbortOnPending() throws Exception { - final RemoteInterpreter intpA = (RemoteInterpreter) interpreterSetting.getDefaultInterpreter("user1", "note1"); + Properties p = new Properties(); + final InterpreterGroup intpGroup = new InterpreterGroup(); + Map env = new HashMap<>(); + env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); + + final RemoteInterpreter intpA = new RemoteInterpreter( + p, + "note", + MockInterpreterA.class.getName(), + new File(INTERPRETER_SCRIPT).getAbsolutePath(), + "fake", + "fakeRepo", + env, + 10 * 1000, + this, + null, + "anonymous", + false); + + intpGroup.put("note", new LinkedList()); + intpGroup.get("note").add(intpA); + intpA.setInterpreterGroup(intpGroup); + intpA.open(); - Scheduler scheduler = schedulerSvc.createOrGetRemoteScheduler("test", "note", intpA, 10); + Scheduler scheduler = schedulerSvc.createOrGetRemoteScheduler("test", "note", + intpA.getInterpreterProcess(), + 10); Job job1 = new Job("jobId1", "jobName1", null, 200) { Object results; @@ -184,7 +205,7 @@ public void testAbortOnPending() throws Exception { new AuthenticationInfo(), new HashMap(), new GUI(), - null, + new AngularObjectRegistry(intpGroup.getId(), null), new LocalResourcePool("pool1"), new LinkedList(), null); @@ -234,7 +255,7 @@ public void setResult(Object results) { new AuthenticationInfo(), new HashMap(), new GUI(), - null, + new AngularObjectRegistry(intpGroup.getId(), null), new LocalResourcePool("pool1"), new LinkedList(), null); @@ -337,7 +358,7 @@ public void onRemoteRunParagraph(String noteId, String PsaragraphID) throws Exce } @Override - public void onParaInfosReceived(String noteId, String paragraphId, - String interpreterSettingId, Map metaInfos) { + public void onParaInfosReceived(String noteId, String paragraphId, + String interpreterSettingId, Map metaInfos) { } } diff --git a/zeppelin-zengine/src/test/resources/conf/interpreter.json b/zeppelin-zengine/src/test/resources/conf/interpreter.json deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/zeppelin-zengine/src/test/resources/conf/interpreter.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/zeppelin-zengine/src/test/resources/interpreter/mock/interpreter-setting.json b/zeppelin-zengine/src/test/resources/interpreter/mock/interpreter-setting.json new file mode 100644 index 00000000000..65568ef8a5c --- /dev/null +++ b/zeppelin-zengine/src/test/resources/interpreter/mock/interpreter-setting.json @@ -0,0 +1,12 @@ +[ + { + "group": "mock11", + "name": "mock11", + "className": "org.apache.zeppelin.interpreter.mock.MockInterpreter11", + "properties": { + }, + "editor": { + "language": "java" + } + } +] diff --git a/zeppelin-zengine/src/test/resources/interpreter/mock1/interpreter-setting.json b/zeppelin-zengine/src/test/resources/interpreter/mock1/interpreter-setting.json deleted file mode 100644 index 0e6fb21b73b..00000000000 --- a/zeppelin-zengine/src/test/resources/interpreter/mock1/interpreter-setting.json +++ /dev/null @@ -1,19 +0,0 @@ -[ - { - "group": "mock1", - "name": "mock1", - "className": "org.apache.zeppelin.interpreter.mock.MockInterpreter1", - "properties": { - }, - "option": { - "remote": false, - "port": -1, - "perNote": "shared", - "perUser": "shared", - "isExistingProcess": false, - "setPermission": false, - "users": [], - "isUserImpersonate": false - } - } -] diff --git a/zeppelin-zengine/src/test/resources/interpreter/mock2/interpreter-setting.json b/zeppelin-zengine/src/test/resources/interpreter/mock2/interpreter-setting.json deleted file mode 100644 index aca418a2a27..00000000000 --- a/zeppelin-zengine/src/test/resources/interpreter/mock2/interpreter-setting.json +++ /dev/null @@ -1,19 +0,0 @@ -[ - { - "group": "mock2", - "name": "mock2", - "className": "org.apache.zeppelin.interpreter.mock.MockInterpreter2", - "properties": { - }, - "option": { - "remote": false, - "port": -1, - "perNote": "shared", - "perUser": "isolated", - "isExistingProcess": false, - "setPermission": false, - "users": [], - "isUserImpersonate": false - } - } -] diff --git a/zeppelin-zengine/src/test/resources/interpreter/mock_resource_pool/interpreter-setting.json b/zeppelin-zengine/src/test/resources/interpreter/mock_resource_pool/interpreter-setting.json deleted file mode 100644 index 4dfe0a75b5e..00000000000 --- a/zeppelin-zengine/src/test/resources/interpreter/mock_resource_pool/interpreter-setting.json +++ /dev/null @@ -1,19 +0,0 @@ -[ - { - "group": "mock_resource_pool", - "name": "mock_resource_pool", - "className": "org.apache.zeppelin.interpreter.remote.mock.MockInterpreterResourcePool", - "properties": { - }, - "option": { - "remote": true, - "port": -1, - "perNote": "shared", - "perUser": "shared", - "isExistingProcess": false, - "setPermission": false, - "users": [], - "isUserImpersonate": false - } - } -] diff --git a/zeppelin-zengine/src/test/resources/log4j.properties b/zeppelin-zengine/src/test/resources/log4j.properties index 74f619b70ee..001a222535d 100644 --- a/zeppelin-zengine/src/test/resources/log4j.properties +++ b/zeppelin-zengine/src/test/resources/log4j.properties @@ -35,6 +35,7 @@ log4j.logger.org.apache.hadoop.mapred=WARN log4j.logger.org.apache.hadoop.hive.ql=WARN log4j.logger.org.apache.hadoop.hive.metastore=WARN log4j.logger.org.apache.haadoop.hive.service.HiveServer=WARN +log4j.logger.org.apache.zeppelin.scheduler=WARN log4j.logger.org.quartz=WARN log4j.logger.DataNucleus=WARN @@ -44,6 +45,4 @@ log4j.logger.DataNucleus.Datastore=ERROR # Log all JDBC parameters log4j.logger.org.hibernate.type=ALL -log4j.logger.org.apache.zeppelin.interpreter=DEBUG -log4j.logger.org.apache.zeppelin.scheduler=DEBUG From e47b30a88f09434cd266946d3858cf34275be6a6 Mon Sep 17 00:00:00 2001 From: Paolo Genissel Date: Sun, 20 Aug 2017 09:28:46 +0200 Subject: [PATCH 029/492] [ZEPPELIN-2848] Added new type of user to only run notebook ### What is this PR for? The idea of this PR is to provide a new kind of user : Runner. Basically, what it does is that it just removes write authorization and allow user to read and run note. ### What type of PR is it? [Feature] ### Todos * [ ] - Task ### What is the Jira issue? [ZEPPELIN-2848] https://issues.apache.org/jira/browse/ZEPPELIN-2848 ### How should this be tested? - Log in as admin - Create new notebook and create a paragraph with the interpreter you want - Assign runner right to user1 - Log in as user1 - Try to run the paragraph (should work) - Try to modify the paragraph (should fail) - Log in as user2 - Try to run the paragraph (should fail) ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? Yes * Does this needs documentation? No Author: Paolo Genissel Author: gfalcone Author: Paolo Genissel Closes #2526 from gfalcone/new_type_runner and squashes the following commits: 96bba66 [gfalcone] Fix typo on notebook_authorization.md 8ab4512 [gfalcone] Update notebook_authorization.md 22a1eb3 [Paolo Genissel] Fixed typo d621792 [Paolo Genissel] Fix NotebookSecurityRestApiTest a67af0f [Paolo Genissel] Fix test 5c43ca9 [Paolo Genissel] Added new type of user --- docs/setup/security/notebook_authorization.md | 10 ++- docs/usage/rest_api/notebook.md | 6 ++ .../apache/zeppelin/rest/NotebookRestApi.java | 58 +++++++++++++---- .../zeppelin/socket/NotebookServer.java | 27 ++++++-- .../integration/AuthenticationIT.java | 2 + .../rest/NotebookSecurityRestApiTest.java | 11 ++-- .../src/app/notebook/notebook.controller.js | 6 +- zeppelin-web/src/app/notebook/notebook.css | 5 ++ zeppelin-web/src/app/notebook/notebook.html | 10 ++- .../notebook/NotebookAuthorization.java | 62 ++++++++++++++++++- .../notebook/repo/NotebookRepoSync.java | 4 ++ .../zeppelin/notebook/NotebookTest.java | 33 ++++++++-- .../notebook/repo/NotebookRepoSyncTest.java | 5 ++ 13 files changed, 203 insertions(+), 36 deletions(-) diff --git a/docs/setup/security/notebook_authorization.md b/docs/setup/security/notebook_authorization.md index 0d5510410c4..fe0e27a37b7 100644 --- a/docs/setup/security/notebook_authorization.md +++ b/docs/setup/security/notebook_authorization.md @@ -36,13 +36,17 @@ As you can see, each Zeppelin notebooks has 3 entities : * Owners ( users or groups ) * Readers ( users or groups ) * Writers ( users or groups ) +* Runners ( users or groups )
Fill out the each forms with comma seperated **users** and **groups** configured in `conf/shiro.ini` file. If the form is empty (*), it means that any users can perform that operation. -If someone who doesn't have **read** permission is trying to access the notebook or someone who doesn't have **write** permission is trying to edit the notebook, Zeppelin will ask to login or block the user. +If someone who doesn't have **read** permission is trying to access the notebook or someone who doesn't have **write** permission is trying to edit the notebook, +or someone who doesn't have **run** permission is trying to run a paragraph Zeppelin will ask to login or block the user. + +By default, owners and writers have **write** permission, owners, writers and runners have **run** permission, owners, writers, runners and readers have **read** permission
@@ -63,13 +67,13 @@ or set `zeppelin.notebook.public` property to `false` in `conf/zeppelin-site.xml ``` -Behind the scenes, when you create a new note only the `owners` field is filled with current user, leaving `readers` and `writers` fields empty. All the notes with at least one empty authorization field are considered to be in `public` workspace. Thus when setting `zeppelin.notebook.public` (or corresponding `ZEPPELIN_NOTEBOOK_PUBLIC`) to false, newly created notes have `readers` and `writers` fields filled with current user, making note appear as in `private` workspace. +Behind the scenes, when you create a new note only the `owners` field is filled with current user, leaving `readers`, `runners` and `writers` fields empty. All the notes with at least one empty authorization field are considered to be in `public` workspace. Thus when setting `zeppelin.notebook.public` (or corresponding `ZEPPELIN_NOTEBOOK_PUBLIC`) to false, newly created notes have `readers`, `runners`, `writers` fields filled with current user, making note appear as in `private` workspace. ## How it works In this section, we will explain the detail about how the notebook authorization works in backend side. ### NotebookServer -The [NotebookServer](https://github.com/apache/zeppelin/blob/master/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java) classifies every notebook operations into three categories: **Read**, **Write**, **Manage**. +The [NotebookServer](https://github.com/apache/zeppelin/blob/master/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java) classifies every notebook operations into three categories: **Read**, **Run**, **Write**, **Manage**. Before executing a notebook operation, it checks if the user and the groups associated with the `NotebookSocket` have permissions. For example, before executing a **Read** operation, it checks if the user and the groups have at least one entity that belongs to the **Reader** entities. diff --git a/docs/usage/rest_api/notebook.md b/docs/usage/rest_api/notebook.md index dfb491ae8bb..ff935531aee 100644 --- a/docs/usage/rest_api/notebook.md +++ b/docs/usage/rest_api/notebook.md @@ -1215,6 +1215,9 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, "owners":[ "user1" ], + "runners":[ + "user2" + ], "writers":[ "user2" ] @@ -1259,6 +1262,9 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, "owners": [ "user2" ], + "runners":[ + "user2" + ], "writers": [ "user1" ] diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java index a343879cccc..c170a099017 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java @@ -98,6 +98,7 @@ public Response getNotePermissions(@PathParam("noteId") String noteId) throws IO permissionsMap.put("owners", notebookAuthorization.getOwners(noteId)); permissionsMap.put("readers", notebookAuthorization.getReaders(noteId)); permissionsMap.put("writers", notebookAuthorization.getWriters(noteId)); + permissionsMap.put("runners", notebookAuthorization.getRunners(noteId)); return new JsonResponse<>(Status.OK, "", permissionsMap).build(); } @@ -165,6 +166,18 @@ private void checkIfUserCanRead(String noteId, String errorMsg) { throw new ForbiddenException(errorMsg); } } + + /** + * Check if the current user can run the given note. + */ + private void checkIfUserCanRun(String noteId, String errorMsg) { + Set userAndRoles = Sets.newHashSet(); + userAndRoles.add(SecurityUtils.getPrincipal()); + userAndRoles.addAll(SecurityUtils.getRoles()); + if (!notebookAuthorization.hasRunAuthorization(userAndRoles, noteId)) { + throw new ForbiddenException(errorMsg); + } + } private void checkIfNoteIsNotNull(Note note) { if (note == null) { @@ -199,15 +212,31 @@ public Response putNotePermissions(@PathParam("noteId") String noteId, String re HashMap> permMap = gson.fromJson(req, new TypeToken>>() {}.getType()); Note note = notebook.getNote(noteId); - - LOG.info("Set permissions {} {} {} {} {}", noteId, principal, permMap.get("owners"), - permMap.get("readers"), permMap.get("writers")); + + LOG.info("Set permissions {} {} {} {} {} {}", noteId, principal, permMap.get("owners"), + permMap.get("readers"), permMap.get("runners"), permMap.get("writers")); HashSet readers = permMap.get("readers"); + HashSet runners = permMap.get("runners"); HashSet owners = permMap.get("owners"); HashSet writers = permMap.get("writers"); - // Set readers, if writers and owners is empty -> set to user requesting the change + // Set readers, if runners, writers and owners is empty -> set to user requesting the change if (readers != null && !readers.isEmpty()) { + if (runners.isEmpty()) { + runners = Sets.newHashSet(SecurityUtils.getPrincipal()); + } + if (writers.isEmpty()) { + writers = Sets.newHashSet(SecurityUtils.getPrincipal()); + } + if (owners.isEmpty()) { + owners = Sets.newHashSet(SecurityUtils.getPrincipal()); + } + } + // Set runners, if writers and owners is empty -> set to user requesting the change + if (runners != null && !runners.isEmpty()) { + if (writers.isEmpty()) { + writers = Sets.newHashSet(SecurityUtils.getPrincipal()); + } if (owners.isEmpty()) { owners = Sets.newHashSet(SecurityUtils.getPrincipal()); } @@ -220,10 +249,12 @@ public Response putNotePermissions(@PathParam("noteId") String noteId, String re } notebookAuthorization.setReaders(noteId, readers); + notebookAuthorization.setRunners(noteId, runners); notebookAuthorization.setWriters(noteId, writers); notebookAuthorization.setOwners(noteId, owners); - LOG.debug("After set permissions {} {} {}", notebookAuthorization.getOwners(noteId), - notebookAuthorization.getReaders(noteId), notebookAuthorization.getWriters(noteId)); + LOG.debug("After set permissions {} {} {} {}", notebookAuthorization.getOwners(noteId), + notebookAuthorization.getReaders(noteId), notebookAuthorization.getRunners(noteId), + notebookAuthorization.getWriters(noteId)); AuthenticationInfo subject = new AuthenticationInfo(SecurityUtils.getPrincipal()); note.persist(subject); notebookServer.broadcastNote(note); @@ -589,7 +620,7 @@ public Response runNoteJobs(@PathParam("noteId") String noteId) Note note = notebook.getNote(noteId); AuthenticationInfo subject = new AuthenticationInfo(SecurityUtils.getPrincipal()); checkIfNoteIsNotNull(note); - checkIfUserCanWrite(noteId, "Insufficient privileges you cannot run job for this note"); + checkIfUserCanRun(noteId, "Insufficient privileges you cannot run job for this note"); try { note.runAll(subject); @@ -616,7 +647,7 @@ public Response stopNoteJobs(@PathParam("noteId") String noteId) LOG.info("stop note jobs {} ", noteId); Note note = notebook.getNote(noteId); checkIfNoteIsNotNull(note); - checkIfUserCanWrite(noteId, "Insufficient privileges you cannot stop this job for this note"); + checkIfUserCanRun(noteId, "Insufficient privileges you cannot stop this job for this note"); for (Paragraph p : note.getParagraphs()) { if (!p.isTerminated()) { @@ -690,7 +721,7 @@ public Response runParagraph(@PathParam("noteId") String noteId, Note note = notebook.getNote(noteId); checkIfNoteIsNotNull(note); - checkIfUserCanWrite(noteId, "Insufficient privileges you cannot run job for this note"); + checkIfUserCanRun(noteId, "Insufficient privileges you cannot run job for this note"); Paragraph paragraph = note.getParagraph(paragraphId); checkIfParagraphIsNotNull(paragraph); @@ -728,7 +759,7 @@ public Response runParagraphSynchronously(@PathParam("noteId") String noteId, Note note = notebook.getNote(noteId); checkIfNoteIsNotNull(note); - checkIfUserCanWrite(noteId, "Insufficient privileges you cannot run paragraph"); + checkIfUserCanRun(noteId, "Insufficient privileges you cannot run paragraph"); Paragraph paragraph = note.getParagraph(paragraphId); checkIfParagraphIsNotNull(paragraph); @@ -766,7 +797,7 @@ public Response stopParagraph(@PathParam("noteId") String noteId, LOG.info("stop paragraph job {} ", noteId); Note note = notebook.getNote(noteId); checkIfNoteIsNotNull(note); - checkIfUserCanWrite(noteId, "Insufficient privileges you cannot stop paragraph"); + checkIfUserCanRun(noteId, "Insufficient privileges you cannot stop paragraph"); Paragraph p = note.getParagraph(paragraphId); checkIfParagraphIsNotNull(p); p.abort(); @@ -791,7 +822,7 @@ public Response registerCronJob(@PathParam("noteId") String noteId, String messa Note note = notebook.getNote(noteId); checkIfNoteIsNotNull(note); - checkIfUserCanWrite(noteId, "Insufficient privileges you cannot set a cron job for this note"); + checkIfUserCanRun(noteId, "Insufficient privileges you cannot set a cron job for this note"); if (!CronExpression.isValidExpression(request.getCronString())) { return new JsonResponse<>(Status.BAD_REQUEST, "wrong cron expressions.").build(); @@ -922,7 +953,8 @@ public Response search(@QueryParam("q") String queryTerm) { String noteId = Id[0]; if (!notebookAuthorization.isOwner(noteId, userAndRoles) && !notebookAuthorization.isReader(noteId, userAndRoles) && - !notebookAuthorization.isWriter(noteId, userAndRoles)) { + !notebookAuthorization.isWriter(noteId, userAndRoles) && + !notebookAuthorization.isRunner(noteId, userAndRoles)) { notesFound.remove(i); i--; } 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 3ddeec034e2..f0e0bb299f6 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 @@ -751,6 +751,25 @@ private boolean hasParagraphReaderPermission(NotebookSocket conn, return true; } + /** + * @return false if user doesn't have runner permission for this paragraph + */ + private boolean hasParagraphRunnerPermission(NotebookSocket conn, + Notebook notebook, String noteId, + HashSet userAndRoles, + String principal, String op) + throws IOException { + + NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization(); + if (!notebookAuthorization.isRunner(noteId, userAndRoles)) { + permissionError(conn, op, principal, userAndRoles, + notebookAuthorization.getOwners(noteId)); + return false; + } + + return true; + } + /** * @return false if user doesn't have writer permission for this paragraph */ @@ -1621,7 +1640,7 @@ private void cancelParagraph(NotebookSocket conn, HashSet userAndRoles, String noteId = getOpenNoteId(conn); - if (!hasParagraphWriterPermission(conn, notebook, noteId, + if (!hasParagraphRunnerPermission(conn, notebook, noteId, userAndRoles, fromMessage.principal, "write")) { return; } @@ -1639,7 +1658,7 @@ private void runAllParagraphs(NotebookSocket conn, HashSet userAndRoles, return; } - if (!hasParagraphWriterPermission(conn, notebook, noteId, + if (!hasParagraphRunnerPermission(conn, notebook, noteId, userAndRoles, fromMessage.principal, "run all paragraphs")) { return; } @@ -1732,7 +1751,7 @@ private void runParagraph(NotebookSocket conn, HashSet userAndRoles, Not String noteId = getOpenNoteId(conn); - if (!hasParagraphWriterPermission(conn, notebook, noteId, + if (!hasParagraphRunnerPermission(conn, notebook, noteId, userAndRoles, fromMessage.principal, "write")) { return; } @@ -2060,7 +2079,7 @@ public void onRemoteRunParagraph(String noteId, String paragraphId) throws Excep Set userAndRoles = Sets.newHashSet(); userAndRoles.add(SecurityUtils.getPrincipal()); userAndRoles.addAll(SecurityUtils.getRoles()); - if (!notebookIns.getNotebookAuthorization().hasWriteAuthorization(userAndRoles, noteId)) { + if (!notebookIns.getNotebookAuthorization().hasRunAuthorization(userAndRoles, noteId)) { throw new ForbiddenException(String.format("can't execute note %s", noteId)); } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java index 38fe5744d9d..7debf1b5d76 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java @@ -258,6 +258,8 @@ public void testGroupPermission() throws Exception { MAX_BROWSER_TIMEOUT_SEC).sendKeys("finance "); pollingWait(By.xpath(".//*[@id='selectReaders']/following::span//input"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("finance "); + pollingWait(By.xpath(".//*[@id='selectRunners']/following::span//input"), + MAX_BROWSER_TIMEOUT_SEC).sendKeys("finance "); pollingWait(By.xpath(".//*[@id='selectWriters']/following::span//input"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("finance "); pollingWait(By.xpath("//button[@ng-click='savePermissions()']"), MAX_BROWSER_TIMEOUT_SEC) diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookSecurityRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookSecurityRestApiTest.java index 367a199da77..c3b0977f1fa 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookSecurityRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookSecurityRestApiTest.java @@ -81,7 +81,7 @@ public void testThatOtherUserCannotAccessNoteIfPermissionSet() throws IOExceptio String noteId = createNoteForUser("test", "admin", "password1"); //set permission - String payload = "{ \"owners\": [\"admin\"], \"readers\": [\"user2\"], \"writers\": [\"user2\"] }"; + String payload = "{ \"owners\": [\"admin\"], \"readers\": [\"user2\"], \"runners\": [\"user2\"], \"writers\": [\"user2\"] }"; PutMethod put = httpPut("/notebook/" + noteId + "/permissions", payload , "admin", "password1"); assertThat("test set note permission method:", put, isAllowed()); put.releaseConnection(); @@ -98,7 +98,7 @@ public void testThatWriterCannotRemoveNote() throws IOException { String noteId = createNoteForUser("test", "admin", "password1"); //set permission - String payload = "{ \"owners\": [\"admin\", \"user1\"], \"readers\": [\"user2\"], \"writers\": [\"user2\"] }"; + String payload = "{ \"owners\": [\"admin\", \"user1\"], \"readers\": [\"user2\"], \"runners\": [\"user2\"], \"writers\": [\"user2\"] }"; PutMethod put = httpPut("/notebook/" + noteId + "/permissions", payload , "admin", "password1"); assertThat("test set note permission method:", put, isAllowed()); put.releaseConnection(); @@ -180,7 +180,7 @@ private void createParagraphForUser(String noteId, String user, String pwd, Stri } private void setPermissionForNote(String noteId, String user, String pwd) throws IOException { - String payload = "{\"owners\":[\"" + user + "\"],\"readers\":[\"" + user + "\"],\"writers\":[\"" + user + "\"]}"; + String payload = "{\"owners\":[\"" + user + "\"],\"readers\":[\"" + user + "\"],\"runners\":[\"" + user + "\"],\"writers\":[\"" + user + "\"]}"; PutMethod put = httpPut(("/notebook/" + noteId + "/permissions"), payload, user, pwd); put.releaseConnection(); } @@ -206,10 +206,11 @@ private void searchNoteBasedOnPermission(String searchText, String user, String ArrayList owners = permissions.get("owners"); ArrayList readers = permissions.get("readers"); ArrayList writers = permissions.get("writers"); + ArrayList runners = permissions.get("runners"); - if (owners.size() != 0 && readers.size() != 0 && writers.size() != 0) { + if (owners.size() != 0 && readers.size() != 0 && writers.size() != 0 && runners.size() != 0) { assertEquals("User has permissions ", true, (owners.contains(user) || readers.contains(user) || - writers.contains(user))); + writers.contains(user) || runners.contains(user))); } getPermission.releaseConnection(); } diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index 4b8b23fe024..bdd47c24326 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -696,6 +696,7 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, $scope.setIamOwner() angular.element('#selectOwners').select2(selectJson) angular.element('#selectReaders').select2(selectJson) + angular.element('#selectRunners').select2(selectJson) angular.element('#selectWriters').select2(selectJson) if (callback) { callback() @@ -735,6 +736,7 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, function convertPermissionsToArray () { $scope.permissions.owners = angular.element('#selectOwners').val() $scope.permissions.readers = angular.element('#selectReaders').val() + $scope.permissions.runners = angular.element('#selectRunners').val() $scope.permissions.writers = angular.element('#selectWriters').val() angular.element('.permissionsForm select').find('option:not([is-select2="false"])').remove() } @@ -1013,7 +1015,8 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, closable: true, title: 'Permissions Saved Successfully', message: 'Owners : ' + $scope.permissions.owners + '\n\n' + 'Readers : ' + - $scope.permissions.readers + '\n\n' + 'Writers : ' + $scope.permissions.writers + $scope.permissions.readers + '\n\n' + 'Runners : ' + $scope.permissions.runners + + '\n\n' + 'Writers : ' + $scope.permissions.writers }) $scope.showPermissions = false }) @@ -1058,6 +1061,7 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, $scope.closePermissions() angular.element('#selectOwners').select2({}) angular.element('#selectReaders').select2({}) + angular.element('#selectRunners').select2({}) angular.element('#selectWriters').select2({}) } else { $scope.openPermissions() diff --git a/zeppelin-web/src/app/notebook/notebook.css b/zeppelin-web/src/app/notebook/notebook.css index a7b7a7f7d96..3a096472448 100644 --- a/zeppelin-web/src/app/notebook/notebook.css +++ b/zeppelin-web/src/app/notebook/notebook.css @@ -219,6 +219,11 @@ display: inline-block; } +.permissions .runners { + width:60px; + display: inline-block; +} + .permissions .writers { width:60px; display: inline-block; diff --git a/zeppelin-web/src/app/notebook/notebook.html b/zeppelin-web/src/app/notebook/notebook.html index b96d08a07f1..98b66feaf5d 100644 --- a/zeppelin-web/src/app/notebook/notebook.html +++ b/zeppelin-web/src/app/notebook/notebook.html @@ -81,13 +81,19 @@

Note Permissions (Only note owners can change)

- Owners can change permissions,read and write the note. + Owners can change permissions,read, run and write the note.

Writers - Writers can read and write the note. + Writers can read, run and write the note. +

+

Runners + + Runners can read and run the note.

Readers +

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ diff --git a/zeppelin-web/src/app/notebook/dynamic-forms/dynamic-forms.directive.js b/zeppelin-web/src/app/notebook/dynamic-forms/dynamic-forms.directive.js new file mode 100644 index 00000000000..40a70eb94e0 --- /dev/null +++ b/zeppelin-web/src/app/notebook/dynamic-forms/dynamic-forms.directive.js @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './dynamic-forms.css' + +angular.module('zeppelinWebApp').directive('dynamicForms', DynamicFormDirective) + +function DynamicFormDirective($templateRequest, $compile) { + return { + restrict: 'AE', + scope: { + id: '=id', + hide: '=hide', + disable: '=disable', + actiononchange: '=actiononchange', + forms: '=forms', + params: '=params', + action: '=action', + removeaction: '=removeaction' + }, + + link: function (scope, element, attrs, controller) { + scope.loadForm = this.loadForm + scope.toggleCheckbox = this.toggleCheckbox + $templateRequest('app/notebook/dynamic-forms/dynamic-forms.directive.html').then(function (formsHtml) { + let forms = angular.element(formsHtml) + element.append(forms) + $compile(forms)(scope) + }) + }, + + loadForm: function (formulaire, params) { + let value = formulaire.defaultValue + if (params[formulaire.name]) { + value = params[formulaire.name] + } + + params[formulaire.name] = value + }, + + toggleCheckbox: function (formulaire, option, params) { + let idx = params[formulaire.name].indexOf(option.value) + if (idx > -1) { + params[formulaire.name].splice(idx, 1) + } else { + params[formulaire.name].push(option.value) + } + } + + } +} diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index 456e46313aa..d09a0b23b64 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -27,6 +27,9 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, ngToast.dismiss() $scope.note = null + $scope.actionOnFormSelectionChange = true + $scope.hideForms = false + $scope.disableForms = false $scope.editorToggled = false $scope.tableToggled = false $scope.viewOnly = false @@ -1367,6 +1370,26 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, } } + $scope.$on('saveNoteForms', function (event, data) { + $scope.note.noteForms = data.formsData.forms + $scope.note.noteParams = data.formsData.params + }) + + $scope.isShowNoteForms = function() { + if ($scope.note && !angular.equals({}, $scope.note.noteForms)) { + return true + } + return false + } + + $scope.saveNoteForms = function () { + websocketMsgSrv.saveNoteForms($scope.note) + } + + $scope.removeNoteForms = function (formName) { + websocketMsgSrv.removeNoteForms($scope.note, formName) + } + $scope.$on('$destroy', function () { angular.element(window).off('beforeunload') $scope.killSaveTimer() diff --git a/zeppelin-web/src/app/notebook/notebook.html b/zeppelin-web/src/app/notebook/notebook.html index 9441f6e17d2..f004e9b325f 100644 --- a/zeppelin-web/src/app/notebook/notebook.html +++ b/zeppelin-web/src/app/notebook/notebook.html @@ -119,6 +119,23 @@

Note Permissions (Only note owners can change)

+
+
+

Note forms

+
+
+
+ +
+
-
-
- -
- -
-
- -
-
- -
-
- -
-
- -
-
-
diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index 7d95d3b3200..c5788416adf 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -466,9 +466,9 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca $scope.runParagraph(paragraphText, true, false) } - $scope.runParagraphFromButton = function (paragraphText) { + $scope.runParagraphFromButton = function () { // we come here from the view, so we don't need to call `$digest()` - $scope.runParagraph(paragraphText, false, false) + $scope.runParagraph($scope.getEditorValue(), false, false) } $scope.turnOnAutoRun = function (paragraph) { @@ -657,24 +657,6 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca commitParagraph(paragraph) } - $scope.loadForm = function (formulaire, params) { - let value = formulaire.defaultValue - if (params[formulaire.name]) { - value = params[formulaire.name] - } - - $scope.paragraph.settings.params[formulaire.name] = value - } - - $scope.toggleCheckbox = function (formulaire, option) { - let idx = $scope.paragraph.settings.params[formulaire.name].indexOf(option.value) - if (idx > -1) { - $scope.paragraph.settings.params[formulaire.name].splice(idx, 1) - } else { - $scope.paragraph.settings.params[formulaire.name].push(option.value) - } - } - $scope.aceChanged = function (_, editor) { let session = editor.getSession() let dirtyText = session.getValue() diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.test.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.test.js index 29b203c3ec6..94230de7f97 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.test.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.test.js @@ -34,7 +34,7 @@ describe('Controller: ParagraphCtrl', function () { let functions = ['isRunning', 'getIframeDimensions', 'cancelParagraph', 'runParagraph', 'saveParagraph', 'moveUp', 'moveDown', 'insertNew', 'removeParagraph', 'toggleEditor', 'closeEditor', 'openEditor', 'closeTable', 'openTable', 'showTitle', 'hideTitle', 'setTitle', 'showLineNumbers', 'hideLineNumbers', - 'changeColWidth', 'columnWidthClass', 'toggleOutput', 'loadForm', + 'changeColWidth', 'columnWidthClass', 'toggleOutput', 'aceChanged', 'aceLoaded', 'getEditorValue', 'getProgress', 'getExecutionTime', 'isResultOutdated'] functions.forEach(function (fn) { diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.css b/zeppelin-web/src/app/notebook/paragraph/paragraph.css index a0bf299ad24..b17272bd2d5 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.css +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.css @@ -303,28 +303,6 @@ table.table-shortcut { margin-left: -3px; } -/* - Paragraph Forms CSS -*/ - -.paragraphForm.form-horizontal .form-group { - margin-right: 0; - margin-left: 0; -} - -.paragraphForm.form-horizontal .form-group label { - padding-left: 0; -} - -.paragraphForm.form-horizontal .form-group .checkbox-item { - padding-left: 0; - padding-right: 10px; -} - -.paragraphForm.form-horizontal .form-group .checkbox-item input { - margin-right: 2px; -} - /* Ace Text Editor CSS */ diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.html b/zeppelin-web/src/app/notebook/paragraph/paragraph.html index f80fb53391e..10afd1754c0 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.html +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.html @@ -49,7 +49,16 @@
-
+
+ +
currentForms = currentGUI.getForms(); final Map currentParams = currentGUI.getParams(); @@ -403,7 +407,8 @@ public Scheduler getScheduler() { private RemoteInterpreterContext convert(InterpreterContext ic) { return new RemoteInterpreterContext(ic.getNoteId(), ic.getParagraphId(), ic.getReplName(), ic.getParagraphTitle(), ic.getParagraphText(), gson.toJson(ic.getAuthenticationInfo()), - gson.toJson(ic.getConfig()), ic.getGui().toJson(), gson.toJson(ic.getRunners())); + gson.toJson(ic.getConfig()), ic.getGui().toJson(), gson.toJson(ic.getNoteGui()), + gson.toJson(ic.getRunners())); } private InterpreterResult convert(RemoteInterpreterResult result) { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java index 9fb0f0e0590..6e667327172 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java @@ -74,6 +74,9 @@ public class Note implements ParagraphJobListener, JsonSerializable { private String name = ""; private String id; + private Map noteParams = new HashMap<>(); + private LinkedHashMap noteForms = new LinkedHashMap<>(); + private transient ZeppelinConfiguration conf = ZeppelinConfiguration.create(); @@ -158,6 +161,22 @@ public String getName() { return name; } + public Map getNoteParams() { + return noteParams; + } + + public void setNoteParams(Map noteParams) { + this.noteParams = noteParams; + } + + public LinkedHashMap getNoteForms() { + return noteForms; + } + + public void setNoteForms(LinkedHashMap noteForms) { + this.noteForms = noteForms; + } + public String getNameWithoutPath() { String notePath = getName(); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java index 6a0c27af294..10a8548798d 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java @@ -408,14 +408,28 @@ && isUserAuthorizedToAccessInterpreter(interpreterSetting.getOption()) == false) if (interpreter.getFormType() == FormType.NATIVE) { settings.clear(); } else if (interpreter.getFormType() == FormType.SIMPLE) { - // inputs will be built from script scriptText - LinkedHashMap inputs = Input.extractSimpleQueryForm(this.scriptText); + // inputs will be built from script body + LinkedHashMap inputs = Input.extractSimpleQueryForm(script, false); + LinkedHashMap noteInputs = Input.extractSimpleQueryForm(script, true); final AngularObjectRegistry angularRegistry = interpreter.getInterpreterGroup().getAngularObjectRegistry(); - String scriptBody = extractVariablesFromAngularRegistry(this.scriptText, inputs, - angularRegistry); + String scriptBody = extractVariablesFromAngularRegistry(script, inputs, angularRegistry); + settings.setForms(inputs); - script = Input.getSimpleQuery(settings.getParams(), scriptBody); + if (!noteInputs.isEmpty()) { + if (!note.getNoteForms().isEmpty()) { + Map currentNoteForms = note.getNoteForms(); + for (String s : noteInputs.keySet()) { + if (!currentNoteForms.containsKey(s)) { + currentNoteForms.put(s, noteInputs.get(s)); + } + } + } else { + note.setNoteForms(noteInputs); + } + } + script = Input.getSimpleQuery(note.getNoteParams(), scriptBody, true); + script = Input.getSimpleQuery(settings.getParams(), script, false); } logger.debug("RUN : " + script); try { @@ -423,6 +437,11 @@ && isUserAuthorizedToAccessInterpreter(interpreterSetting.getOption()) == false) InterpreterContext.set(context); InterpreterResult ret = interpreter.interpret(script, context); + if (interpreter.getFormType() == FormType.NATIVE) { + note.setNoteParams(context.getNoteGui().getParams()); + note.setNoteForms(context.getNoteGui().getForms()); + } + if (Code.KEEP_PREVIOUS_RESULT == ret.code()) { return getReturn(); } @@ -545,8 +564,8 @@ private InterpreterContext getInterpreterContextWithoutRunner(InterpreterOutput InterpreterContext interpreterContext = new InterpreterContext(note.getId(), getId(), intpText, this.getTitle(), - this.getText(), this.getAuthenticationInfo(), this.getConfig(), this.settings, registry, - resourcePool, runners, output); + this.getText(), this.getAuthenticationInfo(), this.getConfig(), this.settings, + getNoteGui(), registry, resourcePool, runners, output); return interpreterContext; } @@ -575,13 +594,12 @@ private InterpreterContext getInterpreterContext(InterpreterOutput output) { InterpreterContext interpreterContext = new InterpreterContext(note.getId(), getId(), intpText, this.getTitle(), - this.getText(), this.getAuthenticationInfo(), this.getConfig(), this.settings, registry, - resourcePool, runners, output); + this.getText(), this.getAuthenticationInfo(), this.getConfig(), this.settings, + getNoteGui(), registry, resourcePool, runners, output); return interpreterContext; } public InterpreterContextRunner getInterpreterContextRunner() { - return new ParagraphRunner(note, note.getId(), getId()); } @@ -743,6 +761,13 @@ public Map getRuntimeInfos() { return runtimeInfos; } + private GUI getNoteGui() { + GUI gui = new GUI(); + gui.setParams(this.note.getNoteParams()); + gui.setForms(this.note.getNoteForms()); + return gui; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java index d99bd598f51..82d96ae0da5 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java @@ -182,8 +182,10 @@ public static enum OP { NOTE_UPDATED, // [s-c] paragraph updated(name, config) RUN_ALL_PARAGRAPHS, // [c-s] run all paragraphs PARAGRAPH_EXECUTED_BY_SPELL, // [c-s] paragraph was executed by spell - RUN_PARAGRAPH_USING_SPELL, // [s-c] run paragraph using spell - PARAS_INFO // [s-c] paragraph runtime infos + RUN_PARAGRAPH_USING_SPELL, // [s-c] run paragraph using spell + PARAS_INFO, // [s-c] paragraph runtime infos + SAVE_NOTE_FORMS, // save note forms + REMOVE_NOTE_FORMS // remove note forms } private static final Gson gson = new Gson(); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/lifecycle/TimeoutLifecycleManagerTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/lifecycle/TimeoutLifecycleManagerTest.java index 971f37638e4..329cb7a1c03 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/lifecycle/TimeoutLifecycleManagerTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/lifecycle/TimeoutLifecycleManagerTest.java @@ -56,7 +56,7 @@ public void testTimeout_1() throws InterpreterException, InterruptedException, I RemoteInterpreter remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test.echo"); InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "repl", "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), - null, null, new ArrayList(), null); + new GUI(), null, null, new ArrayList(), null); remoteInterpreter.interpret("hello world", context); assertTrue(remoteInterpreter.isOpened()); InterpreterSetting interpreterSetting = interpreterSettingManager.getInterpreterSettingByName("test"); @@ -95,7 +95,7 @@ public Map info() { protected Object jobRun() throws Throwable { InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "repl", "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), - null, null, new ArrayList(), null); + new GUI(), null, null, new ArrayList(), null); return remoteInterpreter.interpret("100000", context); } diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java index 54814c45ecd..658fda379be 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java @@ -86,6 +86,7 @@ public void setUp() throws Exception { new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), new AngularObjectRegistry(intp.getInterpreterGroup().getId(), null), new LocalResourcePool("pool1"), new LinkedList(), null); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java index f52803d2dcb..fa2aa42ecc2 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java @@ -76,6 +76,7 @@ private InterpreterContext createInterpreterContext() { new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), null, null, new LinkedList(), null); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java index c268c817616..7f9978a5aa8 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java @@ -94,7 +94,7 @@ public void testSharedMode() throws InterpreterException, IOException { RemoteInterpreter remoteInterpreter2 = (RemoteInterpreter) interpreter2; InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), null, null, new ArrayList(), null); assertEquals("hello", remoteInterpreter1.interpret("hello", context1).message().get(0).getData()); assertEquals(Interpreter.FormType.NATIVE, interpreter1.getFormType()); @@ -137,7 +137,7 @@ public void testScopedMode() throws InterpreterException, IOException { RemoteInterpreter remoteInterpreter2 = (RemoteInterpreter) interpreter2; InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), null, null, new ArrayList(), null); assertEquals("hello", remoteInterpreter1.interpret("hello", context1).message().get(0).getData()); assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); @@ -183,7 +183,7 @@ public void testIsolatedMode() throws InterpreterException, IOException { RemoteInterpreter remoteInterpreter2 = (RemoteInterpreter) interpreter2; InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), null, null, new ArrayList(), null); assertEquals("hello", remoteInterpreter1.interpret("hello", context1).message().get(0).getData()); assertEquals("hello", remoteInterpreter2.interpret("hello", context1).message().get(0).getData()); @@ -224,7 +224,7 @@ public void testExecuteIncorrectPrecode() throws TTransportException, IOExceptio interpreterSetting.setProperty("zeppelin.SleepInterpreter.precode", "fail test"); Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), null, null, new ArrayList(), null); assertEquals(Code.ERROR, interpreter1.interpret("10", context1).code()); } @@ -235,7 +235,7 @@ public void testExecuteCorrectPrecode() throws TTransportException, IOException, interpreterSetting.setProperty("zeppelin.SleepInterpreter.precode", "1"); Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), null, null, new ArrayList(), null); assertEquals(Code.SUCCESS, interpreter1.interpret("10", context1).code()); } @@ -250,7 +250,7 @@ public void testRemoteInterperterErrorStatus() throws TTransportException, IOExc RemoteInterpreter remoteInterpreter1 = (RemoteInterpreter) interpreter1; InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), null, null, new ArrayList(), null); assertEquals(Code.ERROR, remoteInterpreter1.interpret("hello", context1).code()); } @@ -262,7 +262,7 @@ public void testFIFOScheduler() throws InterruptedException, InterpreterExceptio final Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); final InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), null, null, new ArrayList(), null); // run this dummy interpret method first to launch the RemoteInterpreterProcess to avoid the // time overhead of launching the process. @@ -305,7 +305,7 @@ public void testParallelScheduler() throws InterruptedException, InterpreterExce final Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); final InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), null, null, new ArrayList(), null); // run this dummy interpret method first to launch the RemoteInterpreterProcess to avoid the @@ -378,7 +378,7 @@ public void should_push_local_angular_repo_to_remote() throws Exception { interpreter.getInterpreterGroup().setAngularObjectRegistry(registry); final InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), null, null, new ArrayList(), null); InterpreterResult result = interpreter.interpret("dummy", context); @@ -405,7 +405,7 @@ public void testEnvironmentAndProperty() throws InterpreterException { final Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "get"); final InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", - "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), null, null, new ArrayList(), null); assertEquals("VALUE_1", interpreter1.interpret("getEnv ENV_1", context1).message().get(0).getData()); @@ -431,7 +431,7 @@ public void testConvertDynamicForms() throws InterpreterException { Map expected = new LinkedHashMap<>(gui.getForms()); Interpreter interpreter = interpreterSetting.getDefaultInterpreter("user1", "note1"); InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "repl", null, - null, AuthenticationInfo.ANONYMOUS, new HashMap(), gui, + null, AuthenticationInfo.ANONYMOUS, new HashMap(), gui, new GUI(), null, null, new ArrayList(), null); interpreter.interpret("text", context); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java index c7ccd467a3e..6f7c1971c1b 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/resource/DistributedResourcePoolTest.java @@ -61,6 +61,7 @@ public void setUp() throws Exception { new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), null, null, new LinkedList(), diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java index e43a39d52d7..1253789e7ea 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/scheduler/RemoteSchedulerTest.java @@ -121,6 +121,7 @@ protected Object jobRun() throws Throwable { new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), null, new LocalResourcePool("pool1"), new LinkedList(), null)); @@ -182,6 +183,7 @@ public void testAbortOnPending() throws Exception { new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), null, new LocalResourcePool("pool1"), new LinkedList(), null); @@ -236,6 +238,7 @@ public void setResult(Object results) { new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), null, new LocalResourcePool("pool1"), new LinkedList(), null); From 0274cfb7a4dd775c878461bf35431470f48b72b9 Mon Sep 17 00:00:00 2001 From: Keiji Yoshida Date: Thu, 16 Nov 2017 14:28:43 +0900 Subject: [PATCH 111/492] [ZEPPELIN-3054] Update conf/shiro.ini.template so that non-admin users can restart interpreters on a notebook page ### What is this PR for? On the default configuration of `conf/shiro.ini.template`, non admin users cannot restart interpreters on their notebook pages because the path `/api/interpreter/setting/restart/**` is not allowed to being accessed by non admin users. screen shot 2017-11-16 at 14 07 05 It is convenient that all authenticated users can restart interpreters on their notebook pages by default, so the following line should be inserted into conf/shiro.ini.template: ``` /api/interpreter/setting/restart/** = authc ``` ### What type of PR is it? [Improvement] ### Todos ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-3054 ### How should this be tested? * Tested manually. I confirmed that non admin users can restart interpreters on a notebook page but still cannot access to the interpreter settings page by using `conf/shiro.ini.template`. ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No. * Is there breaking changes for older versions? No. * Does this needs documentation? No. Author: Keiji Yoshida Closes #2673 from kjmrknsn/ZEPPELIN-3054 and squashes the following commits: 37e736d [Keiji Yoshida] [ZEPPELIN-3054] Update conf/shiro.ini.template so that non-admin users can restart interpreters on a notebook page --- conf/shiro.ini.template | 3 +++ 1 file changed, 3 insertions(+) diff --git a/conf/shiro.ini.template b/conf/shiro.ini.template index 756ba79b7a2..b30635966bf 100644 --- a/conf/shiro.ini.template +++ b/conf/shiro.ini.template @@ -96,6 +96,9 @@ admin = * # uncomment the line second last line (/** = anon) and comment the last line (/** = authc) # /api/version = anon +# Allow all authenticated users to restart interpreters on a notebook page. +# Comment out the following line if you would like to authorize only admin users to restart interpreters. +/api/interpreter/setting/restart/** = authc /api/interpreter/** = authc, roles[admin] /api/configurations/** = authc, roles[admin] /api/credential/** = authc, roles[admin] From 2d60d0b678ed72426bed0e90e3954b1d763044b6 Mon Sep 17 00:00:00 2001 From: Prabhjyot Date: Sun, 19 Nov 2017 08:40:11 +0530 Subject: [PATCH 112/492] [ZEPPELIN-3063] Notebook loses formatting when importing from 0.6.x Notebook loses formatting (shows table instead of the graph) when importing from 0.6.x if the respective paragraph doesn't have result in it. [Bug Fix] * [ZEPPELIN-3063](https://issues.apache.org/jira/browse/ZEPPELIN-3063) Before: ![before](https://user-images.githubusercontent.com/674497/32978117-8d4d581a-cc61-11e7-8b48-af389f4be90d.gif) After: ![after](https://user-images.githubusercontent.com/674497/32978119-95ca87c4-cc61-11e7-8276-9e1eab8711ef.gif) * Try importing [this](https://issues.apache.org/jira/secure/attachment/12898326/oldnotebook-0.6-clear.json) notebook, and then on running this notebook, the second paragraph should display BarChart instead of a table. Author: Prabhjyot Author: prabhjyotsingh Closes #2678 from prabhjyotsingh/ZEPPELIN-3063 and squashes the following commits: 3824eed01 [Prabhjyot] Add comment support for 0.6 1975c74e7 [prabhjyotsingh] ZEPPELIN-3063: Notebook loses formatting when importing from 0.6.x Change-Id: Ife7b557f9517fb6b076cd532354d2b431ed81a12 --- .../apache/zeppelin/notebook/Notebook.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java index c1dc46ca2cf..3baf4f16c44 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java @@ -445,6 +445,25 @@ public void convertFromSingleResultToMultipleResultsFormat(Note note) { results.add(new HashMap<>()); } } + config.put("results", results); + } + } else if (ret == null && p.getConfig() != null) { + //ZEPPELIN-3063 Notebook loses formatting when importing from 0.6.x + if (p.getConfig().get("graph") != null && p.getConfig().get("graph") instanceof Map + && !((Map) p.getConfig().get("graph")).get("mode").equals("table")) { + Map config = p.getConfig(); + Object graph = config.remove("graph"); + Object apps = config.remove("apps"); + Object helium = config.remove("helium"); + + List results = new LinkedList<>(); + + HashMap res = new HashMap<>(); + res.put("graph", graph); + res.put("apps", apps); + res.put("helium", helium); + results.add(res); + config.put("results", results); } } From 7db9ab47224d29c5f53b81bb65383c3581f8817f Mon Sep 17 00:00:00 2001 From: Keiji Yoshida Date: Mon, 20 Nov 2017 22:54:39 +0900 Subject: [PATCH 113/492] [ZEPPELIN-2995] "auto-restart interpreter on cron execution" should restart interpreter to specific note, not all interpreters ### What is this PR for? Make "auto-restart interpreter on cron execution" restart the interpreters which are specific to the note, not all interpreters. This issue was reported by https://github.com/apache/zeppelin/pull/1302#issuecomment-336521420. ### What type of PR is it? [Bug Fix] ### Todos ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-2995 ### How should this be tested? * Tested Manually. * I confirmed that the "auto-restart interpreter on cron execution" feature restarted only the interpreters which were specific to the notebook. ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No. * Is there breaking changes for older versions? No. * Does this needs documentation? No. Author: Keiji Yoshida Closes #2681 from kjmrknsn/ZEPPELIN-2995 and squashes the following commits: 43765a5 [Keiji Yoshida] [ZEPPELIN-2995] "auto-restart interpreter on cron execution" should restart interpreter to specific note, not all interpreters --- .../apache/zeppelin/notebook/Notebook.java | 11 ++- .../zeppelin/notebook/NotebookTest.java | 82 +++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java index 3baf4f16c44..8de981e6e32 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java @@ -901,10 +901,14 @@ public void execute(JobExecutionContext context) throws JobExecutionException { } boolean releaseResource = false; + String cronExecutingUser = null; try { Map config = note.getConfig(); - if (config != null && config.containsKey("releaseresource")) { - releaseResource = (boolean) note.getConfig().get("releaseresource"); + if (config != null) { + if (config.containsKey("releaseresource")) { + releaseResource = (boolean) config.get("releaseresource"); + } + cronExecutingUser = (String) config.get("cronExecutingUser"); } } catch (ClassCastException e) { logger.error(e.getMessage(), e); @@ -913,7 +917,8 @@ public void execute(JobExecutionContext context) throws JobExecutionException { for (InterpreterSetting setting : notebook.getInterpreterSettingManager() .getInterpreterSettings(note.getId())) { try { - notebook.getInterpreterSettingManager().restart(setting.getId()); + notebook.getInterpreterSettingManager().restart(setting.getId(), noteId, + cronExecutingUser != null ? cronExecutingUser : "anonymous"); } catch (InterpreterException e) { logger.error("Fail to restart interpreter: " + setting.getId(), e); } diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java index 511b4e5116d..ba9e177690e 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java @@ -450,6 +450,88 @@ public void testAutoRestartInterpreterAfterSchedule() throws InterruptedExceptio notebook.removeNote(note.getId(), anonymous); } + @Test + public void testCronWithReleaseResourceClosesOnlySpecificInterpreters() + throws IOException, InterruptedException { + // create a cron scheduled note. + Note cronNote = notebook.createNote(anonymous); + interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), cronNote.getId(), + Arrays.asList(interpreterSettingManager.getInterpreterSettingByName("mock1").getId())); + cronNote.setConfig(new HashMap() { + { + put("cron", "1/5 * * * * ?"); + put("cronExecutingUser", anonymous.getUser()); + put("releaseresource", true); + } + }); + RemoteInterpreter cronNoteInterpreter = + (RemoteInterpreter) interpreterFactory.getInterpreter(anonymous.getUser(), + cronNote.getId(), "mock1"); + + // create a paragraph of the cron scheduled note. + Paragraph cronNoteParagraph = cronNote.addNewParagraph(AuthenticationInfo.ANONYMOUS); + cronNoteParagraph.setConfig(new HashMap() { + { put("enabled", true); } + }); + cronNoteParagraph.setText("%mock1 sleep 1000"); + + // create another note + Note anotherNote = notebook.createNote(anonymous); + interpreterSettingManager.setInterpreterBinding(anonymous.getUser(), anotherNote.getId(), + Arrays.asList(interpreterSettingManager.getInterpreterSettingByName("mock2").getId())); + RemoteInterpreter anotherNoteInterpreter = + (RemoteInterpreter) interpreterFactory.getInterpreter(anonymous.getUser(), + anotherNote.getId(), "mock2"); + + // create a paragraph of another note + Paragraph anotherNoteParagraph = anotherNote.addNewParagraph(AuthenticationInfo.ANONYMOUS); + anotherNoteParagraph.setConfig(new HashMap() { + { put("enabled", true); } + }); + anotherNoteParagraph.setText("%mock2 echo 1"); + + // run the paragraph of another note + anotherNote.run(anotherNoteParagraph.getId()); + + // wait until anotherNoteInterpreter is opened + while (!anotherNoteInterpreter.isOpened()) { + Thread.yield(); + } + + // refresh the cron schedule + notebook.refreshCron(cronNote.getId()); + + // wait until cronNoteInterpreter is opened + while (!cronNoteInterpreter.isOpened()) { + Thread.yield(); + } + + // wait until cronNoteInterpreter is closed + while (cronNoteInterpreter.isOpened()) { + Thread.yield(); + } + + // wait for a few seconds + Thread.sleep(5 * 1000); + + // test that anotherNoteInterpreter is still opened + assertTrue(anotherNoteInterpreter.isOpened()); + + // remove cron scheduler + cronNote.setConfig(new HashMap() { + { + put("cron", null); + put("cronExecutingUser", null); + put("releaseresource", null); + } + }); + notebook.refreshCron(cronNote.getId()); + + // remove notebooks + notebook.removeNote(cronNote.getId(), anonymous); + notebook.removeNote(anotherNote.getId(), anonymous); + } + @Test public void testExportAndImportNote() throws IOException, CloneNotSupportedException, InterruptedException, InterpreterException, SchedulerException, RepositoryException { From 6f8b29f5f53d0ea871e618365367587d2377a793 Mon Sep 17 00:00:00 2001 From: Tess Date: Tue, 31 Oct 2017 20:48:45 -0400 Subject: [PATCH 114/492] [ZEPPELIN-3062] Removes ctrl+s default behavior from notebook ### What is this PR for? As a programmer I habitually press CTRL + S out of fear for my sanity. Other web text editing tools (google docs, jupyter notebooks) ignore the base functionality of CTRL + S of saving the HTML page, and do nothing instead. This is a small change that will makes the user experience of Zeppelin notebooks much much better. ### What type of PR is it? Improvement ### Todos ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-3062 ### How should this be tested? * I tested manually: 1. Ran `yarn run dev` 2. Went to `localhost:9000` 3. Created a new note, now anytime a user presses 'CTRL+S' in the scope of the note nothing happens, and the user can continue coding uninterrupted. ### Screenshots (if appropriate) ### Questions: Author: Tess Closes #2677 from tessbianchi/ZEPPELIN-3062 and squashes the following commits: db80533 [Tess] Removes ctrl+s default behavior from notebook --- zeppelin-web/src/app/notebook/notebook.controller.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index d09a0b23b64..48fc6e7154e 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -173,15 +173,23 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, // register mouseevent handler for focus paragraph document.addEventListener('click', $scope.focusParagraphOnClick) - $scope.keyboardShortcut = function (keyEvent) { + let keyboardShortcut = function (keyEvent) { // handle keyevent if (!$scope.viewOnly && !$scope.revisionView) { $scope.$broadcast('keyEvent', keyEvent) } } + $scope.keydownEvent = function (keyEvent) { + if ((keyEvent.ctrlKey || keyEvent.metaKey) && String.fromCharCode(keyEvent.which).toLowerCase() === 's') { + keyEvent.preventDefault() + } + + keyboardShortcut(keyEvent) + } + // register mouseevent handler for focus paragraph - document.addEventListener('keydown', $scope.keyboardShortcut) + document.addEventListener('keydown', $scope.keydownEvent) $scope.paragraphOnDoubleClick = function (paragraphId) { $scope.$broadcast('doubleClickParagraph', paragraphId) From 4f95555edca35f2576cdfe32c7467c5aad6583f8 Mon Sep 17 00:00:00 2001 From: prabhjyotsingh Date: Thu, 23 Nov 2017 14:35:48 +0530 Subject: [PATCH 115/492] ZEPPELIN-3072: Zeppelin UI becomes slow/unresponsive if there are too many notebooks Zeppelin UI becomes slow/unresponsive if there are too many notebooks Have attached a notebook directory in [JIRA](https://issues.apache.org/jira/secure/attachment/12898650/notebook.zip) with 500+ notebooks, now with these notebooks, every time user goes to homepage Zeppelin UI becomes unresponsive for few seconds. [Bug Fix | Improvement] * [x] - Fix search box * [x] - Order of notebook * [ZEPPELIN-3072](https://issues.apache.org/jira/browse/ZEPPELIN-3072) Create 500+ notebook, or import it from [JIRA](https://issues.apache.org/jira/secure/attachment/12898650/notebook.zip), now observe UI it becomes slow/laggy while homepage is rendering. Before: ![before](https://user-images.githubusercontent.com/674497/33070354-c36acdfa-cedd-11e7-81f9-ff0b526622f3.gif) After: ![after](https://user-images.githubusercontent.com/674497/33070353-c3317988-cedd-11e7-9431-fcf596928c3b.gif) * Does the licenses files need update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: prabhjyotsingh Closes #2683 from prabhjyotsingh/ZEPPELIN-3072 and squashes the following commits: 06b8ef801 [prabhjyotsingh] add license file 483a3ff77 [prabhjyotsingh] navbar dropdown bug eb506ba9e [prabhjyotsingh] fix test 00ec295dc [prabhjyotsingh] fix sort and serch b4cbba891 [prabhjyotsingh] ZEPPELIN-3072: Zeppelin UI becomes slow/unresponsive if there are too many notebooks Change-Id: Ibc157312b726b9704cab088192a39e942d8da43d --- LICENSE | 1 + licenses/LICENSE-ngInfiniteScroll-1.3.4 | 22 +++++++ zeppelin-web/bower.json | 3 +- zeppelin-web/karma.conf.js | 1 + zeppelin-web/src/app/app.js | 1 + zeppelin-web/src/app/home/home.controller.js | 5 ++ zeppelin-web/src/app/home/home.html | 10 +-- zeppelin-web/src/app/home/notebook.html | 8 +-- .../array-ordering/array-ordering.service.js | 4 +- .../expand-collapse.directive.js | 6 +- .../navbar/navbar-note-list-elem.html | 16 ++--- .../components/navbar/navbar.controller.js | 5 ++ .../src/components/navbar/navbar.html | 8 +-- .../components/note-list/note-list.factory.js | 3 +- .../note-list/note-list.factory.test.js | 64 +++++++++---------- zeppelin-web/src/index.html | 1 + 16 files changed, 98 insertions(+), 60 deletions(-) create mode 100644 licenses/LICENSE-ngInfiniteScroll-1.3.4 diff --git a/LICENSE b/LICENSE index 142bd4978e7..c1f6f7eb109 100644 --- a/LICENSE +++ b/LICENSE @@ -235,6 +235,7 @@ The text of each license is also included at licenses/LICENSE-[project]-[version (The MIT License) Simple line icons v1.0.0 (http://thesabbir.github.io/simple-line-icons/) - https://github.com/thesabbir/simple-line-icons/tree/1.0.0 (The MIT License) jekyll-bootstrap 0.3.0 (https://github.com/plusjade/jekyll-bootstrap) - https://github.com/plusjade/jekyll-bootstrap (The MIT License) jekyll 1.3.0 (http://jekyllrb.com/) - https://github.com/jekyll/jekyll/blob/v1.3.0/LICENSE + (The MIT License) ngInfiniteScroll 1.3.4 (https://github.com/sroze/ngInfiniteScroll) - https://github.com/sroze/ngInfiniteScroll/blob/master/LICENSE ======================================================================== MIT-style licenses diff --git a/licenses/LICENSE-ngInfiniteScroll-1.3.4 b/licenses/LICENSE-ngInfiniteScroll-1.3.4 new file mode 100644 index 00000000000..44ae2bfc404 --- /dev/null +++ b/licenses/LICENSE-ngInfiniteScroll-1.3.4 @@ -0,0 +1,22 @@ +Copyright (c) 2012 Michelle Tilley + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/zeppelin-web/bower.json b/zeppelin-web/bower.json index 2b5135f9b8e..3de9f5653a4 100644 --- a/zeppelin-web/bower.json +++ b/zeppelin-web/bower.json @@ -33,7 +33,8 @@ "select2": "^4.0.3", "MathJax": "2.7.0", "ngclipboard": "^1.1.1", - "jsdiff": "3.3.0" + "jsdiff": "3.3.0", + "ngInfiniteScroll": "^1.3.4" }, "devDependencies": { "angular-mocks": "1.5.7" diff --git a/zeppelin-web/karma.conf.js b/zeppelin-web/karma.conf.js index 8a03bec1c2f..3e573a98f17 100644 --- a/zeppelin-web/karma.conf.js +++ b/zeppelin-web/karma.conf.js @@ -87,6 +87,7 @@ module.exports = function(config) { 'bower_components/clipboard/dist/clipboard.js', 'bower_components/ngclipboard/dist/ngclipboard.js', 'bower_components/jsdiff/diff.js', + 'bower_components/ngInfiniteScroll/build/ng-infinite-scroll.js', 'bower_components/angular-mocks/angular-mocks.js', // endbower diff --git a/zeppelin-web/src/app/app.js b/zeppelin-web/src/app/app.js index d46d026858a..5a4c0161588 100644 --- a/zeppelin-web/src/app/app.js +++ b/zeppelin-web/src/app/app.js @@ -44,6 +44,7 @@ const requiredModules = [ 'ngResource', 'ngclipboard', 'angularViewportWatch', + 'infinite-scroll', 'ui.grid', 'ui.grid.exporter', 'ui.grid.edit', 'ui.grid.rowEdit', diff --git a/zeppelin-web/src/app/home/home.controller.js b/zeppelin-web/src/app/home/home.controller.js index 2cf84395558..d2823dd6f6f 100644 --- a/zeppelin-web/src/app/home/home.controller.js +++ b/zeppelin-web/src/app/home/home.controller.js @@ -24,6 +24,7 @@ function HomeCtrl ($scope, noteListFactory, websocketMsgSrv, $rootScope, arrayOr vm.websocketMsgSrv = websocketMsgSrv vm.arrayOrderingSrv = arrayOrderingSrv vm.noteActionService = noteActionService + vm.numberOfNotesDisplayed = window.innerHeight / 20 vm.notebookHome = false vm.noteCustomHome = true @@ -85,6 +86,10 @@ function HomeCtrl ($scope, noteListFactory, websocketMsgSrv, $rootScope, arrayOr } }) + $scope.loadMoreNotes = function () { + vm.numberOfNotesDisplayed += 10 + } + $scope.renameNote = function (nodeId, nodePath) { vm.noteActionService.renameNote(nodeId, nodePath) } diff --git a/zeppelin-web/src/app/home/home.html b/zeppelin-web/src/app/home/home.html index 1ab971898fd..0285754113a 100644 --- a/zeppelin-web/src/app/home/home.html +++ b/zeppelin-web/src/app/home/home.html @@ -40,16 +40,16 @@
Create new note
diff --git a/zeppelin-web/src/app/home/notebook.html b/zeppelin-web/src/app/home/notebook.html index a6f2416a691..ff1eb75f299 100644 --- a/zeppelin-web/src/app/home/notebook.html +++ b/zeppelin-web/src/app/home/notebook.html @@ -27,12 +27,12 @@
Create new note
  • -
    -
  • +
  • -
    -
  • +
diff --git a/zeppelin-web/src/components/array-ordering/array-ordering.service.js b/zeppelin-web/src/components/array-ordering/array-ordering.service.js index 850a5da1cf2..6fa1ad9c28a 100644 --- a/zeppelin-web/src/components/array-ordering/array-ordering.service.js +++ b/zeppelin-web/src/components/array-ordering/array-ordering.service.js @@ -35,8 +35,8 @@ function ArrayOrderingService(TRASH_FOLDER_ID) { } this.noteComparator = function (v1, v2) { - let note1 = v1.value - let note2 = v2.value + let note1 = v1.value || v1 + let note2 = v2.value || v2 if (note1.id === TRASH_FOLDER_ID) { return 1 diff --git a/zeppelin-web/src/components/navbar/expand-collapse/expand-collapse.directive.js b/zeppelin-web/src/components/navbar/expand-collapse/expand-collapse.directive.js index 95e068180db..e4280e865a8 100644 --- a/zeppelin-web/src/components/navbar/expand-collapse/expand-collapse.directive.js +++ b/zeppelin-web/src/components/navbar/expand-collapse/expand-collapse.directive.js @@ -21,11 +21,11 @@ function expandCollapseDirective() { restrict: 'EA', link: function (scope, element, attrs) { angular.element(element).click(function (event) { - if (angular.element(element).find('.expandable:visible').length > 1) { - angular.element(element).find('.expandable:visible').slideUp('slow') + if (angular.element(element).next('.expandable:visible').length > 1) { + angular.element(element).next('.expandable:visible').slideUp('slow') angular.element(element).find('i.fa-folder-open').toggleClass('fa-folder fa-folder-open') } else { - angular.element(element).find('.expandable').first().slideToggle('200', function () { + angular.element(element).next('.expandable').first().slideToggle('200', function () { // do not toggle trash folder if (angular.element(element).find('.fa-trash-o').length === 0) { angular.element(element).find('i').first().toggleClass('fa-folder fa-folder-open') diff --git a/zeppelin-web/src/components/navbar/navbar-note-list-elem.html b/zeppelin-web/src/components/navbar/navbar-note-list-elem.html index ad1f771f2b2..911f1f1f3e8 100644 --- a/zeppelin-web/src/components/navbar/navbar-note-list-elem.html +++ b/zeppelin-web/src/components/navbar/navbar-note-list-elem.html @@ -38,13 +38,13 @@ - + diff --git a/zeppelin-web/src/components/navbar/navbar.controller.js b/zeppelin-web/src/components/navbar/navbar.controller.js index 0ac2f18bb56..e92813b6da1 100644 --- a/zeppelin-web/src/components/navbar/navbar.controller.js +++ b/zeppelin-web/src/components/navbar/navbar.controller.js @@ -30,6 +30,7 @@ function NavCtrl ($scope, $rootScope, $http, $routeParams, $location, vm.showLoginWindow = showLoginWindow vm.TRASH_FOLDER_ID = TRASH_FOLDER_ID vm.isFilterNote = isFilterNote + vm.numberOfNotesDisplayed = 10 $scope.query = {q: ''} @@ -153,6 +154,10 @@ function NavCtrl ($scope, $rootScope, $http, $routeParams, $location, }) } + $scope.loadMoreNotes = function () { + vm.numberOfNotesDisplayed += 10 + } + $scope.calculateTooltipPlacement = function (note) { if (note !== undefined && note.name !== undefined) { let length = note.name.length diff --git a/zeppelin-web/src/components/navbar/navbar.html b/zeppelin-web/src/components/navbar/navbar.html index 597ed511e68..59d65c9936b 100644 --- a/zeppelin-web/src/components/navbar/navbar.html +++ b/zeppelin-web/src/components/navbar/navbar.html @@ -46,13 +46,13 @@
  • -
    -
  • +
  • -
    -
  • +
  • diff --git a/zeppelin-web/src/components/note-list/note-list.factory.js b/zeppelin-web/src/components/note-list/note-list.factory.js index 21abbc046d8..5e2c513821c 100644 --- a/zeppelin-web/src/components/note-list/note-list.factory.js +++ b/zeppelin-web/src/components/note-list/note-list.factory.js @@ -14,7 +14,7 @@ angular.module('zeppelinWebApp').factory('noteListFactory', NoteListFactory) -function NoteListFactory(TRASH_FOLDER_ID) { +function NoteListFactory(arrayOrderingSrv, TRASH_FOLDER_ID) { 'ngInject' const notes = { @@ -42,6 +42,7 @@ function NoteListFactory(TRASH_FOLDER_ID) { return root }, notes.root) + notes.root.children.sort(arrayOrderingSrv.noteComparator) } } diff --git a/zeppelin-web/src/components/note-list/note-list.factory.test.js b/zeppelin-web/src/components/note-list/note-list.factory.test.js index 58d5d420b5c..c16504c8785 100644 --- a/zeppelin-web/src/components/note-list/note-list.factory.test.js +++ b/zeppelin-web/src/components/note-list/note-list.factory.test.js @@ -38,38 +38,38 @@ describe('Factory: NoteList', function () { let folderList = noteList.root.children expect(folderList.length).toBe(5) - expect(folderList[0].name).toBe('A') - expect(folderList[0].id).toBe('000001') - expect(folderList[1].name).toBe('B') + expect(folderList[3].name).toBe('A') + expect(folderList[3].id).toBe('000001') + expect(folderList[4].name).toBe('B') expect(folderList[2].name).toBe('000003') - expect(folderList[3].name).toBe('C') - expect(folderList[3].id).toBe('C') - expect(folderList[3].children.length).toBe(3) - expect(folderList[3].children[0].name).toBe('CA') - expect(folderList[3].children[0].id).toBe('000004') - expect(folderList[3].children[0].children).toBeUndefined() - expect(folderList[3].children[1].name).toBe('CB') - expect(folderList[3].children[1].id).toBe('000005') - expect(folderList[3].children[1].children).toBeUndefined() - expect(folderList[3].children[2].name).toBe('CB') - expect(folderList[3].children[2].id).toBe('C/CB') - expect(folderList[3].children[2].children.length).toBe(3) - expect(folderList[3].children[2].children[0].name).toBe('CBA') - expect(folderList[3].children[2].children[0].id).toBe('000006') - expect(folderList[3].children[2].children[0].children).toBeUndefined() - expect(folderList[3].children[2].children[1].name).toBe('CBA') - expect(folderList[3].children[2].children[1].id).toBe('000007') - expect(folderList[3].children[2].children[1].children).toBeUndefined() - expect(folderList[3].children[2].children[2].name).toBe('CBB') - expect(folderList[3].children[2].children[2].id).toBe('000008') - expect(folderList[3].children[2].children[2].children).toBeUndefined() - expect(folderList[4].name).toBe('D') - expect(folderList[4].id).toBe('D') - expect(folderList[4].children.length).toBe(1) - expect(folderList[4].children[0].name).toBe('D[A') - expect(folderList[4].children[0].id).toBe('D/D[A') - expect(folderList[4].children[0].children[0].name).toBe('DA]B') - expect(folderList[4].children[0].children[0].id).toBe('000009') - expect(folderList[4].children[0].children[0].children).toBeUndefined() + expect(folderList[0].name).toBe('C') + expect(folderList[0].id).toBe('C') + expect(folderList[0].children.length).toBe(3) + expect(folderList[0].children[0].name).toBe('CA') + expect(folderList[0].children[0].id).toBe('000004') + expect(folderList[0].children[0].children).toBeUndefined() + expect(folderList[0].children[1].name).toBe('CB') + expect(folderList[0].children[1].id).toBe('000005') + expect(folderList[0].children[1].children).toBeUndefined() + expect(folderList[0].children[2].name).toBe('CB') + expect(folderList[0].children[2].id).toBe('C/CB') + expect(folderList[0].children[2].children.length).toBe(3) + expect(folderList[0].children[2].children[0].name).toBe('CBA') + expect(folderList[0].children[2].children[0].id).toBe('000006') + expect(folderList[0].children[2].children[0].children).toBeUndefined() + expect(folderList[0].children[2].children[1].name).toBe('CBA') + expect(folderList[0].children[2].children[1].id).toBe('000007') + expect(folderList[0].children[2].children[1].children).toBeUndefined() + expect(folderList[0].children[2].children[2].name).toBe('CBB') + expect(folderList[0].children[2].children[2].id).toBe('000008') + expect(folderList[0].children[2].children[2].children).toBeUndefined() + expect(folderList[1].name).toBe('D') + expect(folderList[1].id).toBe('D') + expect(folderList[1].children.length).toBe(1) + expect(folderList[1].children[0].name).toBe('D[A') + expect(folderList[1].children[0].id).toBe('D/D[A') + expect(folderList[1].children[0].children[0].name).toBe('DA]B') + expect(folderList[1].children[0].children[0].id).toBe('000009') + expect(folderList[1].children[0].children[0].children).toBeUndefined() }) }) diff --git a/zeppelin-web/src/index.html b/zeppelin-web/src/index.html index 9a126f1b437..15a5085f635 100644 --- a/zeppelin-web/src/index.html +++ b/zeppelin-web/src/index.html @@ -166,6 +166,7 @@ + From acc1a7cfe78d0d8f69eff68ebb8f5249977e2634 Mon Sep 17 00:00:00 2001 From: Jens Grassel Date: Thu, 16 Nov 2017 12:09:35 +0100 Subject: [PATCH 116/492] Add missing build dependencies. Add `apt-get install` lines for R dependencies that are needed to build successfully. ### What is this PR for? This pull request adds installation instructions for building which relate to missing dependencies resulting in failing to build (if using R). ### What type of PR is it? [Documentation] Author: Jens Grassel Closes #2674 from jan0sch/docs-missing-build-deps and squashes the following commits: aa581c1 [Jens Grassel] Add missing build dependencies. --- docs/setup/basics/how_to_build.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/setup/basics/how_to_build.md b/docs/setup/basics/how_to_build.md index f5eb96945d1..f78c631ab04 100644 --- a/docs/setup/basics/how_to_build.md +++ b/docs/setup/basics/how_to_build.md @@ -254,6 +254,8 @@ sudo apt-get install git sudo apt-get install openjdk-7-jdk sudo apt-get install npm sudo apt-get install libfontconfig +sudo apt-get install r-base-dev +sudo apt-get install r-cran-evaluate ``` From 83164c84313a4594baa3127c05f83411dd3f05d6 Mon Sep 17 00:00:00 2001 From: byamthev Date: Tue, 14 Nov 2017 13:42:18 +0200 Subject: [PATCH 117/492] ZEPPELIN-3037 Configure Http Request Header Size Limit for Jetty ### What is this PR for? In some deployment scenarios it is necessary to increase jetty.request.header.size, which default value is 8192. This will reduce the chance of HTTP Error 413 Request entity too large. There should be a mechanism to configure this setting. ### What type of PR is it? [Feature] ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3037 ### How should this be tested? * There is an integration test (automated unit test included) for testing this feature. * To test manually, after increasing setting, make any http request to zeppelin with a request header bigger than 8K , you should not get an HTTP Error 413 Request entity too large. ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? Yes. Author: byamthev Closes #2663 from byamthev/zeppelin3037 and squashes the following commits: 8ff2620 [byamthev] [ZEPPELIN-3037] Configure Http Request Header Size Limit for Jetty --- conf/zeppelin-site.xml.template | 9 +++ .../zeppelin/conf/ZeppelinConfiguration.java | 5 ++ .../zeppelin/server/ZeppelinServer.java | 17 +++-- .../configuration/RequestHeaderSizeTest.java | 66 +++++++++++++++++++ .../conf/ZeppelinConfigurationTest.java | 7 ++ 5 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 zeppelin-server/src/test/java/org/apache/zeppelin/configuration/RequestHeaderSizeTest.java diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template index b59d8783727..3c5bbeae59a 100755 --- a/conf/zeppelin-site.xml.template +++ b/conf/zeppelin-site.xml.template @@ -443,6 +443,15 @@ Hardcoding Application Server name to Prevent Fingerprinting --> + + + From 4dc6bf57080e11ebf0533bbedcdba780c5e71398 Mon Sep 17 00:00:00 2001 From: liguohui Date: Tue, 28 Nov 2017 22:53:48 +0800 Subject: [PATCH 120/492] [ZEPPELIN-3076]Chart field is also draggable and sortable in the 'keys', 'groups' and 'values' ### What is this PR for? Current the `keys`, `groups` and `values` is only droppable. It is a little inconvenient if I want to drag it to other place or sort it. This feature let `keys`, `groups` and `values` not only `droppable`, but also `draggable` and `sorttable`. ### What type of PR is it? [Feature] ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-3076 [ZEPPELIN-3076] ### How should this be tested? I have Add the Screenshots for the test. Just as the Screenshots showing, all the element in `keys`, `groups` and `values` is draggable and sorttable. ![untitled project4](https://user-images.githubusercontent.com/5969176/33252922-ed1ab0b4-d37b-11e7-8a5c-b3dbb6765d18.gif) ### Questions: * Does the licenses files need update? NO * Is there breaking changes for older versions? NO * Does this needs documentation? NO Author: liguohui Closes #2686 from liguohuicmss/draggable-field-keys-groups-values2 and squashes the following commits: 08b86b8 [liguohui] add a new line ebb8436 [liguohui] Chart field is also draggable and sortable in the 'keys', 'groups' and 'values' --- .../src/app/tabledata/pivot_settings.html | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/zeppelin-web/src/app/tabledata/pivot_settings.html b/zeppelin-web/src/app/tabledata/pivot_settings.html index abfe73069be..68de019c275 100644 --- a/zeppelin-web/src/app/tabledata/pivot_settings.html +++ b/zeppelin-web/src/app/tabledata/pivot_settings.html @@ -49,7 +49,11 @@ data-drop="true" jqyoui-droppable="{multiple:true, onDrop:'save()'}" class="list-unstyled" style="border-radius: 6px; margin-top: 7px;"> -
  • +
  • {{item.name}} @@ -69,7 +73,11 @@ jqyoui-droppable="{multiple:true, onDrop:'save()'}" class="list-unstyled" style="border-radius: 6px; margin-top: 7px;"> -
  • +
  • {{item.name}} @@ -89,7 +97,11 @@ jqyoui-droppable="{multiple:true, onDrop:'save()'}" class="list-unstyled" style="border-radius: 6px; margin-top: 7px;"> -
  • +
  • + From 625b268553efe199710b772d4c589d8d526e366f Mon Sep 17 00:00:00 2001 From: Keiji Yoshida Date: Sat, 2 Dec 2017 00:55:06 +0900 Subject: [PATCH 121/492] [hotfix] Downgrade JGit from 4.9.0 to 4.5.4 ### What is this PR for? The version of JGit was updated to 4.9.0 at https://github.com/apache/zeppelin/pull/2658. However, this version does not support Java 7 and Travis CI test always fails now: https://travis-ci.org/kjmrknsn/zeppelin/builds/310104872 To fix this issue, downgrade JGit from 4.9.0 to 4.5.4 which is the latest JGit version which supports Java 7: https://projects.eclipse.org/projects/technology.jgit. I confirmed that Travis CI test was passed with JGit 4.5.4: https://travis-ci.org/kjmrknsn/zeppelin/builds/310107611 Now, all PRs to Zeppelin don't pass Travis CI tests, so it's preferable that this PR is merged soon. ### What type of PR is it? [Bug Fix] ### Todos ### What is the Jira issue? ### How should this be tested? I confirmed that Travis CI test passed with JGit 4.5.4: https://travis-ci.org/kjmrknsn/zeppelin/builds/310107611 ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? Yes. zeppelin-distribution/src/bin_license/LICENSE was updated at this PR. * Is there breaking changes for older versions? No. * Does this needs documentation? No. Author: Keiji Yoshida Closes #2693 from kjmrknsn/ci-test-against-jgit-4.5.4 and squashes the following commits: 27b08c9 [Keiji Yoshida] CI Test against JGit 4.5.4 --- zeppelin-distribution/src/bin_license/LICENSE | 2 +- zeppelin-zengine/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/zeppelin-distribution/src/bin_license/LICENSE b/zeppelin-distribution/src/bin_license/LICENSE index 7b23313b21d..37fbce1b1ff 100644 --- a/zeppelin-distribution/src/bin_license/LICENSE +++ b/zeppelin-distribution/src/bin_license/LICENSE @@ -311,7 +311,7 @@ The text of each license is also included at licenses/LICENSE-[project]-[version The following components are provided under the BSD-style License. - (New BSD License) JGit (org.eclipse.jgit:org.eclipse.jgit:jar:4.9.0.201710071750-r - https://eclipse.org/jgit/) + (New BSD License) JGit (org.eclipse.jgit:org.eclipse.jgit:jar:4.5.4.201711221230-r - https://eclipse.org/jgit/) (New BSD License) Kryo (com.esotericsoftware.kryo:kryo:3.0.3 - http://code.google.com/p/kryo/) (New BSD License) MinLog (com.esotericsoftware.minlog:minlog:1.3 - http://code.google.com/p/minlog/) (New BSD License) ReflectASM (com.esotericsoftware.reflectasm:reflectasm:1.07 - http://code.google.com/p/reflectasm/) diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index a29d4a0955b..fd2d1dca961 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -46,7 +46,7 @@ 5.3.1 0.9.8 1.4.01 - 4.9.0.201710071750-r + 4.5.4.201711221230-r 1.3 From 38ba2d47569300dd2a31bc9141d7439b0d370091 Mon Sep 17 00:00:00 2001 From: tinkoff-dwh Date: Thu, 30 Nov 2017 10:55:14 +0300 Subject: [PATCH 122/492] [Zeppelin-2571] & [Zeppelin-465] Run paragraphs: from first/current to current/last ### What is this PR for? This pr add the ability to run all paragraphs from the first to the current and from the current to the last. This makes it easier to update the data if changes are made in one of the related paragraphs. ### What type of PR is it? Feature ### What is the Jira issue? [ZEPPELIN-2571](https://issues.apache.org/jira/browse/ZEPPELIN-2571) - (Add a "Run to here" option on paragraphs) [ZEPPELIN-465](https://issues.apache.org/jira/browse/ZEPPELIN-465) - (Capability to run all cells below the current cell) ### How should this be tested? 1. Click on the "Run: from first to this" or "Run: from this to last" in the dropdown menu. Paragraphs from the first/current to the current/last will be started in order. 2. Press the key combination: CTRL + SHIFT + ENTER. A window will appear with the choice of the desired action. ![capture2](https://user-images.githubusercontent.com/25951039/33269914-58eff828-d393-11e7-9ebf-6437ec11c8f2.PNG) Choose one of two actions. The selected action will be performed. ### Screenshots (if appropriate) ![capture1_edit](https://user-images.githubusercontent.com/25951039/33269915-5951b19e-d393-11e7-831b-42d4523908ae.png) ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: tinkoff-dwh Closes #2688 from tinkoff-dwh/ZEPPELIN-2571&465 and squashes the following commits: 304c196 [tinkoff-dwh] [ZEPPELIN-2517]&[ZEPPELIN-465] fix shortcut description 3d57b30 [tinkoff-dwh] [ZEPPELIN-2517]&[ZEPPELIN-465] save paragraph's focus 70f130c [tinkoff-dwh] [Zeppelin-2517]&[Zeppelin-465] some text change 8ca1df2 [tinkoff-dwh] [Zeppelin-2517]&[Zeppelin-465] line reduction a7a4158 [tinkoff-dwh] [ZEPPELIN-2517]&[ZEPPELIN-465] logic change 9fbcdb6 [tinkoff-dwh] [ZEPPELIN-2517]&[ZEPPELIN-465] add keyboard shortcut bfc3891 [tinkoff-dwh] [ZEPPELIN-2517]&[ZEPPELIN-465] Run paragraphs from 1st to this. From this to last --- .../src/app/notebook/notebook.controller.js | 90 ++++++++++++++++++- .../notebook/paragraph/paragraph-control.html | 18 ++++ .../paragraph/paragraph.controller.js | 56 ++++++++++-- zeppelin-web/src/app/notebook/shortcut.html | 11 +++ 4 files changed, 166 insertions(+), 9 deletions(-) diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index 48fc6e7154e..1fa63231ea8 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -164,7 +164,7 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, for (let i = 0; i < $scope.note.paragraphs.length; i++) { let paragraphId = $scope.note.paragraphs[i].id if (jQuery.contains(angular.element('#' + paragraphId + '_container')[0], clickEvent.target)) { - $scope.$broadcast('focusParagraph', paragraphId, 0, true) + $scope.$broadcast('focusParagraph', paragraphId, 0, null, true) break } } @@ -512,7 +512,7 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, para.focus = true // we need `$timeout` since angular DOM might not be initialized - $timeout(() => { $scope.$broadcast('focusParagraph', para.id, 0, false) }) + $timeout(() => { $scope.$broadcast('focusParagraph', para.id, 0, null, false) }) } }) } @@ -1188,6 +1188,92 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, ** $scope.$on functions below */ + $scope.$on('runAllAbove', function (event, paragraph, isNeedConfirm) { + let allParagraphs = $scope.note.paragraphs + let toRunParagraphs = [] + + for (let i = 0; allParagraphs[i] !== paragraph; i++) { + if (i === allParagraphs.length - 1) { return } // if paragraph not in array of all paragraphs + toRunParagraphs.push(allParagraphs[i]) + } + + const paragraphs = toRunParagraphs.map(p => { + return { + id: p.id, + title: p.title, + paragraph: p.text, + config: p.config, + params: p.settings.params + } + }) + + if (!isNeedConfirm) { + websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs) + } else { + BootstrapDialog.confirm({ + closable: true, + title: '', + message: 'Run all above?', + callback: function (result) { + if (result) { + websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs) + } + } + }) + } + + $scope.saveCursorPosition(paragraph) + }) + + $scope.$on('runAllBelowAndCurrent', function (event, paragraph, isNeedConfirm) { + let allParagraphs = $scope.note.paragraphs + let toRunParagraphs = [] + + for (let i = allParagraphs.length - 1; allParagraphs[i] !== paragraph; i--) { + if (i < 0) { return } // if paragraph not in array of all paragraphs + toRunParagraphs.push(allParagraphs[i]) + } + + toRunParagraphs.push(paragraph) + toRunParagraphs.reverse() + + const paragraphs = toRunParagraphs.map(p => { + return { + id: p.id, + title: p.title, + paragraph: p.text, + config: p.config, + params: p.settings.params + } + }) + + if (!isNeedConfirm) { + websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs) + } else { + BootstrapDialog.confirm({ + closable: true, + title: '', + message: 'Run current and all below?', + callback: function (result) { + if (result) { + websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs) + } + } + }) + } + + $scope.saveCursorPosition(paragraph) + }) + + $scope.saveCursorPosition = function (paragraph) { + let angParagEditor = angular + .element('#' + paragraph.id + '_paragraphColumn_main') + .scope().editor + let col = angParagEditor.selection.lead.column + let row = angParagEditor.selection.lead.row + $scope.$broadcast('focusParagraph', paragraph.id, row + 1, col) + } + $scope.$on('setConnectedStatus', function (event, param) { if (connectedOnce && param) { initNotebook() diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html b/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html index d6599725cc0..0b4ca1e4d67 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html @@ -140,6 +140,24 @@ Insert new
  • +
  • + + + + Ctrl+Shift+Enter + Run all above + +
  • +
  • + + + + Ctrl+Shift+Enter + Run all below + +
  • Ctrl+Shift+C diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index c5788416adf..d3ed3466e76 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -471,6 +471,43 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca $scope.runParagraph($scope.getEditorValue(), false, false) } + $scope.runAllToThis = function(paragraph) { + $scope.$emit('runAllAbove', paragraph, true) + } + + $scope.runAllFromThis = function(paragraph) { + $scope.$emit('runAllBelowAndCurrent', paragraph, true) + } + + $scope.runAllToOrFromThis = function (paragraph) { + BootstrapDialog.show({ + message: 'Run paragraphs:', + title: '', + buttons: [{ + label: 'Close', + action: function(dialog) { + dialog.close() + } + }, + { + label: 'Run all above', + cssClass: 'btn-primary', + action: function(dialog) { + $scope.$emit('runAllAbove', paragraph, false) + dialog.close() + } + }, + { + label: 'Run current and all below', + cssClass: 'btn-primary', + action: function(dialog) { + $scope.$emit('runAllBelowAndCurrent', paragraph, false) + dialog.close() + } + }] + }) + } + $scope.turnOnAutoRun = function (paragraph) { paragraph.config.runOnSelectionChange = !paragraph.config.runOnSelectionChange commitParagraph(paragraph) @@ -1446,8 +1483,10 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca // move focus to next paragraph // $timeout stops chaining effect of focus propogation $timeout(() => $scope.$emit('moveFocusToNextParagraph', paragraphId)) - } else if (keyEvent.shiftKey && keyCode === 13) { // Shift + Enter + } else if (!keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 13) { // Shift + Enter $scope.runParagraphFromShortcut($scope.getEditorValue()) + } else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 13) { // Ctrl + Shift + Enter + $scope.runAllToOrFromThis($scope.paragraph) } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 67) { // Ctrl + Alt + c $scope.cancelParagraph($scope.paragraph) } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 68) { // Ctrl + Alt + d @@ -1500,7 +1539,10 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca } }) - $scope.$on('focusParagraph', function (event, paragraphId, cursorPos, mouseEvent) { + $scope.$on('focusParagraph', function (event, paragraphId, cursorPosRow, cursorPosCol, mouseEvent) { + if (cursorPosCol === null || cursorPosCol === undefined) { + cursorPosCol = 0 + } if ($scope.paragraph.id === paragraphId) { // focus editor if (!$scope.paragraph.config.editorHide) { @@ -1508,14 +1550,14 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca $scope.editor.focus() // move cursor to the first row (or the last row) let row - if (cursorPos >= 0) { - row = cursorPos - $scope.editor.gotoLine(row, 0) + if (cursorPosRow >= 0) { + row = cursorPosRow + $scope.editor.gotoLine(row, cursorPosCol) } else { row = $scope.editor.session.getLength() - $scope.editor.gotoLine(row, 0) + $scope.editor.gotoLine(row, cursorPosCol) } - $scope.scrollToCursor($scope.paragraph.id, 0) + $scope.scrollToCursor($scope.paragraph.id, cursorPosCol) } } handleFocus(true) diff --git a/zeppelin-web/src/app/notebook/shortcut.html b/zeppelin-web/src/app/notebook/shortcut.html index c4b4009ee11..9bc55973a53 100644 --- a/zeppelin-web/src/app/notebook/shortcut.html +++ b/zeppelin-web/src/app/notebook/shortcut.html @@ -37,6 +37,17 @@

    Keyboard shortcuts

    + + +
    Run all above/below paragraphs
    + + +
    + Ctrl + Shift + Enter +
    + + +
    Cancel
    From a82e3ec3a335863a30959cabbc3371169e2c1ce9 Mon Sep 17 00:00:00 2001 From: liguohui Date: Thu, 30 Nov 2017 15:01:27 +0800 Subject: [PATCH 123/492] [ZEPPELIN-3075]Fix unqiue algo for the web side in pivot.js file. ### What is this PR for? unique() algorithm is not correct in pivot.js file. If the input is `[2, 3, 3, 3, 4, 5]` and the output will be `[2, 3, 3, 4, 5]`. The number `3` is still duplicated. ### What type of PR is it? [Bug Fix] ### Todos * [ ] - Task ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-3075 [ZEPPELIN-3075] ### How should this be tested? This is very easy and no need test. ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: liguohui Closes #2685 from liguohuicmss/pivot-unqiue-algo and squashes the following commits: 2063175d5 [liguohui] delete the multi empty line ea582d95e [liguohui] delete some spaces at the end of the line 79c763a6b [liguohui] add a empty line 99cf93da9 [liguohui] Revert "Chart field is also draggable and sortable in the 'keys', 'groups' and 'values'" fdde39f52 [liguohui] add unit test for unique algo in pivot.js f99674724 [liguohui] Chart field is also draggable and sortable in the 'keys', 'groups' and 'values' 943e80a96 [liguohui] Fix unqiue algo for the web side in pivot.js file. --- zeppelin-web/src/app/tabledata/pivot.js | 1 + .../src/app/tabledata/tabledata.test.js | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/zeppelin-web/src/app/tabledata/pivot.js b/zeppelin-web/src/app/tabledata/pivot.js index 1c938ea8280..a0f61b219a4 100644 --- a/zeppelin-web/src/app/tabledata/pivot.js +++ b/zeppelin-web/src/app/tabledata/pivot.js @@ -89,6 +89,7 @@ export default class PivotTransformation extends Transformation { for (let j = i + 1; j < list.length; j++) { if (angular.equals(list[i], list[j])) { list.splice(j, 1) + j-- } } } diff --git a/zeppelin-web/src/app/tabledata/tabledata.test.js b/zeppelin-web/src/app/tabledata/tabledata.test.js index 7e41de4becf..3de2fa3fb72 100644 --- a/zeppelin-web/src/app/tabledata/tabledata.test.js +++ b/zeppelin-web/src/app/tabledata/tabledata.test.js @@ -13,6 +13,7 @@ */ import TableData from './tabledata.js' +import PivotTransformation from './pivot.js' describe('TableData build', function () { let td @@ -39,3 +40,47 @@ describe('TableData build', function () { expect(td.comment).toBe('hello') }) }) + +describe('PivotTransformation build', function() { + let pt + + beforeEach(function () { + console.log(PivotTransformation) + pt = new PivotTransformation() + }) + + it('check the result of keys, groups and values unique', function() { + // set inited mock data + let config = { + common: { + pivot: { + keys: [{index: 4, name: '4'}, + {index: 3, name: '3'}, + {index: 4, name: '4'}, + {index: 3, name: '3'}, + {index: 3, name: '3'}, + {index: 3, name: '3'}, + {index: 3, name: '3'}, + {index: 5, name: '5'}], + groups: [], + values: [] + } + } + } + pt.tableDataColumns = [ + {index: 1, name: '1'}, + {index: 2, name: '2'}, + {index: 3, name: '3'}, + {index: 4, name: '4'}, + {index: 5, name: '5'}] + + pt.setConfig(config) + + pt.removeUnknown() + + expect(config.common.pivot.keys.length).toBe(3) + expect(config.common.pivot.keys[0].index).toBe(4) + expect(config.common.pivot.keys[1].index).toBe(3) + expect(config.common.pivot.keys[2].index).toBe(5) + }) +}) From 4a679fc055a7e6b03faa3c215562cc295714bc99 Mon Sep 17 00:00:00 2001 From: tinkoff-dwh Date: Thu, 7 Dec 2017 03:25:16 +0500 Subject: [PATCH 124/492] [FIX] fix autocomplete ### What is this PR for? After refactoring of Interpreter autocomplete (from server side) not works without first Run of interpreter. This PR fix it. ### What type of PR is it? [Fix] ### How should this be tested? * Create new Note (JDBC interpreter), try to use autocomplete (schema, tables) ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: tinkoff-dwh Closes #2691 from tinkoff-dwh/fix_autocomplete and squashes the following commits: e9bad01 [tinkoff-dwh] remove trim from completion 141dff5 [tinkoff-dwh] [FIX] fix autocomplete --- .../interpreter/remote/RemoteInterpreter.java | 3 +- .../apache/zeppelin/notebook/Paragraph.java | 34 ++++++++----------- .../zeppelin/notebook/ParagraphTest.java | 4 +-- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java index 8964210c1c6..4ad36cf1b28 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java @@ -346,8 +346,7 @@ public List completion(final String buf, final int cursor final InterpreterContext interpreterContext) throws InterpreterException { if (!isOpened) { - LOGGER.warn("completion is called when RemoterInterpreter is not opened for " + className); - return new ArrayList<>(); + open(); } RemoteInterpreterProcess interpreterProcess = null; try { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java index 10a8548798d..5ec132931f6 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java @@ -191,7 +191,7 @@ public void setText(String newText) { this.scriptText = this.text.substring(headingSpace.length() + intpText.length() + 1).trim(); } else { this.intpText = ""; - this.scriptText = this.text; + this.scriptText = this.text.trim(); } } } @@ -250,14 +250,17 @@ public List completion(String buffer, int cursor) { return note.getInterpreterCompletion(); } } - String trimmedBuffer = buffer != null ? buffer.trim() : null; - cursor = calculateCursorPosition(buffer, trimmedBuffer, cursor); + this.interpreter = getBindedInterpreter(); + + setText(buffer); + + cursor = calculateCursorPosition(buffer, cursor); InterpreterContext interpreterContext = getInterpreterContextWithoutRunner(null); try { if (this.interpreter != null) { - return this.interpreter.completion(scriptText, cursor, interpreterContext); + return this.interpreter.completion(this.scriptText, cursor, interpreterContext); } else { return null; } @@ -266,24 +269,15 @@ public List completion(String buffer, int cursor) { } } - public int calculateCursorPosition(String buffer, String trimmedBuffer, int cursor) { - int countWhitespacesAtStart = buffer.indexOf(trimmedBuffer); - if (countWhitespacesAtStart > 0) { - cursor -= countWhitespacesAtStart; - } + public int calculateCursorPosition(String buffer, int cursor) { + // scriptText trimmed - // parse text to get interpreter component - String repl = null; - if (trimmedBuffer != null) { - Matcher matcher = REPL_PATTERN.matcher(trimmedBuffer); - if (matcher.matches()) { - repl = matcher.group(2); - } + if (this.scriptText.isEmpty()) { + return 0; } - - if (repl != null && cursor > repl.length()) { - String body = trimmedBuffer.substring(repl.length() + 1); - cursor -= repl.length() + 1 + body.indexOf(body.trim()); + int countCharactersBeforeScript = buffer.indexOf(this.scriptText); + if (countCharactersBeforeScript > 0) { + cursor -= countCharactersBeforeScript; } return cursor; diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java index 9e9ce27ea9e..e46b7393ee1 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/ParagraphTest.java @@ -271,7 +271,6 @@ public void returnUnchangedResultsWithDifferentUser() throws Throwable { @Test public void testCursorPosition() { Paragraph paragraph = spy(new Paragraph()); - doReturn(null).when(paragraph).getIntpText(); // left = buffer, middle = cursor position into source code, right = cursor position after parse List> dataSet = Arrays.asList( Triple.of("%jdbc schema.", 13, 7), @@ -294,7 +293,8 @@ public void testCursorPosition() { ); for (Triple data : dataSet) { - Integer actual = paragraph.calculateCursorPosition(data.getLeft(), data.getLeft().trim(), data.getMiddle()); + paragraph.setText(data.getLeft()); + Integer actual = paragraph.calculateCursorPosition(data.getLeft(), data.getMiddle()); assertEquals(data.getRight(), actual); } } From 3505625c26a45df14d6412b73da0b33aac68e908 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Mon, 11 Dec 2017 20:53:08 +0800 Subject: [PATCH 125/492] [HOTFIX]: Fix IPythonInterpreter unit test ### What is this PR for? This is for hotfix of `IPythonInterpreter` unit test failure. Just specify the version of ipython in `install_external_dependencies.sh`, otherwise latest ipython version will be installed, and the behavior may change. ### What type of PR is it? [Hot Fix] ### Todos * [ ] - Task ### What is the Jira issue? * ### How should this be tested? * CI pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2703 from zjffdu/ipython_version and squashes the following commits: b4c7b42 [Jeff Zhang] HotFix: Fix IPythonInterpreter unit test --- testing/install_external_dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/install_external_dependencies.sh b/testing/install_external_dependencies.sh index daa670bc7dd..e34296e3ab6 100755 --- a/testing/install_external_dependencies.sh +++ b/testing/install_external_dependencies.sh @@ -44,6 +44,6 @@ if [[ -n "$PYTHON" ]] ; then conda update -q conda conda info -a conda config --add channels conda-forge - conda install -q matplotlib pandasql ipython jupyter_client ipykernel matplotlib bokeh=0.12.6 + conda install -q matplotlib pandasql ipython=5.4.1 jupyter_client ipykernel matplotlib bokeh=0.12.6 pip install -q grpcio ggplot fi From 549bce6738ffd7f460867d3f5ee00a9e2ec14125 Mon Sep 17 00:00:00 2001 From: Liu Date: Tue, 5 Dec 2017 15:24:20 +0800 Subject: [PATCH 126/492] [ZEPPELIN-3014] NPE bug fix and Error message enhancement with Kylin Interpreter ### What is this PR for? A few sentences describing the overall goals of the pull request's commits. First time? Check out the contributing guide - https://zeppelin.apache.org/contribution/contributions.html ### What type of PR is it? Bug Fix ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3014 ### How should this be tested? * Setup Travis CI as described on https://zeppelin.apache.org/contribution/contributions.html#continuous-integration * Use existing unit tests in kylin module. ### Screenshots (if appropriate) #### before: NPE when result set is empty ![image](https://user-images.githubusercontent.com/18542573/32154048-f1b8ba58-bcfb-11e7-98cc-98cdf484f2d5.png) #### after: no NPE when result set is empty, just an empty table ![image](https://user-images.githubusercontent.com/18542573/32154069-110215d0-bcfc-11e7-87e9-cc049001f1c7.png) #### before: when query fails, only error code is returned, no error message ![image](https://user-images.githubusercontent.com/18542573/32154088-29651938-bcfc-11e7-9e66-cd2cfccba054.png) #### after: when query fails, both error code and error message are displayed to users ![image](https://user-images.githubusercontent.com/18542573/32154096-3d3ab01c-bcfc-11e7-8cf3-d710d96b8c5a.png) ### Questions: * Does the licenses files need update? No. * Is there breaking changes for older versions? No. * Does this needs documentation? No. Author: Liu Closes #2645 from jinxliu/kylin-intp-new and squashes the following commits: d5692bf [Liu] refactor 85b6424 [Liu] add test for empty result set 4596470 [Liu] ZEPPELIN-3014: NPE bug fix and Error message enhancement with Kylin Interpreter --- .../zeppelin/kylin/KylinErrorResponse.java | 63 ++++++++++++++++ .../zeppelin/kylin/KylinInterpreter.java | 71 +++++++++++-------- .../zeppelin/kylin/KylinInterpreterTest.java | 24 +++++++ 3 files changed, 130 insertions(+), 28 deletions(-) create mode 100644 kylin/src/main/java/org/apache/zeppelin/kylin/KylinErrorResponse.java diff --git a/kylin/src/main/java/org/apache/zeppelin/kylin/KylinErrorResponse.java b/kylin/src/main/java/org/apache/zeppelin/kylin/KylinErrorResponse.java new file mode 100644 index 00000000000..00439e8c626 --- /dev/null +++ b/kylin/src/main/java/org/apache/zeppelin/kylin/KylinErrorResponse.java @@ -0,0 +1,63 @@ +/* + * 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.kylin; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import org.apache.zeppelin.common.JsonSerializable; + +/** + * class for Kylin Error Response. + */ +class KylinErrorResponse implements JsonSerializable { + private static final Gson gson = new Gson(); + + private String stacktrace; + private String exception; + private String url; + private String code; + private Object data; + private String msg; + + public KylinErrorResponse(String stacktrace, String exception, String url, + String code, Object data, String msg) { + this.stacktrace = stacktrace; + this.exception = exception; + this.url = url; + this.code = code; + this.data = data; + this.msg = msg; + } + + public String getException() { + return exception; + } + + public String toJson() { + return gson.toJson(this); + } + + public static KylinErrorResponse fromJson(String json) { + try { + return gson.fromJson(json, KylinErrorResponse.class); + } catch (JsonSyntaxException ex) { + return null; + } + } + +} diff --git a/kylin/src/main/java/org/apache/zeppelin/kylin/KylinInterpreter.java b/kylin/src/main/java/org/apache/zeppelin/kylin/KylinInterpreter.java index 6b68d288e47..c7cd689a745 100755 --- a/kylin/src/main/java/org/apache/zeppelin/kylin/KylinInterpreter.java +++ b/kylin/src/main/java/org/apache/zeppelin/kylin/KylinInterpreter.java @@ -18,6 +18,7 @@ package org.apache.zeppelin.kylin; import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; @@ -30,9 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; import java.util.List; import java.util.Properties; import java.util.regex.Matcher; @@ -166,28 +165,42 @@ public String getSQL(String cmd) { } private InterpreterResult executeQuery(String sql) throws IOException { - HttpResponse response = prepareRequest(sql); + String result; - if (response.getStatusLine().getStatusCode() != 200) { - logger.error("failed to execute query: " + response.getEntity().getContent().toString()); - return new InterpreterResult(InterpreterResult.Code.ERROR, - "Failed : HTTP error code " + response.getStatusLine().getStatusCode()); - } - - BufferedReader br = new BufferedReader( - new InputStreamReader((response.getEntity().getContent()))); - StringBuilder sb = new StringBuilder(); + try { + int code = response.getStatusLine().getStatusCode(); + result = IOUtils.toString(response.getEntity().getContent(), "UTF-8"); + + if (code != 200) { + StringBuilder errorMessage = new StringBuilder("Failed : HTTP error code " + code + " ."); + logger.error("Failed to execute query: " + result); + + KylinErrorResponse kylinErrorResponse = KylinErrorResponse.fromJson(result); + if (kylinErrorResponse == null) { + logger.error("Cannot get json from string: " + result); + // when code is 401, the response is html, not json + if (code == 401) { + errorMessage.append(" Error message: Unauthorized. This request requires " + + "HTTP authentication. Please make sure your have set your credentials " + + "correctly."); + } else { + errorMessage.append(" Error message: " + result + " ."); + } + } else { + String exception = kylinErrorResponse.getException(); + logger.error("The exception is " + exception); + errorMessage.append(" Error message: " + exception + " ."); + } - String output; - logger.info("Output from Server .... \n"); - while ((output = br.readLine()) != null) { - logger.info(output); - sb.append(output).append('\n'); + return new InterpreterResult(InterpreterResult.Code.ERROR, errorMessage.toString()); + } + } catch (NullPointerException | IOException e) { + throw new IOException(e); } - InterpreterResult rett = new InterpreterResult(InterpreterResult.Code.SUCCESS, - formatResult(sb.toString())); - return rett; + + return new InterpreterResult(InterpreterResult.Code.SUCCESS, + formatResult(result)); } String formatResult(String msg) { @@ -205,16 +218,18 @@ String formatResult(String msg) { table = mr.group(1); } - String[] row = table.split("],\\["); - for (int i = 0; i < row.length; i++) { - String[] col = row[i].split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1); - for (int j = 0; j < col.length; j++) { - if (col[j] != null) { - col[j] = col[j].replaceAll("^\"|\"$", ""); + if (table != null && !table.isEmpty()) { + String[] row = table.split("],\\["); + for (int i = 0; i < row.length; i++) { + String[] col = row[i].split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1); + for (int j = 0; j < col.length; j++) { + if (col[j] != null) { + col[j] = col[j].replaceAll("^\"|\"$", ""); + } + res.append(col[j] + " \t"); } - res.append(col[j] + " \t"); + res.append(" \n"); } - res.append(" \n"); } return res.toString(); } diff --git a/kylin/src/test/java/org/apache/zeppelin/kylin/KylinInterpreterTest.java b/kylin/src/test/java/org/apache/zeppelin/kylin/KylinInterpreterTest.java index 4471a076893..35f0f3c2ebb 100755 --- a/kylin/src/test/java/org/apache/zeppelin/kylin/KylinInterpreterTest.java +++ b/kylin/src/test/java/org/apache/zeppelin/kylin/KylinInterpreterTest.java @@ -108,6 +108,30 @@ public void testParseResult() { Assert.assertEquals(expected, actual); } + @Test + public void testParseEmptyResult() { + String msg = "{\"columnMetas\":[{\"isNullable\":1,\"displaySize\":256,\"label\":\"COUNTRY\",\"name\":\"COUNTRY\"," + + "\"schemaName\":\"DEFAULT\",\"catelogName\":null,\"tableName\":\"SALES_TABLE\",\"precision\":256," + + "\"scale\":0,\"columnType\":12,\"columnTypeName\":\"VARCHAR\",\"writable\":false,\"readOnly\":true," + + "\"definitelyWritable\":false,\"autoIncrement\":false,\"caseSensitive\":true,\"searchable\":false," + + "\"currency\":false,\"signed\":true},{\"isNullable\":1,\"displaySize\":256,\"label\":\"CURRENCY\"," + + "\"name\":\"CURRENCY\",\"schemaName\":\"DEFAULT\",\"catelogName\":null,\"tableName\":\"SALES_TABLE\"," + + "\"precision\":256,\"scale\":0,\"columnType\":12,\"columnTypeName\":\"VARCHAR\",\"writable\":false," + + "\"readOnly\":true,\"definitelyWritable\":false,\"autoIncrement\":false,\"caseSensitive\":true," + + "\"searchable\":false,\"currency\":false,\"signed\":true},{\"isNullable\":0,\"displaySize\":19," + + "\"label\":\"COUNT__\",\"name\":\"COUNT__\",\"schemaName\":\"DEFAULT\",\"catelogName\":null," + + "\"tableName\":\"SALES_TABLE\",\"precision\":19,\"scale\":0,\"columnType\":-5,\"columnTypeName\":" + + "\"BIGINT\",\"writable\":false,\"readOnly\":true,\"definitelyWritable\":false,\"autoIncrement\":false," + + "\"caseSensitive\":true,\"searchable\":false,\"currency\":false,\"signed\":true}],\"results\":" + + "[]," + "\"cube\":\"Sample_Cube\",\"affectedRowCount\":0,\"isException\":false,\"exceptionMessage\":null," + + "\"duration\":134,\"totalScanCount\":1,\"hitExceptionCache\":false,\"storageCacheUsed\":false," + + "\"partial\":false}"; + String expected="%table COUNTRY \tCURRENCY \tCOUNT__ \t \n"; + KylinInterpreter t = new MockKylinInterpreter(getDefaultProperties()); + String actual = t.formatResult(msg); + Assert.assertEquals(expected, actual); + } + private Properties getDefaultProperties() { Properties prop = new Properties(); prop.put("kylin.api.username", "ADMIN"); From 13f8e6cc65ffa629e24b6947cbd2ee63ba8159f2 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Sun, 12 Nov 2017 09:18:41 +0800 Subject: [PATCH 127/492] ZEPPELIN-3085 Introduce generic ConfInterpreter for more fine-grained control of interpreter setting ### What is this PR for? Zeppelin's interpreter setting is shared by all the users and notes, if you want to have different setting you have to create new interpreter, e.g. you can create `spark_jar1` for running spark with dependency jar1 and `spark_jar2` for running spark with dependency jar2. This approach works, but not so convenient. `ConfInterpreter` can provide more fine-grained control on interpreter setting and more flexibility. `ConfInterpreter` is a generic interpreter that could be used by any interpreters. The input format should be property file format. In the first paragraph, we use ConfInterpreter to make custom configuration of spark interpreter (set app name, yarn-client mode & add spark-csv dependencies). Then you can run the second paragraph which use spark-csv. ![conf_interpreter](https://user-images.githubusercontent.com/164491/33419465-74a3fae8-d5e5-11e7-8b25-76407804d979.png) It can be used to make custom setting for any interpreter. `ConfInterpreter` would run in the zeppelin server side, it would update the interpreter properties before you launch the interpreter process, so it needs to run before interpreter process launched. And when interpreter process is launched is determined by interpreter mode setting. So users needs to understand the interpreter mode setting of zeppelin and be aware when interpreter process is launched. E.g. If we set spark interpreter setting as isolated per note. Under this setting, each note will launch one interpreter process. In this scenario, user need to put `ConfInterpreter` as the first paragraph as the above example. Otherwise the customized setting can not be applied (The paragraph using ConfInterpreter will report ERROR). ### What type of PR is it? [Feature | Documentation] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3085 ### How should this be tested? * Unit test, System test is added, also manually verified it. ### Screenshots (if appropriate) ![conf_interpreter](https://user-images.githubusercontent.com/164491/33419465-74a3fae8-d5e5-11e7-8b25-76407804d979.png) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2692 from zjffdu/ZEPPELIN-3085 and squashes the following commits: 87ce20f [Jeff Zhang] ZEPPELIN-3085. Introduce generic ConfInterpreter for more fine-grained control of interpreter setting --- .../img/screenshots/conf_interpreter.png | Bin 0 -> 39388 bytes docs/usage/interpreter/overview.md | 13 +++ .../rest/ZeppelinSparkClusterTest.java | 30 ++++++ .../zeppelin/interpreter/ConfInterpreter.java | 92 ++++++++++++++++ .../interpreter/InterpreterSetting.java | 43 ++++++-- .../interpreter/ManagedInterpreterGroup.java | 8 +- .../interpreter/remote/RemoteInterpreter.java | 7 +- .../apache/zeppelin/notebook/Paragraph.java | 1 + .../interpreter/ConfInterpreterTest.java | 102 ++++++++++++++++++ .../ManagedInterpreterGroupTest.java | 4 +- 10 files changed, 287 insertions(+), 13 deletions(-) create mode 100644 docs/assets/themes/zeppelin/img/screenshots/conf_interpreter.png create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ConfInterpreter.java create mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/ConfInterpreterTest.java diff --git a/docs/assets/themes/zeppelin/img/screenshots/conf_interpreter.png b/docs/assets/themes/zeppelin/img/screenshots/conf_interpreter.png new file mode 100644 index 0000000000000000000000000000000000000000..156c3575c9ba289702b2e0cf05725f5615f730a0 GIT binary patch literal 39388 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�eg;EyN`fxMNpp~OZs)-4^ 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 uRou~@jre&=6DdUq1} + 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 56056d65cc6..615675544ec 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 @@ -535,6 +535,8 @@ public void testSparkZeppelinContextDynamicForms() throws IOException { assertEquals("1", result[1]); assertEquals("items: Seq[Object] = Buffer(2)", result[2]); assertEquals("2", result[3]); + + ZeppelinServer.notebook.removeNote(note.getId(), anonymous); } @Test @@ -568,5 +570,33 @@ public void testPySparkZeppelinContextDynamicForms() throws IOException { assertEquals("default_name", result[0]); assertEquals("1", result[1]); assertEquals("2", result[2]); + + ZeppelinServer.notebook.removeNote(note.getId(), anonymous); + } + + @Test + public void testConfInterpreter() throws IOException { + Note note = ZeppelinServer.notebook.createNote(AuthenticationInfo.ANONYMOUS); + Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); + Map config = p.getConfig(); + config.put("enabled", true); + p.setConfig(config); + p.setText("%spark.conf spark.jars.packages\tcom.databricks:spark-csv_2.11:1.2.0"); + p.setAuthenticationInfo(anonymous); + note.run(p.getId()); + waitForFinish(p); + assertEquals(Status.FINISHED, p.getStatus()); + + Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); + p1.setConfig(config); + p1.setText("%spark\nimport com.databricks.spark.csv._"); + p1.setAuthenticationInfo(anonymous); + note.run(p1.getId()); + + waitForFinish(p1); + assertEquals(Status.FINISHED, p1.getStatus()); + + ZeppelinServer.notebook.removeNote(note.getId(), anonymous); + } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ConfInterpreter.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ConfInterpreter.java new file mode 100644 index 00000000000..d50c57b4da5 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ConfInterpreter.java @@ -0,0 +1,92 @@ +/* + * 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.interpreter; + +import org.apache.commons.lang.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Properties; + +/** + * Special Interpreter for Interpreter Configuration customization. It is attached to each + * InterpreterGroup implicitly by Zeppelin. + */ +public class ConfInterpreter extends Interpreter { + + private static Logger LOGGER = LoggerFactory.getLogger(ConfInterpreter.class); + + private String interpreterGroupId; + private InterpreterSetting interpreterSetting; + + + public ConfInterpreter(Properties properties, + String interpreterGroupId, + InterpreterSetting interpreterSetting) { + super(properties); + this.interpreterGroupId = interpreterGroupId; + this.interpreterSetting = interpreterSetting; + } + + @Override + public void open() throws InterpreterException { + + } + + @Override + public void close() throws InterpreterException { + + } + + @Override + public InterpreterResult interpret(String st, InterpreterContext context) + throws InterpreterException { + + try { + Properties finalProperties = new Properties(); + finalProperties.putAll(getProperties()); + Properties newProperties = new Properties(); + newProperties.load(new StringReader(st)); + finalProperties.putAll(newProperties); + LOGGER.debug("Properties for InterpreterGroup: " + interpreterGroupId + " is " + + finalProperties); + interpreterSetting.setInterpreterGroupProperties(interpreterGroupId, finalProperties); + return new InterpreterResult(InterpreterResult.Code.SUCCESS); + } catch (IOException e) { + LOGGER.error("Fail to update interpreter setting", e); + return new InterpreterResult(InterpreterResult.Code.ERROR, ExceptionUtils.getStackTrace(e)); + } + } + + @Override + public void cancel(InterpreterContext context) throws InterpreterException { + + } + + @Override + public FormType getFormType() throws InterpreterException { + return null; + } + + @Override + public int getProgress(InterpreterContext context) throws InterpreterException { + return 0; + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java index 26fcd8e93d4..d5ff947ad0c 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java @@ -138,9 +138,11 @@ public class InterpreterSetting { // launcher in future when we have other launcher implementation. e.g. third party launcher // service like livy private transient InterpreterLauncher launcher; - /////////////////////////////////////////////////////////////////////////////////////////// private transient LifecycleManager lifecycleManager; + /////////////////////////////////////////////////////////////////////////////////////////// + + /** * Builder class for InterpreterSetting @@ -648,12 +650,11 @@ public void clearNoteIdAndParaMap() { /////////////////////////////////////////////////////////////////////////////////////// // This is the only place to create interpreters. For now we always create multiple interpreter // together (one session). We don't support to create single interpreter yet. - List createInterpreters(String user, String sessionId) { + List createInterpreters(String user, String interpreterGroupId, String sessionId) { List interpreters = new ArrayList<>(); List interpreterInfos = getInterpreterInfos(); for (InterpreterInfo info : interpreterInfos) { - Interpreter interpreter = null; - interpreter = new RemoteInterpreter(getJavaProperties(), sessionId, + Interpreter interpreter = new RemoteInterpreter(getJavaProperties(), sessionId, info.getClassName(), user, lifecycleManager); if (info.isDefaultInterpreter()) { interpreters.add(0, interpreter); @@ -663,15 +664,17 @@ List createInterpreters(String user, String sessionId) { LOGGER.info("Interpreter {} created for user: {}, sessionId: {}", interpreter.getClassName(), user, sessionId); } + interpreters.add(new ConfInterpreter(getJavaProperties(), interpreterGroupId, this)); return interpreters; } - synchronized RemoteInterpreterProcess createInterpreterProcess() throws IOException { + synchronized RemoteInterpreterProcess createInterpreterProcess(Properties properties) + throws IOException { if (launcher == null) { createLauncher(); } InterpreterLaunchContext launchContext = new - InterpreterLaunchContext(getJavaProperties(), option, interpreterRunner, id, group, name); + InterpreterLaunchContext(properties, option, interpreterRunner, id, group, name); RemoteInterpreterProcess process = (RemoteInterpreterProcess) launcher.launch(launchContext); process.setRemoteInterpreterEventPoller( new RemoteInterpreterEventPoller(remoteInterpreterProcessListener, appEventListener)); @@ -716,6 +719,11 @@ private String getInterpreterClassFromInterpreterSetting(String replName) { return info.getClassName(); } } + //TODO(zjffdu) It requires user can not create interpreter with name `conf`, + // conf is a reserved word of interpreter name + if (replName.equals("conf")) { + return ConfInterpreter.class.getName(); + } return null; } @@ -728,6 +736,29 @@ private ManagedInterpreterGroup createInterpreterGroup(String groupId) { return interpreterGroup; } + /** + * Throw exception when interpreter process has already launched + * + * @param interpreterGroupId + * @param properties + * @throws IOException + */ + public void setInterpreterGroupProperties(String interpreterGroupId, Properties properties) + throws IOException { + ManagedInterpreterGroup interpreterGroup = this.interpreterGroups.get(interpreterGroupId); + for (List session : interpreterGroup.sessions.values()) { + for (Interpreter intp : session) { + if (!intp.getProperties().equals(properties) && + interpreterGroup.getRemoteInterpreterProcess() != null && + interpreterGroup.getRemoteInterpreterProcess().isRunning()) { + throw new IOException("Can not change interpreter properties when interpreter process " + + "has already been launched"); + } + intp.setProperties(properties); + } + } + } + private void loadInterpreterDependencies() { setStatus(Status.DOWNLOADING_DEPENDENCIES); setErrorReason(null); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java index 219204f0417..2378f140daa 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.util.Collection; import java.util.List; +import java.util.Properties; /** * ManagedInterpreterGroup runs under zeppelin server @@ -54,10 +55,11 @@ public InterpreterSetting getInterpreterSetting() { return interpreterSetting; } - public synchronized RemoteInterpreterProcess getOrCreateInterpreterProcess() throws IOException { + public synchronized RemoteInterpreterProcess getOrCreateInterpreterProcess(Properties properties) + throws IOException { if (remoteInterpreterProcess == null) { LOGGER.info("Create InterpreterProcess for InterpreterGroup: " + getId()); - remoteInterpreterProcess = interpreterSetting.createInterpreterProcess(); + remoteInterpreterProcess = interpreterSetting.createInterpreterProcess(properties); } return remoteInterpreterProcess; } @@ -131,7 +133,7 @@ public synchronized List getOrCreateSession(String user, String ses if (sessions.containsKey(sessionId)) { return sessions.get(sessionId); } else { - List interpreters = interpreterSetting.createInterpreters(user, sessionId); + List interpreters = interpreterSetting.createInterpreters(user, id, sessionId); for (Interpreter interpreter : interpreters) { interpreter.setInterpreterGroup(this); } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java index 4ad36cf1b28..6defd9ba825 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java @@ -25,6 +25,7 @@ import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.GUI; import org.apache.zeppelin.display.Input; +import org.apache.zeppelin.interpreter.ConfInterpreter; import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterContextRunner; @@ -101,7 +102,7 @@ public synchronized RemoteInterpreterProcess getOrCreateInterpreterProcess() thr return this.interpreterProcess; } ManagedInterpreterGroup intpGroup = getInterpreterGroup(); - this.interpreterProcess = intpGroup.getOrCreateInterpreterProcess(); + this.interpreterProcess = intpGroup.getOrCreateInterpreterProcess(properties); synchronized (interpreterProcess) { if (!interpreterProcess.isRunning()) { interpreterProcess.start(this.getUserName(), false); @@ -130,7 +131,9 @@ public void open() throws InterpreterException { for (Interpreter interpreter : getInterpreterGroup() .getOrCreateSession(this.getUserName(), sessionId)) { try { - ((RemoteInterpreter) interpreter).internal_create(); + if (!(interpreter instanceof ConfInterpreter)) { + ((RemoteInterpreter) interpreter).internal_create(); + } } catch (IOException e) { throw new InterpreterException(e); } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java index 5ec132931f6..32b9b73263c 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java @@ -351,6 +351,7 @@ public boolean execute(boolean blocking) { setStatus(Job.Status.ERROR); throw intpException; } + setStatus(Status.READY); if (getConfig().get("enabled") == null || (Boolean) getConfig().get("enabled")) { setAuthenticationInfo(getAuthenticationInfo()); interpreter.getScheduler().submit(this); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/ConfInterpreterTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/ConfInterpreterTest.java new file mode 100644 index 00000000000..4d74c7cbfb5 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/ConfInterpreterTest.java @@ -0,0 +1,102 @@ +/* + * 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.interpreter; + +import com.sun.net.httpserver.Authenticator; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class ConfInterpreterTest extends AbstractInterpreterTest { + + @Test + public void testCorrectConf() throws IOException, InterpreterException { + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "test.conf") instanceof ConfInterpreter); + ConfInterpreter confInterpreter = (ConfInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test.conf"); + + InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), + null, null, new ArrayList(), null); + InterpreterResult result = confInterpreter.interpret("property_1\tnew_value\nnew_property\tdummy_value", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code); + + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "test") instanceof RemoteInterpreter); + RemoteInterpreter remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test"); + remoteInterpreter.interpret("hello world", context); + assertEquals(7, remoteInterpreter.getProperties().size()); + assertEquals("new_value", remoteInterpreter.getProperty("property_1")); + assertEquals("dummy_value", remoteInterpreter.getProperty("new_property")); + assertEquals("value_3", remoteInterpreter.getProperty("property_3")); + + // rerun the paragraph with the same properties would result in SUCCESS + result = confInterpreter.interpret("property_1\tnew_value\nnew_property\tdummy_value", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code); + + // run the paragraph with the same properties would result in ERROR + result = confInterpreter.interpret("property_1\tnew_value_2\nnew_property\tdummy_value", context); + assertEquals(InterpreterResult.Code.ERROR, result.code); + } + + @Test + public void testEmptyConf() throws IOException, InterpreterException { + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "test.conf") instanceof ConfInterpreter); + ConfInterpreter confInterpreter = (ConfInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test.conf"); + + InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), + null, null, new ArrayList(), null); + InterpreterResult result = confInterpreter.interpret("", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code); + + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "test") instanceof RemoteInterpreter); + RemoteInterpreter remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test"); + assertEquals(6, remoteInterpreter.getProperties().size()); + assertEquals("value_1", remoteInterpreter.getProperty("property_1")); + assertEquals("value_3", remoteInterpreter.getProperty("property_3")); + } + + + @Test + public void testRunningAfterOtherInterpreter() throws IOException, InterpreterException { + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "test.conf") instanceof ConfInterpreter); + ConfInterpreter confInterpreter = (ConfInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test.conf"); + + InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), + null, null, new ArrayList(), null); + RemoteInterpreter remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test"); + InterpreterResult result = remoteInterpreter.interpret("hello world", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code); + + result = confInterpreter.interpret("property_1\tnew_value\nnew_property\tdummy_value", context); + assertEquals(InterpreterResult.Code.ERROR, result.code); + } + +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroupTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroupTest.java index 74bd2010855..aa7374991b2 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroupTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroupTest.java @@ -62,7 +62,7 @@ public void testInterpreterGroup() { // create session_1 List interpreters = interpreterGroup.getOrCreateSession("user1", "session_1"); - assertEquals(2, interpreters.size()); + assertEquals(3, interpreters.size()); assertEquals(EchoInterpreter.class.getName(), interpreters.get(0).getClassName()); assertEquals(DoubleEchoInterpreter.class.getName(), interpreters.get(1).getClassName()); assertEquals(1, interpreterGroup.getSessionNum()); @@ -73,7 +73,7 @@ public void testInterpreterGroup() { // create session_2 List interpreters2 = interpreterGroup.getOrCreateSession("user1", "session_2"); - assertEquals(2, interpreters2.size()); + assertEquals(3, interpreters2.size()); assertEquals(EchoInterpreter.class.getName(), interpreters2.get(0).getClassName()); assertEquals(DoubleEchoInterpreter.class.getName(), interpreters2.get(1).getClassName()); assertEquals(2, interpreterGroup.getSessionNum()); From 4c8f20ae33ceb47209402c0469791d7a19571471 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 5 Dec 2017 16:27:12 +0800 Subject: [PATCH 128/492] ZEPPELIN-3051. Support Interpreter Process Recovery ### What is this PR for? This PR is for the purpose of recover running interpreter process when zeppelin server is restarted. This would be useful when restarting zeppelin without interrupt current running interpreter processes, should be useful when admin do maintenance or upgrading. Interface `RecoveryStorage` is used for storing the information of running interpreter process. Currently it only has one implementation `FileSystemRecoveryStorage`, other implementation could be done later (such as zookeeper based). `InterpreterLauncher` is the component where to recover the running interpreter process. Test: * RecoveryTest.java * FileSystemRecoveryStorageTest.java Design Doc: https://docs.google.com/document/d/1Plm3Hd40aGdNaXmjdsoY4ek3f-gTijTMGMkNjAZN39Y/edit?usp=sharing ### What type of PR is it? [Feature] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3051 ### How should this be tested? Unit test & Integration Test is added. Also manually verified. ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2668 from zjffdu/ZEPPELIN-3051 and squashes the following commits: a4c9b9c [Jeff Zhang] address comments 575b7b9 [Jeff Zhang] fix the pid of interpreter process id 02b118f [Jeff Zhang] address comments da7cbb9 [Jeff Zhang] ZEPPELIN-3051. Support Interpreter Process Recovery --- bin/interpreter.sh | 2 +- bin/stop-interpreter.sh | 47 +++++ bin/zeppelin-daemon.sh | 12 -- conf/zeppelin-site.xml.template | 41 +++++ docs/usage/interpreter/overview.md | 8 + .../main/resources/interpreter-setting.json | 2 +- .../zeppelin/conf/ZeppelinConfiguration.java | 17 ++ .../launcher/InterpreterClient.java | 14 +- .../launcher/InterpreterLaunchContext.java | 7 + .../launcher/InterpreterLauncher.java | 5 +- .../interpreter/recovery/RecoveryStorage.java | 80 +++++++++ zeppelin-server/notebook/.python.recovery.crc | Bin 0 -> 12 bytes zeppelin-server/notebook/python.recovery | 1 + zeppelin-server/pom.xml | 15 ++ .../zeppelin/server/ZeppelinServer.java | 10 +- .../zeppelin/recovery/RecoveryTest.java | 162 +++++++++++++++++ .../zeppelin/rest/AbstractTestRestApi.java | 13 +- .../interpreter/InterpreterSetting.java | 45 ++++- .../InterpreterSettingManager.java | 38 +++- .../interpreter/ManagedInterpreterGroup.java | 25 ++- .../launcher/ShellScriptLauncher.java | 30 +++- .../launcher/SparkInterpreterLauncher.java | 5 +- .../recovery/FileSystemRecoveryStorage.java | 139 +++++++++++++++ .../recovery/NullRecoveryStorage.java | 54 ++++++ .../interpreter/recovery/StopInterpreter.java | 40 +++++ .../interpreter/remote/RemoteInterpreter.java | 11 +- .../RemoteInterpreterManagedProcess.java | 3 +- .../remote/RemoteInterpreterProcess.java | 6 - .../RemoteInterpreterRunningProcess.java | 28 ++- .../zeppelin/notebook/FileSystemStorage.java | 168 ++++++++++++++++++ .../notebook/repo/FileSystemNotebookRepo.java | 124 +++---------- .../apache/zeppelin/util/ReflectionUtils.java | 99 +++++++++++ .../interpreter/AbstractInterpreterTest.java | 2 +- .../launcher/ShellScriptLauncherTest.java | 7 +- .../SparkInterpreterLauncherTest.java | 31 ++-- .../FileSystemRecoveryStorageTest.java | 92 ++++++++++ 36 files changed, 1195 insertions(+), 188 deletions(-) create mode 100755 bin/stop-interpreter.sh create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/recovery/RecoveryStorage.java create mode 100644 zeppelin-server/notebook/.python.recovery.crc create mode 100644 zeppelin-server/notebook/python.recovery create mode 100644 zeppelin-server/src/test/java/org/apache/zeppelin/recovery/RecoveryTest.java create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/FileSystemRecoveryStorage.java create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/NullRecoveryStorage.java create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/StopInterpreter.java create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FileSystemStorage.java create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/util/ReflectionUtils.java create mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/recovery/FileSystemRecoveryStorageTest.java diff --git a/bin/interpreter.sh b/bin/interpreter.sh index 458ffc00d47..f23ca823e62 100755 --- a/bin/interpreter.sh +++ b/bin/interpreter.sh @@ -220,8 +220,8 @@ if [[ ! -z "$ZEPPELIN_IMPERSONATE_USER" ]] && [[ -n "${suid}" || -z "${SPARK_SUB fi eval $INTERPRETER_RUN_COMMAND & - pid=$! + if [[ -z "${pid}" ]]; then exit 1; else diff --git a/bin/stop-interpreter.sh b/bin/stop-interpreter.sh new file mode 100755 index 00000000000..e6ff16e9e9f --- /dev/null +++ b/bin/stop-interpreter.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# +# 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. +# +# Stop Zeppelin Interpreter Processes +# + +bin=$(dirname "${BASH_SOURCE-$0}") +bin=$(cd "${bin}">/dev/null; pwd) + +. "${bin}/common.sh" + +export ZEPPELIN_FORCE_STOP=1 + +ZEPPELIN_STOP_INTERPRETER_MAIN=org.apache.zeppelin.interpreter.recovery.StopInterpreter +ZEPPELIN_LOGFILE="${ZEPPELIN_LOG_DIR}/stop-interpreter.log" +JAVA_OPTS+=" -Dzeppelin.log.file=${ZEPPELIN_LOGFILE}" + +if [[ -d "${ZEPPELIN_HOME}/zeppelin-zengine/target/classes" ]]; then + ZEPPELIN_CLASSPATH+=":${ZEPPELIN_HOME}/zeppelin-zengine/target/classes" +fi + +if [[ -d "${ZEPPELIN_HOME}/zeppelin-interpreter/target/classes" ]]; then + ZEPPELIN_CLASSPATH+=":${ZEPPELIN_HOME}/zeppelin-interpreter/target/classes" +fi + +addJarInDir "${ZEPPELIN_HOME}/zeppelin-interpreter/target/lib" +addJarInDir "${ZEPPELIN_HOME}/zeppelin-server/target/lib" +addJarInDir "${ZEPPELIN_HOME}/lib" +addJarInDir "${ZEPPELIN_HOME}/lib/interpreter" + +CLASSPATH+=":${ZEPPELIN_CLASSPATH}" +$ZEPPELIN_RUNNER $JAVA_OPTS -cp $CLASSPATH $ZEPPELIN_STOP_INTERPRETER_MAIN ${@} diff --git a/bin/zeppelin-daemon.sh b/bin/zeppelin-daemon.sh index 5982aee2e0f..e8988497513 100755 --- a/bin/zeppelin-daemon.sh +++ b/bin/zeppelin-daemon.sh @@ -217,18 +217,6 @@ function stop() { action_msg "${ZEPPELIN_NAME} stop" "${SET_OK}" fi fi - - # list all pid that used in remote interpreter and kill them - for f in ${ZEPPELIN_PID_DIR}/*.pid; do - if [[ ! -f ${f} ]]; then - continue; - fi - - pid=$(cat ${f}) - wait_for_zeppelin_to_die $pid 20 - $(rm -f ${f}) - done - } function find_zeppelin_process() { diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template index 3c5bbeae59a..d566a717884 100755 --- a/conf/zeppelin-site.xml.template +++ b/conf/zeppelin-site.xml.template @@ -480,4 +480,45 @@ 10000:10010 --> + + + + + + + + + + + + diff --git a/docs/usage/interpreter/overview.md b/docs/usage/interpreter/overview.md index dd5ed220c88..035c381b8a9 100644 --- a/docs/usage/interpreter/overview.md +++ b/docs/usage/interpreter/overview.md @@ -144,3 +144,11 @@ So users needs to understand the ([interpreter mode setting ](../usage/interpret In this scenario, user need to put `ConfInterpreter` as the first paragraph as the below example. Otherwise the customized setting can not be applied (Actually it would report ERROR) + +## Interpreter Process Recovery + +Before 0.8.0, shutting down Zeppelin also mean to shutdown all the running interpreter processes. Usually admin will shutdown Zeppelin server for maintenance or upgrade, but don't want to shut down the running interpreter processes. +In such cases, interpreter process recovery is necessary. Starting from 0.8.0, user can enable interpreter process recovering via setting `zeppelin.recovery.storage.class` as +`org.apache.zeppelin.interpreter.recovery.FileSystemRecoveryStorage` or other implementations if available in future, by default it is `org.apache.zeppelin.interpreter.recovery.NullRecoveryStorage` + which means recovery is not enabled. Enable recover means shutting down Zeppelin would not terminating interpreter process, +and when Zeppelin is restarted, it would try to reconnect to the existing running interpreter processes. If you want to kill all the interpreter processes after terminating Zeppelin even when recovery is enabled, you can run `bin/stop-interpreter.sh` diff --git a/spark/src/main/resources/interpreter-setting.json b/spark/src/main/resources/interpreter-setting.json index 485f6950df0..d656532eb07 100644 --- a/spark/src/main/resources/interpreter-setting.json +++ b/spark/src/main/resources/interpreter-setting.json @@ -61,7 +61,7 @@ "description": "Spark master uri. ex) spark://masterhost:7077", "type": "string" }, - "zeppelin.spark.unSupportedVersionCheck": { + "zeppelin.spark.enableSupportedVersionCheck": { "envName": null, "propertyName": "zeppelin.spark.enableSupportedVersionCheck", "defaultValue": true, diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java index 438c661f8bc..77279edcd39 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java @@ -355,6 +355,19 @@ public String getNotebookDir() { return getString(ConfVars.ZEPPELIN_NOTEBOOK_DIR); } + public String getRecoveryDir() { + return getRelativeDir(ConfVars.ZEPPELIN_RECOVERY_DIR); + } + + public String getRecoveryStorageClass() { + return getString(ConfVars.ZEPPELIN_RECOVERY_STORAGE_CLASS); + } + + public boolean isRecoveryEnabled() { + return !getString(ConfVars.ZEPPELIN_RECOVERY_STORAGE_CLASS).equals( + "org.apache.zeppelin.interpreter.recovery.NullRecoveryStorage"); + } + public String getUser() { return getString(ConfVars.ZEPPELIN_NOTEBOOK_S3_USER); } @@ -658,6 +671,10 @@ public static enum ConfVars { ZEPPELIN_INTERPRETER_OUTPUT_LIMIT("zeppelin.interpreter.output.limit", 1024 * 100), ZEPPELIN_ENCODING("zeppelin.encoding", "UTF-8"), ZEPPELIN_NOTEBOOK_DIR("zeppelin.notebook.dir", "notebook"), + ZEPPELIN_RECOVERY_DIR("zeppelin.recovery.dir", "recovery"), + ZEPPELIN_RECOVERY_STORAGE_CLASS("zeppelin.recovery.storage.class", + "org.apache.zeppelin.interpreter.recovery.NullRecoveryStorage"), + // use specified notebook (id) as homescreen ZEPPELIN_NOTEBOOK_HOMESCREEN("zeppelin.notebook.homescreen", null), // whether homescreen notebook will be hidden from notebook list or not diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterClient.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterClient.java index b991079feca..813dad86881 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterClient.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterClient.java @@ -19,8 +19,20 @@ /** * Interface to InterpreterClient which is created by InterpreterLauncher. This is the component - * that is used to for the communication fromzeppelin-server process to zeppelin interpreter process + * that is used to for the communication from zeppelin-server process to zeppelin interpreter + * process. */ public interface InterpreterClient { + String getInterpreterSettingName(); + + void start(String userName, Boolean isUserImpersonate); + + void stop(); + + String getHost(); + + int getPort(); + + boolean isRunning(); } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLaunchContext.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLaunchContext.java index 9e253555a90..6901e2c7a62 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLaunchContext.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLaunchContext.java @@ -30,6 +30,7 @@ public class InterpreterLaunchContext { private Properties properties; private InterpreterOption option; private InterpreterRunner runner; + private String interpreterGroupId; private String interpreterSettingId; private String interpreterSettingGroup; private String interpreterSettingName; @@ -37,12 +38,14 @@ public class InterpreterLaunchContext { public InterpreterLaunchContext(Properties properties, InterpreterOption option, InterpreterRunner runner, + String interpreterGroupId, String interpreterSettingId, String interpreterSettingGroup, String interpreterSettingName) { this.properties = properties; this.option = option; this.runner = runner; + this.interpreterGroupId = interpreterGroupId; this.interpreterSettingId = interpreterSettingId; this.interpreterSettingGroup = interpreterSettingGroup; this.interpreterSettingName = interpreterSettingName; @@ -60,6 +63,10 @@ public InterpreterRunner getRunner() { return runner; } + public String getInterpreterGroupId() { + return interpreterGroupId; + } + public String getInterpreterSettingId() { return interpreterSettingId; } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLauncher.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLauncher.java index 5d0acf3515a..1cee20e7a04 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLauncher.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLauncher.java @@ -18,6 +18,7 @@ package org.apache.zeppelin.interpreter.launcher; import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.recovery.RecoveryStorage; import java.io.IOException; import java.util.Properties; @@ -29,9 +30,11 @@ public abstract class InterpreterLauncher { protected ZeppelinConfiguration zConf; protected Properties properties; + protected RecoveryStorage recoveryStorage; - public InterpreterLauncher(ZeppelinConfiguration zConf) { + public InterpreterLauncher(ZeppelinConfiguration zConf, RecoveryStorage recoveryStorage) { this.zConf = zConf; + this.recoveryStorage = recoveryStorage; } public abstract InterpreterClient launch(InterpreterLaunchContext context) throws IOException; diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/recovery/RecoveryStorage.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/recovery/RecoveryStorage.java new file mode 100644 index 00000000000..8bbe8302fcf --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/recovery/RecoveryStorage.java @@ -0,0 +1,80 @@ +/* + * 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.interpreter.recovery; + +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.launcher.InterpreterClient; + +import java.io.IOException; +import java.util.Map; + + +/** + * Interface for storing interpreter process recovery metadata. + * + */ +public abstract class RecoveryStorage { + + protected ZeppelinConfiguration zConf; + protected Map restoredClients; + + public RecoveryStorage(ZeppelinConfiguration zConf) throws IOException { + this.zConf = zConf; + } + + /** + * Update RecoveryStorage when new InterpreterClient is started + * @param client + * @throws IOException + */ + public abstract void onInterpreterClientStart(InterpreterClient client) throws IOException; + + /** + * Update RecoveryStorage when InterpreterClient is stopped + * @param client + * @throws IOException + */ + public abstract void onInterpreterClientStop(InterpreterClient client) throws IOException; + + /** + * + * It is only called when Zeppelin Server is started. + * + * @return + * @throws IOException + */ + public abstract Map restore() throws IOException; + + + /** + * It is called after constructor + * + * @throws IOException + */ + public void init() throws IOException { + this.restoredClients = restore(); + } + + public InterpreterClient getInterpreterClient(String interpreterGroupId) { + if (restoredClients.containsKey(interpreterGroupId)) { + return restoredClients.get(interpreterGroupId); + } else { + return null; + } + } +} diff --git a/zeppelin-server/notebook/.python.recovery.crc b/zeppelin-server/notebook/.python.recovery.crc new file mode 100644 index 0000000000000000000000000000000000000000..6bd3e7ae43b861a0504394ec70405b321f2d9b0a GIT binary patch literal 12 TcmYc;N@ieSU}E^);Is??6cz)X literal 0 HcmV?d00001 diff --git a/zeppelin-server/notebook/python.recovery b/zeppelin-server/notebook/python.recovery new file mode 100644 index 00000000000..eaf4938fdad --- /dev/null +++ b/zeppelin-server/notebook/python.recovery @@ -0,0 +1 @@ +2CZA1DVUG:shared_process 192.168.3.2:55410 \ No newline at end of file diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 08ede293e4d..925c637fcfc 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -349,6 +349,21 @@ + + maven-surefire-plugin + ${plugin.surefire.version} + + -Xmx2g -Xms1g -Dfile.encoding=UTF-8 + + ${tests.to.exclude} + + + 1 + + + + + org.scala-tools maven-scala-plugin 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 0b66a437d5b..f8625c2357c 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 @@ -162,7 +162,7 @@ public ZeppelinServer() throws Exception { public static void main(String[] args) throws InterruptedException { - ZeppelinConfiguration conf = ZeppelinConfiguration.create(); + final ZeppelinConfiguration conf = ZeppelinConfiguration.create(); conf.setProperty("args", args); jettyWebServer = setupJettyServer(conf); @@ -199,7 +199,9 @@ public static void main(String[] args) throws InterruptedException { LOG.info("Shutting down Zeppelin Server ... "); try { jettyWebServer.stop(); - notebook.getInterpreterSettingManager().close(); + if (!conf.isRecoveryEnabled()) { + ZeppelinServer.notebook.getInterpreterSettingManager().close(); + } notebook.close(); Thread.sleep(3000); } catch (Exception e) { @@ -222,7 +224,9 @@ public static void main(String[] args) throws InterruptedException { } jettyWebServer.join(); - ZeppelinServer.notebook.getInterpreterSettingManager().close(); + if (!conf.isRecoveryEnabled()) { + ZeppelinServer.notebook.getInterpreterSettingManager().close(); + } } private static Server setupJettyServer(ZeppelinConfiguration conf) { diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/recovery/RecoveryTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/recovery/RecoveryTest.java new file mode 100644 index 00000000000..37277ee0c36 --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/recovery/RecoveryTest.java @@ -0,0 +1,162 @@ +/* + * 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.recovery; + +import com.google.common.io.Files; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.io.FileUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.ManagedInterpreterGroup; +import org.apache.zeppelin.interpreter.recovery.FileSystemRecoveryStorage; +import org.apache.zeppelin.interpreter.recovery.StopInterpreter; +import org.apache.zeppelin.notebook.Note; +import org.apache.zeppelin.notebook.Paragraph; +import org.apache.zeppelin.rest.AbstractTestRestApi; +import org.apache.zeppelin.scheduler.Job; +import org.apache.zeppelin.server.ZeppelinServer; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +public class RecoveryTest extends AbstractTestRestApi { + + private Gson gson = new Gson(); + private static File recoveryDir = null; + + @BeforeClass + public static void init() throws Exception { + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_RECOVERY_STORAGE_CLASS.getVarName(), + FileSystemRecoveryStorage.class.getName()); + recoveryDir = Files.createTempDir(); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_RECOVERY_DIR.getVarName(), recoveryDir.getAbsolutePath()); + startUp(RecoveryTest.class.getSimpleName()); + } + + @AfterClass + public static void destroy() throws Exception { + shutDown(); + FileUtils.deleteDirectory(recoveryDir); + } + + @Test + public void testRecovery() throws Exception { + Note note1 = ZeppelinServer.notebook.createNote(AuthenticationInfo.ANONYMOUS); + + // run python interpreter and create new variable `user` + Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); + p1.setText("%python user='abc'"); + PostMethod post = httpPost("/notebook/job/" + note1.getId(), ""); + assertThat(post, isAllowed()); + Map resp = gson.fromJson(post.getResponseBodyAsString(), new TypeToken>() { + }.getType()); + assertEquals(resp.get("status"), "OK"); + post.releaseConnection(); + assertEquals(Job.Status.FINISHED, p1.getStatus()); + + // shutdown zeppelin and restart it + shutDown(); + startUp(RecoveryTest.class.getSimpleName()); + + // run the paragraph again, but change the text to print variable `user` + note1 = ZeppelinServer.notebook.getNote(note1.getId()); + p1 = note1.getParagraph(p1.getId()); + p1.setText("%python print(user)"); + post = httpPost("/notebook/job/" + note1.getId(), ""); + assertEquals(resp.get("status"), "OK"); + post.releaseConnection(); + assertEquals(Job.Status.FINISHED, p1.getStatus()); + assertEquals("abc\n", p1.getResult().message().get(0).getData()); + } + + @Test + public void testRecovery_2() throws Exception { + Note note1 = ZeppelinServer.notebook.createNote(AuthenticationInfo.ANONYMOUS); + + // run python interpreter and create new variable `user` + Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); + p1.setText("%python user='abc'"); + PostMethod post = httpPost("/notebook/job/" + note1.getId(), ""); + assertThat(post, isAllowed()); + Map resp = gson.fromJson(post.getResponseBodyAsString(), new TypeToken>() { + }.getType()); + assertEquals(resp.get("status"), "OK"); + post.releaseConnection(); + assertEquals(Job.Status.FINISHED, p1.getStatus()); + + // restart the python interpreter + ZeppelinServer.notebook.getInterpreterSettingManager().restart( + ((ManagedInterpreterGroup) p1.getBindedInterpreter().getInterpreterGroup()) + .getInterpreterSetting().getId() + ); + + // shutdown zeppelin and restart it + shutDown(); + startUp(RecoveryTest.class.getSimpleName()); + + // run the paragraph again, but change the text to print variable `user`. + // can not recover the python interpreter, because it has been shutdown. + note1 = ZeppelinServer.notebook.getNote(note1.getId()); + p1 = note1.getParagraph(p1.getId()); + p1.setText("%python print(user)"); + post = httpPost("/notebook/job/" + note1.getId(), ""); + assertEquals(resp.get("status"), "OK"); + post.releaseConnection(); + assertEquals(Job.Status.ERROR, p1.getStatus()); + } + + @Test + public void testRecovery_3() throws Exception { + Note note1 = ZeppelinServer.notebook.createNote(AuthenticationInfo.ANONYMOUS); + + // run python interpreter and create new variable `user` + Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); + p1.setText("%python user='abc'"); + PostMethod post = httpPost("/notebook/job/" + note1.getId(), ""); + assertThat(post, isAllowed()); + Map resp = gson.fromJson(post.getResponseBodyAsString(), new TypeToken>() { + }.getType()); + assertEquals(resp.get("status"), "OK"); + post.releaseConnection(); + assertEquals(Job.Status.FINISHED, p1.getStatus()); + + // shutdown zeppelin and restart it + shutDown(); + StopInterpreter.main(new String[]{}); + + startUp(RecoveryTest.class.getSimpleName()); + + // run the paragraph again, but change the text to print variable `user`. + // can not recover the python interpreter, because it has been shutdown. + note1 = ZeppelinServer.notebook.getNote(note1.getId()); + p1 = note1.getParagraph(p1.getId()); + p1.setText("%python print(user)"); + post = httpPost("/notebook/job/" + note1.getId(), ""); + assertEquals(resp.get("status"), "OK"); + post.releaseConnection(); + assertEquals(Job.Status.ERROR, p1.getStatus()); + } +} 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 431e3647b27..7c083650814 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 @@ -318,8 +318,10 @@ protected static void shutDown() throws Exception { if (!wasRunning) { // restart interpreter to stop all interpreter processes List settingList = ZeppelinServer.notebook.getInterpreterSettingManager().get(); - for (InterpreterSetting setting : settingList) { - ZeppelinServer.notebook.getInterpreterSettingManager().restart(setting.getId()); + if (!ZeppelinServer.notebook.getConf().isRecoveryEnabled()) { + for (InterpreterSetting setting : settingList) { + ZeppelinServer.notebook.getInterpreterSettingManager().restart(setting.getId()); + } } if (shiroIni != null) { FileUtils.deleteQuietly(shiroIni); @@ -350,7 +352,12 @@ protected static void shutDown() throws Exception { .clearProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_ANONYMOUS_ALLOWED.getVarName()); } - FileUtils.deleteDirectory(confDir); + if (!ZeppelinServer.notebook.getConf().isRecoveryEnabled()) { + // don't delete interpreter.json when recovery is enabled. otherwise the interpreter setting + // id will change after zeppelin restart, then we can not recover interpreter process + // properly + FileUtils.deleteDirectory(confDir); + } } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java index d5ff947ad0c..424aa27a166 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java @@ -38,6 +38,8 @@ import org.apache.zeppelin.interpreter.launcher.ShellScriptLauncher; import org.apache.zeppelin.interpreter.launcher.SparkInterpreterLauncher; import org.apache.zeppelin.interpreter.lifecycle.NullLifecycleManager; +import org.apache.zeppelin.interpreter.recovery.NullRecoveryStorage; +import org.apache.zeppelin.interpreter.recovery.RecoveryStorage; import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry; import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterEventPoller; @@ -144,6 +146,9 @@ public class InterpreterSetting { + private transient RecoveryStorage recoveryStorage; + /////////////////////////////////////////////////////////////////////////////////////////// + /** * Builder class for InterpreterSetting */ @@ -242,6 +247,11 @@ public Builder setLifecycleManager(LifecycleManager lifecycleManager) { return this; } + public Builder setRecoveryStorage(RecoveryStorage recoveryStorage) { + interpreterSetting.recoveryStorage = recoveryStorage; + return this; + } + public InterpreterSetting create() { // post processing interpreterSetting.postProcessing(); @@ -261,6 +271,13 @@ void postProcessing() { if (this.lifecycleManager == null) { this.lifecycleManager = new NullLifecycleManager(conf); } + if (this.recoveryStorage == null) { + try { + this.recoveryStorage = new NullRecoveryStorage(conf, interpreterSettingManager); + } catch (IOException e) { + // ignore this exception as NullRecoveryStorage will do nothing. + } + } } /** @@ -285,9 +302,9 @@ public InterpreterSetting(InterpreterSetting o) { private void createLauncher() { if (group.equals("spark")) { - this.launcher = new SparkInterpreterLauncher(this.conf); + this.launcher = new SparkInterpreterLauncher(this.conf, this.recoveryStorage); } else { - this.launcher = new ShellScriptLauncher(this.conf); + this.launcher = new ShellScriptLauncher(this.conf, this.recoveryStorage); } } @@ -344,6 +361,15 @@ public InterpreterSetting setLifecycleManager(LifecycleManager lifecycleManager) return this; } + public InterpreterSetting setRecoveryStorage(RecoveryStorage recoveryStorage) { + this.recoveryStorage = recoveryStorage; + return this; + } + + public RecoveryStorage getRecoveryStorage() { + return recoveryStorage; + } + public LifecycleManager getLifecycleManager() { return lifecycleManager; } @@ -408,7 +434,12 @@ public ManagedInterpreterGroup getOrCreateInterpreterGroup(String user, String n } void removeInterpreterGroup(String groupId) { - this.interpreterGroups.remove(groupId); + try { + interpreterGroupWriteLock.lock(); + this.interpreterGroups.remove(groupId); + } finally { + interpreterGroupWriteLock.unlock(); + } } public ManagedInterpreterGroup getInterpreterGroup(String user, String noteId) { @@ -425,7 +456,6 @@ ManagedInterpreterGroup getInterpreterGroup(String groupId) { return interpreterGroups.get(groupId); } - @VisibleForTesting public ArrayList getAllInterpreterGroups() { try { interpreterGroupReadLock.lock(); @@ -668,16 +698,19 @@ List createInterpreters(String user, String interpreterGroupId, Str return interpreters; } - synchronized RemoteInterpreterProcess createInterpreterProcess(Properties properties) + synchronized RemoteInterpreterProcess createInterpreterProcess(String interpreterGroupId, + Properties properties) throws IOException { if (launcher == null) { createLauncher(); } InterpreterLaunchContext launchContext = new - InterpreterLaunchContext(properties, option, interpreterRunner, id, group, name); + InterpreterLaunchContext(properties, option, interpreterRunner, + interpreterGroupId, id, group, name); RemoteInterpreterProcess process = (RemoteInterpreterProcess) launcher.launch(launchContext); process.setRemoteInterpreterEventPoller( new RemoteInterpreterEventPoller(remoteInterpreterProcessListener, appEventListener)); + recoveryStorage.onInterpreterClientStart(process); return process; } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java index 0b7efd5db63..42f82fad214 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java @@ -34,12 +34,16 @@ import org.apache.zeppelin.display.AngularObjectRegistryListener; import org.apache.zeppelin.helium.ApplicationEventListener; import org.apache.zeppelin.interpreter.Interpreter.RegisteredInterpreter; +import org.apache.zeppelin.interpreter.recovery.FileSystemRecoveryStorage; +import org.apache.zeppelin.interpreter.recovery.NullRecoveryStorage; +import org.apache.zeppelin.interpreter.recovery.RecoveryStorage; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; import org.apache.zeppelin.resource.Resource; import org.apache.zeppelin.resource.ResourcePool; import org.apache.zeppelin.resource.ResourceSet; +import org.apache.zeppelin.util.ReflectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonatype.aether.repository.Authentication; @@ -118,6 +122,7 @@ public class InterpreterSettingManager { private ApplicationEventListener appEventListener; private DependencyResolver dependencyResolver; private LifecycleManager lifecycleManager; + private RecoveryStorage recoveryStorage; public InterpreterSettingManager(ZeppelinConfiguration zeppelinConfiguration, AngularObjectRegistryListener angularObjectRegistryListener, @@ -154,13 +159,17 @@ public InterpreterSettingManager(ZeppelinConfiguration conf, this.angularObjectRegistryListener = angularObjectRegistryListener; this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; this.appEventListener = appEventListener; - try { - this.lifecycleManager = (LifecycleManager) - Class.forName(conf.getLifecycleManagerClass()).getConstructor(ZeppelinConfiguration.class) - .newInstance(conf); - } catch (Exception e) { - throw new IOException("Fail to create LifecycleManager", e); - } + + this.recoveryStorage = ReflectionUtils.createClazzInstance(conf.getRecoveryStorageClass(), + new Class[] {ZeppelinConfiguration.class, InterpreterSettingManager.class}, + new Object[] {conf, this}); + this.recoveryStorage.init(); + LOGGER.info("Using RecoveryStorage: " + this.recoveryStorage.getClass().getName()); + + this.lifecycleManager = ReflectionUtils.createClazzInstance(conf.getLifecycleManagerClass(), + new Class[] {ZeppelinConfiguration.class}, + new Object[] {conf}); + LOGGER.info("Using LifecycleManager: " + this.lifecycleManager.getClass().getName()); init(); } @@ -174,6 +183,7 @@ private void initInterpreterSetting(InterpreterSetting interpreterSetting) { .setAppEventListener(appEventListener) .setDependencyResolver(dependencyResolver) .setLifecycleManager(lifecycleManager) + .setRecoveryStorage(recoveryStorage) .postProcessing(); } @@ -307,8 +317,16 @@ public boolean accept(Path entry) throws IOException { saveToFile(); } + public RemoteInterpreterProcessListener getRemoteInterpreterProcessListener() { + return remoteInterpreterProcessListener; + } + + public ApplicationEventListener getAppEventListener() { + return appEventListener; + } + private boolean registerInterpreterFromResource(ClassLoader cl, String interpreterDir, - String interpreterJson) throws IOException { + String interpreterJson) throws IOException { URL[] urls = recursiveBuildLibList(new File(interpreterDir)); ClassLoader tempClassLoader = new URLClassLoader(urls, null); @@ -507,6 +525,10 @@ public List call(RemoteInterpreterService.Client client) throws Exceptio return resourceSet; } + public RecoveryStorage getRecoveryStorage() { + return recoveryStorage; + } + public void removeResourcesBelongsToParagraph(String noteId, String paragraphId) { for (ManagedInterpreterGroup intpGroup : getAllInterpreterGroup()) { ResourceSet resourceSet = new ResourceSet(); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java index 2378f140daa..641c0ac23ef 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java @@ -55,15 +55,31 @@ public InterpreterSetting getInterpreterSetting() { return interpreterSetting; } - public synchronized RemoteInterpreterProcess getOrCreateInterpreterProcess(Properties properties) + public synchronized RemoteInterpreterProcess getOrCreateInterpreterProcess(String userName, + Properties properties) throws IOException { if (remoteInterpreterProcess == null) { LOGGER.info("Create InterpreterProcess for InterpreterGroup: " + getId()); - remoteInterpreterProcess = interpreterSetting.createInterpreterProcess(properties); + remoteInterpreterProcess = interpreterSetting.createInterpreterProcess(id, properties); + synchronized (remoteInterpreterProcess) { + if (!remoteInterpreterProcess.isRunning()) { + remoteInterpreterProcess.start(userName, false); + remoteInterpreterProcess.getRemoteInterpreterEventPoller() + .setInterpreterProcess(remoteInterpreterProcess); + remoteInterpreterProcess.getRemoteInterpreterEventPoller().setInterpreterGroup(this); + remoteInterpreterProcess.getRemoteInterpreterEventPoller().start(); + getInterpreterSetting().getRecoveryStorage() + .onInterpreterClientStart(remoteInterpreterProcess); + } + } } return remoteInterpreterProcess; } + public RemoteInterpreterProcess getInterpreterProcess() { + return remoteInterpreterProcess; + } + public RemoteInterpreterProcess getRemoteInterpreterProcess() { return remoteInterpreterProcess; } @@ -94,6 +110,11 @@ public synchronized void close(String sessionId) { if (remoteInterpreterProcess != null) { LOGGER.info("Kill RemoteInterpreterProcess"); remoteInterpreterProcess.stop(); + try { + interpreterSetting.getRecoveryStorage().onInterpreterClientStop(remoteInterpreterProcess); + } catch (IOException e) { + LOGGER.error("Fail to store recovery data", e); + } remoteInterpreterProcess = null; } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncher.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncher.java index 8c86129f64a..6ddcacf275a 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncher.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncher.java @@ -21,50 +21,68 @@ import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.interpreter.InterpreterOption; import org.apache.zeppelin.interpreter.InterpreterRunner; +import org.apache.zeppelin.interpreter.recovery.RecoveryStorage; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterManagedProcess; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterRunningProcess; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * Interpreter Launcher which use shell script to launch the interpreter process. - * */ public class ShellScriptLauncher extends InterpreterLauncher { private static final Logger LOGGER = LoggerFactory.getLogger(ShellScriptLauncher.class); - public ShellScriptLauncher(ZeppelinConfiguration zConf) { - super(zConf); + public ShellScriptLauncher(ZeppelinConfiguration zConf, RecoveryStorage recoveryStorage) { + super(zConf, recoveryStorage); } @Override - public InterpreterClient launch(InterpreterLaunchContext context) { + public InterpreterClient launch(InterpreterLaunchContext context) throws IOException { LOGGER.info("Launching Interpreter: " + context.getInterpreterSettingGroup()); this.properties = context.getProperties(); InterpreterOption option = context.getOption(); InterpreterRunner runner = context.getRunner(); String groupName = context.getInterpreterSettingGroup(); String name = context.getInterpreterSettingName(); - int connectTimeout = zConf.getInt(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_CONNECT_TIMEOUT); + if (option.isExistingProcess()) { return new RemoteInterpreterRunningProcess( + context.getInterpreterSettingName(), connectTimeout, option.getHost(), option.getPort()); } else { + // try to recover it first + if (zConf.isRecoveryEnabled()) { + InterpreterClient recoveredClient = + recoveryStorage.getInterpreterClient(context.getInterpreterGroupId()); + if (recoveredClient != null) { + if (recoveredClient.isRunning()) { + LOGGER.info("Recover interpreter process: " + recoveredClient.getHost() + ":" + + recoveredClient.getPort()); + return recoveredClient; + } else { + LOGGER.warn("Cannot recover interpreter process: " + recoveredClient.getHost() + ":" + + recoveredClient.getPort() + ", as it is already terminated."); + } + } + } + // create new remote process String localRepoPath = zConf.getInterpreterLocalRepoPath() + "/" + context.getInterpreterSettingId(); return new RemoteInterpreterManagedProcess( runner != null ? runner.getPath() : zConf.getInterpreterRemoteRunnerPath(), - zConf.getCallbackPortRange(), zConf.getInterpreterPortRange(), + zConf.getCallbackPortRange(), zConf.getInterpreterPortRange(), zConf.getInterpreterDir() + "/" + groupName, localRepoPath, buildEnvFromProperties(), connectTimeout, name); } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncher.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncher.java index 32a0530af18..e8a9cdf881e 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncher.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncher.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.recovery.RecoveryStorage; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,8 +36,8 @@ public class SparkInterpreterLauncher extends ShellScriptLauncher { private static final Logger LOGGER = LoggerFactory.getLogger(SparkInterpreterLauncher.class); - public SparkInterpreterLauncher(ZeppelinConfiguration zConf) { - super(zConf); + public SparkInterpreterLauncher(ZeppelinConfiguration zConf, RecoveryStorage recoveryStorage) { + super(zConf, recoveryStorage); } @Override diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/FileSystemRecoveryStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/FileSystemRecoveryStorage.java new file mode 100644 index 00000000000..5a0c8adf6cd --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/FileSystemRecoveryStorage.java @@ -0,0 +1,139 @@ +/* + * 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.interpreter.recovery; + +import org.apache.commons.lang.StringUtils; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.PathFilter; +import org.apache.hadoop.io.IOUtils; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.InterpreterSetting; +import org.apache.zeppelin.interpreter.InterpreterSettingManager; +import org.apache.zeppelin.interpreter.ManagedInterpreterGroup; +import org.apache.zeppelin.interpreter.launcher.InterpreterClient; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterEventPoller; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterRunningProcess; +import org.apache.zeppelin.notebook.FileSystemStorage; +import org.apache.zeppelin.notebook.repo.FileSystemNotebookRepo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * Hadoop compatible FileSystem based RecoveryStorage implementation. + * + * Save InterpreterProcess in the format of: + * InterpreterGroupId host:port + */ +public class FileSystemRecoveryStorage extends RecoveryStorage { + + private static final Logger LOGGER = LoggerFactory.getLogger(FileSystemRecoveryStorage.class); + + private InterpreterSettingManager interpreterSettingManager; + private FileSystemStorage fs; + private Path recoveryDir; + + public FileSystemRecoveryStorage(ZeppelinConfiguration zConf, + InterpreterSettingManager interpreterSettingManager) + throws IOException { + super(zConf); + this.interpreterSettingManager = interpreterSettingManager; + this.zConf = zConf; + this.fs = FileSystemStorage.get(zConf); + this.recoveryDir = this.fs.makeQualified(new Path(zConf.getRecoveryDir())); + LOGGER.info("Using folder {} to store recovery data", recoveryDir); + this.fs.tryMkDir(recoveryDir); + } + + @Override + public void onInterpreterClientStart(InterpreterClient client) throws IOException { + save(client.getInterpreterSettingName()); + } + + @Override + public void onInterpreterClientStop(InterpreterClient client) throws IOException { + save(client.getInterpreterSettingName()); + } + + private void save(String interpreterSettingName) throws IOException { + InterpreterSetting interpreterSetting = + interpreterSettingManager.getInterpreterSettingByName(interpreterSettingName); + List recoveryContent = new ArrayList<>(); + for (ManagedInterpreterGroup interpreterGroup : interpreterSetting.getAllInterpreterGroups()) { + RemoteInterpreterProcess interpreterProcess = interpreterGroup.getInterpreterProcess(); + if (interpreterProcess != null) { + recoveryContent.add(interpreterGroup.getId() + "\t" + interpreterProcess.getHost() + ":" + + interpreterProcess.getPort()); + } + } + LOGGER.debug("Updating recovery data for interpreterSetting: " + interpreterSettingName); + LOGGER.debug("Recovery Data: " + StringUtils.join(recoveryContent, System.lineSeparator())); + Path recoveryFile = new Path(recoveryDir, interpreterSettingName + ".recovery"); + fs.writeFile(StringUtils.join(recoveryContent, System.lineSeparator()), recoveryFile, true); + } + + @Override + public Map restore() throws IOException { + Map clients = new HashMap<>(); + List paths = fs.list(new Path(recoveryDir + "/*.recovery")); + + for (Path path : paths) { + String fileName = path.getName(); + String interpreterSettingName = fileName.substring(0, + fileName.length() - ".recovery".length()); + String recoveryContent = fs.readFile(path); + if (!StringUtils.isBlank(recoveryContent)) { + for (String line : recoveryContent.split(System.lineSeparator())) { + String[] tokens = line.split("\t"); + String groupId = tokens[0]; + String[] hostPort = tokens[1].split(":"); + int connectTimeout = + zConf.getInt(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_CONNECT_TIMEOUT); + RemoteInterpreterRunningProcess client = new RemoteInterpreterRunningProcess( + interpreterSettingName, connectTimeout, hostPort[0], Integer.parseInt(hostPort[1])); + // interpreterSettingManager may be null when this class is used when it is used + // stop-interpreter.sh + if (interpreterSettingManager != null) { + client.setRemoteInterpreterEventPoller(new RemoteInterpreterEventPoller( + interpreterSettingManager.getRemoteInterpreterProcessListener(), + interpreterSettingManager.getAppEventListener())); + } + clients.put(groupId, client); + LOGGER.info("Recovering Interpreter Process: " + hostPort[0] + ":" + hostPort[1]); + } + } + } + + return clients; + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/NullRecoveryStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/NullRecoveryStorage.java new file mode 100644 index 00000000000..3a7d12c70f3 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/NullRecoveryStorage.java @@ -0,0 +1,54 @@ +/* + * 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.interpreter.recovery; + +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.InterpreterSettingManager; +import org.apache.zeppelin.interpreter.launcher.InterpreterClient; + +import java.io.IOException; +import java.util.Map; + + +/** + * RecoveryStorage that do nothing, used when recovery is not enabled. + * + */ +public class NullRecoveryStorage extends RecoveryStorage { + + public NullRecoveryStorage(ZeppelinConfiguration zConf, + InterpreterSettingManager interpreterSettingManager) + throws IOException { + super(zConf); + } + + @Override + public void onInterpreterClientStart(InterpreterClient client) throws IOException { + + } + + @Override + public void onInterpreterClientStop(InterpreterClient client) throws IOException { + + } + + @Override + public Map restore() throws IOException { + return null; + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/StopInterpreter.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/StopInterpreter.java new file mode 100644 index 00000000000..d74b1621e7e --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/StopInterpreter.java @@ -0,0 +1,40 @@ +package org.apache.zeppelin.interpreter.recovery; + +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.InterpreterSettingManager; +import org.apache.zeppelin.interpreter.launcher.InterpreterClient; +import org.apache.zeppelin.util.ReflectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Map; + + +/** + * Utility class for stopping interpreter in the case that you want to stop all the + * interpreter process even when you enable recovery, or you want to kill interpreter process + * to avoid orphan process. + */ +public class StopInterpreter { + + private static Logger LOGGER = LoggerFactory.getLogger(StopInterpreter.class); + + public static void main(String[] args) throws IOException { + ZeppelinConfiguration zConf = ZeppelinConfiguration.create(); + RecoveryStorage recoveryStorage = null; + + recoveryStorage = ReflectionUtils.createClazzInstance(zConf.getRecoveryStorageClass(), + new Class[] {ZeppelinConfiguration.class, InterpreterSettingManager.class}, + new Object[] {zConf, null}); + + LOGGER.info("Using RecoveryStorage: " + recoveryStorage.getClass().getName()); + Map restoredClients = recoveryStorage.restore(); + if (restoredClients != null) { + for (InterpreterClient client : restoredClients.values()) { + LOGGER.info("Stop Interpreter Process: " + client.getHost() + ":" + client.getPort()); + client.stop(); + } + } + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java index 6defd9ba825..bda8010d93c 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java @@ -102,16 +102,7 @@ public synchronized RemoteInterpreterProcess getOrCreateInterpreterProcess() thr return this.interpreterProcess; } ManagedInterpreterGroup intpGroup = getInterpreterGroup(); - this.interpreterProcess = intpGroup.getOrCreateInterpreterProcess(properties); - synchronized (interpreterProcess) { - if (!interpreterProcess.isRunning()) { - interpreterProcess.start(this.getUserName(), false); - interpreterProcess.getRemoteInterpreterEventPoller() - .setInterpreterProcess(interpreterProcess); - interpreterProcess.getRemoteInterpreterEventPoller().setInterpreterGroup(intpGroup); - interpreterProcess.getRemoteInterpreterEventPoller().start(); - } - } + this.interpreterProcess = intpGroup.getOrCreateInterpreterProcess(getUserName(), properties); return interpreterProcess; } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java index 27e826c70a1..3dd5bfa3493 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java @@ -214,7 +214,7 @@ public void stop() { callbackServer.stop(); } if (isRunning()) { - logger.info("kill interpreter process"); + logger.info("Kill interpreter process"); try { callRemoteFunction(new RemoteFunction() { @Override @@ -263,7 +263,6 @@ public String getInterpreterDir() { return interpreterDir; } - @VisibleForTesting public String getInterpreterSettingName() { return interpreterSettingName; } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java index 88cc4894bed..08653ae390f 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java @@ -51,12 +51,6 @@ public void setRemoteInterpreterEventPoller(RemoteInterpreterEventPoller eventPo this.remoteInterpreterEventPoller = eventPoller; } - public abstract String getHost(); - public abstract int getPort(); - public abstract void start(String userName, Boolean isUserImpersonate); - public abstract void stop(); - public abstract boolean isRunning(); - public int getConnectTimeout() { return connectTimeout; } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java index d8715a0d499..0e87e4f7d4f 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java @@ -17,6 +17,7 @@ package org.apache.zeppelin.interpreter.remote; import org.apache.zeppelin.helium.ApplicationEventListener; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,13 +28,16 @@ public class RemoteInterpreterRunningProcess extends RemoteInterpreterProcess { private final Logger logger = LoggerFactory.getLogger(RemoteInterpreterRunningProcess.class); private final String host; private final int port; + private final String interpreterSettingName; public RemoteInterpreterRunningProcess( + String interpreterSettingName, int connectTimeout, String host, int port ) { super(connectTimeout); + this.interpreterSettingName = interpreterSettingName; this.host = host; this.port = port; } @@ -48,6 +52,11 @@ public int getPort() { return port; } + @Override + public String getInterpreterSettingName() { + return interpreterSettingName; + } + @Override public void start(String userName, Boolean isUserImpersonate) { // assume process is externally managed. nothing to do @@ -55,7 +64,24 @@ public void start(String userName, Boolean isUserImpersonate) { @Override public void stop() { - // assume process is externally managed. nothing to do + // assume process is externally managed. nothing to do. But will kill it + // when you want to force stop it. ENV ZEPPELIN_FORCE_STOP control that. + if (System.getenv("ZEPPELIN_FORCE_STOP") != null) { + if (isRunning()) { + logger.info("Kill interpreter process"); + try { + callRemoteFunction(new RemoteFunction() { + @Override + public Void call(RemoteInterpreterService.Client client) throws Exception { + client.shutdown(); + return null; + } + }); + } catch (Exception e) { + logger.warn("ignore the exception when shutting down interpreter process.", e); + } + } + } } @Override diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FileSystemStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FileSystemStorage.java new file mode 100644 index 00000000000..6f3d3f97f5c --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FileSystemStorage.java @@ -0,0 +1,168 @@ +package org.apache.zeppelin.notebook; + +import org.apache.commons.lang.StringUtils; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.RawLocalFileSystem; +import org.apache.hadoop.io.IOUtils; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.List; + + +/** + * Hadoop FileSystem wrapper. Support both secure and no-secure mode + */ +public class FileSystemStorage { + + private static Logger LOGGER = LoggerFactory.getLogger(FileSystemStorage.class); + + private static FileSystemStorage instance; + + private ZeppelinConfiguration zConf; + private Configuration hadoopConf; + private boolean isSecurityEnabled = false; + private FileSystem fs; + + private FileSystemStorage(ZeppelinConfiguration zConf) throws IOException { + this.zConf = zConf; + this.hadoopConf = new Configuration(); + this.hadoopConf.set("fs.file.impl", RawLocalFileSystem.class.getName()); + this.isSecurityEnabled = UserGroupInformation.isSecurityEnabled(); + + if (isSecurityEnabled) { + String keytab = zConf.getString( + ZeppelinConfiguration.ConfVars.ZEPPELIN_SERVER_KERBEROS_KEYTAB); + String principal = zConf.getString( + ZeppelinConfiguration.ConfVars.ZEPPELIN_SERVER_KERBEROS_PRINCIPAL); + if (StringUtils.isBlank(keytab) || StringUtils.isBlank(principal)) { + throw new IOException("keytab and principal can not be empty, keytab: " + keytab + + ", principal: " + principal); + } + UserGroupInformation.loginUserFromKeytab(principal, keytab); + } + + try { + this.fs = FileSystem.get(new URI(zConf.getNotebookDir()), this.hadoopConf); + LOGGER.info("Creating FileSystem: " + this.fs.getClass().getCanonicalName()); + } catch (URISyntaxException e) { + throw new IOException(e); + } + } + + public static synchronized FileSystemStorage get(ZeppelinConfiguration zConf) throws IOException { + if (instance == null) { + instance = new FileSystemStorage(zConf); + } + return instance; + } + + public Path makeQualified(Path path) { + return fs.makeQualified(path); + } + + public void tryMkDir(final Path dir) throws IOException { + callHdfsOperation(new HdfsOperation() { + @Override + public Void call() throws IOException { + if (!fs.exists(dir)) { + fs.mkdirs(dir); + LOGGER.info("Create dir {} in hdfs", dir.toString()); + } + if (fs.isFile(dir)) { + throw new IOException(dir.toString() + " is file instead of directory, please remove " + + "it or specify another directory"); + } + fs.mkdirs(dir); + return null; + } + }); + } + + public List list(final Path path) throws IOException { + return callHdfsOperation(new HdfsOperation>() { + @Override + public List call() throws IOException { + List paths = new ArrayList<>(); + for (FileStatus status : fs.globStatus(path)) { + paths.add(status.getPath()); + } + return paths; + } + }); + } + + public boolean delete(final Path path) throws IOException { + return callHdfsOperation(new HdfsOperation() { + @Override + public Boolean call() throws IOException { + return fs.delete(path, true); + } + }); + } + + public String readFile(final Path file) throws IOException { + return callHdfsOperation(new HdfsOperation() { + @Override + public String call() throws IOException { + LOGGER.debug("Read from file: " + file); + ByteArrayOutputStream noteBytes = new ByteArrayOutputStream(); + IOUtils.copyBytes(fs.open(file), noteBytes, hadoopConf); + return new String(noteBytes.toString( + zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_ENCODING))); + } + }); + } + + public void writeFile(final String content, final Path file, boolean writeTempFileFirst) + throws IOException { + callHdfsOperation(new HdfsOperation() { + @Override + public Void call() throws IOException { + InputStream in = new ByteArrayInputStream(content.getBytes( + zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_ENCODING))); + Path tmpFile = new Path(file.toString() + ".tmp"); + IOUtils.copyBytes(in, fs.create(tmpFile), hadoopConf); + fs.delete(file, true); + fs.rename(tmpFile, file); + return null; + } + }); + } + + private interface HdfsOperation { + T call() throws IOException; + } + + public synchronized T callHdfsOperation(final HdfsOperation func) throws IOException { + if (isSecurityEnabled) { + UserGroupInformation.getLoginUser().reloginFromKeytab(); + try { + return UserGroupInformation.getCurrentUser().doAs(new PrivilegedExceptionAction() { + @Override + public T run() throws Exception { + return func.call(); + } + }); + } catch (InterruptedException e) { + throw new IOException(e); + } + } else { + return func.call(); + } + } + +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/FileSystemNotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/FileSystemNotebookRepo.java index ba858e69255..d8ec0e5400b 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/FileSystemNotebookRepo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/FileSystemNotebookRepo.java @@ -8,6 +8,7 @@ import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.security.UserGroupInformation; import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.notebook.FileSystemStorage; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.NoteInfo; import org.apache.zeppelin.user.AuthenticationInfo; @@ -37,108 +38,45 @@ public class FileSystemNotebookRepo implements NotebookRepo { private static final Logger LOGGER = LoggerFactory.getLogger(FileSystemNotebookRepo.class); - private Configuration hadoopConf; - private ZeppelinConfiguration zConf; - private boolean isSecurityEnabled = false; - private FileSystem fs; + private FileSystemStorage fs; private Path notebookDir; public FileSystemNotebookRepo(ZeppelinConfiguration zConf) throws IOException { - this.zConf = zConf; - this.hadoopConf = new Configuration(); - - this.isSecurityEnabled = UserGroupInformation.isSecurityEnabled(); - if (isSecurityEnabled) { - String keytab = zConf.getString( - ZeppelinConfiguration.ConfVars.ZEPPELIN_SERVER_KERBEROS_KEYTAB); - String principal = zConf.getString( - ZeppelinConfiguration.ConfVars.ZEPPELIN_SERVER_KERBEROS_PRINCIPAL); - if (StringUtils.isBlank(keytab) || StringUtils.isBlank(principal)) { - throw new IOException("keytab and principal can not be empty, keytab: " + keytab - + ", principal: " + principal); - } - UserGroupInformation.loginUserFromKeytab(principal, keytab); - } + this.fs = FileSystemStorage.get(zConf); + this.notebookDir = this.fs.makeQualified(new Path(zConf.getNotebookDir())); + LOGGER.info("Using folder {} to store notebook", notebookDir); + this.fs.tryMkDir(notebookDir); - try { - this.fs = FileSystem.get(new URI(zConf.getNotebookDir()), new Configuration()); - LOGGER.info("Creating FileSystem: " + this.fs.getClass().getCanonicalName()); - this.notebookDir = fs.makeQualified(new Path(zConf.getNotebookDir())); - LOGGER.info("Using folder {} to store notebook", notebookDir); - } catch (URISyntaxException e) { - throw new IOException(e); - } - if (!fs.exists(notebookDir)) { - fs.mkdirs(notebookDir); - LOGGER.info("Create notebook dir {} in hdfs", notebookDir.toString()); - } - if (fs.isFile(notebookDir)) { - throw new IOException("notebookDir {} is file instead of directory, please remove it or " + - "specify another directory"); - } } @Override public List list(AuthenticationInfo subject) throws IOException { - return callHdfsOperation(new HdfsOperation>() { - @Override - public List call() throws IOException { - List noteInfos = new ArrayList<>(); - for (FileStatus status : fs.globStatus(new Path(notebookDir, "*/note.json"))) { - NoteInfo noteInfo = new NoteInfo(status.getPath().getParent().getName(), "", null); - noteInfos.add(noteInfo); - } - return noteInfos; - } - }); + List notePaths = fs.list(new Path(notebookDir, "*/note.json")); + List noteInfos = new ArrayList<>(); + for (Path path : notePaths) { + NoteInfo noteInfo = new NoteInfo(path.getParent().getName(), "", null); + noteInfos.add(noteInfo); + } + return noteInfos; } @Override public Note get(final String noteId, AuthenticationInfo subject) throws IOException { - return callHdfsOperation(new HdfsOperation() { - @Override - public Note call() throws IOException { - Path notePath = new Path(notebookDir.toString() + "/" + noteId + "/note.json"); - LOGGER.debug("Read note from file: " + notePath); - ByteArrayOutputStream noteBytes = new ByteArrayOutputStream(); - IOUtils.copyBytes(fs.open(notePath), noteBytes, hadoopConf); - return Note.fromJson(new String(noteBytes.toString( - zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_ENCODING)))); - } - }); + String content = this.fs.readFile( + new Path(notebookDir.toString() + "/" + noteId + "/note.json")); + return Note.fromJson(content); } @Override public void save(final Note note, AuthenticationInfo subject) throws IOException { - callHdfsOperation(new HdfsOperation() { - @Override - public Void call() throws IOException { - Path notePath = new Path(notebookDir.toString() + "/" + note.getId() + "/note.json"); - Path tmpNotePath = new Path(notebookDir.toString() + "/" + note.getId() + "/.note.json"); - LOGGER.debug("Saving note to file: " + notePath); - if (fs.exists(tmpNotePath)) { - fs.delete(tmpNotePath, true); - } - InputStream in = new ByteArrayInputStream(note.toJson().getBytes( - zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_ENCODING))); - IOUtils.copyBytes(in, fs.create(tmpNotePath), hadoopConf); - fs.delete(notePath, true); - fs.rename(tmpNotePath, notePath); - return null; - } - }); + this.fs.writeFile(note.toJson(), + new Path(notebookDir.toString() + "/" + note.getId() + "/note.json"), + true); } @Override public void remove(final String noteId, AuthenticationInfo subject) throws IOException { - callHdfsOperation(new HdfsOperation() { - @Override - public Void call() throws IOException { - Path noteFolder = new Path(notebookDir.toString() + "/" + noteId); - fs.delete(noteFolder, true); - return null; - } - }); + this.fs.delete(new Path(notebookDir.toString() + "/" + noteId)); } @Override @@ -182,26 +120,4 @@ public List getSettings(AuthenticationInfo subject) { public void updateSettings(Map settings, AuthenticationInfo subject) { LOGGER.warn("updateSettings is not implemented for HdfsNotebookRepo"); } - - private interface HdfsOperation { - T call() throws IOException; - } - - public synchronized T callHdfsOperation(final HdfsOperation func) throws IOException { - if (isSecurityEnabled) { - UserGroupInformation.getLoginUser().reloginFromKeytab(); - try { - return UserGroupInformation.getCurrentUser().doAs(new PrivilegedExceptionAction() { - @Override - public T run() throws Exception { - return func.call(); - } - }); - } catch (InterruptedException e) { - throw new IOException(e); - } - } else { - return func.call(); - } - } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/util/ReflectionUtils.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/util/ReflectionUtils.java new file mode 100644 index 00000000000..ca09992a7d8 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/util/ReflectionUtils.java @@ -0,0 +1,99 @@ +/* + * 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.util; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + + +/** + * Utility class for creating instances via java reflection. + * + */ +public class ReflectionUtils { + + public static Class getClazz(String className) throws IOException { + Class clazz = null; + try { + clazz = Class.forName(className, true, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new IOException("Unable to load class: " + className, e); + } + + return clazz; + } + + private static T getNewInstance(Class clazz) throws IOException { + T instance; + try { + instance = clazz.newInstance(); + } catch (InstantiationException e) { + throw new IOException( + "Unable to instantiate class with 0 arguments: " + clazz.getName(), e); + } catch (IllegalAccessException e) { + throw new IOException( + "Unable to instantiate class with 0 arguments: " + clazz.getName(), e); + } + return instance; + } + + private static T getNewInstance(Class clazz, + Class[] parameterTypes, + Object[] parameters) + throws IOException { + T instance; + try { + Constructor constructor = clazz.getConstructor(parameterTypes); + instance = constructor.newInstance(parameters); + } catch (InstantiationException e) { + throw new IOException( + "Unable to instantiate class with " + parameters.length + " arguments: " + + clazz.getName(), e); + } catch (IllegalAccessException e) { + throw new IOException( + "Unable to instantiate class with " + parameters.length + " arguments: " + + clazz.getName(), e); + } catch (NoSuchMethodException e) { + throw new IOException( + "Unable to instantiate class with " + parameters.length + " arguments: " + + clazz.getName(), e); + } catch (InvocationTargetException e) { + throw new IOException( + "Unable to instantiate class with " + parameters.length + " arguments: " + + clazz.getName(), e); + } + return instance; + } + + public static T createClazzInstance(String className) throws IOException { + Class clazz = getClazz(className); + @SuppressWarnings("unchecked") + T instance = (T) getNewInstance(clazz); + return instance; + } + + public static T createClazzInstance(String className, + Class[] parameterTypes, + Object[] parameters) throws IOException { + Class clazz = getClazz(className); + T instance = (T) getNewInstance(clazz, parameterTypes, parameters); + return instance; + } + + +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java index 9df402d35e8..16c8c1d8cec 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java @@ -33,7 +33,7 @@ public abstract class AbstractInterpreterTest { protected File interpreterDir; protected File confDir; protected File notebookDir; - protected ZeppelinConfiguration conf = new ZeppelinConfiguration(); + protected ZeppelinConfiguration conf; @Before public void setUp() throws Exception { diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncherTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncherTest.java index 0c7f4baacfb..f7988e35701 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncherTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncherTest.java @@ -22,6 +22,7 @@ import org.apache.zeppelin.interpreter.remote.RemoteInterpreterManagedProcess; import org.junit.Test; +import java.io.IOException; import java.util.Properties; import static org.junit.Assert.assertEquals; @@ -30,14 +31,14 @@ public class ShellScriptLauncherTest { @Test - public void testLauncher() { + public void testLauncher() throws IOException { ZeppelinConfiguration zConf = new ZeppelinConfiguration(); - ShellScriptLauncher launcher = new ShellScriptLauncher(zConf); + ShellScriptLauncher launcher = new ShellScriptLauncher(zConf, null); Properties properties = new Properties(); properties.setProperty("ENV_1", "VALUE_1"); properties.setProperty("property_1", "value_1"); InterpreterOption option = new InterpreterOption(); - InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "groupId", "groupName", "name"); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "intpGroupId", "groupId", "groupName", "name"); InterpreterClient client = launcher.launch(context); assertTrue( client instanceof RemoteInterpreterManagedProcess); RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncherTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncherTest.java index b788ebdeeea..3d7e251b079 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncherTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncherTest.java @@ -22,6 +22,7 @@ import org.apache.zeppelin.interpreter.remote.RemoteInterpreterManagedProcess; import org.junit.Test; +import java.io.IOException; import java.util.Properties; import static org.junit.Assert.assertEquals; @@ -30,9 +31,9 @@ public class SparkInterpreterLauncherTest { @Test - public void testLocalMode() { + public void testLocalMode() throws IOException { ZeppelinConfiguration zConf = new ZeppelinConfiguration(); - SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf); + SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf, null); Properties properties = new Properties(); properties.setProperty("SPARK_HOME", "/user/spark"); properties.setProperty("property_1", "value_1"); @@ -41,7 +42,7 @@ public void testLocalMode() { properties.setProperty("spark.jars", "jar_1"); InterpreterOption option = new InterpreterOption(); - InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "groupId", "spark", "spark"); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "intpGroupId", "groupId", "spark", "spark"); InterpreterClient client = launcher.launch(context); assertTrue( client instanceof RemoteInterpreterManagedProcess); RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; @@ -55,9 +56,9 @@ public void testLocalMode() { } @Test - public void testYarnClientMode_1() { + public void testYarnClientMode_1() throws IOException { ZeppelinConfiguration zConf = new ZeppelinConfiguration(); - SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf); + SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf, null); Properties properties = new Properties(); properties.setProperty("SPARK_HOME", "/user/spark"); properties.setProperty("property_1", "value_1"); @@ -66,7 +67,7 @@ public void testYarnClientMode_1() { properties.setProperty("spark.jars", "jar_1"); InterpreterOption option = new InterpreterOption(); - InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "groupId", "spark", "spark"); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "intpGroupId", "groupId", "spark", "spark"); InterpreterClient client = launcher.launch(context); assertTrue( client instanceof RemoteInterpreterManagedProcess); RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; @@ -80,9 +81,9 @@ public void testYarnClientMode_1() { } @Test - public void testYarnClientMode_2() { + public void testYarnClientMode_2() throws IOException { ZeppelinConfiguration zConf = new ZeppelinConfiguration(); - SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf); + SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf, null); Properties properties = new Properties(); properties.setProperty("SPARK_HOME", "/user/spark"); properties.setProperty("property_1", "value_1"); @@ -92,7 +93,7 @@ public void testYarnClientMode_2() { properties.setProperty("spark.jars", "jar_1"); InterpreterOption option = new InterpreterOption(); - InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "groupId", "spark", "spark"); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "intpGroupId", "groupId", "spark", "spark"); InterpreterClient client = launcher.launch(context); assertTrue( client instanceof RemoteInterpreterManagedProcess); RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; @@ -106,9 +107,9 @@ public void testYarnClientMode_2() { } @Test - public void testYarnClusterMode_1() { + public void testYarnClusterMode_1() throws IOException { ZeppelinConfiguration zConf = new ZeppelinConfiguration(); - SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf); + SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf, null); Properties properties = new Properties(); properties.setProperty("SPARK_HOME", "/user/spark"); properties.setProperty("property_1", "value_1"); @@ -117,7 +118,7 @@ public void testYarnClusterMode_1() { properties.setProperty("spark.jars", "jar_1"); InterpreterOption option = new InterpreterOption(); - InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "groupId", "spark", "spark"); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "intpGroupId", "groupId", "spark", "spark"); InterpreterClient client = launcher.launch(context); assertTrue( client instanceof RemoteInterpreterManagedProcess); RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; @@ -132,9 +133,9 @@ public void testYarnClusterMode_1() { } @Test - public void testYarnClusterMode_2() { + public void testYarnClusterMode_2() throws IOException { ZeppelinConfiguration zConf = new ZeppelinConfiguration(); - SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf); + SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf, null); Properties properties = new Properties(); properties.setProperty("SPARK_HOME", "/user/spark"); properties.setProperty("property_1", "value_1"); @@ -144,7 +145,7 @@ public void testYarnClusterMode_2() { properties.setProperty("spark.jars", "jar_1"); InterpreterOption option = new InterpreterOption(); - InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "groupId", "spark", "spark"); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "intpGroupId", "groupId", "spark", "spark"); InterpreterClient client = launcher.launch(context); assertTrue( client instanceof RemoteInterpreterManagedProcess); RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/recovery/FileSystemRecoveryStorageTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/recovery/FileSystemRecoveryStorageTest.java new file mode 100644 index 00000000000..cf1899c13e5 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/recovery/FileSystemRecoveryStorageTest.java @@ -0,0 +1,92 @@ +package org.apache.zeppelin.interpreter.recovery; + +import com.google.common.io.Files; +import org.apache.commons.io.FileUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.interpreter.AbstractInterpreterTest; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterContextRunner; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterOption; +import org.apache.zeppelin.interpreter.InterpreterSetting; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +import static org.junit.Assert.assertEquals; + +public class FileSystemRecoveryStorageTest extends AbstractInterpreterTest { + + private File recoveryDir = null; + + @Before + public void setUp() throws Exception { + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_RECOVERY_STORAGE_CLASS.getVarName(), + FileSystemRecoveryStorage.class.getName()); + recoveryDir = Files.createTempDir(); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_RECOVERY_DIR.getVarName(), recoveryDir.getAbsolutePath()); + super.setUp(); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + FileUtils.deleteDirectory(recoveryDir); + } + + @Test + public void testSingleInterpreterProcess() throws InterpreterException, IOException { + InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("test"); + interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); + + Interpreter interpreter1 = interpreterSetting.getDefaultInterpreter("user1", "note1"); + RemoteInterpreter remoteInterpreter1 = (RemoteInterpreter) interpreter1; + InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + new GUI(), null, null, new ArrayList(), null); + remoteInterpreter1.interpret("hello", context1); + + assertEquals(1, interpreterSettingManager.getRecoveryStorage().restore().size()); + + interpreterSetting.close(); + assertEquals(0, interpreterSettingManager.getRecoveryStorage().restore().size()); + } + + @Test + public void testMultipleInterpreterProcess() throws InterpreterException, IOException { + InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("test"); + interpreterSetting.getOption().setPerUser(InterpreterOption.ISOLATED); + + Interpreter interpreter1 = interpreterSetting.getDefaultInterpreter("user1", "note1"); + RemoteInterpreter remoteInterpreter1 = (RemoteInterpreter) interpreter1; + InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + new GUI(), null, null, new ArrayList(), null); + remoteInterpreter1.interpret("hello", context1); + assertEquals(1, interpreterSettingManager.getRecoveryStorage().restore().size()); + + Interpreter interpreter2 = interpreterSetting.getDefaultInterpreter("user2", "note2"); + RemoteInterpreter remoteInterpreter2 = (RemoteInterpreter) interpreter2; + InterpreterContext context2 = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + new GUI(), null, null, new ArrayList(), null); + remoteInterpreter2.interpret("hello", context2); + + assertEquals(2, interpreterSettingManager.getRecoveryStorage().restore().size()); + + interpreterSettingManager.restart(interpreterSetting.getId(), "note1", "user1"); + assertEquals(1, interpreterSettingManager.getRecoveryStorage().restore().size()); + + interpreterSetting.close(); + assertEquals(0, interpreterSettingManager.getRecoveryStorage().restore().size()); + } + +} From 971631c1a19edd12e38f61deb8ca6440f29d5148 Mon Sep 17 00:00:00 2001 From: Lee moon soo Date: Thu, 7 Dec 2017 14:50:42 -0800 Subject: [PATCH 129/492] [MINOR] Fix notebook title bar margin ### What is this PR for? Notebook title bar location is little bit shifted to left. This minor fix notebook title bar margin. See screenshots below. ### What type of PR is it? Bug Fix ### Todos * [x] - fix margin ### Screenshots (if appropriate) Before ![image](https://user-images.githubusercontent.com/1540981/33742650-3469c2fe-db5e-11e7-8aa6-936d0a28de3a.png) After ![image](https://user-images.githubusercontent.com/1540981/33742640-250c3dc8-db5e-11e7-8af3-e8a9d7105963.png) ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: Lee moon soo Closes #2699 from Leemoonsoo/minor_noteaction_margin and squashes the following commits: 703f45830 [Lee moon soo] fix noteAction magin --- zeppelin-web/src/app/notebook/notebook.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zeppelin-web/src/app/notebook/notebook.css b/zeppelin-web/src/app/notebook/notebook.css index a7508bce542..262ae8e6ccf 100644 --- a/zeppelin-web/src/app/notebook/notebook.css +++ b/zeppelin-web/src/app/notebook/notebook.css @@ -130,8 +130,8 @@ } .noteAction { - margin-left: -10px; - margin-right: -10px; + margin-left: 0px; + margin-right: 0px; font-family: 'Roboto', sans-serif; background: white; position: fixed; From f8cd64cb50028031ab321144697d98822a60c63f Mon Sep 17 00:00:00 2001 From: Naman Mishra Date: Tue, 12 Dec 2017 10:45:34 +0530 Subject: [PATCH 130/492] [ZEPPELIN-3091] Correct aggregation functionality in charts ### What is this PR for? The aggregation functions interpret NaN columns as 1 which leads to incorrect output being shown in charts. This PR fixes this by correcting the sum, min, max and average aggregation method. ### What type of PR is it? Bug Fix ### Todos ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-3091 ### How should this be tested? * Update the data field of a paragraph results with %table to contain null values, e.g. `"data": "age\tvalue\n19\t4\n20\t3\n21\t7\n22\t9\n23\t20\n24\t24\n25\t44\n26\t77\n27\t94\n28\t103\n29\t97\n20\t5\n20\tnull\n"` The "null" values should be ignored for sum, min, max but included for count (and hence average). ### Screenshots (if appropriate) ![correct_sum](https://user-images.githubusercontent.com/6438072/33609178-14e05988-d9ed-11e7-9f1b-99e0141c5153.png) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Naman Mishra Closes #2696 from namanmishra91/ZEPPELIN-3091 and squashes the following commits: d8a57c238 [Naman Mishra] Add test 38ad39c65 [Naman Mishra] Merge branch 'master' into ZEPPELIN-3091 568ae3f2a [Naman Mishra] Correct aggregation functionality in charts --- zeppelin-web/src/app/tabledata/pivot.js | 36 ++++++++++----- .../src/app/tabledata/tabledata.test.js | 45 +++++++++++++++++++ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/zeppelin-web/src/app/tabledata/pivot.js b/zeppelin-web/src/app/tabledata/pivot.js index a0f61b219a4..da2990043b0 100644 --- a/zeppelin-web/src/app/tabledata/pivot.js +++ b/zeppelin-web/src/app/tabledata/pivot.js @@ -138,8 +138,8 @@ export default class PivotTransformation extends Transformation { pivot (data, keys, groups, values) { let aggrFunc = { sum: function (a, b) { - let varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0 - let varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0 + let varA = (a !== undefined) ? (isNaN(a) ? 0 : parseFloat(a)) : 0 + let varB = (b !== undefined) ? (isNaN(b) ? 0 : parseFloat(b)) : 0 return varA + varB }, count: function (a, b) { @@ -148,22 +148,38 @@ export default class PivotTransformation extends Transformation { return varA + varB }, min: function (a, b) { - let varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0 - let varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0 - return Math.min(varA, varB) + let aIsValid = isValidNumber(a) + let bIsValid = isValidNumber(b) + if (!aIsValid) { + return parseFloat(b) + } else if (!bIsValid) { + return parseFloat(a) + } else { + return Math.min(parseFloat(a), parseFloat(b)) + } }, max: function (a, b) { - let varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0 - let varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0 - return Math.max(varA, varB) + let aIsValid = isValidNumber(a) + let bIsValid = isValidNumber(b) + if (!aIsValid) { + return parseFloat(b) + } else if (!bIsValid) { + return parseFloat(a) + } else { + return Math.max(parseFloat(a), parseFloat(b)) + } }, avg: function (a, b, c) { - let varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0 - let varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0 + let varA = (a !== undefined) ? (isNaN(a) ? 0 : parseFloat(a)) : 0 + let varB = (b !== undefined) ? (isNaN(b) ? 0 : parseFloat(b)) : 0 return varA + varB } } + let isValidNumber = function(num) { + return num !== undefined && !isNaN(num) + } + let aggrFuncDiv = { sum: false, count: false, diff --git a/zeppelin-web/src/app/tabledata/tabledata.test.js b/zeppelin-web/src/app/tabledata/tabledata.test.js index 3de2fa3fb72..e24b0733924 100644 --- a/zeppelin-web/src/app/tabledata/tabledata.test.js +++ b/zeppelin-web/src/app/tabledata/tabledata.test.js @@ -83,4 +83,49 @@ describe('PivotTransformation build', function() { expect(config.common.pivot.keys[1].index).toBe(3) expect(config.common.pivot.keys[2].index).toBe(5) }) + + it('should aggregate values correctly', function() { + let td = new TableData() + td.loadParagraphResult({ + type: 'TABLE', + msg: 'key\tvalue\na\t10\na\tnull\na\t0\na\t1\n' + }) + + let config = { + common: { + pivot: { + keys: [ + { + 'name': 'key', + 'index': 0.0, + } + ], + groups: [], + values: [ + { + 'name': 'value', + 'index': 1.0, + 'aggr': 'sum' + } + ] + } + } + } + + pt.setConfig(config) + let transformed = pt.transform(td) + expect(transformed.rows['a']['value(sum)'].value).toBe(11) + + pt.config.common.pivot.values[0].aggr = 'max' + transformed = pt.transform(td) + expect(transformed.rows['a']['value(max)'].value).toBe(10) + + pt.config.common.pivot.values[0].aggr = 'min' + transformed = pt.transform(td) + expect(transformed.rows['a']['value(min)'].value).toBe(0) + + pt.config.common.pivot.values[0].aggr = 'count' + transformed = pt.transform(td) + expect(transformed.rows['a']['value(count)'].value).toBe(4) + }) }) From 246f696719193dc8a3274f3f9e7ebc912ab3141a Mon Sep 17 00:00:00 2001 From: Andrea Santurbano Date: Tue, 5 Dec 2017 18:43:47 +0100 Subject: [PATCH 131/492] [ZEPPELIN-3101] updated network label, added link to network display in index.md ### What is this PR for? The docs index must show the reference to the network visualization as for the the other types ### What type of PR is it? [Improvement] ### Todos * [x] - Add missing link ### What is the Jira issue? [ZEPPELIN-3101](https://issues.apache.org/jira/projects/ZEPPELIN/issues/ZEPPELIN-3101) ### How should this be tested? 1. cd `docs/` 2. build: `bundle exec jekyll build --safe` 3. check the link is present ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: Andrea Santurbano Closes #2702 from conker84/graph-docs and squashes the following commits: 58d58cc4c [Andrea Santurbano] updated network label, added link to network display in index.md --- docs/_includes/themes/zeppelin/_navigation.html | 2 +- docs/index.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/_includes/themes/zeppelin/_navigation.html b/docs/_includes/themes/zeppelin/_navigation.html index bccb5b46915..95d83ea27c2 100644 --- a/docs/_includes/themes/zeppelin/_navigation.html +++ b/docs/_includes/themes/zeppelin/_navigation.html @@ -44,7 +44,7 @@
  • Text Display
  • HTML Display
  • Table Display
  • -
  • Network
  • +
  • Network Display
  • Angular Display using Backend API
  • Angular Display using Frontend API
  • diff --git a/docs/index.md b/docs/index.md index 8f3b551c6a4..587ae93ed58 100644 --- a/docs/index.md +++ b/docs/index.md @@ -59,6 +59,7 @@ limitations under the License. * [Text Display (`%text`)](./usage/display_system/basic.html#text) * [HTML Display (`%html`)](./usage/display_system/basic.html#html) * [Table Display (`%table`)](./usage/display_system/basic.html#table) + * [Network Display (`%network`)](./usage/display_system/basic.html#network) * [Angular Display using Backend API (`%angular`)](./usage/display_system/angular_backend.html) * [Angular Display using Frontend API (`%angular`)](./usage/display_system/angular_frontend.html) * Interpreter From a75aa52b681b224a8543a42bc69bf6df66f99c60 Mon Sep 17 00:00:00 2001 From: Jongyoul Lee Date: Mon, 18 Dec 2017 12:14:44 +0900 Subject: [PATCH 132/492] [ZEPPELIN-3100] Upgrade node and npm version ### What is this PR for? Reducing total build time ### What type of PR is it? [Improvement] ### Todos * [x] - Upgrade node and npm version ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3100 ### Screenshots (if appropriate) Before: screen shot 2017-12-11 at 5 57 46 pm After: screen shot 2017-12-11 at 5 57 46 pm ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jongyoul Lee Closes #2707 from jongyoul/ZEPPELIN-3100 and squashes the following commits: 3312c2d2 [Jongyoul Lee] Fixed rat f2697c14 [Jongyoul Lee] Upgraded node and npm version to reduce build time of web --- pom.xml | 6 +- zeppelin-web/package-lock.json | 11751 +++++++++++++++++++++++++++++++ zeppelin-web/pom.xml | 22 +- 3 files changed, 11763 insertions(+), 16 deletions(-) create mode 100644 zeppelin-web/package-lock.json diff --git a/pom.xml b/pom.xml index 7ff9acd7ae0..641880e06d5 100644 --- a/pom.xml +++ b/pom.xml @@ -91,9 +91,8 @@ 1.12.5 - v6.9.1 - v0.18.1 - 4.2.0 + v8.9.3 + 5.5.1 1.3 @@ -1009,6 +1008,7 @@ **/src/fonts/source-code-pro* **/src/**/**.test.js **/e2e/**/**.spec.js + package-lock.json **/src/main/java/org/apache/zeppelin/jdbc/SqlCompleter.java diff --git a/zeppelin-web/package-lock.json b/zeppelin-web/package-lock.json new file mode 100644 index 00000000000..443c7b77034 --- /dev/null +++ b/zeppelin-web/package-lock.json @@ -0,0 +1,11751 @@ +{ + "name": "zeppelin-web", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/node": { + "version": "6.0.92", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.92.tgz", + "integrity": "sha512-awEYSSTn7dauwVCYSx2CJaPTu0Z1Ht2oR1b2AD3CYao6ZRb+opb6EL43fzmD7eMFgMHzTBWSUzlWSD+S8xN0Nw==", + "dev": true + }, + "@types/q": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", + "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", + "dev": true + }, + "@types/selenium-webdriver": { + "version": "2.53.43", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-2.53.43.tgz", + "integrity": "sha512-UBYHWph6P3tutkbXpW6XYg9ZPbTKjw/YC2hGG1/GEvWwTbvezBUv3h+mmUFw79T3RFPnmedpiXdOBbXX+4l0jg==", + "dev": true + }, + "CSSselect": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/CSSselect/-/CSSselect-0.7.0.tgz", + "integrity": "sha1-5AVMZ7RnRl88lQDA2gqnh4xLq9I=", + "requires": { + "CSSwhat": "0.4.7", + "boolbase": "1.0.0", + "domutils": "1.4.3", + "nth-check": "1.0.1" + } + }, + "CSSwhat": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/CSSwhat/-/CSSwhat-0.4.7.tgz", + "integrity": "sha1-hn2g/zn3eGEyQsRM/qg/CqTr35s=" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", + "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "dev": true, + "requires": { + "mime-types": "2.1.17", + "negotiator": "0.6.1" + } + }, + "acorn": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.2.1.tgz", + "integrity": "sha512-jG0u7c4Ly+3QkkW18V+NRDN+4bWHdln30NL1ZL2AvFZZmQe/BfopYCtghCKKVBUSetZ4QKcyA0pY6/4Gw8Pv8w==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "3.3.0" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "adm-zip": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.4.tgz", + "integrity": "sha1-ph7VrmkFw66lizplfSUDMJEFJzY=", + "dev": true + }, + "after": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.1.tgz", + "integrity": "sha1-q11PuIP1loFtNRX495HAr0ht1ic=", + "dev": true + }, + "agent-base": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-2.1.1.tgz", + "integrity": "sha1-1t4Q1a9hMtW9aSQn1G/FOFOQlMc=", + "dev": true, + "requires": { + "extend": "3.0.1", + "semver": "5.0.3" + }, + "dependencies": { + "semver": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz", + "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=", + "dev": true + } + } + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "alter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/alter/-/alter-0.2.0.tgz", + "integrity": "sha1-x1iICGF1cgNKrmJICvJrHU0cs80=", + "dev": true, + "requires": { + "stable": "0.1.6" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, + "angular": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/angular/-/angular-1.5.11.tgz", + "integrity": "sha1-jFunOG8VllyazzQp9ogVU6raMNY=" + }, + "angular-ui-grid": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/angular-ui-grid/-/angular-ui-grid-4.0.11.tgz", + "integrity": "sha1-lqp1KkH2CiVMGeBSV2iehMZhhiY=", + "requires": { + "angular": "1.5.11" + } + }, + "angular-viewport-watch": { + "version": "github:shahata/angular-viewport-watch#182923b3934e63817b6fc7b640ecb5c4a011f74c" + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "ansi_up": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ansi_up/-/ansi_up-2.0.2.tgz", + "integrity": "sha1-m1TeUIxcV59baWjmXBuGPgaAq5I=" + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "applause": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/applause/-/applause-1.2.2.tgz", + "integrity": "sha1-qEaFeegfZzl7tWNMKZU77c0PVsA=", + "dev": true, + "requires": { + "cson-parser": "1.3.5", + "js-yaml": "3.7.0", + "lodash": "3.10.1" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + } + } + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arraybuffer.slice": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", + "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "autoprefixer": { + "version": "6.7.7", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", + "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", + "dev": true, + "requires": { + "browserslist": "1.7.7", + "caniuse-db": "1.0.30000782", + "normalize-range": "0.1.2", + "num2fraction": "1.2.2", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true + }, + "babel-cli": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-cli/-/babel-cli-6.26.0.tgz", + "integrity": "sha1-UCq1SHTX24itALiHoGODzgPQAvE=", + "dev": true, + "requires": { + "babel-core": "6.26.0", + "babel-polyfill": "6.26.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "chokidar": "1.7.0", + "commander": "2.12.2", + "convert-source-map": "1.5.1", + "fs-readdir-recursive": "1.1.0", + "glob": "7.1.2", + "lodash": "4.17.4", + "output-file-sync": "1.1.2", + "path-is-absolute": "1.0.1", + "slash": "1.0.0", + "source-map": "0.5.7", + "v8flags": "2.1.1" + }, + "dependencies": { + "commander": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", + "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-core": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz", + "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-generator": "6.26.0", + "babel-helpers": "6.24.1", + "babel-messages": "6.23.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "convert-source-map": "1.5.1", + "debug": "2.6.9", + "json5": "0.5.1", + "lodash": "4.17.4", + "minimatch": "3.0.4", + "path-is-absolute": "1.0.1", + "private": "0.1.8", + "slash": "1.0.0", + "source-map": "0.5.7" + }, + "dependencies": { + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz", + "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=", + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.4", + "source-map": "0.5.7", + "trim-right": "1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "dev": true, + "requires": { + "babel-helper-explode-assignable-expression": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.4" + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "dev": true, + "requires": { + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.4" + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "dev": true, + "requires": { + "babel-helper-optimise-call-expression": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-loader": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-6.4.1.tgz", + "integrity": "sha1-CzQRLVsHSKjc2/Uaz2+b1C1QuMo=", + "dev": true, + "requires": { + "find-cache-dir": "0.1.1", + "loader-utils": "0.2.17", + "mkdirp": "0.5.1", + "object-assign": "4.1.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", + "dev": true + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", + "dev": true + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", + "dev": true + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-functions": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.4" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "dev": true, + "requires": { + "babel-helper-define-map": "6.26.0", + "babel-helper-function-name": "6.24.1", + "babel-helper-optimise-call-expression": "6.24.1", + "babel-helper-replace-supers": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz", + "integrity": "sha1-DYOUApt9xqvhqX7xgeAHWN0uXYo=", + "dev": true, + "requires": { + "babel-plugin-transform-strict-mode": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "dev": true, + "requires": { + "babel-helper-replace-supers": "6.24.1", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "dev": true, + "requires": { + "babel-helper-call-delegate": "6.24.1", + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "dev": true, + "requires": { + "babel-helper-regex": "6.26.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "dev": true, + "requires": { + "babel-helper-regex": "6.26.0", + "babel-runtime": "6.26.0", + "regexpu-core": "2.0.0" + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "dev": true, + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "6.24.1", + "babel-plugin-syntax-exponentiation-operator": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", + "dev": true, + "requires": { + "regenerator-transform": "0.10.1" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "core-js": "2.5.2", + "regenerator-runtime": "0.10.5" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "dev": true + } + } + }, + "babel-preset-env": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.6.1.tgz", + "integrity": "sha512-W6VIyA6Ch9ePMI7VptNn2wBM6dbG0eSz25HEiL40nQXCsXGTGZSTZu1Iap+cj3Q0S5a7T9+529l/5Bkvd+afNA==", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "6.22.0", + "babel-plugin-syntax-trailing-function-commas": "6.22.0", + "babel-plugin-transform-async-to-generator": "6.24.1", + "babel-plugin-transform-es2015-arrow-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoping": "6.26.0", + "babel-plugin-transform-es2015-classes": "6.24.1", + "babel-plugin-transform-es2015-computed-properties": "6.24.1", + "babel-plugin-transform-es2015-destructuring": "6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "6.24.1", + "babel-plugin-transform-es2015-for-of": "6.23.0", + "babel-plugin-transform-es2015-function-name": "6.24.1", + "babel-plugin-transform-es2015-literals": "6.22.0", + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", + "babel-plugin-transform-es2015-modules-systemjs": "6.24.1", + "babel-plugin-transform-es2015-modules-umd": "6.24.1", + "babel-plugin-transform-es2015-object-super": "6.24.1", + "babel-plugin-transform-es2015-parameters": "6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", + "babel-plugin-transform-es2015-spread": "6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "6.24.1", + "babel-plugin-transform-es2015-template-literals": "6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "6.24.1", + "babel-plugin-transform-exponentiation-operator": "6.24.1", + "babel-plugin-transform-regenerator": "6.26.0", + "browserslist": "2.10.0", + "invariant": "2.2.2", + "semver": "5.4.1" + }, + "dependencies": { + "browserslist": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.10.0.tgz", + "integrity": "sha512-WyvzSLsuAVPOjbljXnyeWl14Ae+ukAT8MUuagKVzIDvwBxl4UAwD1xqtyQs2eWYPGUKMeC3Ol62goqYuKqTTcw==", + "dev": true, + "requires": { + "caniuse-lite": "1.0.30000782", + "electron-to-chromium": "1.3.28" + } + } + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "dev": true, + "requires": { + "babel-core": "6.26.0", + "babel-runtime": "6.26.0", + "core-js": "2.5.2", + "home-or-tmp": "2.0.0", + "lodash": "4.17.4", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "2.5.2", + "regenerator-runtime": "0.11.1" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-arraybuffer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.2.tgz", + "integrity": "sha1-R030qfLaJOBd8xWMOx2zw81GoVQ=", + "dev": true + }, + "base64-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", + "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==", + "dev": true + }, + "base64id": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-0.1.0.tgz", + "integrity": "sha1-As4P3u4M709ACA4ec+g08LG/zj8=", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "benchmark": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-1.0.0.tgz", + "integrity": "sha1-Lx4vpMNZ8REiqhgwgiGOlX45DHM=", + "dev": true + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dev": true, + "requires": { + "callsite": "1.0.0" + } + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "dev": true + }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=", + "dev": true + }, + "blocking-proxy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz", + "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==", + "dev": true, + "requires": { + "minimist": "1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "dev": true + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "content-type": "1.0.4", + "debug": "2.6.9", + "depd": "1.1.1", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "1.6.15" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "dev": true, + "requires": { + "hoek": "4.2.0" + } + }, + "bower": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/bower/-/bower-1.8.2.tgz", + "integrity": "sha1-rfU1KcjUrwLvJPuNU0HBQZ0z4vc=", + "dev": true + }, + "bower-config": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/bower-config/-/bower-config-0.5.3.tgz", + "integrity": "sha1-mPxbQah4cO+cu5KXY1z4H1UF/bE=", + "dev": true, + "requires": { + "graceful-fs": "2.0.3", + "mout": "0.9.1", + "optimist": "0.6.1", + "osenv": "0.0.3" + }, + "dependencies": { + "graceful-fs": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", + "dev": true + } + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "browserify-aes": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-0.4.0.tgz", + "integrity": "sha1-BnFJtmjfMcS1hTPgLQHoBthgjiw=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "dev": true, + "requires": { + "pako": "0.2.9" + }, + "dependencies": { + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", + "dev": true + } + } + }, + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "dev": true, + "requires": { + "caniuse-db": "1.0.30000782", + "electron-to-chromium": "1.3.28" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "1.2.1", + "ieee754": "1.1.8", + "isarray": "1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "0.2.0" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "camel-case": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-1.2.2.tgz", + "integrity": "sha1-Gsp8TRlTWaLOmVV5NDPG5VQlEfI=", + "requires": { + "sentence-case": "1.1.3", + "upper-case": "1.1.3" + } + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "caniuse-api": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", + "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=", + "dev": true, + "requires": { + "browserslist": "1.7.7", + "caniuse-db": "1.0.30000782", + "lodash.memoize": "4.1.2", + "lodash.uniq": "4.5.0" + } + }, + "caniuse-db": { + "version": "1.0.30000782", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000782.tgz", + "integrity": "sha1-2IFbzhV4w1Cs7REyUHMBIF4Pq1M=", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30000782", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000782.tgz", + "integrity": "sha1-W4K4w4XyU0h0XEccpRMgr7G38lQ=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "change-case": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-2.1.6.tgz", + "integrity": "sha1-UUryBRMVimj+fwDf9MMy1sKY0vk=", + "requires": { + "camel-case": "1.2.2", + "constant-case": "1.1.2", + "dot-case": "1.1.2", + "is-lower-case": "1.1.3", + "is-upper-case": "1.1.2", + "lower-case": "1.1.4", + "param-case": "1.1.2", + "pascal-case": "1.1.2", + "path-case": "1.1.2", + "sentence-case": "1.1.3", + "snake-case": "1.1.2", + "swap-case": "1.1.2", + "title-case": "1.1.2", + "upper-case": "1.1.3", + "upper-case-first": "1.1.2" + } + }, + "cheerio": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.12.4.tgz", + "integrity": "sha1-wZlibp4esNQjOpGkeT5/iqppoYs=", + "requires": { + "cheerio-select": "0.0.3", + "entities": "0.5.0", + "htmlparser2": "3.1.4", + "underscore": "1.4.4" + } + }, + "cheerio-select": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-0.0.3.tgz", + "integrity": "sha1-PyQgEU88ywsbB1wkXM+q5dYXo4g=", + "requires": { + "CSSselect": "0.7.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "fsevents": "1.1.3", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "clap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", + "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", + "dev": true, + "requires": { + "chalk": "1.1.3" + } + }, + "clean-css": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-2.2.23.tgz", + "integrity": "sha1-BZC1R4tRbEkD7cLYm9P9vdKGMow=", + "requires": { + "commander": "2.2.0" + } + }, + "cli": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/cli/-/cli-0.6.6.tgz", + "integrity": "sha1-Aq1Eo4Cr8nraxebwzdewQ9dMU+M=", + "requires": { + "exit": "0.1.2", + "glob": "3.2.11" + } + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "1.0.1" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, + "clone": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.3.tgz", + "integrity": "sha1-KY1+IjFmD0DAA8LtMUDezz9TCF8=", + "dev": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "coa": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", + "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", + "dev": true, + "requires": { + "q": "1.5.1" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "coffee-script": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", + "dev": true + }, + "color": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", + "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", + "dev": true, + "requires": { + "clone": "1.0.3", + "color-convert": "1.9.1", + "color-string": "0.3.0" + } + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "color-string": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "colormin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colormin/-/colormin-1.1.2.tgz", + "integrity": "sha1-6i90IKcrlogaOKrlnsEkpvcpgTM=", + "dev": true, + "requires": { + "color": "0.11.4", + "css-color-names": "0.0.4", + "has": "1.0.1" + } + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "combine-lists": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz", + "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.2.0.tgz", + "integrity": "sha1-F1rUuTF/P/YV8gHB5XIk9Vo+kd8=" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", + "dev": true + }, + "component-emitter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", + "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=", + "dev": true + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, + "compressible": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.12.tgz", + "integrity": "sha1-xZpcmdt2dn6YdlAOJx72OzSTvWY=", + "dev": true, + "requires": { + "mime-db": "1.30.0" + } + }, + "compression": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.1.tgz", + "integrity": "sha1-7/JgPvwuIs+G810uuTWJ+YdTc9s=", + "dev": true, + "requires": { + "accepts": "1.3.4", + "bytes": "3.0.0", + "compressible": "2.0.12", + "debug": "2.6.9", + "on-headers": "1.0.1", + "safe-buffer": "5.1.1", + "vary": "1.1.2" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "typedarray": "0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "connect": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.5.tgz", + "integrity": "sha1-+43ee6B2OHfQ7J352sC0tA5yx9o=", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.0.6", + "parseurl": "1.3.2", + "utils-merge": "1.0.1" + }, + "dependencies": { + "finalhandler": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.6.tgz", + "integrity": "sha1-AHrqM9Gk0+QgF/YkhIrVjSEvgU8=", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.3.1", + "unpipe": "1.0.0" + } + } + } + }, + "connect-history-api-fallback": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", + "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=", + "dev": true + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "0.1.4" + } + }, + "constant-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-1.1.2.tgz", + "integrity": "sha1-jsLKW6ND4Aqjjb9OIA/VrJB+/WM=", + "requires": { + "snake-case": "1.1.2", + "upper-case": "1.1.3" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "dev": true + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", + "dev": true + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-webpack-plugin": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.2.3.tgz", + "integrity": "sha512-cL/Wl3Y1QmmKThl/mWeGB+HH3YH+25tn8nhqEGsZda4Yn7GqGnDZ+TbeKJ7A6zvrxyNhhuviYAxn/tCyyAqh8Q==", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "glob": "7.1.2", + "is-glob": "4.0.0", + "loader-utils": "0.2.17", + "lodash": "4.17.4", + "minimatch": "3.0.4" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + } + } + }, + "core-js": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.2.tgz", + "integrity": "sha1-vEZIZW59ydyA19PHu8Fy2W50TmM=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cosmiconfig": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz", + "integrity": "sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A==", + "dev": true, + "requires": { + "is-directory": "0.3.1", + "js-yaml": "3.7.0", + "minimist": "1.2.0", + "object-assign": "4.1.1", + "os-homedir": "1.0.2", + "parse-json": "2.2.0", + "require-from-string": "1.2.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "which": "1.3.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + } + } + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "dev": true, + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "dev": true, + "requires": { + "hoek": "4.2.0" + } + } + } + }, + "crypto-browserify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.3.0.tgz", + "integrity": "sha1-ufx1u0oO1h3PHNXa6W6zDJw+UGw=", + "dev": true, + "requires": { + "browserify-aes": "0.4.0", + "pbkdf2-compat": "2.0.1", + "ripemd160": "0.2.0", + "sha.js": "2.2.6" + } + }, + "cson-parser": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/cson-parser/-/cson-parser-1.3.5.tgz", + "integrity": "sha1-fsZ14DkUVTO/KmqFYHPxWZ2cLSQ=", + "dev": true, + "requires": { + "coffee-script": "1.12.7" + }, + "dependencies": { + "coffee-script": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", + "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", + "dev": true + } + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + }, + "css-loader": { + "version": "0.26.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.26.4.tgz", + "integrity": "sha1-th6eMNuUMD5v/IkvEOzQmtAlof0=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "css-selector-tokenizer": "0.7.0", + "cssnano": "3.10.0", + "loader-utils": "1.1.0", + "lodash.camelcase": "4.3.0", + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-modules-extract-imports": "1.1.0", + "postcss-modules-local-by-default": "1.2.0", + "postcss-modules-scope": "1.1.0", + "postcss-modules-values": "1.3.0", + "source-list-map": "0.1.8" + }, + "dependencies": { + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + } + } + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "1.0.0", + "css-what": "2.1.0", + "domutils": "1.5.1", + "nth-check": "1.0.1" + }, + "dependencies": { + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0.1.0", + "domelementtype": "1.3.0" + } + } + } + }, + "css-selector-tokenizer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", + "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=", + "dev": true, + "requires": { + "cssesc": "0.1.0", + "fastparse": "1.1.1", + "regexpu-core": "1.0.0" + }, + "dependencies": { + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "dev": true, + "requires": { + "regenerate": "1.3.3", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + } + } + }, + "css-what": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", + "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", + "dev": true + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", + "dev": true + }, + "csslint": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/csslint/-/csslint-0.10.0.tgz", + "integrity": "sha1-OmoE51Zcjp0ZvrSXZ8fslug2WAU=", + "dev": true, + "requires": { + "parserlib": "0.2.5" + } + }, + "cssnano": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz", + "integrity": "sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg=", + "dev": true, + "requires": { + "autoprefixer": "6.7.7", + "decamelize": "1.2.0", + "defined": "1.0.0", + "has": "1.0.1", + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-calc": "5.3.1", + "postcss-colormin": "2.2.2", + "postcss-convert-values": "2.6.1", + "postcss-discard-comments": "2.0.4", + "postcss-discard-duplicates": "2.1.0", + "postcss-discard-empty": "2.1.0", + "postcss-discard-overridden": "0.1.1", + "postcss-discard-unused": "2.2.3", + "postcss-filter-plugins": "2.0.2", + "postcss-merge-idents": "2.1.7", + "postcss-merge-longhand": "2.0.2", + "postcss-merge-rules": "2.1.2", + "postcss-minify-font-values": "1.0.5", + "postcss-minify-gradients": "1.0.5", + "postcss-minify-params": "1.2.2", + "postcss-minify-selectors": "2.1.1", + "postcss-normalize-charset": "1.1.1", + "postcss-normalize-url": "3.0.8", + "postcss-ordered-values": "2.2.3", + "postcss-reduce-idents": "2.4.0", + "postcss-reduce-initial": "1.0.1", + "postcss-reduce-transforms": "1.0.4", + "postcss-svgo": "2.1.6", + "postcss-unique-selectors": "2.0.2", + "postcss-value-parser": "3.3.0", + "postcss-zindex": "2.2.0" + } + }, + "csso": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", + "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=", + "dev": true, + "requires": { + "clap": "1.2.3", + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "0.10.37" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "date-time": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-0.1.1.tgz", + "integrity": "sha1-7S9tk9l5DOL9ZtW1/z7dW7y/Owc=", + "dev": true + }, + "dateformat": { + "version": "1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "deep-equal": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.0.0.tgz", + "integrity": "sha1-mWedO70EcVb81FDT0B7rkGhpHoM=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "dev": true, + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.11" + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.0", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.6.2" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "diff": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-2.2.3.tgz", + "integrity": "sha1-YOr9DSjukG5Oj/ClLBIpUhAzv5k=", + "dev": true + }, + "doctrine": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.2.tgz", + "integrity": "sha512-y0tm5Pq6ywp3qSTZ1vPgVdAnbDEoeoc5wlOHXoY1c4Wug/a7JvqHIl7BTvwodaHmejWkK/9dSb3sCYfyo/om8A==", + "dev": true, + "requires": { + "esutils": "2.0.2" + } + }, + "dom-converter": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", + "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=", + "dev": true, + "requires": { + "utila": "0.3.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", + "dev": true + } + } + }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "requires": { + "custom-event": "1.0.1", + "ent": "2.2.0", + "extend": "3.0.1", + "void-elements": "2.0.1" + } + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "dev": true, + "requires": { + "domelementtype": "1.1.3", + "entities": "1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", + "dev": true + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", + "dev": true + } + } + }, + "domain-browser": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", + "dev": true + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" + }, + "domhandler": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.0.3.tgz", + "integrity": "sha1-iJ+N9iZAOvB4jinWbV1cb36/D9Y=", + "requires": { + "domelementtype": "1.3.0" + } + }, + "domutils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.4.3.tgz", + "integrity": "sha1-CGVRN5bGswYDGFDhdVFrr4C3Km8=", + "requires": { + "domelementtype": "1.3.0" + } + }, + "dot-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-1.1.2.tgz", + "integrity": "sha1-HnOCaQDeKNbeVIC8HeMdCEKwa+w=", + "requires": { + "sentence-case": "1.1.3" + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "each-async": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/each-async/-/each-async-0.1.3.tgz", + "integrity": "sha1-tDYCWwjaL4ZggCVRnjCWdj3t/KM=", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.28", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.28.tgz", + "integrity": "sha1-jdTmRYCGZE6fnwoc8y4qH53/2e4=", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encodeurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=", + "dev": true + }, + "engine.io": { + "version": "1.6.10", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.6.10.tgz", + "integrity": "sha1-+H2E4b0h0aLsf43u8MYgVKzfsno=", + "dev": true, + "requires": { + "accepts": "1.1.4", + "base64id": "0.1.0", + "debug": "2.2.0", + "engine.io-parser": "1.2.4", + "ws": "1.0.1" + }, + "dependencies": { + "accepts": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.1.4.tgz", + "integrity": "sha1-1xyW99QdD+2iw4zRToonwEFY30o=", + "dev": true, + "requires": { + "mime-types": "2.0.14", + "negotiator": "0.4.9" + } + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "mime-db": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", + "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc=", + "dev": true + }, + "mime-types": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", + "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=", + "dev": true, + "requires": { + "mime-db": "1.12.0" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "negotiator": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.9.tgz", + "integrity": "sha1-kuRrbbU8fkIe1koryU8IvnYw3z8=", + "dev": true + } + } + }, + "engine.io-client": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.6.9.tgz", + "integrity": "sha1-HWrUgEilCDyVCWlDsp0279shJAE=", + "dev": true, + "requires": { + "component-emitter": "1.1.2", + "component-inherit": "0.0.3", + "debug": "2.2.0", + "engine.io-parser": "1.2.4", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parsejson": "0.0.1", + "parseqs": "0.0.2", + "parseuri": "0.0.4", + "ws": "1.0.1", + "xmlhttprequest-ssl": "1.5.1", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "engine.io-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.2.4.tgz", + "integrity": "sha1-4Il7C/FOeS1M0qWVBVORnFaUjEI=", + "dev": true, + "requires": { + "after": "0.8.1", + "arraybuffer.slice": "0.0.6", + "base64-arraybuffer": "0.1.2", + "blob": "0.0.4", + "has-binary": "0.1.6", + "utf8": "2.1.0" + }, + "dependencies": { + "has-binary": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.6.tgz", + "integrity": "sha1-JTJvOc+k9hath4eJTjryz7x7bhA=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "enhanced-resolve": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz", + "integrity": "sha1-TW5omzcl+GCQknzMhs2fFjW4ni4=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "memory-fs": "0.2.0", + "tapable": "0.1.10" + }, + "dependencies": { + "memory-fs": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", + "integrity": "sha1-8rslNovBIeORwlIN6Slpyu4KApA=", + "dev": true + } + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "entities": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-0.5.0.tgz", + "integrity": "sha1-9hHLWuIhBQ4AEsZpeVA/164ZzEk=" + }, + "errno": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.5.tgz", + "integrity": "sha512-tv2H+e3KBnMmNRuoVG24uorOj3XfYo+/nJJd07PUISRr0kaMKQKL5kyD+6ANXk1ZIIsvbORsjvHnCfC4KIc7uQ==", + "dev": true, + "requires": { + "prr": "1.0.1" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "es-abstract": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.10.0.tgz", + "integrity": "sha512-/uh/DhdqIOSkAWifU+8nG78vlQxdLckUdI/sPgy0VhuXi2qJ7T8czBmqIYtLQVpCIFYafChnsRsB5pyb1JdmCQ==", + "dev": true, + "requires": { + "es-to-primitive": "1.1.1", + "function-bind": "1.1.1", + "has": "1.0.1", + "is-callable": "1.1.3", + "is-regex": "1.0.4" + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "dev": true, + "requires": { + "is-callable": "1.1.3", + "is-date-object": "1.0.1", + "is-symbol": "1.0.1" + } + }, + "es5-ext": { + "version": "0.10.37", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.37.tgz", + "integrity": "sha1-DudB0Ui4AGm6J9AgOTdWryV978M=", + "dev": true, + "requires": { + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37", + "es6-symbol": "3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37", + "es6-iterator": "2.0.3", + "es6-set": "0.1.5", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", + "dev": true + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37", + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37", + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "2.7.3", + "estraverse": "1.9.3", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.2.0" + }, + "dependencies": { + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true, + "requires": { + "es6-map": "0.1.5", + "es6-weak-map": "2.0.2", + "esrecurse": "4.2.0", + "estraverse": "4.2.0" + } + }, + "eslint": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", + "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "chalk": "1.1.3", + "concat-stream": "1.6.0", + "debug": "2.6.9", + "doctrine": "2.0.2", + "escope": "3.6.0", + "espree": "3.5.2", + "esquery": "1.0.0", + "estraverse": "4.2.0", + "esutils": "2.0.2", + "file-entry-cache": "2.0.0", + "glob": "7.1.2", + "globals": "9.18.0", + "ignore": "3.3.7", + "imurmurhash": "0.1.4", + "inquirer": "0.12.0", + "is-my-json-valid": "2.16.1", + "is-resolvable": "1.0.0", + "js-yaml": "3.7.0", + "json-stable-stringify": "1.0.1", + "levn": "0.3.0", + "lodash": "4.17.4", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "optionator": "0.8.2", + "path-is-inside": "1.0.2", + "pluralize": "1.2.1", + "progress": "1.1.8", + "require-uncached": "1.0.3", + "shelljs": "0.7.8", + "strip-bom": "3.0.0", + "strip-json-comments": "2.0.1", + "table": "3.8.3", + "text-table": "0.2.0", + "user-home": "2.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true, + "requires": { + "os-homedir": "1.0.2" + } + } + } + }, + "eslint-config-google": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.7.1.tgz", + "integrity": "sha1-VZj4SY6eB4Qg80uASVuNlZ9lH7I=", + "dev": true + }, + "eslint-config-standard": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-10.2.1.tgz", + "integrity": "sha1-wGHk0GbzedwXzVYsZOgZtN1FRZE=", + "dev": true + }, + "eslint-import-resolver-node": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz", + "integrity": "sha512-yUtXS15gIcij68NmXmP9Ni77AQuCN0itXbCc/jWd8C6/yKZaSNXicpC8cgvjnxVdmfsosIXrjpzFq7GcDryb6A==", + "dev": true, + "requires": { + "debug": "2.6.9", + "resolve": "1.5.0" + } + }, + "eslint-module-utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz", + "integrity": "sha512-jDI/X5l/6D1rRD/3T43q8Qgbls2nq5km5KSqiwlyUbGo5+04fXhMKdCPhjwbqAa6HXWaMxj8Q4hQDIh7IadJQw==", + "dev": true, + "requires": { + "debug": "2.6.9", + "pkg-dir": "1.0.0" + } + }, + "eslint-plugin-import": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz", + "integrity": "sha512-Rf7dfKJxZ16QuTgVv1OYNxkZcsu/hULFnC+e+w0Gzi6jMC3guQoWQgxYxc54IDRinlb6/0v5z/PxxIKmVctN+g==", + "dev": true, + "requires": { + "builtin-modules": "1.1.1", + "contains-path": "0.1.0", + "debug": "2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "0.3.1", + "eslint-module-utils": "2.1.1", + "has": "1.0.1", + "lodash.cond": "4.5.2", + "minimatch": "3.0.4", + "read-pkg-up": "2.0.0" + }, + "dependencies": { + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "2.0.2", + "isarray": "1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + } + } + }, + "eslint-plugin-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-4.2.3.tgz", + "integrity": "sha512-vIUQPuwbVYdz/CYnlTLsJrRy7iXHQjdEe5wz0XhhdTym3IInM/zZLlPf9nZ2mThsH0QcsieCOWs2vOeCy/22LQ==", + "dev": true, + "requires": { + "ignore": "3.3.7", + "minimatch": "3.0.4", + "object-assign": "4.1.1", + "resolve": "1.5.0", + "semver": "5.3.0" + }, + "dependencies": { + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } + } + }, + "eslint-plugin-promise": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-3.6.0.tgz", + "integrity": "sha512-YQzM6TLTlApAr7Li8vWKR+K3WghjwKcYzY0d2roWap4SLK+kzuagJX/leTetIDWsFcTFnKNJXWupDCD6aZkP2Q==", + "dev": true + }, + "eslint-plugin-standard": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-3.0.1.tgz", + "integrity": "sha1-NNDJFbRe3G8BA5PH7vOCOwhWXPI=", + "dev": true + }, + "eslint-watch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/eslint-watch/-/eslint-watch-3.1.3.tgz", + "integrity": "sha1-44gqk/ArpNinl1YvqcOBXjGBxLo=", + "dev": true, + "requires": { + "babel-polyfill": "6.26.0", + "bluebird": "3.5.1", + "chalk": "2.3.0", + "chokidar": "1.7.0", + "debug": "3.1.0", + "keypress": "0.2.1", + "lodash": "4.17.4", + "optionator": "0.8.2", + "source-map-support": "0.5.0", + "strip-ansi": "4.0.0", + "text-table": "0.2.0", + "unicons": "0.0.3" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.0.tgz", + "integrity": "sha512-vUoN3I7fHQe0R/SJLKRdKYuEdRGogsviXFkHHo17AWaTGv17VLnxw+CFXvqy+y4ORZ3doWLQcxRYfwKrsd/H7Q==", + "dev": true, + "requires": { + "source-map": "0.6.1" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "espree": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.2.tgz", + "integrity": "sha512-sadKeYwaR/aJ3stC2CdvgXu1T16TdYN+qwCpcWbMnGJ8s0zNWemzrvb2GbD4OhmJ/fwpJjudThAlLobGbWZbCQ==", + "dev": true, + "requires": { + "acorn": "5.2.1", + "acorn-jsx": "3.0.1" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "esquery": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", + "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "dev": true, + "requires": { + "estraverse": "4.2.0" + } + }, + "esrecurse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", + "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", + "dev": true, + "requires": { + "estraverse": "4.2.0", + "object-assign": "4.1.1" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.37" + } + }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "dev": true, + "requires": { + "duplexer": "0.1.1", + "from": "0.1.7", + "map-stream": "0.1.0", + "pause-stream": "0.0.11", + "split": "0.3.3", + "stream-combiner": "0.0.4", + "through": "2.3.8" + } + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "eventemitter3": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", + "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "eventsource": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz", + "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=", + "dev": true, + "requires": { + "original": "1.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "expand-braces": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", + "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=", + "dev": true, + "requires": { + "array-slice": "0.2.3", + "array-unique": "0.2.1", + "braces": "0.1.5" + }, + "dependencies": { + "braces": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz", + "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=", + "dev": true, + "requires": { + "expand-range": "0.1.1" + } + }, + "expand-range": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", + "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", + "dev": true, + "requires": { + "is-number": "0.1.1", + "repeat-string": "0.2.2" + } + }, + "is-number": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", + "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", + "dev": true + }, + "repeat-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz", + "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=", + "dev": true + } + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, + "express": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", + "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", + "dev": true, + "requires": { + "accepts": "1.3.4", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "1.1.1", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "etag": "1.8.1", + "finalhandler": "1.1.0", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "1.1.2", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "2.0.2", + "qs": "6.5.1", + "range-parser": "1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.1", + "serve-static": "1.13.1", + "setprototypeof": "1.1.0", + "statuses": "1.3.1", + "type-is": "1.6.15", + "utils-merge": "1.0.1", + "vary": "1.1.2" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "extract-text-webpack-plugin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-1.0.1.tgz", + "integrity": "sha1-yVvzy6rEnclvHcbgclSfu2VMzSw=", + "dev": true, + "requires": { + "async": "1.5.2", + "loader-utils": "0.2.17", + "webpack-sources": "0.1.5" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + } + } + }, + "extract-zip": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", + "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", + "dev": true, + "requires": { + "concat-stream": "1.6.0", + "debug": "2.6.9", + "mkdirp": "0.5.0", + "yauzl": "2.4.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastparse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", + "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", + "dev": true + }, + "faye-websocket": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.4.4.tgz", + "integrity": "sha1-wUxbO/FNdBf/v9mQwKdJXNnzN7w=", + "dev": true + }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "1.2.0" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5", + "object-assign": "4.1.1" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "1.3.0", + "object-assign": "4.1.1" + } + }, + "file-loader": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-0.9.0.tgz", + "integrity": "sha1-HS2t3UJM5tGwfP4/eXMb7TYXq0I=", + "dev": true, + "requires": { + "loader-utils": "0.2.17" + } + }, + "file-sync-cmp": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz", + "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", + "dev": true + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.3.1", + "unpipe": "1.0.0" + } + }, + "find-cache-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", + "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", + "dev": true, + "requires": { + "commondir": "1.0.1", + "mkdirp": "0.5.1", + "pkg-dir": "1.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "findup-sync": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", + "dev": true, + "requires": { + "glob": "3.2.11", + "lodash": "2.4.2" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + } + } + }, + "flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "dev": true, + "requires": { + "circular-json": "0.3.3", + "del": "2.2.2", + "graceful-fs": "4.1.11", + "write": "0.2.1" + } + }, + "flatten": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", + "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", + "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "dev": true + }, + "fs-extra": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", + "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "2.4.0", + "klaw": "1.3.1" + } + }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", + "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.8.0", + "node-pre-gyp": "0.6.39" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.9" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true, + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.7", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true, + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "bundled": true, + "dev": true + }, + "mime-types": { + "version": "2.1.15", + "bundled": true, + "dev": true, + "requires": { + "mime-db": "1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.39", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "1.0.2", + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true, + "dev": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "bundled": true, + "dev": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.0.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.0.1", + "bundled": true, + "dev": true + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.2.9", + "rimraf": "2.6.1", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "dev": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "uuid": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gaze": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz", + "integrity": "sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8=", + "dev": true, + "requires": { + "globule": "0.1.0" + } + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "1.0.2" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, + "github-markdown-css": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-2.6.0.tgz", + "integrity": "sha1-zcdLq1ZrA51/u3RgH3ghsQnPWRg=" + }, + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "requires": { + "inherits": "2.0.3", + "minimatch": "0.3.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + } + } + }, + "globule": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz", + "integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=", + "dev": true, + "requires": { + "glob": "3.1.21", + "lodash": "1.0.2", + "minimatch": "0.2.14" + }, + "dependencies": { + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "1.2.3", + "inherits": "1.0.2", + "minimatch": "0.2.14" + } + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + }, + "lodash": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz", + "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + } + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "grunt": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", + "dev": true, + "requires": { + "async": "0.1.22", + "coffee-script": "1.3.3", + "colors": "0.6.2", + "dateformat": "1.0.2-1.2.3", + "eventemitter2": "0.4.14", + "exit": "0.1.2", + "findup-sync": "0.1.3", + "getobject": "0.1.0", + "glob": "3.1.21", + "grunt-legacy-log": "0.1.3", + "grunt-legacy-util": "0.2.0", + "hooker": "0.2.3", + "iconv-lite": "0.2.11", + "js-yaml": "2.0.5", + "lodash": "0.9.2", + "minimatch": "0.2.14", + "nopt": "1.0.10", + "rimraf": "2.2.8", + "underscore.string": "2.2.1", + "which": "1.0.9" + }, + "dependencies": { + "argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", + "dev": true, + "requires": { + "underscore": "1.7.0", + "underscore.string": "2.4.0" + }, + "dependencies": { + "underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", + "dev": true + } + } + }, + "async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true + }, + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "1.2.3", + "inherits": "1.0.2", + "minimatch": "0.2.14" + } + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "iconv-lite": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", + "dev": true + }, + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + }, + "js-yaml": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", + "dev": true, + "requires": { + "argparse": "0.1.16", + "esprima": "1.0.4" + } + }, + "lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + }, + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "dev": true + } + } + }, + "grunt-angular-templates": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/grunt-angular-templates/-/grunt-angular-templates-0.5.9.tgz", + "integrity": "sha1-KJm+INlDitGbDQqAaqjseiOyWyo=", + "requires": { + "html-minifier": "0.6.9" + } + }, + "grunt-cache-bust": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/grunt-cache-bust/-/grunt-cache-bust-1.3.0.tgz", + "integrity": "sha1-YtkgjiMV8cIMFgg6kHzkq8JJv1Q=", + "dev": true + }, + "grunt-cli": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-0.1.13.tgz", + "integrity": "sha1-6evEBHYx9QEtkidww5N4EzytEPQ=", + "dev": true, + "requires": { + "findup-sync": "0.1.3", + "nopt": "1.0.10", + "resolve": "0.3.1" + }, + "dependencies": { + "resolve": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.3.1.tgz", + "integrity": "sha1-NMY0R8ZkxwWY0cmxJvxDsqJDEKQ=", + "dev": true + } + } + }, + "grunt-concurrent": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/grunt-concurrent/-/grunt-concurrent-0.5.0.tgz", + "integrity": "sha1-SlGaTCh4JfDeBxX3O4XRUMdQ2fc=", + "dev": true, + "requires": { + "async": "0.2.10", + "pad-stdio": "0.1.1" + } + }, + "grunt-contrib-concat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-0.4.0.tgz", + "integrity": "sha1-uH988VO/ZGiBQvlHFhFWAT+8fHQ=", + "dev": true, + "requires": { + "chalk": "0.4.0" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "1.0.0", + "has-color": "0.1.7", + "strip-ansi": "0.1.1" + } + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + } + } + }, + "grunt-contrib-copy": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-0.5.0.tgz", + "integrity": "sha1-QQB1rEWlhWuhkbHMclclRQ1KAhU=", + "dev": true + }, + "grunt-contrib-cssmin": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-cssmin/-/grunt-contrib-cssmin-0.9.0.tgz", + "integrity": "sha1-JyQfAWCohmZZ2rQNyMJ3bAHsfOI=", + "dev": true, + "requires": { + "chalk": "0.4.0", + "clean-css": "2.1.8", + "maxmin": "0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "1.0.0", + "has-color": "0.1.7", + "strip-ansi": "0.1.1" + } + }, + "clean-css": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-2.1.8.tgz", + "integrity": "sha1-K0sv1g8yRBCWIWriWiH6p0WA3IM=", + "dev": true, + "requires": { + "commander": "2.1.0" + } + }, + "commander": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz", + "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=", + "dev": true + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + } + } + }, + "grunt-contrib-htmlmin": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-htmlmin/-/grunt-contrib-htmlmin-0.3.0.tgz", + "integrity": "sha1-yWCAIEj2CZJenQ7xsGcJBLTFo/0=", + "dev": true, + "requires": { + "chalk": "0.4.0", + "html-minifier": "0.6.9", + "pretty-bytes": "0.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "1.0.0", + "has-color": "0.1.7", + "strip-ansi": "0.1.1" + } + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + } + } + }, + "grunt-contrib-uglify": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-0.4.1.tgz", + "integrity": "sha1-1D87xuAsM1Vj+MT58IE/tLD/ebE=", + "dev": true, + "requires": { + "chalk": "0.4.0", + "maxmin": "0.1.0", + "uglify-js": "2.4.24" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "1.0.0", + "has-color": "0.1.7", + "strip-ansi": "0.1.1" + } + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + } + } + }, + "grunt-contrib-watch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-0.6.1.tgz", + "integrity": "sha1-ZP3LolpjX1tNobbOb5DaCutuPxU=", + "dev": true, + "requires": { + "async": "0.2.10", + "gaze": "0.5.2", + "lodash": "2.4.2", + "tiny-lr-fork": "0.0.5" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + } + } + }, + "grunt-dom-munger": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/grunt-dom-munger/-/grunt-dom-munger-3.4.0.tgz", + "integrity": "sha1-LQ2Plk9amVEekUrR1T8fccWrbYk=", + "requires": { + "cheerio": "0.12.4" + } + }, + "grunt-filerev": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/grunt-filerev/-/grunt-filerev-0.2.1.tgz", + "integrity": "sha1-Svngz+2nuwFnB2VpeREimBH29NM=", + "dev": true, + "requires": { + "chalk": "0.4.0", + "each-async": "0.1.3" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "1.0.0", + "has-color": "0.1.7", + "strip-ansi": "0.1.1" + } + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + } + } + }, + "grunt-htmlhint": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/grunt-htmlhint/-/grunt-htmlhint-0.9.13.tgz", + "integrity": "sha1-cXACPzDi5wUnkjQrSNW7+RK512w=", + "dev": true, + "requires": { + "htmlhint": "0.9.13" + } + }, + "grunt-legacy-log": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", + "dev": true, + "requires": { + "colors": "0.6.2", + "grunt-legacy-log-utils": "0.1.1", + "hooker": "0.2.3", + "lodash": "2.4.2", + "underscore.string": "2.3.3" + }, + "dependencies": { + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-log-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", + "dev": true, + "requires": { + "colors": "0.6.2", + "lodash": "2.4.2", + "underscore.string": "2.3.3" + }, + "dependencies": { + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-util": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", + "dev": true, + "requires": { + "async": "0.1.22", + "exit": "0.1.2", + "getobject": "0.1.0", + "hooker": "0.2.3", + "lodash": "0.9.2", + "underscore.string": "2.2.1", + "which": "1.0.9" + }, + "dependencies": { + "async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + } + } + }, + "grunt-newer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/grunt-newer/-/grunt-newer-0.7.0.tgz", + "integrity": "sha1-N22dm2TOXGSLa/ob2pj3vCGT5B4=", + "dev": true, + "requires": { + "async": "0.2.10", + "rimraf": "2.2.6" + }, + "dependencies": { + "rimraf": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz", + "integrity": "sha1-xZWXVpsU2VatKcrMQr3d9fDqT0w=", + "dev": true + } + } + }, + "grunt-ng-annotate": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/grunt-ng-annotate/-/grunt-ng-annotate-0.10.0.tgz", + "integrity": "sha1-9dw7TDOlZlgkEzELeJhVZuCoS24=", + "dev": true, + "requires": { + "lodash.clonedeep": "3.0.2", + "ng-annotate": "0.15.4" + } + }, + "grunt-postcss": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/grunt-postcss/-/grunt-postcss-0.7.2.tgz", + "integrity": "sha1-V7dke4d9Qq0yz51M0RAID/+0OKs=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "diff": "2.2.3", + "es6-promise": "3.3.1", + "postcss": "5.2.18" + } + }, + "grunt-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/grunt-replace/-/grunt-replace-1.0.1.tgz", + "integrity": "sha1-kKeVMvuJBB/kJ8h9QlI4sPiGZRo=", + "dev": true, + "requires": { + "applause": "1.2.2", + "chalk": "1.1.3", + "file-sync-cmp": "0.1.1", + "lodash": "4.17.4" + } + }, + "grunt-svgmin": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/grunt-svgmin/-/grunt-svgmin-0.4.0.tgz", + "integrity": "sha1-8Z0RkwIq4AgOD65dMT4S73yuCq4=", + "dev": true, + "requires": { + "chalk": "0.4.0", + "each-async": "0.1.3", + "pretty-bytes": "0.1.2", + "svgo": "0.4.5" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", + "dev": true, + "requires": { + "underscore": "1.7.0", + "underscore.string": "2.4.0" + } + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "1.0.0", + "has-color": "0.1.7", + "strip-ansi": "0.1.1" + } + }, + "coa": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/coa/-/coa-0.4.1.tgz", + "integrity": "sha1-uvb0nHrZ8gxZevObP8HlCQ/og4s=", + "dev": true, + "requires": { + "q": "0.9.7" + } + }, + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true + }, + "js-yaml": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.1.3.tgz", + "integrity": "sha1-D/tWF75VUlh4Bj16Fq7n/dKC6Ew=", + "dev": true, + "requires": { + "argparse": "0.1.16", + "esprima": "1.0.4" + } + }, + "q": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/q/-/q-0.9.7.tgz", + "integrity": "sha1-TeLmyzspCIyeTLwDv51C+5bOL3U=", + "dev": true + }, + "sax": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz", + "integrity": "sha1-VjsZx8HeiS4Jv8Ty/DDjwn8JUrk=", + "dev": true + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + }, + "svgo": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.4.5.tgz", + "integrity": "sha1-ulYVX7FzNyiVbAG0BSIe5+eJoqQ=", + "dev": true, + "requires": { + "coa": "0.4.1", + "colors": "0.6.2", + "js-yaml": "2.1.3", + "sax": "0.6.1", + "whet.extend": "0.9.9" + } + }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "dev": true + }, + "underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", + "dev": true + } + } + }, + "grunt-usemin": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/grunt-usemin/-/grunt-usemin-2.6.2.tgz", + "integrity": "sha1-KxNroCJkqakdlNQkyNNya9iNt9o=", + "dev": true, + "requires": { + "chalk": "0.5.1", + "debug": "2.1.3", + "lodash": "2.4.2" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "1.1.0", + "escape-string-regexp": "1.0.5", + "has-ansi": "0.1.0", + "strip-ansi": "0.3.0", + "supports-color": "0.2.0" + } + }, + "debug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", + "integrity": "sha1-zoqxte6PvuK/o7Yzyrk9NmtjQY4=", + "dev": true, + "requires": { + "ms": "0.7.0" + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "0.2.1" + } + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "ms": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz", + "integrity": "sha1-hlvpTC5zl62KV9pqYzpuLzB5i4M=", + "dev": true + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + } + } + }, + "grunt-wiredep": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-wiredep/-/grunt-wiredep-2.0.0.tgz", + "integrity": "sha1-ID9vYT95nW3XLOBE0NzvZNrx8uU=", + "dev": true, + "requires": { + "wiredep": "2.2.2" + } + }, + "gzip-size": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-0.1.1.tgz", + "integrity": "sha1-rjNIO2/IIk6DQilt4Qjvk3V/duA=", + "dev": true, + "requires": { + "concat-stream": "1.6.0", + "zlib-browserify": "0.0.3" + } + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "optional": true + } + } + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "dev": true, + "requires": { + "ajv": "5.5.1", + "har-schema": "2.0.0" + }, + "dependencies": { + "ajv": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.1.tgz", + "integrity": "sha1-s4u4h22ehr7plJVqBOch6IskjrI=", + "dev": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + } + } + }, + "has": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "dev": true, + "requires": { + "function-bind": "1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-binary": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", + "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, + "has-color": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", + "dev": true + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "hasha": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", + "integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=", + "dev": true, + "requires": { + "is-stream": "1.1.0", + "pinkie-promise": "2.0.1" + } + }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "dev": true, + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.0", + "sntp": "2.1.0" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "headroom.js": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/headroom.js/-/headroom.js-0.9.4.tgz", + "integrity": "sha1-DE5rRWO7ad9Vrs3vq6MidWby31o=" + }, + "hoek": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==", + "dev": true + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", + "dev": true + }, + "html-comment-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.1.tgz", + "integrity": "sha1-ZouTd26q5V696POtRkswekljYl4=", + "dev": true + }, + "html-minifier": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-0.6.9.tgz", + "integrity": "sha1-UQXcI29efhqLplHUq5gThvx6vlM=", + "requires": { + "change-case": "2.1.6", + "clean-css": "2.2.23", + "cli": "0.6.6", + "relateurl": "0.2.7", + "uglify-js": "2.4.24" + } + }, + "html-webpack-plugin": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-2.30.1.tgz", + "integrity": "sha1-f5xCG36pHsRg9WUn1430hO51N9U=", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "html-minifier": "3.5.7", + "loader-utils": "0.2.17", + "lodash": "4.17.4", + "pretty-error": "2.1.1", + "toposort": "1.0.6" + }, + "dependencies": { + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "requires": { + "no-case": "2.3.2", + "upper-case": "1.1.3" + } + }, + "clean-css": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.9.tgz", + "integrity": "sha1-Nc7ornaHpJuYA09w3gDE7dOCYwE=", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, + "commander": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", + "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==", + "dev": true + }, + "html-minifier": { + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.7.tgz", + "integrity": "sha512-GISXn6oKDo7+gVpKOgZJTbHMCUI2TSGfpg/8jgencWhWJsvEmsvp3M8emX7QocsXsYznWloLib3OeSfeyb/ewg==", + "dev": true, + "requires": { + "camel-case": "3.0.0", + "clean-css": "4.1.9", + "commander": "2.12.2", + "he": "1.1.1", + "ncname": "1.0.0", + "param-case": "2.1.1", + "relateurl": "0.2.7", + "uglify-js": "3.2.2" + } + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "dev": true, + "requires": { + "no-case": "2.3.2" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "uglify-js": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.2.2.tgz", + "integrity": "sha512-++1NO/zZIEdWf6cDIGceSJQPX31SqIpbVAHwFG5+240MtZqPG/NIPoinj8zlXQtAfMBqEt1Jyv2FiLP3n9gVhQ==", + "dev": true, + "requires": { + "commander": "2.12.2", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + } + } + }, + "htmlhint": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/htmlhint/-/htmlhint-0.9.13.tgz", + "integrity": "sha1-CBY8seaqUFBI67C0EGOnygfcbIg=", + "dev": true, + "requires": { + "async": "1.4.2", + "colors": "1.0.3", + "commander": "2.6.0", + "csslint": "0.10.0", + "glob": "5.0.15", + "jshint": "2.8.0", + "parse-glob": "3.0.4", + "strip-json-comments": "1.0.4", + "xml": "1.0.0" + }, + "dependencies": { + "async": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.4.2.tgz", + "integrity": "sha1-bJ7csRztTw3S8tQNsNSaEJwIiqs=", + "dev": true + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + }, + "commander": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", + "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "strip-json-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", + "dev": true + } + } + }, + "htmlparser2": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.1.4.tgz", + "integrity": "sha1-csvn1dVsAaz2H897kzMx9ORbNvA=", + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.0.3", + "domutils": "1.1.6", + "readable-stream": "1.0.34" + }, + "dependencies": { + "domutils": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz", + "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=", + "requires": { + "domelementtype": "1.3.0" + } + } + } + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "dev": true, + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.3.1" + }, + "dependencies": { + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", + "dev": true + } + } + }, + "http-parser-js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.9.tgz", + "integrity": "sha1-6hoE+2St/wJC6ZdPKX3Uw8rSceE=", + "dev": true + }, + "http-proxy": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.16.2.tgz", + "integrity": "sha1-Bt/ykpUr9k2+hHH6nfcwZtTzd0I=", + "dev": true, + "requires": { + "eventemitter3": "1.2.0", + "requires-port": "1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz", + "integrity": "sha1-ZC6ISIUdZvCdTxJJEoRtuutBuDM=", + "dev": true, + "requires": { + "http-proxy": "1.16.2", + "is-glob": "3.1.0", + "lodash": "4.17.4", + "micromatch": "2.3.11" + }, + "dependencies": { + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "https-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=", + "dev": true + }, + "https-proxy-agent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz", + "integrity": "sha1-NffabEjOTdv6JkiRrFk+5f+GceY=", + "dev": true, + "requires": { + "agent-base": "2.1.1", + "debug": "2.6.9", + "extend": "3.0.1" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true + }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", + "dev": true + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "dev": true + }, + "ignore": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", + "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", + "dev": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true + }, + "imports-loader": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/imports-loader/-/imports-loader-0.7.1.tgz", + "integrity": "sha1-8gS180cCoywdt9SNidXoZ6BEElM=", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "source-map": "0.5.7" + }, + "dependencies": { + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "inquirer": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true, + "requires": { + "ansi-escapes": "1.4.0", + "ansi-regex": "2.1.1", + "chalk": "1.1.3", + "cli-cursor": "1.0.2", + "cli-width": "2.2.0", + "figures": "1.7.0", + "lodash": "4.17.4", + "readline2": "1.0.1", + "run-async": "0.1.0", + "rx-lite": "3.1.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "through": "2.3.8" + } + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "invariant": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "ipaddr.js": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", + "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=", + "dev": true + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "1.11.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-callable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", + "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", + "dev": true + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-lower-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-1.1.3.tgz", + "integrity": "sha1-fhR75HaNxGbbO/shzGCzHmrWk5M=", + "requires": { + "lower-case": "1.1.4" + } + }, + "is-my-json-valid": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz", + "integrity": "sha512-ochPsqWS1WXj8ZnMIV0vnNXooaMhp7cyL4FMSIPKTtnV0Ha/T19G2b9kkhcNsabV9bxYkze7/aLZJb/bYuFduQ==", + "dev": true, + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true, + "requires": { + "is-path-inside": "1.0.1" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "1.0.1" + } + }, + "is-resolvable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", + "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", + "dev": true, + "requires": { + "tryit": "1.0.3" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-svg": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz", + "integrity": "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=", + "dev": true, + "requires": { + "html-comment-regex": "1.1.1" + } + }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-upper-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-1.1.2.tgz", + "integrity": "sha1-jQsfp+eTOh5YSDYA7H2WYcuvdW8=", + "requires": { + "upper-case": "1.1.3" + } + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "isbinaryfile": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.2.tgz", + "integrity": "sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.9", + "async": "1.5.2", + "escodegen": "1.8.1", + "esprima": "2.7.3", + "glob": "5.0.15", + "handlebars": "4.0.11", + "js-yaml": "3.7.0", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "once": "1.4.0", + "resolve": "1.1.7", + "supports-color": "3.2.3", + "which": "1.3.0", + "wordwrap": "1.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1.0.9" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "istanbul-instrumenter-loader": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-0.2.0.tgz", + "integrity": "sha1-ZD5OXk6PlGaGOimpd9KDqzcsAZw=", + "dev": true, + "requires": { + "istanbul": "0.4.5", + "loader-utils": "0.2.17", + "object-assign": "4.1.1" + } + }, + "jasmine": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz", + "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=", + "dev": true, + "requires": { + "exit": "0.1.2", + "glob": "7.1.2", + "jasmine-core": "2.8.0" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + } + } + }, + "jasmine-core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz", + "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=", + "dev": true + }, + "jasmine-spec-reporter": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz", + "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==", + "dev": true, + "requires": { + "colors": "1.1.2" + } + }, + "jasminewd2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz", + "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=", + "dev": true + }, + "js-base64": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.0.tgz", + "integrity": "sha512-Wehd+7Pf9tFvGb+ydPm9TjYjV8X1YHOVyG8QyELZxEMqOhemVwGRmoG8iQ/soqI3n8v4xn59zaLxiCJiaaRzKA==", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", + "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "2.7.3" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "jshint": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.8.0.tgz", + "integrity": "sha1-HQmjvZE8TK36gb8Y1YK9hb/+DUQ=", + "dev": true, + "requires": { + "cli": "0.6.6", + "console-browserify": "1.1.0", + "exit": "0.1.2", + "htmlparser2": "3.8.3", + "lodash": "3.7.0", + "minimatch": "2.0.10", + "shelljs": "0.3.0", + "strip-json-comments": "1.0.4" + }, + "dependencies": { + "domhandler": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", + "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", + "dev": true, + "requires": { + "domelementtype": "1.3.0" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0.1.0", + "domelementtype": "1.3.0" + } + }, + "entities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", + "dev": true + }, + "htmlparser2": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", + "dev": true, + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.3.0", + "domutils": "1.5.1", + "entities": "1.0.0", + "readable-stream": "1.1.14" + } + }, + "lodash": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.7.0.tgz", + "integrity": "sha1-Nni9irmVBXwHreg27S7wh9qBHUU=", + "dev": true + }, + "minimatch": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", + "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "shelljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", + "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", + "dev": true + }, + "strip-json-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", + "dev": true + } + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.2.6.tgz", + "integrity": "sha1-9u/JPAagTemuxTBT3yVZuxniA4s=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jszip": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz", + "integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==", + "dev": true, + "requires": { + "core-js": "2.3.0", + "es6-promise": "3.0.2", + "lie": "3.1.1", + "pako": "1.0.6", + "readable-stream": "2.0.6" + }, + "dependencies": { + "core-js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz", + "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=", + "dev": true + }, + "es6-promise": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", + "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + } + } + }, + "karma": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/karma/-/karma-1.3.0.tgz", + "integrity": "sha1-srlOj0mfrdAGnVT5rvSk1I7FzB8=", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "body-parser": "1.18.2", + "chokidar": "1.7.0", + "colors": "1.1.2", + "combine-lists": "1.0.1", + "connect": "3.6.5", + "core-js": "2.5.2", + "di": "0.0.1", + "dom-serialize": "2.2.1", + "expand-braces": "0.1.2", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "http-proxy": "1.16.2", + "isbinaryfile": "3.0.2", + "lodash": "3.10.1", + "log4js": "0.6.38", + "mime": "1.4.1", + "minimatch": "3.0.4", + "optimist": "0.6.1", + "qjobs": "1.1.5", + "range-parser": "1.2.0", + "rimraf": "2.6.2", + "socket.io": "1.4.7", + "source-map": "0.5.7", + "tmp": "0.0.28", + "useragent": "2.2.1" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "karma-coverage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-1.1.1.tgz", + "integrity": "sha1-Wv+LOc9plNwi3kyENix2ABtjfPY=", + "dev": true, + "requires": { + "dateformat": "1.0.12", + "istanbul": "0.4.5", + "lodash": "3.10.1", + "minimatch": "3.0.4", + "source-map": "0.5.7" + }, + "dependencies": { + "dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "dev": true, + "requires": { + "get-stdin": "4.0.1", + "meow": "3.7.0" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "karma-jasmine": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.0.2.tgz", + "integrity": "sha1-wLOrMnvyB9tg4X+ifbN8/e9djmw=", + "dev": true + }, + "karma-phantomjs-launcher": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.4.tgz", + "integrity": "sha1-0jyjSAG9qYY60xjju0vUBisTrNI=", + "dev": true, + "requires": { + "lodash": "4.17.4", + "phantomjs-prebuilt": "2.1.16" + } + }, + "karma-sourcemap-loader": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz", + "integrity": "sha1-kTIsd/jxPUb+0GKwQuEAnUxFBdg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "karma-spec-reporter": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/karma-spec-reporter/-/karma-spec-reporter-0.0.31.tgz", + "integrity": "sha1-SDDccUihVcfXoYbmMjOaDYD63sM=", + "dev": true, + "requires": { + "colors": "1.1.2" + } + }, + "karma-webpack": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-1.8.1.tgz", + "integrity": "sha1-OdX9Lt7qPMPvW0BZibN9Ww5qO04=", + "dev": true, + "requires": { + "async": "0.9.2", + "loader-utils": "0.2.17", + "lodash": "3.10.1", + "source-map": "0.1.43", + "webpack-dev-middleware": "1.12.2" + }, + "dependencies": { + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "kew": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", + "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", + "dev": true + }, + "keypress": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.2.1.tgz", + "integrity": "sha1-HoBFQlABjbrUw/6USX1uZ7YmnHc=", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "dev": true, + "requires": { + "immediate": "3.0.6" + } + }, + "load-grunt-tasks": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/load-grunt-tasks/-/load-grunt-tasks-0.4.0.tgz", + "integrity": "sha1-+CRmP/uiUbV079pak1r6zv4KlfQ=", + "dev": true, + "requires": { + "findup-sync": "0.1.3", + "multimatch": "0.1.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "strip-bom": "3.0.0" + } + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1", + "object-assign": "4.1.1" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, + "lodash._arraycopy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz", + "integrity": "sha1-due3wfH7klRzdIeKVi7Qaj5Q9uE=", + "dev": true + }, + "lodash._arrayeach": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz", + "integrity": "sha1-urFWsqkNPxu9XGU0AzSeXlkz754=", + "dev": true + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash.keys": "3.1.2" + } + }, + "lodash._baseclone": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz", + "integrity": "sha1-MDUZv2OT/n5C802LYw73eU41Qrc=", + "dev": true, + "requires": { + "lodash._arraycopy": "3.0.0", + "lodash._arrayeach": "3.0.0", + "lodash._baseassign": "3.2.0", + "lodash._basefor": "3.0.3", + "lodash.isarray": "3.0.4", + "lodash.keys": "3.1.2" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basefor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.3.tgz", + "integrity": "sha1-dVC06SGO8J+tJDQ7YSAhx5tMIMI=", + "dev": true + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, + "lodash.clonedeep": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz", + "integrity": "sha1-oKHkDYKl6on/WxR7hETtY9koJ9s=", + "dev": true, + "requires": { + "lodash._baseclone": "3.3.0", + "lodash._bindcallback": "3.0.1" + } + }, + "lodash.cond": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", + "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", + "dev": true + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "log4js": { + "version": "0.6.38", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz", + "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "semver": "4.3.6" + }, + "dependencies": { + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true + } + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true, + "requires": { + "js-tokens": "3.0.2" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" + }, + "lpad": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/lpad/-/lpad-0.2.1.tgz", + "integrity": "sha1-EQWHpVgYSFrWoBliXjknykxSw+4=", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" + }, + "macaddress": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/macaddress/-/macaddress-0.2.8.tgz", + "integrity": "sha1-WQTcU3w57G2+/q6QIycTX6hRHxI=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "dev": true + }, + "math-expression-evaluator": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", + "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=", + "dev": true + }, + "maxmin": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-0.1.0.tgz", + "integrity": "sha1-ldgcUonjqdMPf8fcVZwCTlAwydA=", + "dev": true, + "requires": { + "chalk": "0.4.0", + "gzip-size": "0.1.1", + "pretty-bytes": "0.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "1.0.0", + "has-color": "0.1.7", + "strip-ansi": "0.1.1" + } + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + } + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "0.1.5", + "readable-stream": "2.3.3" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + }, + "dependencies": { + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=", + "dev": true + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "dev": true, + "requires": { + "mime-db": "1.30.0" + } + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "moment": { + "version": "2.19.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.4.tgz", + "integrity": "sha512-1xFTAknSLfc47DIxHDUbnJWC+UwgWxATmymaxIPQpmMh7LBm7ZbwVEsuushqwL2GYZU0jie4xO+TK44hJPjNSQ==" + }, + "moment-duration-format": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/moment-duration-format/-/moment-duration-format-1.3.0.tgz", + "integrity": "sha1-VBdxtfh6BJzGVUBHXTrZZnN9aQg=" + }, + "mout": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/mout/-/mout-0.9.1.tgz", + "integrity": "sha1-hPDz/WrMcxf2PeKv/cwM7gCbBHc=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "multimatch": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-0.1.0.tgz", + "integrity": "sha1-CZ2fj4RjrDbPv6JzYLwWzuh97WQ=", + "dev": true, + "requires": { + "lodash": "2.4.2", + "minimatch": "0.2.14" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + } + } + }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + }, + "nan": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", + "dev": true, + "optional": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "ncname": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ncname/-/ncname-1.0.0.tgz", + "integrity": "sha1-W1etGLHKCShk72Kwse2BlPODtxw=", + "dev": true, + "requires": { + "xml-char-classes": "1.0.0" + } + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "dev": true + }, + "ng-annotate": { + "version": "0.15.4", + "resolved": "https://registry.npmjs.org/ng-annotate/-/ng-annotate-0.15.4.tgz", + "integrity": "sha1-ZQdSXI8vKPh4e824mPVtmzEGbpM=", + "dev": true, + "requires": { + "acorn": "0.11.0", + "alter": "0.2.0", + "convert-source-map": "0.4.1", + "optimist": "0.6.1", + "ordered-ast-traverse": "1.1.1", + "simple-fmt": "0.1.0", + "simple-is": "0.2.0", + "source-map": "0.1.43", + "stable": "0.1.6", + "stringmap": "0.2.2", + "stringset": "0.2.1", + "tryor": "0.1.2" + }, + "dependencies": { + "acorn": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-0.11.0.tgz", + "integrity": "sha1-bpXwJTrRYf8BJ9symD5eLlNS1Zo=", + "dev": true + }, + "convert-source-map": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.4.1.tgz", + "integrity": "sha1-+RmgCZ/jH4D8Wh0OswMWGzlAcMc=", + "dev": true + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "ng-annotate-loader": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ng-annotate-loader/-/ng-annotate-loader-0.2.0.tgz", + "integrity": "sha1-1GLcBj3WnSzdcaoEpGxu0KAG5SM=", + "dev": true, + "requires": { + "loader-utils": "0.2.17", + "ng-annotate": "1.2.1", + "normalize-path": "2.1.1", + "source-map": "0.5.7" + }, + "dependencies": { + "acorn": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.6.4.tgz", + "integrity": "sha1-6x9FtKQ/ox0DcBpexG87Umc+kO4=", + "dev": true + }, + "convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + }, + "ng-annotate": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ng-annotate/-/ng-annotate-1.2.1.tgz", + "integrity": "sha1-64vBpnMccNCK9rAsPq8abj+55rs=", + "dev": true, + "requires": { + "acorn": "2.6.4", + "alter": "0.2.0", + "convert-source-map": "1.1.3", + "optimist": "0.6.1", + "ordered-ast-traverse": "1.1.1", + "simple-fmt": "0.1.0", + "simple-is": "0.2.0", + "source-map": "0.5.7", + "stable": "0.1.6", + "stringmap": "0.2.2", + "stringset": "0.2.1", + "tryor": "0.1.2" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "requires": { + "lower-case": "1.1.4" + } + }, + "node-libs-browser": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-0.7.0.tgz", + "integrity": "sha1-PicsCBnjCJNeJmdECNevDhSRuDs=", + "dev": true, + "requires": { + "assert": "1.4.1", + "browserify-zlib": "0.1.4", + "buffer": "4.9.1", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.3.0", + "domain-browser": "1.1.7", + "events": "1.1.1", + "https-browserify": "0.0.1", + "os-browserify": "0.2.1", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "readable-stream": "2.3.3", + "stream-browserify": "2.0.1", + "stream-http": "2.7.2", + "string_decoder": "0.10.31", + "timers-browserify": "2.0.4", + "tty-browserify": "0.0.0", + "url": "0.11.0", + "util": "0.10.3", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + }, + "dependencies": { + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1.1.1" + } + }, + "noptify": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/noptify/-/noptify-0.0.3.tgz", + "integrity": "sha1-WPZUpz2XU98MUdlobckhBKZ/S7s=", + "dev": true, + "requires": { + "nopt": "2.0.0" + }, + "dependencies": { + "nopt": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.0.0.tgz", + "integrity": "sha1-ynQW8gpeP5w7hhgPlilfo9C1Lg0=", + "dev": true, + "requires": { + "abbrev": "1.1.1" + } + } + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.4.1", + "validate-npm-package-license": "3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, + "requires": { + "object-assign": "4.1.1", + "prepend-http": "1.0.4", + "query-string": "4.3.4", + "sort-keys": "1.1.2" + } + }, + "npm-run-all": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-3.1.2.tgz", + "integrity": "sha1-x+P69KoKWb8Nz8EmARZhUWkhcc8=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "cross-spawn": "4.0.2", + "minimatch": "3.0.4", + "object-assign": "4.1.1", + "pinkie-promise": "2.0.1", + "ps-tree": "1.1.0", + "read-pkg": "1.1.0", + "read-pkg-up": "1.0.1", + "shell-quote": "1.6.1", + "string.prototype.padend": "3.0.0" + }, + "dependencies": { + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "nth-check": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", + "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "requires": { + "boolbase": "1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", + "dev": true + }, + "object-keys": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", + "dev": true + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "open": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", + "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.8", + "wordwrap": "0.0.2" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=", + "dev": true + }, + "ordered-ast-traverse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ordered-ast-traverse/-/ordered-ast-traverse-1.1.1.tgz", + "integrity": "sha1-aEOhcLwO7otSDMjdwd3TqjD6BXw=", + "dev": true, + "requires": { + "ordered-esprima-props": "1.1.0" + } + }, + "ordered-esprima-props": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ordered-esprima-props/-/ordered-esprima-props-1.1.0.tgz", + "integrity": "sha1-qYJwht9fAQqmDpvQK24DNc6i/8s=", + "dev": true + }, + "original": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.0.tgz", + "integrity": "sha1-kUf5P6FpbQS+YeAb1QuurKZWvTs=", + "dev": true, + "requires": { + "url-parse": "1.0.5" + }, + "dependencies": { + "url-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.0.5.tgz", + "integrity": "sha1-CFSGBCKv3P7+tsllxmLUgAFpkns=", + "dev": true, + "requires": { + "querystringify": "0.0.4", + "requires-port": "1.0.0" + } + } + } + }, + "os-browserify": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.2.1.tgz", + "integrity": "sha1-Y/xMzuXS13Y9Jrv4YBB45sLgBE8=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.0.3.tgz", + "integrity": "sha1-zWrY3bKQkVrZ4idlV2Al1BHynLY=", + "dev": true + }, + "output-file-sync": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz", + "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "mkdirp": "0.5.1", + "object-assign": "4.1.1" + } + }, + "p-limit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", + "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=", + "dev": true + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.1.0" + } + }, + "pad-stdio": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pad-stdio/-/pad-stdio-0.1.1.tgz", + "integrity": "sha1-fC+ZxNlpYzxgxbVRJZwHVQeK6yo=", + "dev": true, + "requires": { + "lpad": "0.2.1" + } + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, + "param-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-1.1.2.tgz", + "integrity": "sha1-3LCRpDwlm5Io8cNB57akTqC/l0M=", + "requires": { + "sentence-case": "1.1.3" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "parsejson": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.1.tgz", + "integrity": "sha1-mxDGwNglq1ieaFFTgm3go7oni8w=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseqs": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.2.tgz", + "integrity": "sha1-nf5wss3aw4i95PNbHyQPpYrb5sc=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parserlib": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/parserlib/-/parserlib-0.2.5.tgz", + "integrity": "sha1-hZB92GBaoGq7PdKV1QuyuPpN0Rc=", + "dev": true + }, + "parseuri": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.4.tgz", + "integrity": "sha1-gGWCo5iH4eoY3V4v4OAZAiaOk1A=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true + }, + "pascal-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-1.1.2.tgz", + "integrity": "sha1-Pl1kogBDgwp8STRMLXS0G+DJyZs=", + "requires": { + "camel-case": "1.2.2", + "upper-case-first": "1.1.2" + } + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-1.1.2.tgz", + "integrity": "sha1-UM5roNO+090LXCqcRVNpdDRAlRQ=", + "requires": { + "sentence-case": "1.1.3" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "2.3.0" + } + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "pbkdf2-compat": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz", + "integrity": "sha1-tuDI+plJTZTgURV1gCpZpcFC8og=", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "phantomjs-prebuilt": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz", + "integrity": "sha1-79ISpKOWbTZHaE6ouniFSb4q7+8=", + "dev": true, + "requires": { + "es6-promise": "4.1.1", + "extract-zip": "1.6.6", + "fs-extra": "1.0.0", + "hasha": "2.2.0", + "kew": "0.7.0", + "progress": "1.1.8", + "request": "2.83.0", + "request-progress": "2.0.1", + "which": "1.3.0" + }, + "dependencies": { + "es6-promise": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz", + "integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng==", + "dev": true + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + } + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "requires": { + "find-up": "1.1.2" + } + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "dev": true, + "requires": { + "chalk": "1.1.3", + "js-base64": "2.4.0", + "source-map": "0.5.7", + "supports-color": "3.2.3" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "postcss-calc": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz", + "integrity": "sha1-d7rnypKK2FcW4v2kLyYb98HWW14=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-message-helpers": "2.0.0", + "reduce-css-calc": "1.3.0" + } + }, + "postcss-colormin": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz", + "integrity": "sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks=", + "dev": true, + "requires": { + "colormin": "1.1.2", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-convert-values": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz", + "integrity": "sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-discard-comments": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz", + "integrity": "sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-discard-duplicates": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz", + "integrity": "sha1-uavye4isGIFYpesSq8riAmO5GTI=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-discard-empty": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz", + "integrity": "sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-discard-overridden": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz", + "integrity": "sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-discard-unused": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz", + "integrity": "sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "uniqs": "2.0.0" + } + }, + "postcss-filter-plugins": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz", + "integrity": "sha1-bYWGJTTXNaxCDkqFgG4fXUKG2Ew=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "uniqid": "4.1.1" + } + }, + "postcss-load-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", + "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1", + "postcss-load-options": "1.2.0", + "postcss-load-plugins": "2.3.0" + } + }, + "postcss-load-options": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", + "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-load-plugins": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", + "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", + "dev": true, + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-loader": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-1.3.3.tgz", + "integrity": "sha1-piHqH6KQYqg5cqRvVEhncTAZFus=", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-load-config": "1.2.0" + }, + "dependencies": { + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + } + } + }, + "postcss-merge-idents": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz", + "integrity": "sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=", + "dev": true, + "requires": { + "has": "1.0.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-merge-longhand": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz", + "integrity": "sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-merge-rules": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz", + "integrity": "sha1-0d9d+qexrMO+VT8OnhDofGG19yE=", + "dev": true, + "requires": { + "browserslist": "1.7.7", + "caniuse-api": "1.6.1", + "postcss": "5.2.18", + "postcss-selector-parser": "2.2.3", + "vendors": "1.0.1" + } + }, + "postcss-message-helpers": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", + "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=", + "dev": true + }, + "postcss-minify-font-values": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz", + "integrity": "sha1-S1jttWZB66fIR0qzUmyv17vey2k=", + "dev": true, + "requires": { + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-minify-gradients": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz", + "integrity": "sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-minify-params": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz", + "integrity": "sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM=", + "dev": true, + "requires": { + "alphanum-sort": "1.0.2", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0", + "uniqs": "2.0.0" + } + }, + "postcss-minify-selectors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz", + "integrity": "sha1-ssapjAByz5G5MtGkllCBFDEXNb8=", + "dev": true, + "requires": { + "alphanum-sort": "1.0.2", + "has": "1.0.1", + "postcss": "5.2.18", + "postcss-selector-parser": "2.2.3" + } + }, + "postcss-modules-extract-imports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", + "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "dev": true, + "requires": { + "postcss": "6.0.14" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "dev": true, + "requires": { + "css-selector-tokenizer": "0.7.0", + "postcss": "6.0.14" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "dev": true, + "requires": { + "css-selector-tokenizer": "0.7.0", + "postcss": "6.0.14" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "dev": true, + "requires": { + "icss-replace-symbols": "1.1.0", + "postcss": "6.0.14" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "postcss-normalize-charset": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz", + "integrity": "sha1-757nEhLX/nWceO0WL2HtYrXLk/E=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-normalize-url": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz", + "integrity": "sha1-EI90s/L82viRov+j6kWSJ5/HgiI=", + "dev": true, + "requires": { + "is-absolute-url": "2.1.0", + "normalize-url": "1.9.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-ordered-values": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz", + "integrity": "sha1-7sbCpntsQSqNsgQud/6NpD+VwR0=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-reduce-idents": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz", + "integrity": "sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-reduce-initial": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz", + "integrity": "sha1-aPgGlfBF0IJjqHmtJA343WT2ROo=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-reduce-transforms": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz", + "integrity": "sha1-/3b02CEkN7McKYpC0uFEQCV3GuE=", + "dev": true, + "requires": { + "has": "1.0.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", + "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "dev": true, + "requires": { + "flatten": "1.0.2", + "indexes-of": "1.0.1", + "uniq": "1.0.1" + } + }, + "postcss-svgo": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.6.tgz", + "integrity": "sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=", + "dev": true, + "requires": { + "is-svg": "2.1.0", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0", + "svgo": "0.7.2" + } + }, + "postcss-unique-selectors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz", + "integrity": "sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0=", + "dev": true, + "requires": { + "alphanum-sort": "1.0.2", + "postcss": "5.2.18", + "uniqs": "2.0.0" + } + }, + "postcss-value-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", + "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", + "dev": true + }, + "postcss-zindex": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz", + "integrity": "sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=", + "dev": true, + "requires": { + "has": "1.0.1", + "postcss": "5.2.18", + "uniqs": "2.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-bytes": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-0.1.2.tgz", + "integrity": "sha1-zZApTVihyk6KXQ+5yCJZmIgazwA=", + "dev": true + }, + "pretty-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", + "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", + "dev": true, + "requires": { + "renderkid": "2.0.1", + "utila": "0.4.0" + } + }, + "pretty-ms": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-0.1.0.tgz", + "integrity": "sha1-fGnMhmumeU6e7wFo/u6t4Lr6fiI=", + "dev": true + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "propprop": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/propprop/-/propprop-0.3.1.tgz", + "integrity": "sha1-oEmjVouJZEAGfRXY7J8zc15XAXg=", + "dev": true + }, + "protractor": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.2.1.tgz", + "integrity": "sha512-oasutUD/4WUAZdQRo6EtWlVmvgwxWCkcc49XaTiETRPDffRoj8JAQg6gpv42aiP+qyUuK6qJS/XYNBI4Me44Gw==", + "dev": true, + "requires": { + "@types/node": "6.0.92", + "@types/q": "0.0.32", + "@types/selenium-webdriver": "2.53.43", + "blocking-proxy": "1.0.1", + "chalk": "1.1.3", + "glob": "7.1.2", + "jasmine": "2.8.0", + "jasminewd2": "2.2.0", + "optimist": "0.6.1", + "q": "1.4.1", + "saucelabs": "1.3.0", + "selenium-webdriver": "3.6.0", + "source-map-support": "0.4.18", + "webdriver-js-extender": "1.0.0", + "webdriver-manager": "12.0.6" + }, + "dependencies": { + "adm-zip": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.7.tgz", + "integrity": "sha1-hgbCy/HEJs6MjsABdER/1Jtur8E=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "q": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "dev": true + }, + "webdriver-manager": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.0.6.tgz", + "integrity": "sha1-PfGkgZdwELTL+MnYXHpXeCjA5ws=", + "dev": true, + "requires": { + "adm-zip": "0.4.7", + "chalk": "1.1.3", + "del": "2.2.2", + "glob": "7.1.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "q": "1.4.1", + "request": "2.83.0", + "rimraf": "2.6.2", + "semver": "5.4.1", + "xml2js": "0.4.19" + } + } + } + }, + "proxy-addr": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", + "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", + "dev": true, + "requires": { + "forwarded": "0.1.2", + "ipaddr.js": "1.5.2" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "ps-tree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", + "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=", + "dev": true, + "requires": { + "event-stream": "3.3.4" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qjobs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.1.5.tgz", + "integrity": "sha1-ZZ3p8s+NzCehSBJ28gU3cnI4LnM=", + "dev": true + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "requires": { + "object-assign": "4.1.1", + "strict-uri-encode": "1.1.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "querystringify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-0.0.4.tgz", + "integrity": "sha1-DPf4T5Rj/wrlHExLFC2VvjdyTZw=", + "dev": true + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + } + }, + "raw-loader": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", + "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "2.0.0", + "normalize-package-data": "2.4.0", + "path-type": "2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "read-pkg": "2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + } + } + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.3", + "set-immediate-shim": "1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "mute-stream": "0.0.5" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "1.5.0" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "math-expression-evaluator": "1.2.17", + "reduce-function-call": "1.0.2" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "reduce-function-call": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz", + "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=", + "dev": true, + "requires": { + "balanced-match": "0.4.2" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "regenerate": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", + "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "private": "0.1.8" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "dev": true, + "requires": { + "regenerate": "1.3.3", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "renderkid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz", + "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=", + "dev": true, + "requires": { + "css-select": "1.2.0", + "dom-converter": "0.1.4", + "htmlparser2": "3.3.0", + "strip-ansi": "3.0.1", + "utila": "0.3.3" + }, + "dependencies": { + "domhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz", + "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=", + "dev": true, + "requires": { + "domelementtype": "1.3.0" + } + }, + "domutils": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz", + "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=", + "dev": true, + "requires": { + "domelementtype": "1.3.0" + } + }, + "htmlparser2": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", + "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", + "dev": true, + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.1.0", + "domutils": "1.1.6", + "readable-stream": "1.0.34" + } + }, + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", + "dev": true + } + } + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "request": { + "version": "2.83.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", + "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "dev": true, + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.1", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "request-progress": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", + "integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=", + "dev": true, + "requires": { + "throttleit": "1.0.0" + } + }, + "require-from-string": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", + "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=", + "dev": true + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "0.1.0", + "resolve-from": "1.0.1" + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", + "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "1.1.1", + "onetime": "1.1.0" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + } + } + }, + "ripemd160": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-0.2.0.tgz", + "integrity": "sha1-K/GYveFnys+lHAqSjoS2i74XH84=", + "dev": true + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true, + "requires": { + "once": "1.4.0" + } + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "saucelabs": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.3.0.tgz", + "integrity": "sha1-0kDoAJ33+ocwbsRXimm6O1xCT+4=", + "dev": true, + "requires": { + "https-proxy-agent": "1.0.0" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "scrollmonitor": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/scrollmonitor/-/scrollmonitor-1.2.4.tgz", + "integrity": "sha512-HBQpeZVAYETbNk0DAmi+X4hdTQMk5WRa/Udez9o8yC8GcRiPDgBxyEdV9g9Su/TWOuUeVfVGfNcyboEyzkte4Q==" + }, + "selenium-webdriver": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", + "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", + "dev": true, + "requires": { + "jszip": "3.1.5", + "rimraf": "2.6.2", + "tmp": "0.0.30", + "xml2js": "0.4.19" + }, + "dependencies": { + "tmp": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", + "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + } + } + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "dev": true + }, + "send": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", + "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "1.1.1", + "destroy": "1.0.4", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "etag": "1.8.1", + "fresh": "0.5.2", + "http-errors": "1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.3.1" + } + }, + "sentence-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-1.1.3.tgz", + "integrity": "sha1-gDSq/CFFdy06vhUJqkLJ4QQtwTk=", + "requires": { + "lower-case": "1.1.4" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "1.0.3", + "http-errors": "1.6.2", + "mime-types": "2.1.17", + "parseurl": "1.3.2" + } + }, + "serve-static": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", + "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", + "dev": true, + "requires": { + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "parseurl": "1.3.2", + "send": "0.16.1" + } + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "sha.js": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.2.6.tgz", + "integrity": "sha1-F93t3F9yL7ZlAWWIlUYZd4ZzFbo=", + "dev": true + }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true, + "requires": { + "array-filter": "0.0.1", + "array-map": "0.0.0", + "array-reduce": "0.0.0", + "jsonify": "0.0.0" + } + }, + "shelljs": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", + "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", + "dev": true, + "requires": { + "glob": "7.1.2", + "interpret": "1.1.0", + "rechoir": "0.6.2" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + } + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-fmt": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/simple-fmt/-/simple-fmt-0.1.0.tgz", + "integrity": "sha1-GRv1ZqWeZTBILLJatTtKjchcOms=", + "dev": true + }, + "simple-is": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz", + "integrity": "sha1-Krt1qt453rXMgVzhDmGRFkhQuvA=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "snake-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-1.1.2.tgz", + "integrity": "sha1-DC8l4wUVjZoY09l3BmGH/vilpmo=", + "requires": { + "sentence-case": "1.1.3" + } + }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "dev": true, + "requires": { + "hoek": "4.2.0" + } + }, + "socket.io": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.4.7.tgz", + "integrity": "sha1-krf3y4jFeX1NruJ5/oB12+bT+hw=", + "dev": true, + "requires": { + "debug": "2.2.0", + "engine.io": "1.6.10", + "has-binary": "0.1.7", + "socket.io-adapter": "0.4.0", + "socket.io-client": "1.4.6", + "socket.io-parser": "2.2.6" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "socket.io-adapter": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.4.0.tgz", + "integrity": "sha1-+5+CqxqmUpC/csNleVW5MKmRok8=", + "dev": true, + "requires": { + "debug": "2.2.0", + "socket.io-parser": "2.2.2" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "socket.io-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.2.2.tgz", + "integrity": "sha1-PXr2tkSX6Va32f53X5mXFgJ/lBc=", + "dev": true, + "requires": { + "benchmark": "1.0.0", + "component-emitter": "1.1.2", + "debug": "0.7.4", + "isarray": "0.0.1", + "json3": "3.2.6" + }, + "dependencies": { + "debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=", + "dev": true + } + } + } + } + }, + "socket.io-client": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.4.6.tgz", + "integrity": "sha1-SbC6U379FbgpfIQBbmQuHHx1LD0=", + "dev": true, + "requires": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "1.2.0", + "debug": "2.2.0", + "engine.io-client": "1.6.9", + "has-binary": "0.1.7", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseuri": "0.0.4", + "socket.io-parser": "2.2.6", + "to-array": "0.1.4" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz", + "integrity": "sha1-zNETqGOI0GSC0D3j/H35hSa6jv4=", + "dev": true + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "socket.io-parser": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.2.6.tgz", + "integrity": "sha1-ON/WHfUNz4qx2eIJEyK/kCuii5k=", + "dev": true, + "requires": { + "benchmark": "1.0.0", + "component-emitter": "1.1.2", + "debug": "2.2.0", + "isarray": "0.0.1", + "json3": "3.3.2" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "sockjs": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", + "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "dev": true, + "requires": { + "faye-websocket": "0.10.0", + "uuid": "3.1.0" + }, + "dependencies": { + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "requires": { + "websocket-driver": "0.7.0" + } + } + } + }, + "sockjs-client": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.4.tgz", + "integrity": "sha1-W6vjhrd15M8U51IJEUUmVAFsixI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "eventsource": "0.1.6", + "faye-websocket": "0.11.1", + "inherits": "2.0.3", + "json3": "3.3.2", + "url-parse": "1.2.0" + }, + "dependencies": { + "faye-websocket": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", + "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", + "dev": true, + "requires": { + "websocket-driver": "0.7.0" + } + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "1.1.0" + } + }, + "source-list-map": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz", + "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=", + "dev": true + }, + "source-map": { + "version": "0.1.34", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", + "integrity": "sha1-p8/omux7FoLDsZjQrPtH19CQVms=", + "requires": { + "amdefine": "1.0.1" + } + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true, + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "dev": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "dev": true + }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dev": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, + "stable": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.6.tgz", + "integrity": "sha1-kQ9dKu17Ugxud3SZwfMuE5/eyxA=", + "dev": true + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "stream-cache": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stream-cache/-/stream-cache-0.0.2.tgz", + "integrity": "sha1-GsWtaDJCjKVWZ9ve45Xa1ObbEY8=", + "dev": true + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true, + "requires": { + "duplexer": "0.1.1" + } + }, + "stream-http": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", + "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", + "dev": true, + "requires": { + "builtin-status-codes": "3.0.0", + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string-replace-webpack-plugin": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/string-replace-webpack-plugin/-/string-replace-webpack-plugin-0.1.3.tgz", + "integrity": "sha1-c8ZX51nWbP6Arh4M8JGqJW0OcVw=", + "dev": true, + "requires": { + "async": "0.2.10", + "css-loader": "0.9.1", + "file-loader": "0.8.5", + "loader-utils": "0.2.17", + "style-loader": "0.8.3" + }, + "dependencies": { + "css-loader": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.9.1.tgz", + "integrity": "sha1-LhqgDOfjDvLGp6SzAKCAp8l54Nw=", + "dev": true, + "optional": true, + "requires": { + "csso": "1.3.12", + "loader-utils": "0.2.17", + "source-map": "0.1.43" + } + }, + "csso": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/csso/-/csso-1.3.12.tgz", + "integrity": "sha1-/GKGlKLTiTiqrEmWdTIY/TEc254=", + "dev": true, + "optional": true + }, + "file-loader": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-0.8.5.tgz", + "integrity": "sha1-knXQMf54DyfUf19K8CvUNxPMFRs=", + "dev": true, + "optional": true, + "requires": { + "loader-utils": "0.2.17" + } + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "style-loader": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.8.3.tgz", + "integrity": "sha1-9Pkut9tjdodI8VBlzWcA9aHIU1c=", + "dev": true, + "optional": true, + "requires": { + "loader-utils": "0.2.17" + } + } + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string.prototype.padend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz", + "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.10.0", + "function-bind": "1.1.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "stringmap": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stringmap/-/stringmap-0.2.2.tgz", + "integrity": "sha1-VWwTeyWPlCuHdvWy71gqoGnX0bE=", + "dev": true + }, + "stringset": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/stringset/-/stringset-0.2.1.tgz", + "integrity": "sha1-7yWcTjSTRDd/zRyRPdLoSMnAQrU=", + "dev": true + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "style-loader": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.13.2.tgz", + "integrity": "sha1-dFMzhM9pjHEEx5URULSXF63C87s=", + "dev": true, + "requires": { + "loader-utils": "1.1.0" + }, + "dependencies": { + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + } + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + }, + "svgo": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", + "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=", + "dev": true, + "requires": { + "coa": "1.0.4", + "colors": "1.1.2", + "csso": "2.3.2", + "js-yaml": "3.7.0", + "mkdirp": "0.5.1", + "sax": "1.2.4", + "whet.extend": "0.9.9" + } + }, + "swap-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-1.1.2.tgz", + "integrity": "sha1-w5IDpFhzhfrTyFCgvRvK+ggZdOM=", + "requires": { + "lower-case": "1.1.4", + "upper-case": "1.1.3" + } + }, + "table": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "requires": { + "ajv": "4.11.8", + "ajv-keywords": "1.5.1", + "chalk": "1.1.3", + "lodash": "4.17.4", + "slice-ansi": "0.0.4", + "string-width": "2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "tapable": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", + "integrity": "sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=", + "dev": true + }, + "tape": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/tape/-/tape-0.2.2.tgz", + "integrity": "sha1-ZMz6S37PSgBgAH5hcW1CR4FnFjc=", + "dev": true, + "requires": { + "deep-equal": "0.0.0", + "defined": "0.0.0", + "jsonify": "0.0.0" + }, + "dependencies": { + "defined": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-0.0.0.tgz", + "integrity": "sha1-817qfXBekzuvE7LwOz+D2SFAOz4=", + "dev": true + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } + }, + "time-grunt": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/time-grunt/-/time-grunt-0.3.2.tgz", + "integrity": "sha1-8wE2RbAeaOJ4AqPkxHAs7KC9/68=", + "dev": true, + "requires": { + "chalk": "0.4.0", + "date-time": "0.1.1", + "hooker": "0.2.3", + "pretty-ms": "0.1.0", + "text-table": "0.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "1.0.0", + "has-color": "0.1.7", + "strip-ansi": "0.1.1" + } + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + } + } + }, + "time-stamp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-2.0.0.tgz", + "integrity": "sha1-lcakRTDhW6jW9KPsuMOj+sRto1c=", + "dev": true + }, + "timers-browserify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.4.tgz", + "integrity": "sha512-uZYhyU3EX8O7HQP+J9fTVYwsq90Vr68xPEFo7yrVImIxYvHgukBEgOB/SgGoorWVTzGM/3Z+wUNnboA4M8jWrg==", + "dev": true, + "requires": { + "setimmediate": "1.0.5" + } + }, + "tiny-lr-fork": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/tiny-lr-fork/-/tiny-lr-fork-0.0.5.tgz", + "integrity": "sha1-Hpnh4qhGm3NquX2X7vqYxx927Qo=", + "dev": true, + "requires": { + "debug": "0.7.4", + "faye-websocket": "0.4.4", + "noptify": "0.0.3", + "qs": "0.5.6" + }, + "dependencies": { + "debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=", + "dev": true + }, + "qs": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/qs/-/qs-0.5.6.tgz", + "integrity": "sha1-MbGtBYVnZRxSaSFQa5qHk5EaA4Q=", + "dev": true + } + } + }, + "title-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/title-case/-/title-case-1.1.2.tgz", + "integrity": "sha1-+uSmrlRr+iLQg6DuqRCkDRLtT1o=", + "requires": { + "sentence-case": "1.1.3", + "upper-case": "1.1.3" + } + }, + "tmp": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.28.tgz", + "integrity": "sha1-Fyc1t/YU6nrzlmT6hM8N5OUV0SA=", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "toposort": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.6.tgz", + "integrity": "sha1-wxdI5V0hDv/AD9zcfW5o19e7nOw=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "dev": true, + "requires": { + "punycode": "1.4.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tryit": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", + "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", + "dev": true + }, + "tryor": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tryor/-/tryor-0.1.2.tgz", + "integrity": "sha1-gUXkynyv9ArN48z5Rui4u3W0Fys=", + "dev": true + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.17" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-js": { + "version": "2.4.24", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", + "integrity": "sha1-+tV1XB4Vd2WLsG/5q25UjJW+vW4=", + "requires": { + "async": "0.2.10", + "source-map": "0.1.34", + "uglify-to-browserify": "1.0.2", + "yargs": "3.5.4" + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=" + }, + "ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", + "dev": true + }, + "underscore": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", + "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=" + }, + "underscore.string": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", + "dev": true + }, + "unicons": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/unicons/-/unicons-0.0.3.tgz", + "integrity": "sha1-bmp6Gm6uuwHKPYsSrZaHJ56rpSQ=", + "dev": true + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqid": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-4.1.1.tgz", + "integrity": "sha1-iSIN32t1GuUrX3JISGNShZa7hME=", + "dev": true, + "requires": { + "macaddress": "0.2.8" + } + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" + }, + "upper-case-first": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-1.1.2.tgz", + "integrity": "sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU=", + "requires": { + "upper-case": "1.1.3" + } + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-parse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz", + "integrity": "sha512-DT1XbYAfmQP65M/mE6OALxmXzZ/z1+e5zk2TcSKe/KiYbNGZxgtttzC0mR/sjopbpOXcbniq7eIKmocJnUWlEw==", + "dev": true, + "requires": { + "querystringify": "1.0.0", + "requires-port": "1.0.0" + }, + "dependencies": { + "querystringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz", + "integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs=", + "dev": true + } + } + }, + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", + "dev": true + }, + "useragent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.2.1.tgz", + "integrity": "sha1-z1k+9PLRdYdei7ZY6pLhik/QbY4=", + "dev": true, + "requires": { + "lru-cache": "2.2.4", + "tmp": "0.0.28" + }, + "dependencies": { + "lru-cache": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.2.4.tgz", + "integrity": "sha1-bGWGGb7PFAMdDQtZSxYELOTcBj0=", + "dev": true + } + } + }, + "utf8": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.0.tgz", + "integrity": "sha1-DP7FyAUtRKI+OqqQgQToB1+V39U=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", + "dev": true + }, + "v8flags": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "dev": true, + "requires": { + "user-home": "1.1.1" + } + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "vendors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.1.tgz", + "integrity": "sha1-N61zyO5Bf7PVgOeFMSMH0nSEfyI=", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true + }, + "watchpack": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-0.2.9.tgz", + "integrity": "sha1-Yuqkq15bo1/fwBgnVibjwPXj+ws=", + "dev": true, + "requires": { + "async": "0.9.2", + "chokidar": "1.7.0", + "graceful-fs": "4.1.11" + }, + "dependencies": { + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true + } + } + }, + "webdriver-js-extender": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-1.0.0.tgz", + "integrity": "sha1-gcUzqeM9W/tZe05j4s2yW1R3dRU=", + "dev": true, + "requires": { + "@types/selenium-webdriver": "2.53.43", + "selenium-webdriver": "2.53.3" + }, + "dependencies": { + "sax": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz", + "integrity": "sha1-VjsZx8HeiS4Jv8Ty/DDjwn8JUrk=", + "dev": true + }, + "selenium-webdriver": { + "version": "2.53.3", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-2.53.3.tgz", + "integrity": "sha1-0p/1qVff8aG0ncRXdW5OS/vc4IU=", + "dev": true, + "requires": { + "adm-zip": "0.4.4", + "rimraf": "2.6.2", + "tmp": "0.0.24", + "ws": "1.0.1", + "xml2js": "0.4.4" + } + }, + "tmp": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.24.tgz", + "integrity": "sha1-1qXhmNFKmDXMby18PZ4wJCjIzxI=", + "dev": true + }, + "xml2js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", + "integrity": "sha1-MREBAAMAiuGSQOuhdJe1fHKcVV0=", + "dev": true, + "requires": { + "sax": "0.6.1", + "xmlbuilder": "9.0.4" + } + } + } + }, + "webpack": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-1.15.0.tgz", + "integrity": "sha1-T/MfU9sDM55VFkqdRo7gMklo/pg=", + "dev": true, + "requires": { + "acorn": "3.3.0", + "async": "1.5.2", + "clone": "1.0.3", + "enhanced-resolve": "0.9.1", + "interpret": "0.6.6", + "loader-utils": "0.2.17", + "memory-fs": "0.3.0", + "mkdirp": "0.5.1", + "node-libs-browser": "0.7.0", + "optimist": "0.6.1", + "supports-color": "3.2.3", + "tapable": "0.1.10", + "uglify-js": "2.7.5", + "watchpack": "0.2.9", + "webpack-core": "0.6.9" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "interpret": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-0.6.6.tgz", + "integrity": "sha1-/s16GOfOXKar+5U+H4YhOknxYls=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "memory-fs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.3.0.tgz", + "integrity": "sha1-e8xrYp46Q+hx1+Kaymrop/FcuyA=", + "dev": true, + "requires": { + "errno": "0.1.5", + "readable-stream": "2.3.3" + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "uglify-js": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.7.5.tgz", + "integrity": "sha1-RhLAx7qu4rp8SH3kkErhIgefLKg=", + "dev": true, + "requires": { + "async": "0.2.10", + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + } + } + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "webpack-core": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz", + "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=", + "dev": true, + "requires": { + "source-list-map": "0.1.8", + "source-map": "0.4.4" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "webpack-dev-middleware": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz", + "integrity": "sha512-FCrqPy1yy/sN6U/SaEZcHKRXGlqU0DUaEBL45jkUYoB8foVb6wCnbIJ1HKIx+qUFTW+3JpVcCJCxZ8VATL4e+A==", + "dev": true, + "requires": { + "memory-fs": "0.4.1", + "mime": "1.6.0", + "path-is-absolute": "1.0.1", + "range-parser": "1.2.0", + "time-stamp": "2.0.0" + }, + "dependencies": { + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + } + } + }, + "webpack-dev-server": { + "version": "1.16.5", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-1.16.5.tgz", + "integrity": "sha1-DL1fLSrI1OWTqs1clwLnu9XlmJI=", + "dev": true, + "requires": { + "compression": "1.7.1", + "connect-history-api-fallback": "1.5.0", + "express": "4.16.2", + "http-proxy-middleware": "0.17.4", + "open": "0.0.5", + "optimist": "0.6.1", + "serve-index": "1.9.1", + "sockjs": "0.3.19", + "sockjs-client": "1.1.4", + "stream-cache": "0.0.2", + "strip-ansi": "3.0.1", + "supports-color": "3.2.3", + "webpack-dev-middleware": "1.12.2" + } + }, + "webpack-sources": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-0.1.5.tgz", + "integrity": "sha1-qh86vw8NdNtxEcQOUAuE+WZkB1A=", + "dev": true, + "requires": { + "source-list-map": "0.1.8", + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "websocket-driver": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", + "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "dev": true, + "requires": { + "http-parser-js": "0.4.9", + "websocket-extensions": "0.1.3" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "dev": true + }, + "whet.extend": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", + "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=", + "dev": true + }, + "which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "wiredep": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/wiredep/-/wiredep-2.2.2.tgz", + "integrity": "sha1-FETRirLkk3UEEJP+3d3Rto97ZrM=", + "dev": true, + "requires": { + "bower-config": "0.5.3", + "chalk": "0.5.1", + "glob": "4.5.3", + "lodash": "2.4.2", + "minimist": "1.2.0", + "propprop": "0.3.1", + "through2": "0.6.5" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "1.1.0", + "escape-string-regexp": "1.0.5", + "has-ansi": "0.1.0", + "strip-ansi": "0.3.0", + "supports-color": "0.2.0" + } + }, + "glob": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-4.5.3.tgz", + "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "2.0.10", + "once": "1.4.0" + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "0.2.1" + } + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "minimatch": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", + "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + } + } + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "0.5.1" + } + }, + "ws": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.0.1.tgz", + "integrity": "sha1-fQsqLljN3YGQOcKcneZQReGzEOk=", + "dev": true, + "requires": { + "options": "0.0.6", + "ultron": "1.0.2" + } + }, + "xml": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.0.tgz", + "integrity": "sha1-3j7pEkd74vJQtg9hLzSoxNphbv4=", + "dev": true + }, + "xml-char-classes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/xml-char-classes/-/xml-char-classes-1.0.0.tgz", + "integrity": "sha1-ZGV4SKIP/F31g6Qq2KJ3tFErvE0=", + "dev": true + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dev": true, + "requires": { + "sax": "1.2.4", + "xmlbuilder": "9.0.4" + } + }, + "xmlbuilder": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz", + "integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8=", + "dev": true + }, + "xmlhttprequest-ssl": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.1.tgz", + "integrity": "sha1-O3dB/qSoZnWXbpCNKW1ERZYfqmc=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", + "integrity": "sha1-2K/49mXpTDS9JZvevRv68N3TU2E=", + "requires": { + "camelcase": "1.2.1", + "decamelize": "1.2.0", + "window-size": "0.1.0", + "wordwrap": "0.0.2" + } + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "1.0.1" + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + }, + "zlib-browserify": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/zlib-browserify/-/zlib-browserify-0.0.3.tgz", + "integrity": "sha1-JAzNv9AgP6hCsTDe77FBQSLIzFA=", + "dev": true, + "requires": { + "tape": "0.2.2" + } + } + } +} diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 227f30b1743..efec03941e4 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -46,7 +46,6 @@ https://nodejs.org/dist/ http://registry.npmjs.org/npm/-/ - https://github.com/yarnpkg/yarn/releases/download/ @@ -67,28 +66,25 @@ ${plugin.frontend.nodeDownloadRoot} ${plugin.frontend.npmDownloadRoot} - ${plugin.frontend.yarnDownloadRoot} - install node and yarn + install node - install-node-and-yarn install-node-and-npm ${node.version} - ${yarn.version} ${npm.version} - yarn install + npm install - yarn + npm ${web.e2e.enabled} @@ -97,9 +93,9 @@ - yarn build + npm build - yarn + npm ${web.e2e.enabled} @@ -108,9 +104,9 @@ - yarn test + npm test - yarn + npm test @@ -120,9 +116,9 @@ - yarn e2e + npm e2e - yarn + npm integration-test From 4bf0f3a910e01107c00f28e4d138e52c5013f132 Mon Sep 17 00:00:00 2001 From: tinkoff-dwh Date: Thu, 14 Dec 2017 09:45:58 +0300 Subject: [PATCH 133/492] [ZEPPELIN-3007] display a note name without any path in the tab title ### What is this PR for? This PR modifies the display of the name of the note. Now, instead of the full path, only the name of the note is displayed. ### What type of PR is it? Improvement ### What is the Jira issue? [ZEPPELIN-3007](https://issues.apache.org/jira/browse/ZEPPELIN-3007) ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: tinkoff-dwh Closes #2698 from tinkoff-dwh/ZEPPELIN-3007 and squashes the following commits: 526e99f1f [tinkoff-dwh] Merge branch 'master' into ZEPPELIN-3007 aefb3f50b [tinkoff-dwh] [ZEPPELIN-3007] change actionBar note name view bdfe474af [tinkoff-dwh] [ZEPPELIN-3007] add ' - Zeppelin' to page title ad53d767d [tinkoff-dwh] [ZEPPELIN-3007] display a note name without any path in the tab title --- .../src/app/notebook/notebook-actionBar.html | 11 +++-- .../src/app/notebook/notebook.controller.js | 9 +++- zeppelin-web/src/app/notebook/notebook.css | 47 +++++++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index 0db4ff02c25..f1d1dd5959c 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -16,14 +16,15 @@

    - -

    +

    {{noteName(note)}}

    + ng-show="!input.showEditor">{{noteName(note)}}>

    diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index 1fa63231ea8..b02b74ee8d5 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -83,7 +83,14 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, let currentSearchParagraph = 0 $scope.$watch('note', function (value) { - $rootScope.pageTitle = value ? value.name : 'Zeppelin' + let title + if (value) { + title = value.name.substr(value.name.lastIndexOf('/') + 1, value.name.length) + title += ' - Zeppelin' + } else { + title = 'Zeppelin' + } + $rootScope.pageTitle = title }, true) $scope.$on('setConnectedStatus', function (event, param) { diff --git a/zeppelin-web/src/app/notebook/notebook.css b/zeppelin-web/src/app/notebook/notebook.css index 262ae8e6ccf..fe5da5c1e24 100644 --- a/zeppelin-web/src/app/notebook/notebook.css +++ b/zeppelin-web/src/app/notebook/notebook.css @@ -183,6 +183,53 @@ overflow: hidden; } +.ellipsis { + padding-left: 1em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.reverse-ellipsis { + /* Your move. */ + text-overflow: clip; + position: relative; + background-color: #FFF; +} + +.reverse-ellipsis:before { + content: '\02026'; + position: absolute; + z-index: 1; + left: -1em; + background-color: inherit; + padding-left: 1em; + margin-left: 0.5em; +} + +.reverse-ellipsis span { + min-width: 100%; + position: relative; + display: inline-block; + float: right; + overflow: visible; + background-color: inherit; + text-indent: 0.5em; +} + +.reverse-ellipsis span:before { + content: ''; + position: absolute; + display: inline-block; + width: 1em; + height: 1em; + background-color: inherit; + z-index: 200; + left: -.5em; +} + + + .noOverflow { overflow: hidden !important; } From 888a05d1e48a75f237edf7b6bfa7d4e57b100548 Mon Sep 17 00:00:00 2001 From: Keiji Yoshida Date: Mon, 27 Nov 2017 19:48:47 +0900 Subject: [PATCH 134/492] [ZEPPELIN-3077] Cron scheduler is easy to get stuck when one of the cron jobs takes long time or gets stuck ### What is this PR for? The cron scheduler is easy to get stuck when one of the cron jobs takes long time or gets stuck. I sometimes come across the issue that the cron scheduler stops working suddenly. According to the thread dump of ZeppelinServer, all of the DefaultQuartzScheduler_Worker threads were waiting for the job's completion and there was no thread to launch a new job. Here is the contents of the thread dump: ``` "DefaultQuartzScheduler_Worker-10" #76 prio=5 os_prio=0 tid=0x00007fb41d3b4000 nid=0x1b521 sleeping[0x00007fb3daef1000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at org.apache.zeppelin.notebook.Notebook$CronJob.execute(Notebook.java:889) at org.quartz.core.JobRunShell.run(JobRunShell.java:202) at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) - locked <0x00000000c0a7dbf0> (a java.lang.Object) Locked ownable synchronizers: - None "DefaultQuartzScheduler_Worker-9" #75 prio=5 os_prio=0 tid=0x00007fb41d3b2000 nid=0x1b520 waiting on condition [0x00007fb3daff2000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at org.apache.zeppelin.notebook.Notebook$CronJob.execute(Notebook.java:889) at org.quartz.core.JobRunShell.run(JobRunShell.java:202) at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) - locked <0x00000000c0a7a470> (a java.lang.Object) Locked ownable synchronizers: - None ... "DefaultQuartzScheduler_Worker-2" #68 prio=5 os_prio=0 tid=0x00007fb41d3c8800 nid=0x1b519 waiting on condition [0x00007fb3da473000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at org.apache.zeppelin.notebook.Notebook$CronJob.execute(Notebook.java:889) at org.quartz.core.JobRunShell.run(JobRunShell.java:202) at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) - locked <0x00000000c0a7a7b0> (a java.lang.Object) Locked ownable synchronizers: - None "DefaultQuartzScheduler_Worker-1" #67 prio=5 os_prio=0 tid=0x00007fb41d3cc800 nid=0x1b518 waiting on condition [0x00007fb3da372000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at org.apache.zeppelin.notebook.Notebook$CronJob.execute(Notebook.java:889) at org.quartz.core.JobRunShell.run(JobRunShell.java:202) at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) - locked <0x00000000c0a7dd90> (a java.lang.Object) Locked ownable synchronizers: - None ``` The above thread dump says that all of the worker threads get stuck at https://github.com/apache/zeppelin/blob/v0.7.3/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java#L889. One way to reproduce this kind of issue is creating a paragraph whose status is "READY" and "disable run". That makes the paragraph status "READY" permanently and `note.isTerminated()` never turns to `true`. To fix this issue, the following two improvements has been made at this PR: 1. Remove the unnecessary `while (!note.isTerminated()) { ... }` block because the execution of all of the paragraphs is finished after `note.runAll()`. 2. Skip the cron execution if there is a running or pending paragraph. That prevents the Zeppelin cron scheduler from getting stuck by the long running paragraph whose execution duration is greater than the cron execution cycle. ### What type of PR is it? [Bug] ### Todos ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-3077 ### How should this be tested? * Tested manually. 1. The cron scheduler does not get stuck if there is a paragraph whose status is "READY" and "disable run". 2. The following message is printed on the log file when the cron job is launched while the previous cron job still has been running. * `execution of the cron job is skipped because there is a running or pending paragraph (note id: XXXXXXXXX)` ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No. * Is there breaking changes for older versions? No. * Does this needs documentation? Yes. The behavior of the cron job was changed not to run if there is a running or pending paragraph by this PR. Thus, the documentation `docs/usage/other_features/cron_scheduler.md` was also added by this PR. Its layout is as follow: screen shot 2017-11-28 at 18 30 54 Author: Keiji Yoshida Closes #2687 from kjmrknsn/ZEPPELIN-3077 and squashes the following commits: 81e72188d [Keiji Yoshida] [ZEPPELIN-3077] Cron scheduler is easy to get stuck when one of the cron jobs takes long time or gets stuck --- .../themes/zeppelin/_navigation.html | 1 + .../docs-img/cron_scheduler_dialog_box.png | Bin 0 -> 135264 bytes docs/usage/other_features/cron_scheduler.md | 52 ++++++++++++++++++ .../org/apache/zeppelin/notebook/Note.java | 16 ++++++ .../apache/zeppelin/notebook/Notebook.java | 13 ++--- .../zeppelin/notebook/NotebookTest.java | 40 ++++++++++++++ 6 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 docs/assets/themes/zeppelin/img/docs-img/cron_scheduler_dialog_box.png create mode 100644 docs/usage/other_features/cron_scheduler.md diff --git a/docs/_includes/themes/zeppelin/_navigation.html b/docs/_includes/themes/zeppelin/_navigation.html index 95d83ea27c2..796b8bca601 100644 --- a/docs/_includes/themes/zeppelin/_navigation.html +++ b/docs/_includes/themes/zeppelin/_navigation.html @@ -62,6 +62,7 @@
  • Personalized Mode
  • Customizing Zeppelin Homepage
  • Notebook Actions
  • +
  • Cron Scheduler
  • REST API
  • Interpreter API
  • diff --git a/docs/assets/themes/zeppelin/img/docs-img/cron_scheduler_dialog_box.png b/docs/assets/themes/zeppelin/img/docs-img/cron_scheduler_dialog_box.png new file mode 100644 index 0000000000000000000000000000000000000000..50633983b2eed3f44d31d0ef51f3c76872c53a02 GIT binary patch literal 135264 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úrK<=o6FcT0xVeTp8j?zwZY_pi*WWI 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 literal 0 HcmV?d00001 diff --git a/docs/usage/other_features/cron_scheduler.md b/docs/usage/other_features/cron_scheduler.md new file mode 100644 index 00000000000..e8a9975a0ed --- /dev/null +++ b/docs/usage/other_features/cron_scheduler.md @@ -0,0 +1,52 @@ +--- +layout: page +title: "Running a Notebook on a Given Schedule Automatically" +description: "You can run a notebook on a given schedule automatically by setting up a cron scheduler on the notebook." +group: usage/other_features +--- + +{% include JB/setup %} + +# Running a Notebook on a Given Schedule Automatically + +
    + +Apache Zeppelin provides a cron scheduler for each notebook. You can run a notebook on a given schedule automatically by setting up a cron scheduler on the notebook. + +## Setting up a cron scheduler on a notebook + +Click the clock icon on the tool bar and open a cron scheduler dialog box. + + + +There are the following items which you can input or set: + +### Preset + +You can set a cron schedule easily by clicking each option such as `1m` and `5m`. The login user is set as a cron executing user automatically. You can also clear the cron schedule settings by clicking `None`. + +### Cron expression + +You can set the cron schedule by filling in this form. Please see [Cron Trigger Tutorial](http://www.quartz-scheduler.org/documentation/quartz-2.2.x/tutorials/crontrigger) for the available cron syntax. + +### Cron executing user + +You can set the cron executing user by filling in this form and press the enter key. + +### auto-restart interpreter on cron execution + +When this checkbox is set to "on", the interpreters which are binded to the notebook are stopped automatically after the cron execution. This feature is useful if you want to release the interpreter resources after the cron execution. + +> **Note**: A cron execution is skipped if one of the paragraphs is in a state of `RUNNING` or `PENDING` no matter whether it is executed automatically (i.e. by the cron scheduler) or manually by a user opening this notebook. diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java index 6e667327172..19f396ecd05 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java @@ -652,6 +652,22 @@ boolean isTerminated() { return true; } + /** + * Return true if there is a running or pending paragraph + */ + boolean isRunningOrPending() { + synchronized (paragraphs) { + for (Paragraph p : paragraphs) { + Status status = p.getStatus(); + if (status.isRunning() || status.isPending()) { + return true; + } + } + } + + return false; + } + public boolean isTrash() { String path = getName(); if (path.charAt(0) == '/') { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java index 8de981e6e32..d68cd4b75fd 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java @@ -890,16 +890,15 @@ public void execute(JobExecutionContext context) throws JobExecutionException { String noteId = context.getJobDetail().getJobDataMap().getString("noteId"); Note note = notebook.getNote(noteId); - note.runAll(); - while (!note.isTerminated()) { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - logger.error(e.toString(), e); - } + if (note.isRunningOrPending()) { + logger.warn("execution of the cron job is skipped because there is a running or pending " + + "paragraph (note id: {})", noteId); + return; } + note.runAll(); + boolean releaseResource = false; String cronExecutingUser = null; try { diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java index ba9e177690e..83c09327c35 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java @@ -361,6 +361,46 @@ public void testSchedule() throws InterruptedException, IOException { notebook.removeNote(note.getId(), anonymous); } + @Test + public void testScheduleAgainstRunningAndPendingParagraph() throws InterruptedException, IOException { + // create a note + Note note = notebook.createNote(anonymous); + interpreterSettingManager.setInterpreterBinding("user", note.getId(), + interpreterSettingManager.getInterpreterSettingIds()); + + // append running and pending paragraphs to the note + for (Status status: new Status[]{Status.RUNNING, Status.PENDING}) { + Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); + Map config = new HashMap<>(); + p.setConfig(config); + p.setText("p"); + p.setStatus(status); + assertNull(p.getDateFinished()); + } + + // set cron scheduler, once a second + Map config = note.getConfig(); + config.put("enabled", true); + config.put("cron", "* * * * * ?"); + note.setConfig(config); + notebook.refreshCron(note.getId()); + Thread.sleep(2 * 1000); + + // remove cron scheduler. + config.put("cron", null); + note.setConfig(config); + notebook.refreshCron(note.getId()); + Thread.sleep(2 * 1000); + + // check if the executions of the running and pending paragraphs were skipped + for (Paragraph p : note.paragraphs) { + assertNull(p.getDateFinished()); + } + + // remove the note + notebook.removeNote(note.getId(), anonymous); + } + @Test public void testSchedulePoolUsage() throws InterruptedException, IOException { final int timeout = 30; From ed468003323b984b0249d40d88c2378cc65950de Mon Sep 17 00:00:00 2001 From: Jongyoul Lee Date: Mon, 18 Dec 2017 19:06:47 +0900 Subject: [PATCH 135/492] [MINOR] Remove r from zeppelin-web test ### What is this PR for? Reducing test time to remove unused dependencies from some profile. We will reduce whole test time by removing some unused packages. ### What type of PR is it? [Improvement] ### Todos * [x] - Remove R related packages from some profiles ### What is the Jira issue? * MINOR ### How should this be tested? * Check the CI ### Screenshots (if appropriate) Before: ![image](https://user-images.githubusercontent.com/3612566/34191013-676b2958-e587-11e7-8e05-b6bd0df40ede.png) After: ![image](https://user-images.githubusercontent.com/3612566/34191029-73619760-e587-11e7-926d-cfc0040d95e5.png) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jongyoul Lee Closes #2710 from jongyoul/minor/remove-unused-r-in-ci and squashes the following commits: 64d765cd [Jongyoul Lee] Removed r from zeppelin-web test --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4495aa4c245..7db41bfe242 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,11 +56,8 @@ matrix: env: PYTHON="2" WEB_E2E="true" SCALA_VER="2.11" SPARK_VER="2.1.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pscala-2.11" BUILD_FLAG="package -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl ${INTERPRETERS}" TEST_MODULES="-pl zeppelin-web" TEST_PROJECTS="-Pweb-e2e" addons: apt: - sources: - - r-packages-trusty packages: - google-chrome-stable - - r-base-dev # Test core modules # Several tests were excluded from this configuration due to the following issues: From 6caf587e17fb6858e769fb2121b8cd66090ff759 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Wed, 20 Dec 2017 16:21:22 +0530 Subject: [PATCH 136/492] [ZEPPELIN-3090] Support KnoxSSO Authentication ### What is this PR for? Zeppelin to support KnoxSSO Authentication method. ### What type of PR is it? [Bug Fix | Improvement] ### What is the Jira issue? * [https://issues.apache.org/jira/browse/ZEPPELIN-3090](https://issues.apache.org/jira/browse/ZEPPELIN-3090) ### How should this be tested? This will require new shiro conf ``` knoxJwtRealm = org.apache.zeppelin.realm.jwt.KnoxJwtRealm knoxJwtRealm.providerUrl = https://domain.example.com/ knoxJwtRealm.login = gateway/knoxsso/knoxauth/login.html knoxJwtRealm.logout = gateway/knoxssout/api/v1/webssout knoxJwtRealm.redirectParam = originalUrl knoxJwtRealm.cookieName = hadoop-jwt knoxJwtRealm.publicKeyPath = /etc/zeppelin/conf/knox-sso.pem knoxJwtRealm.groupPrincipalMapping = group.principal.mapping knoxJwtRealm.principalMapping = principal.mapping ``` Refer screenshot section for demo ### Screenshots (if appropriate) ![after](https://user-images.githubusercontent.com/674497/33554955-ae8d1874-d924-11e7-88fa-0f06a1b28519.gif) ### Questions: * Does the licenses files need update? yes * Is there breaking changes for older versions? no * Does this needs documentation? yes Author: Prabhjyot Singh Author: prabhjyotsingh Closes #2694 from prabhjyotsingh/KNOX_SSO and squashes the following commits: 05ed844ac [Prabhjyot Singh] Add unit test case 187b5678f [Prabhjyot Singh] fix failing " PersonalizeActionsIT.testGraphAction:263 The output of graph mode is not changed" test. 51f13521c [Prabhjyot Singh] Merge remote-tracking branch 'origin/master' into KNOX_SSO 153176450 [Prabhjyot Singh] Add more validation to KnoxAuthenticationFilter. 123349fc5 [Prabhjyot Singh] remove System.out.println, and some of the redundent lines, added more comment c79979acf [Prabhjyot Singh] Check for expired/deleted SSO cookie c9a137f76 [Prabhjyot Singh] Merge remote-tracking branch 'origin/master' into KNOX_SSO dbca0107a [Prabhjyot Singh] Add documentation for KNOX SSO 99541765d [Prabhjyot Singh] use default config 547c7b391 [Prabhjyot Singh] updating LICENSE 067a3e620 [prabhjyotsingh] fix "javax.servlet.ServletException: java.lang.NullPointerException" 564005ff5 [Prabhjyot Singh] remove "hadoop-common.version" dependency 355b475c5 [Prabhjyot Singh] knox sso --- LICENSE | 1 + conf/shiro.ini.template | 13 + docs/setup/security/shiro_authentication.md | 24 ++ zeppelin-server/pom.xml | 7 +- .../realm/jwt/JWTAuthenticationToken.java | 59 ++++ .../realm/jwt/KnoxAuthenticationFilter.java | 71 +++++ .../zeppelin/realm/jwt/KnoxJwtRealm.java | 289 ++++++++++++++++++ .../zeppelin/realm/jwt/PrincipalMapper.java | 51 ++++ .../realm/jwt/PrincipalMappingException.java | 34 +++ .../realm/jwt/SimplePrincipalMapper.java | 126 ++++++++ .../apache/zeppelin/rest/LoginRestApi.java | 195 +++++++++--- .../zeppelin/rest/AbstractTestRestApi.java | 80 ++++- .../apache/zeppelin/rest/KnoxRestApiTest.java | 82 +++++ zeppelin-web/src/app/app.js | 16 +- .../components/navbar/navbar.controller.js | 11 +- 15 files changed, 991 insertions(+), 68 deletions(-) create mode 100644 zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/JWTAuthenticationToken.java create mode 100644 zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxAuthenticationFilter.java create mode 100644 zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java create mode 100644 zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMapper.java create mode 100644 zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMappingException.java create mode 100644 zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/SimplePrincipalMapper.java create mode 100644 zeppelin-server/src/test/java/org/apache/zeppelin/rest/KnoxRestApiTest.java diff --git a/LICENSE b/LICENSE index c1f6f7eb109..f42b12c78e2 100644 --- a/LICENSE +++ b/LICENSE @@ -257,6 +257,7 @@ The text of each license is also included at licenses/LICENSE-[project]-[version (Apache 2.0) Software under ./bigquery/* was developed at Google (http://www.google.com/). Licensed under the Apache v2.0 License. (Apache 2.0) Roboto Font (https://github.com/google/roboto/) (Apache 2.0) Gson extra (https://github.com/DanySK/gson-extras) + (Apache 2.0) Nimbus JOSE+JWT (https://bitbucket.org/connect2id/nimbus-jose-jwt/wiki/Home) ======================================================================== BSD 3-Clause licenses diff --git a/conf/shiro.ini.template b/conf/shiro.ini.template index b30635966bf..81b31a2b9d8 100644 --- a/conf/shiro.ini.template +++ b/conf/shiro.ini.template @@ -56,6 +56,19 @@ user3 = password4, role2 #zeppelinHubRealm.zeppelinhubUrl = https://www.zeppelinhub.com #securityManager.realms = $zeppelinHubRealm +## A same for configuring Knox SSO Realm +#knoxJwtRealm = org.apache.zeppelin.realm.jwt.KnoxJwtRealm +#knoxJwtRealm.providerUrl = https://domain.example.com/ +#knoxJwtRealm.login = gateway/knoxsso/knoxauth/login.html +#knoxJwtRealm.logout = gateway/knoxssout/api/v1/webssout +#knoxJwtRealm.redirectParam = originalUrl +#knoxJwtRealm.cookieName = hadoop-jwt +#knoxJwtRealm.publicKeyPath = /etc/zeppelin/conf/knox-sso.pem +# +#knoxJwtRealm.groupPrincipalMapping = group.principal.mapping +#knoxJwtRealm.principalMapping = principal.mapping +#authc = org.apache.zeppelin.realm.jwt.KnoxAuthenticationFilter + sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager ### If caching of user is required then uncomment below lines diff --git a/docs/setup/security/shiro_authentication.md b/docs/setup/security/shiro_authentication.md index 33b67d0ca8a..a51f77e7098 100644 --- a/docs/setup/security/shiro_authentication.md +++ b/docs/setup/security/shiro_authentication.md @@ -210,6 +210,30 @@ securityManager.realms = $zeppelinHubRealm > Note: ZeppelinHub is not releated to Apache Zeppelin project. +### Knox SSO +[KnoxSSO](https://knox.apache.org/books/knox-0-13-0/dev-guide.html#KnoxSSO+Integration) provides an abstraction for integrating any number of authentication systems and SSO solutions and enables participating web applications to scale to those solutions more easily. Without the token exchange capabilities offered by KnoxSSO each component UI would need to integrate with each desired solution on its own. + +To enable this, apply the following change in `conf/shiro.ini` under `[main]` section. + +``` +### A sample for configuring Knox JWT Realm +knoxJwtRealm = org.apache.zeppelin.realm.jwt.KnoxJwtRealm +## Domain of Knox SSO +knoxJwtRealm.providerUrl = https://domain.example.com/ +## Url for login +knoxJwtRealm.login = gateway/knoxsso/knoxauth/login.html +## Url for logout +knoxJwtRealm.logout = gateway/knoxssout/api/v1/webssout +knoxJwtRealm.redirectParam = originalUrl +knoxJwtRealm.cookieName = hadoop-jwt +knoxJwtRealm.publicKeyPath = /etc/zeppelin/conf/knox-sso.pem +knoxJwtRealm.groupPrincipalMapping = group.principal.mapping +knoxJwtRealm.principalMapping = principal.mapping +# This is required if KNOX SSO is enabled, to check if "knoxJwtRealm.cookieName" cookie was expired/deleted. +authc = org.apache.zeppelin.realm.jwt.KnoxAuthenticationFilter +``` + + ## Secure Cookie for Zeppelin Sessions (optional) Zeppelin can be configured to set `HttpOnly` flag in the session cookie. With this configuration, Zeppelin cookies can not be accessed via client side scripts thus preventing majority of Cross-site scripting (XSS) attacks. diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 925c637fcfc..296d58f01fd 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -37,7 +37,6 @@ 4.3.6 2.22.2 - 2.6.0 2.2.1 1.13 2.0.1 @@ -214,6 +213,12 @@ gson + + com.nimbusds + nimbus-jose-jwt + 4.41.2 + + org.quartz-scheduler quartz diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/JWTAuthenticationToken.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/JWTAuthenticationToken.java new file mode 100644 index 00000000000..2214125c7c5 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/JWTAuthenticationToken.java @@ -0,0 +1,59 @@ +/* + * 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.realm.jwt; + +import org.apache.shiro.authc.AuthenticationToken; + +/** + * Created for org.apache.zeppelin.server + */ +public class JWTAuthenticationToken implements AuthenticationToken { + + private Object userId; + private String token; + + public JWTAuthenticationToken(Object userId, String token) { + this.userId = userId; + this.token = token; + } + + @Override + public Object getPrincipal() { + return getUserId(); + } + + @Override + public Object getCredentials() { + return getToken(); + } + + public Object getUserId() { + return userId; + } + + public void setUserId(long userId) { + this.userId = userId; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxAuthenticationFilter.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxAuthenticationFilter.java new file mode 100644 index 00000000000..de19664f888 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxAuthenticationFilter.java @@ -0,0 +1,71 @@ +/* + * 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.realm.jwt; + +import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; +import org.apache.shiro.web.servlet.ShiroHttpServletRequest; +import org.apache.zeppelin.utils.SecurityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; + +/** + * Created for org.apache.zeppelin.server + */ +public class KnoxAuthenticationFilter extends FormAuthenticationFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(KnoxAuthenticationFilter.class); + + protected boolean isAccessAllowed(ServletRequest request, + ServletResponse response, Object mappedValue) { + + //Check with existing shiro authentication logic + //https://github.com/apache/shiro/blob/shiro-root-1.3.2/web/src/main/java/org/apache/shiro/ + // web/filter/authc/AuthenticatingFilter.java#L123-L124 + Boolean accessAllowed = super.isAccessAllowed(request, response, mappedValue) || + !isLoginRequest(request, response) && isPermissive(mappedValue); + + if (accessAllowed) { + accessAllowed = false; + KnoxJwtRealm knoxJwtRealm = null; + for (Object realm : SecurityUtils.getRealmsList()) { + if (realm instanceof KnoxJwtRealm) { + knoxJwtRealm = (KnoxJwtRealm) realm; + break; + } + } + if (knoxJwtRealm != null) { + for (Cookie cookie : ((ShiroHttpServletRequest) request).getCookies()) { + if (cookie.getName().equals(knoxJwtRealm.getCookieName())) { + if (knoxJwtRealm.validateToken(cookie.getValue())) { + accessAllowed = true; + } + break; + } + } + } else { + LOGGER.error("Looks like this filter is enabled without enabling KnoxJwtRealm, please refer" + + " to https://zeppelin.apache.org/docs/latest/security/shiroauthentication.html" + + "#knox-sso"); + } + } + return accessAllowed; + } +} diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java new file mode 100644 index 00000000000..c3e9b77cc88 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java @@ -0,0 +1,289 @@ +/* + * 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.realm.jwt; + +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jwt.SignedJWT; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.text.ParseException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.servlet.ServletException; +import org.apache.commons.io.FileUtils; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.Groups; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAccount; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Created for org.apache.zeppelin.server + */ +public class KnoxJwtRealm extends AuthorizingRealm { + + private static final Logger LOGGER = LoggerFactory.getLogger(KnoxJwtRealm.class); + + private String providerUrl; + private String redirectParam; + private String cookieName; + private String publicKeyPath; + private String login; + private String logout; + + private String principalMapping; + private String groupPrincipalMapping; + + private SimplePrincipalMapper mapper = new SimplePrincipalMapper(); + /** + * Configuration object needed by for hadoop classes + */ + private Configuration hadoopConfig; + + /** + * Hadoop Groups implementation. + */ + private Groups hadoopGroups; + + @Override + protected void onInit() { + super.onInit(); + if (principalMapping != null && !principalMapping.isEmpty() + || groupPrincipalMapping != null && !groupPrincipalMapping.isEmpty()) { + try { + mapper.loadMappingTable(principalMapping, groupPrincipalMapping); + } catch (PrincipalMappingException e) { + LOGGER.error("PrincipalMappingException in onInit", e); + } + } + + try { + hadoopConfig = new Configuration(); + hadoopGroups = new Groups(hadoopConfig); + } catch (final Exception e) { + LOGGER.error("Exception in onInit", e); + } + + } + + @Override + public boolean supports(AuthenticationToken token) { + return token != null && token instanceof JWTAuthenticationToken; + } + + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { + JWTAuthenticationToken upToken = (JWTAuthenticationToken) token; + + if (validateToken(upToken.getToken())) { + try { + SimpleAccount account = new SimpleAccount(getName(upToken), upToken.getToken(), getName()); + account.addRole(mapGroupPrincipals(getName(upToken))); + return account; + } catch (ParseException e) { + LOGGER.error("ParseException in doGetAuthenticationInfo", e); + } + } + return null; + } + + private String getName(JWTAuthenticationToken upToken) throws ParseException { + SignedJWT signed = SignedJWT.parse(upToken.getToken()); + String userName = signed.getJWTClaimsSet().getSubject(); + return userName; + } + + protected boolean validateToken(String token) { + try { + SignedJWT signed = SignedJWT.parse(token); + return validateSignature(signed); + } catch (ParseException ex) { + LOGGER.info("ParseException in validateToken", ex); + return false; + } + } + + public static RSAPublicKey parseRSAPublicKey(String pem) + throws IOException, ServletException { + String PEM_HEADER = "-----BEGIN CERTIFICATE-----\n"; + String PEM_FOOTER = "\n-----END CERTIFICATE-----"; + String fullPem = PEM_HEADER + pem + PEM_FOOTER; + PublicKey key = null; + try { + CertificateFactory fact = CertificateFactory.getInstance("X.509"); + ByteArrayInputStream is = new ByteArrayInputStream( + FileUtils.readFileToString(new File(pem)).getBytes("UTF8")); + X509Certificate cer = (X509Certificate) fact.generateCertificate(is); + key = cer.getPublicKey(); + } catch (CertificateException ce) { + String message = null; + if (pem.startsWith(PEM_HEADER)) { + message = "CertificateException - be sure not to include PEM header " + + "and footer in the PEM configuration element."; + } else { + message = "CertificateException - PEM may be corrupt"; + } + throw new ServletException(message, ce); + } catch (UnsupportedEncodingException uee) { + throw new ServletException(uee); + } catch (IOException e) { + throw new IOException(e); + } + return (RSAPublicKey) key; + } + + protected boolean validateSignature(SignedJWT jwtToken) { + boolean valid = false; + if (JWSObject.State.SIGNED == jwtToken.getState()) { + + if (jwtToken.getSignature() != null) { + + try { + RSAPublicKey publicKey = parseRSAPublicKey(publicKeyPath); + JWSVerifier verifier = new RSASSAVerifier(publicKey); + if (verifier != null && jwtToken.verify(verifier)) { + valid = true; + } + } catch (Exception e) { + LOGGER.info("Exception in validateSignature", e); + } + } + } + return valid; + } + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { + Set roles = mapGroupPrincipals(principals.toString()); + return new SimpleAuthorizationInfo(roles); + } + + /** + * Query the Hadoop implementation of {@link Groups} to retrieve groups for + * provided user. + */ + public Set mapGroupPrincipals(final String mappedPrincipalName) { + /* return the groups as seen by Hadoop */ + Set groups = null; + try { + hadoopGroups.refresh(); + final List groupList = hadoopGroups + .getGroups(mappedPrincipalName); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("group found %s, %s", + mappedPrincipalName, groupList.toString())); + } + + groups = new HashSet<>(groupList); + + } catch (final IOException e) { + if (e.toString().contains("No groups found for user")) { + /* no groups found move on */ + LOGGER.info(String.format("No groups found for user %s", mappedPrincipalName)); + + } else { + /* Log the error and return empty group */ + LOGGER.info(String.format("errorGettingUserGroups for %s", mappedPrincipalName)); + } + groups = new HashSet(); + } + return groups; + } + + public String getProviderUrl() { + return providerUrl; + } + + public void setProviderUrl(String providerUrl) { + this.providerUrl = providerUrl; + } + + public String getRedirectParam() { + return redirectParam; + } + + public void setRedirectParam(String redirectParam) { + this.redirectParam = redirectParam; + } + + public String getCookieName() { + return cookieName; + } + + public void setCookieName(String cookieName) { + this.cookieName = cookieName; + } + + public String getPublicKeyPath() { + return publicKeyPath; + } + + public void setPublicKeyPath(String publicKeyPath) { + this.publicKeyPath = publicKeyPath; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getLogout() { + return logout; + } + + public void setLogout(String logout) { + this.logout = logout; + } + + public String getPrincipalMapping() { + return principalMapping; + } + + public void setPrincipalMapping(String principalMapping) { + this.principalMapping = principalMapping; + } + + public String getGroupPrincipalMapping() { + return groupPrincipalMapping; + } + + public void setGroupPrincipalMapping(String groupPrincipalMapping) { + this.groupPrincipalMapping = groupPrincipalMapping; + } +} + diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMapper.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMapper.java new file mode 100644 index 00000000000..d96efa46602 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMapper.java @@ -0,0 +1,51 @@ +/* + * 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.realm.jwt; + +/*** + * + */ +public interface PrincipalMapper { + + /** + * Load the internal principal mapping table from the provided + * string value which conforms to the following semicolon delimited format: + * actual[,another-actual]=mapped;... + * @param principalMapping + */ + public abstract void loadMappingTable(String principalMapping, String groupMapping) + throws PrincipalMappingException; + + /** + * Acquire a mapped principal name from the mapping table + * as appropriate. Otherwise, the provided principalName + * will be used. + * @param principalName + * @return principal name to be used in the assertion + */ + public abstract String mapUserPrincipal(String principalName); + + /** + * Acquire array of group principal names from the mapping table + * as appropriate. Otherwise, return null. + * @param principalName + * @return group principal names to be used in the assertion + */ + public abstract String[] mapGroupPrincipal(String principalName); +} + diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMappingException.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMappingException.java new file mode 100644 index 00000000000..c3ca02f7431 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/PrincipalMappingException.java @@ -0,0 +1,34 @@ +/** + * 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.realm.jwt; + +/*** + * {@link System} + */ +public class PrincipalMappingException extends Exception { + + public PrincipalMappingException(String message) { + super(message); + } + + public PrincipalMappingException(String message, Exception e) { + super(message, e); + } + + +} diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/SimplePrincipalMapper.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/SimplePrincipalMapper.java new file mode 100644 index 00000000000..b1948102e60 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/SimplePrincipalMapper.java @@ -0,0 +1,126 @@ +/** + * 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.realm.jwt; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.StringTokenizer; + + +/*** + * + */ +public class SimplePrincipalMapper implements PrincipalMapper { + + public HashMap principalMappings = null; + public HashMap groupMappings = null; + + public SimplePrincipalMapper() { + } + + /* (non-Javadoc) + * @see org.apache.hadoop.gateway.filter.PrincipalMapper#loadMappingTable(java.lang.String) + */ + @Override + public void loadMappingTable(String principalMapping, String groupMapping) + throws PrincipalMappingException { + if (principalMapping != null) { + principalMappings = parseMapping(principalMapping); + groupMappings = parseMapping(groupMapping); + } + } + + private HashMap parseMapping(String mappings) + throws PrincipalMappingException { + if (mappings == null) { + return null; + } + HashMap table = new HashMap<>(); + try { + StringTokenizer t = new StringTokenizer(mappings, ";"); + if (t.hasMoreTokens()) { + do { + String mapping = t.nextToken(); + String principals = mapping.substring(0, mapping.indexOf('=')); + String value = mapping.substring(mapping.indexOf('=') + 1); + String[] v = value.split(","); + String[] p = principals.split(","); + for (int i = 0; i < p.length; i++) { + table.put(p[i], v); + } + } while (t.hasMoreTokens()); + } + return table; + } catch (Exception e) { + // do not leave table in an unknown state - clear it instead + // no principal mapping will occur + table.clear(); + throw new PrincipalMappingException( + "Unable to load mappings from provided string: " + mappings + + " - no principal mapping will be provided.", e); + } + } + + /* (non-Javadoc) + * @see org.apache.hadoop.gateway.filter.PrincipalMapper#mapPrincipal(java.lang.String) + */ + @Override + public String mapUserPrincipal(String principalName) { + String[] p = null; + if (principalMappings != null) { + p = principalMappings.get(principalName); + } + if (p == null) { + return principalName; + } + + return p[0]; + } + + /* (non-Javadoc) + * @see org.apache.hadoop.gateway.filter.PrincipalMapper#mapPrincipal(java.lang.String) + */ + @Override + public String[] mapGroupPrincipal(String principalName) { + String[] groups = null; + String[] wildCardGroups = null; + + if (groupMappings != null) { + groups = groupMappings.get(principalName); + wildCardGroups = groupMappings.get("*"); + if (groups != null && wildCardGroups != null) { + groups = concat(groups, wildCardGroups); + } else if (wildCardGroups != null) { + return wildCardGroups; + } + } + + return groups; + } + + /** + * @param groups + * @param wildCardGroups + * @return + */ + public static T[] concat(T[] groups, T[] wildCardGroups) { + T[] result = Arrays.copyOf(groups, groups.length + wildCardGroups.length); + System.arraycopy(wildCardGroups, 0, result, groups.length, wildCardGroups.length); + return result; + } +} diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java index bd96684d9e1..3a084cf9e4b 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java @@ -16,25 +16,39 @@ */ package org.apache.zeppelin.rest; -import org.apache.shiro.authc.*; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.IncorrectCredentialsException; +import org.apache.shiro.authc.LockedAccountException; +import org.apache.shiro.authc.UnknownAccountException; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.realm.Realm; import org.apache.shiro.subject.Subject; import org.apache.zeppelin.annotation.ZeppelinApi; import org.apache.zeppelin.notebook.NotebookAuthorization; +import org.apache.zeppelin.realm.jwt.JWTAuthenticationToken; +import org.apache.zeppelin.realm.jwt.KnoxJwtRealm; import org.apache.zeppelin.server.JsonResponse; import org.apache.zeppelin.ticket.TicketContainer; import org.apache.zeppelin.utils.SecurityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.ws.rs.FormParam; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Response; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; - /** * Created for org.apache.zeppelin.rest.message on 17/03/16. */ @@ -42,6 +56,7 @@ @Path("/login") @Produces("application/json") public class LoginRestApi { + private static final Logger LOG = LoggerFactory.getLogger(LoginRestApi.class); /** @@ -52,6 +67,104 @@ public LoginRestApi() { } + @GET + @ZeppelinApi + public Response getLogin(@Context HttpHeaders headers) { + JsonResponse response = null; + if (isKnoxSSOEnabled()) { + KnoxJwtRealm knoxJwtRealm = getJTWRealm(); + Cookie cookie = headers.getCookies().get(knoxJwtRealm.getCookieName()); + if (cookie != null && cookie.getValue() != null) { + Subject currentUser = org.apache.shiro.SecurityUtils.getSubject(); + if (!currentUser.isAuthenticated()) { + JWTAuthenticationToken token = new JWTAuthenticationToken(null, cookie.getValue()); + response = procedeToLogin(currentUser, token); + } + } + if (response == null) { + Map data = new HashMap<>(); + data.put("redirectURL", constructKnoxUrl(knoxJwtRealm, knoxJwtRealm.getLogin())); + response = new JsonResponse(Status.OK, "", data); + } + return response.build(); + } + return new JsonResponse(Status.METHOD_NOT_ALLOWED).build(); + } + + private KnoxJwtRealm getJTWRealm() { + Collection realmsList = SecurityUtils.getRealmsList(); + if (realmsList != null) { + for (Iterator iterator = realmsList.iterator(); iterator.hasNext(); ) { + Realm realm = iterator.next(); + String name = realm.getClass().getName(); + + LOG.debug("RealmClass.getName: " + name); + + if (name.equals("org.apache.zeppelin.realm.jwt.KnoxJwtRealm")) { + return (KnoxJwtRealm) realm; + } + } + } + return null; + } + + private boolean isKnoxSSOEnabled() { + Collection realmsList = SecurityUtils.getRealmsList(); + if (realmsList != null) { + for (Iterator iterator = realmsList.iterator(); iterator.hasNext(); ) { + Realm realm = iterator.next(); + String name = realm.getClass().getName(); + LOG.debug("RealmClass.getName: " + name); + if (name.equals("org.apache.zeppelin.realm.jwt.KnoxJwtRealm")) { + return true; + } + } + } + return false; + } + + private JsonResponse procedeToLogin(Subject currentUser, AuthenticationToken token) { + JsonResponse response = null; + try { + currentUser.getSession().stop(); + currentUser.getSession(true); + currentUser.login(token); + + HashSet roles = SecurityUtils.getRoles(); + String principal = SecurityUtils.getPrincipal(); + String ticket; + if ("anonymous".equals(principal)) { + ticket = "anonymous"; + } else { + ticket = TicketContainer.instance.getTicket(principal); + } + + Map data = new HashMap<>(); + data.put("principal", principal); + data.put("roles", roles.toString()); + data.put("ticket", ticket); + + response = new JsonResponse(Response.Status.OK, "", data); + //if no exception, that's it, we're done! + + //set roles for user in NotebookAuthorization module + NotebookAuthorization.getInstance().setRoles(principal, roles); + } catch (UnknownAccountException uae) { + //username wasn't in the system, show them an error message? + LOG.error("Exception in login: ", uae); + } catch (IncorrectCredentialsException ice) { + //password didn't match, try again? + LOG.error("Exception in login: ", ice); + } catch (LockedAccountException lae) { + //account for that username is locked - can't login. Show them a message? + LOG.error("Exception in login: ", lae); + } catch (AuthenticationException ae) { + //unexpected condition - error? + LOG.error("Exception in login: ", ae); + } + return response; + } + /** * Post Login * Returns userName & password @@ -63,7 +176,7 @@ public LoginRestApi() { @POST @ZeppelinApi public Response postLogin(@FormParam("userName") String userName, - @FormParam("password") String password) { + @FormParam("password") String password) { JsonResponse response = null; // ticket set to anonymous for anonymous user. Simplify testing. Subject currentUser = org.apache.shiro.SecurityUtils.getSubject(); @@ -71,45 +184,10 @@ public Response postLogin(@FormParam("userName") String userName, currentUser.logout(); } if (!currentUser.isAuthenticated()) { - try { - UsernamePasswordToken token = new UsernamePasswordToken(userName, password); - // token.setRememberMe(true); - - currentUser.getSession().stop(); - currentUser.getSession(true); - currentUser.login(token); - - HashSet roles = SecurityUtils.getRoles(); - String principal = SecurityUtils.getPrincipal(); - String ticket; - if ("anonymous".equals(principal)) - ticket = "anonymous"; - else - ticket = TicketContainer.instance.getTicket(principal); - Map data = new HashMap<>(); - data.put("principal", principal); - data.put("roles", roles.toString()); - data.put("ticket", ticket); - - response = new JsonResponse(Response.Status.OK, "", data); - //if no exception, that's it, we're done! - - //set roles for user in NotebookAuthorization module - NotebookAuthorization.getInstance().setRoles(principal, roles); - } catch (UnknownAccountException uae) { - //username wasn't in the system, show them an error message? - LOG.error("Exception in login: ", uae); - } catch (IncorrectCredentialsException ice) { - //password didn't match, try again? - LOG.error("Exception in login: ", ice); - } catch (LockedAccountException lae) { - //account for that username is locked - can't login. Show them a message? - LOG.error("Exception in login: ", lae); - } catch (AuthenticationException ae) { - //unexpected condition - error? - LOG.error("Exception in login: ", ae); - } + UsernamePasswordToken token = new UsernamePasswordToken(userName, password); + + response = procedeToLogin(currentUser, token); } if (response == null) { @@ -129,9 +207,26 @@ public Response logout() { TicketContainer.instance.removeTicket(SecurityUtils.getPrincipal()); currentUser.getSession().stop(); currentUser.logout(); - response = new JsonResponse(Response.Status.UNAUTHORIZED, "", ""); + if (isKnoxSSOEnabled()) { + KnoxJwtRealm knoxJwtRealm = getJTWRealm(); + Map data = new HashMap<>(); + data.put("redirectURL", constructKnoxUrl(knoxJwtRealm, knoxJwtRealm.getLogout())); + response = new JsonResponse(Status.UNAUTHORIZED, "", data); + } else { + response = new JsonResponse(Status.UNAUTHORIZED, "", ""); + + } LOG.warn(response.toString()); return response.build(); } + private String constructKnoxUrl(KnoxJwtRealm knoxJwtRealm, String path) { + StringBuilder redirectURL = new StringBuilder(knoxJwtRealm.getProviderUrl()); + redirectURL.append(path); + if (knoxJwtRealm.getRedirectParam() != null) { + redirectURL.append("?").append(knoxJwtRealm.getRedirectParam()).append("="); + } + return redirectURL.toString(); + } + } 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 7c083650814..ad0e1fd11a2 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 @@ -17,6 +17,9 @@ package org.apache.zeppelin.rest; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; @@ -27,7 +30,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.regex.Pattern; - import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecutor; import org.apache.commons.exec.PumpStreamHandler; @@ -54,10 +56,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import com.google.gson.JsonParser; - public abstract class AbstractTestRestApi { protected static final Logger LOG = LoggerFactory.getLogger(AbstractTestRestApi.class); @@ -90,6 +88,48 @@ public abstract class AbstractTestRestApi { "/api/version = anon\n" + "/** = authc"; + private static String zeppelinShiroKnox = + "[users]\n" + + "admin = password1, admin\n" + + "user1 = password2, role1, role2\n" + + "[main]\n" + + "knoxJwtRealm = org.apache.zeppelin.realm.jwt.KnoxJwtRealm\n" + + "knoxJwtRealm.providerUrl = https://domain.example.com/\n" + + "knoxJwtRealm.login = gateway/knoxsso/knoxauth/login.html\n" + + "knoxJwtRealm.logout = gateway/knoxssout/api/v1/webssout\n" + + "knoxJwtRealm.redirectParam = originalUrl\n" + + "knoxJwtRealm.cookieName = hadoop-jwt\n" + + "knoxJwtRealm.publicKeyPath = knox-sso.pem\n" + + "authc = org.apache.zeppelin.realm.jwt.KnoxAuthenticationFilter\n" + + "sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager\n" + + "securityManager.sessionManager = $sessionManager\n" + + "securityManager.sessionManager.globalSessionTimeout = 86400000\n" + + "shiro.loginUrl = /api/login\n" + + "[roles]\n" + + "admin = *\n" + + "[urls]\n" + + "/api/version = anon\n" + + "/** = authc"; + + private static File knoxSsoPem = null; + private static String KNOX_SSO_PEM = + "-----BEGIN CERTIFICATE-----\n" + + "MIIChjCCAe+gAwIBAgIJALYrdDEXKwcqMA0GCSqGSIb3DQEBBQUAMIGEMQswCQYD\n" + + "VQQGEwJVUzENMAsGA1UECBMEVGVzdDENMAsGA1UEBxMEVGVzdDEPMA0GA1UEChMG\n" + + "SGFkb29wMQ0wCwYDVQQLEwRUZXN0MTcwNQYDVQQDEy5jdHItZTEzNS0xNTEyMDY5\n" + + "MDMyOTc1LTU0NDctMDEtMDAwMDAyLmh3eC5zaXRlMB4XDTE3MTIwNDA5NTIwMFoX\n" + + "DTE4MTIwNDA5NTIwMFowgYQxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIEwRUZXN0MQ0w\n" + + "CwYDVQQHEwRUZXN0MQ8wDQYDVQQKEwZIYWRvb3AxDTALBgNVBAsTBFRlc3QxNzA1\n" + + "BgNVBAMTLmN0ci1lMTM1LTE1MTIwNjkwMzI5NzUtNTQ0Ny0wMS0wMDAwMDIuaHd4\n" + + "LnNpdGUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAILFoXdz3yCy2INncYM2\n" + + "y72fYrONoQIxeeIzeJIibXLTuowSju90Q6aThSyUsQ6NEia2flnlKiCgINTNAodh\n" + + "UPUVGyGT+NMrqJzzpXAll2UUa6gIUPnXYEzYNkMIpbQOAo5BAg7YamaidbPPiT3W\n" + + "wAD1rWo3AMUY+nZJrAi4dEH5AgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAB0R07/lo\n" + + "4hD+WeDEeyLTnsbFnPNXxBT1APMUmmuCjcky/19ZB8OphqTKIITONdOK/XHdjZHG\n" + + "JDOfhBkVknL42lSi45ahUAPS2PZOlQL08MbS8xajP1faterm+aHcdwJVK9dK76RB\n" + + "/bA8TFNPblPxavIOcd+R+RfFmT1YKfYIhco=\n" + + "-----END CERTIFICATE-----"; + protected static File zeppelinHome; protected static File confDir; @@ -127,7 +167,7 @@ public void run() { } }; - private static void start(boolean withAuth, String testClassName) throws Exception { + private static void start(boolean withAuth, String testClassName, boolean withKnox) throws Exception { if (!wasRunning) { // copy the resources files to a temp folder zeppelinHome = new File(".."); @@ -156,7 +196,18 @@ private static void start(boolean withAuth, String testClassName) throws Excepti if (!shiroIni.exists()) { shiroIni.createNewFile(); } - FileUtils.writeStringToFile(shiroIni, zeppelinShiro); + if (withKnox) { + FileUtils.writeStringToFile(shiroIni, + zeppelinShiroKnox.replaceAll("knox-sso.pem", confDir + "/knox-sso.pem")); + knoxSsoPem = new File(confDir, "knox-sso.pem"); + if (!knoxSsoPem.exists()) { + knoxSsoPem.createNewFile(); + } + FileUtils.writeStringToFile(knoxSsoPem, KNOX_SSO_PEM); + } else { + FileUtils.writeStringToFile(shiroIni, zeppelinShiro); + } + } // exclude org.apache.zeppelin.rinterpreter.* for scala 2.11 test @@ -254,13 +305,17 @@ private static void start(boolean withAuth, String testClassName) throws Excepti } } } + + protected static void startUpWithKnoxEnable(String testClassName) throws Exception { + start(true, testClassName, true); + } protected static void startUpWithAuthenticationEnable(String testClassName) throws Exception { - start(true, testClassName); + start(true, testClassName, false); } protected static void startUp(String testClassName) throws Exception { - start(false, testClassName); + start(false, testClassName, false); } private static String getHostname() { @@ -383,6 +438,10 @@ protected static GetMethod httpGet(String path) throws IOException { } protected static GetMethod httpGet(String path, String user, String pwd) throws IOException { + return httpGet(path, user, pwd, StringUtils.EMPTY); + } + + protected static GetMethod httpGet(String path, String user, String pwd, String cookies) throws IOException { LOG.info("Connecting to {}", url + path); HttpClient httpClient = new HttpClient(); GetMethod getMethod = new GetMethod(url + path); @@ -390,6 +449,9 @@ protected static GetMethod httpGet(String path, String user, String pwd) throws if (userAndPasswordAreNotBlank(user, pwd)) { getMethod.setRequestHeader("Cookie", "JSESSIONID="+ getCookie(user, pwd)); } + if (!StringUtils.isBlank(cookies)) { + getMethod.setRequestHeader("Cookie", getMethod.getResponseHeader("Cookie") + ";" + cookies); + } httpClient.executeMethod(getMethod); LOG.info("{} - {}", getMethod.getStatusCode(), getMethod.getStatusText()); return getMethod; diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/KnoxRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/KnoxRestApiTest.java new file mode 100644 index 00000000000..e3034e4dbd7 --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/KnoxRestApiTest.java @@ -0,0 +1,82 @@ +/* + * 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 java.io.IOException; +import java.util.Map; +import org.apache.commons.httpclient.methods.GetMethod; +import org.hamcrest.CoreMatchers; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ErrorCollector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class KnoxRestApiTest extends AbstractTestRestApi { + + private String KNOX_COOKIE = "hadoop-jwt=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlzcyI6IktOT1hTU08iLCJleHAiOjE1MTM3NDU1MDd9.E2cWQo2sq75h0G_9fc9nWkL0SFMI5x_-Z0Zzr0NzQ86X4jfxliWYjr0M17Bm9GfPHRRR66s7YuYXa6DLbB4fHE0cyOoQnkfJFpU_vr1xhy0_0URc5v-Gb829b9rxuQfjKe-37hqbUdkwww2q6QQETVMvzp0rQKprUClZujyDvh0;"; + + @Rule + public ErrorCollector collector = new ErrorCollector(); + + private static final Logger LOG = LoggerFactory.getLogger(KnoxRestApiTest.class); + + Gson gson = new Gson(); + + @BeforeClass + public static void init() throws Exception { + AbstractTestRestApi.startUpWithKnoxEnable(KnoxRestApiTest.class.getSimpleName()); + } + + @AfterClass + public static void destroy() throws Exception { + AbstractTestRestApi.shutDown(); + } + + @Before + public void setUp() { + } + + + @Test + public void testThatOtherUserCanAccessNoteIfPermissionNotSet() throws IOException { + GetMethod loginWithoutCookie = httpGet("/api/security/ticket"); + Map result = gson.fromJson(loginWithoutCookie.getResponseBodyAsString(), Map.class); + collector.checkThat("Path is redirected to /login", loginWithoutCookie.getPath(), + CoreMatchers.containsString("login")); + + collector.checkThat("Path is redirected to /login", loginWithoutCookie.getPath(), + CoreMatchers.containsString("login")); + + collector.checkThat("response contains redirect URL", + ((Map) result.get("body")).get("redirectURL").toString(), CoreMatchers.equalTo( + "https://domain.example.com/gateway/knoxsso/knoxauth/login.html?originalUrl=")); + + GetMethod loginWithCookie = httpGet("/api/security/ticket", "", "", KNOX_COOKIE); + result = gson.fromJson(loginWithCookie.getResponseBodyAsString(), Map.class); + + collector.checkThat("User logged in as admin", + ((Map) result.get("body")).get("principal").toString(), CoreMatchers.equalTo("admin")); + + System.out.println(result); + } + +} diff --git a/zeppelin-web/src/app/app.js b/zeppelin-web/src/app/app.js index 5a4c0161588..ed89dd8abe9 100644 --- a/zeppelin-web/src/app/app.js +++ b/zeppelin-web/src/app/app.js @@ -183,12 +183,16 @@ function auth () { let config = (process.env.PROD) ? {headers: { 'X-Requested-With': 'XMLHttpRequest' }} : {} return $http.get(baseUrlSrv.getRestApiBase() + '/security/ticket', config).then(function (response) { zeppelinWebApp.run(function ($rootScope) { - $rootScope.ticket = angular.fromJson(response.data).body - - $rootScope.ticket.screenUsername = $rootScope.ticket.principal - if ($rootScope.ticket.principal.indexOf('#Pac4j') === 0) { - let re = ', name=(.*?),' - $rootScope.ticket.screenUsername = $rootScope.ticket.principal.match(re)[1] + let res = angular.fromJson(response.data).body + if (res['redirectURL']) { + window.location.href = res['redirectURL'] + window.location.href + } else { + $rootScope.ticket = res + $rootScope.ticket.screenUsername = $rootScope.ticket.principal + if ($rootScope.ticket.principal.indexOf('#Pac4j') === 0) { + let re = ', name=(.*?),' + $rootScope.ticket.screenUsername = $rootScope.ticket.principal.match(re)[1] + } } }) }, function (errorResponse) { diff --git a/zeppelin-web/src/components/navbar/navbar.controller.js b/zeppelin-web/src/components/navbar/navbar.controller.js index e92813b6da1..6f2974edbb5 100644 --- a/zeppelin-web/src/components/navbar/navbar.controller.js +++ b/zeppelin-web/src/components/navbar/navbar.controller.js @@ -86,12 +86,19 @@ function NavCtrl ($scope, $rootScope, $http, $routeParams, $location, websocketMsgSrv.getHomeNote() } - function logout () { + function logout() { let logoutURL = baseUrlSrv.getRestApiBase() + '/login/logout' // for firefox and safari logoutURL = logoutURL.replace('//', '//false:false@') - $http.post(logoutURL).error(function () { + + $http.post(logoutURL).then(function () {}, function (response) { + if (response.data) { + let res = angular.fromJson(response.data).body + if (res['redirectURL']) { + window.location.href = res['redirectURL'] + window.location.href + } + } // force authcBasic (if configured) to logout $http.post(logoutURL).error(function () { $rootScope.userName = '' From 5d09a7f836ebaddd40e644ef82d80320570364aa Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Wed, 20 Dec 2017 18:09:04 +0530 Subject: [PATCH 137/492] ZEPPELIN-3112: Markdown interpreter fails with NPE ### What is this PR for? Since pegdown-parser is not thread-safe while trying to run multiple MarkDown paragraphs at once, sometimes it fails to render HTML. Ref: https://github.com/sirthias/pegdown/blob/master/src/main/java/org/pegdown/PegDownProcessor.java#L32 ### What type of PR is it? [Improvement] ### What is the Jira issue? * [ZEPPELIN-3112](https://issues.apache.org/jira/browse/ZEPPELIN-3112) ### How should this be tested? * This happens rarely, when you try to run all paragraph from UI which has more the 5-6 `%md` paragraph. This is hard to reproduce in 0.8.0, but can easily be done via 0.7.3. Also, have added a sample [notebook](https://issues.apache.org/jira/secure/attachment/12903037/Test%20MD%20fail.json) in the parent JIRA * Have added test case to verify. ### Questions: * Does the licenses files need update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: Prabhjyot Singh Closes #2711 from prabhjyotsingh/ZEPPELIN-3112 and squashes the following commits: e796e52cd [Prabhjyot Singh] ZEPPELIN-3112: call markdownToHtml in synchronized block --- .../zeppelin/markdown/PegdownParser.java | 6 ++- .../zeppelin/markdown/PegdownParserTest.java | 39 +++++++++++++++++-- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/markdown/src/main/java/org/apache/zeppelin/markdown/PegdownParser.java b/markdown/src/main/java/org/apache/zeppelin/markdown/PegdownParser.java index baf18f0d79d..fb99f0510e4 100644 --- a/markdown/src/main/java/org/apache/zeppelin/markdown/PegdownParser.java +++ b/markdown/src/main/java/org/apache/zeppelin/markdown/PegdownParser.java @@ -41,8 +41,10 @@ public PegdownParser() { @Override public String render(String markdownText) { String html = ""; - String parsed = processor.markdownToHtml(markdownText); - + String parsed; + synchronized (processor) { + parsed = processor.markdownToHtml(markdownText); + } if (null == parsed) { throw new RuntimeException("Cannot parse markdown text to HTML using pegdown"); } diff --git a/markdown/src/test/java/org/apache/zeppelin/markdown/PegdownParserTest.java b/markdown/src/test/java/org/apache/zeppelin/markdown/PegdownParserTest.java index 0c545dc3732..2e1d85750e8 100644 --- a/markdown/src/test/java/org/apache/zeppelin/markdown/PegdownParserTest.java +++ b/markdown/src/test/java/org/apache/zeppelin/markdown/PegdownParserTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; +import java.util.ArrayList; import java.util.Properties; import org.apache.zeppelin.interpreter.InterpreterResult; @@ -26,10 +27,8 @@ import static org.junit.Assert.assertThat; import org.hamcrest.CoreMatchers; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; +import org.junit.*; +import org.junit.rules.ErrorCollector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +36,9 @@ public class PegdownParserTest { Logger logger = LoggerFactory.getLogger(PegdownParserTest.class); Markdown md; + @Rule + public ErrorCollector collector = new ErrorCollector(); + @Before public void setUp() throws Exception { Properties props = new Properties(); @@ -50,6 +52,35 @@ public void tearDown() throws Exception { md.close(); } + @Test + public void testMultipleThread() { + ArrayList arrThreads = new ArrayList(); + for (int i = 0; i < 10; i++) { + Thread t = new Thread() { + public void run() { + String r1 = null; + try { + r1 = md.interpret("# H1", null).code().name(); + } catch (Exception e) { + logger.error("testTestMultipleThread failed to interpret", e); + } + collector.checkThat("SUCCESS", + CoreMatchers.containsString(r1)); + } + }; + t.start(); + arrThreads.add(t); + } + + for (int i = 0; i < 10; i++) { + try { + arrThreads.get(i).join(); + } catch (InterruptedException e) { + logger.error("testTestMultipleThread failed to join threads", e); + } + } + } + @Test public void testHeader() { InterpreterResult r1 = md.interpret("# H1", null); From 53e6f743d434ff5ca787db26c8e3740cb16f9e5d Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Fri, 22 Dec 2017 06:22:48 +0800 Subject: [PATCH 138/492] ZEPPELIN-3106. User impersonation in SPARK is not working ### What is this PR for? This PR is for trying to fix the impersonation of spark interpreter in master branch. For spark impersonation, we don't need to ssh, just adding `--proxy-user` is enough. ### What type of PR is it? [Bug Fix] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3106 ### How should this be tested? * Manually verified. ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2705 from zjffdu/ZEPPELIN-3106 and squashes the following commits: 2a6cb14 [Jeff Zhang] address comment d8a7d50 [Jeff Zhang] ZEPPELIN-3106. User impersonation in SPARK is not working --- bin/interpreter.sh | 24 ++++++++++--------- .../launcher/InterpreterClient.java | 2 +- .../launcher/InterpreterLaunchContext.java | 7 ++++++ .../interpreter/InterpreterSetting.java | 3 ++- .../interpreter/ManagedInterpreterGroup.java | 5 ++-- .../launcher/ShellScriptLauncher.java | 8 +++---- .../launcher/SparkInterpreterLauncher.java | 13 ++++++---- .../RemoteInterpreterManagedProcess.java | 14 ++++++++--- .../RemoteInterpreterRunningProcess.java | 2 +- .../launcher/ShellScriptLauncherTest.java | 4 +++- .../SparkInterpreterLauncherTest.java | 13 +++++----- 11 files changed, 61 insertions(+), 34 deletions(-) diff --git a/bin/interpreter.sh b/bin/interpreter.sh index f23ca823e62..687d8a96d29 100755 --- a/bin/interpreter.sh +++ b/bin/interpreter.sh @@ -16,6 +16,7 @@ # limitations under the License. # + bin=$(dirname "${BASH_SOURCE-$0}") bin=$(cd "${bin}">/dev/null; pwd) @@ -50,11 +51,6 @@ while getopts "hc:p:r:d:l:v:u:g:" o; do ;; u) ZEPPELIN_IMPERSONATE_USER="${OPTARG}" - if [[ -z "$ZEPPELIN_IMPERSONATE_CMD" ]]; then - ZEPPELIN_IMPERSONATE_RUN_CMD=`echo "ssh ${ZEPPELIN_IMPERSONATE_USER}@localhost" ` - else - ZEPPELIN_IMPERSONATE_RUN_CMD=$(eval "echo ${ZEPPELIN_IMPERSONATE_CMD} ") - fi ;; g) INTERPRETER_SETTING_NAME=${OPTARG} @@ -96,6 +92,15 @@ INTERPRETER_ID=$(basename "${INTERPRETER_DIR}") ZEPPELIN_PID="${ZEPPELIN_PID_DIR}/zeppelin-interpreter-${INTERPRETER_ID}-${ZEPPELIN_IDENT_STRING}-${HOSTNAME}.pid" ZEPPELIN_LOGFILE="${ZEPPELIN_LOG_DIR}/zeppelin-interpreter-${INTERPRETER_SETTING_NAME}-" +if [[ -z "$ZEPPELIN_IMPERSONATE_CMD" ]]; then + if [[ "${INTERPRETER_ID}" != "spark" || "$ZEPPELIN_IMPERSONATE_SPARK_PROXY_USER" == "false" ]]; then + ZEPPELIN_IMPERSONATE_RUN_CMD=`echo "ssh ${ZEPPELIN_IMPERSONATE_USER}@localhost" ` + fi +else + ZEPPELIN_IMPERSONATE_RUN_CMD=$(eval "echo ${ZEPPELIN_IMPERSONATE_CMD} ") +fi + + if [[ ! -z "$ZEPPELIN_IMPERSONATE_USER" ]]; then ZEPPELIN_LOGFILE+="${ZEPPELIN_IMPERSONATE_USER}-" fi @@ -195,7 +200,7 @@ fi addJarInDirForIntp "${LOCAL_INTERPRETER_REPO}" -if [[ ! -z "$ZEPPELIN_IMPERSONATE_USER" ]]; then +if [[ ! -z "$ZEPPELIN_IMPERSONATE_USER" && "${INTERPRETER_ID}" != "spark" ]]; then suid="$(id -u ${ZEPPELIN_IMPERSONATE_USER})" if [[ -n "${suid}" || -z "${SPARK_SUBMIT}" ]]; then INTERPRETER_RUN_COMMAND=${ZEPPELIN_IMPERSONATE_RUN_CMD}" '" @@ -206,15 +211,12 @@ if [[ ! -z "$ZEPPELIN_IMPERSONATE_USER" ]]; then fi if [[ -n "${SPARK_SUBMIT}" ]]; then - if [[ -n "$ZEPPELIN_IMPERSONATE_USER" ]] && [[ "$ZEPPELIN_IMPERSONATE_SPARK_PROXY_USER" != "false" ]]; then - INTERPRETER_RUN_COMMAND+=' '` echo ${SPARK_SUBMIT} --class ${ZEPPELIN_SERVER} --driver-class-path \"${ZEPPELIN_INTP_CLASSPATH_OVERRIDES}:${ZEPPELIN_INTP_CLASSPATH}\" --driver-java-options \"${JAVA_INTP_OPTS}\" ${SPARK_SUBMIT_OPTIONS} ${ZEPPELIN_SPARK_CONF} --proxy-user ${ZEPPELIN_IMPERSONATE_USER} ${SPARK_APP_JAR} ${CALLBACK_HOST} ${PORT} ${INTP_PORT}` - else - INTERPRETER_RUN_COMMAND+=' '` echo ${SPARK_SUBMIT} --class ${ZEPPELIN_SERVER} --driver-class-path \"${ZEPPELIN_INTP_CLASSPATH_OVERRIDES}:${ZEPPELIN_INTP_CLASSPATH}\" --driver-java-options \"${JAVA_INTP_OPTS}\" ${SPARK_SUBMIT_OPTIONS} ${ZEPPELIN_SPARK_CONF} ${SPARK_APP_JAR} ${CALLBACK_HOST} ${PORT} ${INTP_PORT}` - fi + INTERPRETER_RUN_COMMAND+=' '` echo ${SPARK_SUBMIT} --class ${ZEPPELIN_SERVER} --driver-class-path \"${ZEPPELIN_INTP_CLASSPATH_OVERRIDES}:${ZEPPELIN_INTP_CLASSPATH}\" --driver-java-options \"${JAVA_INTP_OPTS}\" ${SPARK_SUBMIT_OPTIONS} ${ZEPPELIN_SPARK_CONF} ${SPARK_APP_JAR} ${CALLBACK_HOST} ${PORT} ${INTP_PORT}` else INTERPRETER_RUN_COMMAND+=' '` echo ${ZEPPELIN_RUNNER} ${JAVA_INTP_OPTS} ${ZEPPELIN_INTP_MEM} -cp ${ZEPPELIN_INTP_CLASSPATH_OVERRIDES}:${ZEPPELIN_INTP_CLASSPATH} ${ZEPPELIN_SERVER} ${CALLBACK_HOST} ${PORT} ${INTP_PORT}` fi + if [[ ! -z "$ZEPPELIN_IMPERSONATE_USER" ]] && [[ -n "${suid}" || -z "${SPARK_SUBMIT}" ]]; then INTERPRETER_RUN_COMMAND+="'" fi diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterClient.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterClient.java index 813dad86881..26da27032f1 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterClient.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterClient.java @@ -26,7 +26,7 @@ public interface InterpreterClient { String getInterpreterSettingName(); - void start(String userName, Boolean isUserImpersonate); + void start(String userName); void stop(); diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLaunchContext.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLaunchContext.java index 6901e2c7a62..28c40f25b90 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLaunchContext.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLaunchContext.java @@ -30,6 +30,7 @@ public class InterpreterLaunchContext { private Properties properties; private InterpreterOption option; private InterpreterRunner runner; + private String userName; private String interpreterGroupId; private String interpreterSettingId; private String interpreterSettingGroup; @@ -38,6 +39,7 @@ public class InterpreterLaunchContext { public InterpreterLaunchContext(Properties properties, InterpreterOption option, InterpreterRunner runner, + String userName, String interpreterGroupId, String interpreterSettingId, String interpreterSettingGroup, @@ -45,6 +47,7 @@ public InterpreterLaunchContext(Properties properties, this.properties = properties; this.option = option; this.runner = runner; + this.userName = userName; this.interpreterGroupId = interpreterGroupId; this.interpreterSettingId = interpreterSettingId; this.interpreterSettingGroup = interpreterSettingGroup; @@ -78,4 +81,8 @@ public String getInterpreterSettingGroup() { public String getInterpreterSettingName() { return interpreterSettingName; } + + public String getUserName() { + return userName; + } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java index 424aa27a166..397ae108417 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java @@ -699,13 +699,14 @@ List createInterpreters(String user, String interpreterGroupId, Str } synchronized RemoteInterpreterProcess createInterpreterProcess(String interpreterGroupId, + String userName, Properties properties) throws IOException { if (launcher == null) { createLauncher(); } InterpreterLaunchContext launchContext = new - InterpreterLaunchContext(properties, option, interpreterRunner, + InterpreterLaunchContext(properties, option, interpreterRunner, userName, interpreterGroupId, id, group, name); RemoteInterpreterProcess process = (RemoteInterpreterProcess) launcher.launch(launchContext); process.setRemoteInterpreterEventPoller( diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java index 641c0ac23ef..d21a34d57be 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java @@ -60,10 +60,11 @@ public synchronized RemoteInterpreterProcess getOrCreateInterpreterProcess(Strin throws IOException { if (remoteInterpreterProcess == null) { LOGGER.info("Create InterpreterProcess for InterpreterGroup: " + getId()); - remoteInterpreterProcess = interpreterSetting.createInterpreterProcess(id, properties); + remoteInterpreterProcess = interpreterSetting.createInterpreterProcess(id, userName, + properties); synchronized (remoteInterpreterProcess) { if (!remoteInterpreterProcess.isRunning()) { - remoteInterpreterProcess.start(userName, false); + remoteInterpreterProcess.start(userName); remoteInterpreterProcess.getRemoteInterpreterEventPoller() .setInterpreterProcess(remoteInterpreterProcess); remoteInterpreterProcess.getRemoteInterpreterEventPoller().setInterpreterGroup(this); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncher.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncher.java index 6ddcacf275a..e107fb7edec 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncher.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncher.java @@ -84,15 +84,15 @@ public InterpreterClient launch(InterpreterLaunchContext context) throws IOExcep runner != null ? runner.getPath() : zConf.getInterpreterRemoteRunnerPath(), zConf.getCallbackPortRange(), zConf.getInterpreterPortRange(), zConf.getInterpreterDir() + "/" + groupName, localRepoPath, - buildEnvFromProperties(), connectTimeout, name); + buildEnvFromProperties(context), connectTimeout, name, option.isUserImpersonate()); } } - protected Map buildEnvFromProperties() { + protected Map buildEnvFromProperties(InterpreterLaunchContext context) { Map env = new HashMap<>(); - for (Object key : properties.keySet()) { + for (Object key : context.getProperties().keySet()) { if (RemoteInterpreterUtils.isEnvString((String) key)) { - env.put((String) key, properties.getProperty((String) key)); + env.put((String) key, context.getProperties().getProperty((String) key)); } } return env; diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncher.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncher.java index e8a9cdf881e..c462f0a0838 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncher.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncher.java @@ -41,7 +41,7 @@ public SparkInterpreterLauncher(ZeppelinConfiguration zConf, RecoveryStorage rec } @Override - protected Map buildEnvFromProperties() { + protected Map buildEnvFromProperties(InterpreterLaunchContext context) { Map env = new HashMap(); Properties sparkProperties = new Properties(); String sparkMaster = getSparkMaster(properties); @@ -70,6 +70,11 @@ protected Map buildEnvFromProperties() { for (String name : sparkProperties.stringPropertyNames()) { sparkConfBuilder.append(" --conf " + name + "=" + sparkProperties.getProperty(name)); } + String useProxyUserEnv = System.getenv("ZEPPELIN_IMPERSONATE_SPARK_PROXY_USER"); + if (context.getOption().isUserImpersonate() && (StringUtils.isBlank(useProxyUserEnv) || + !useProxyUserEnv.equals("false"))) { + sparkConfBuilder.append(" --proxy-user " + context.getUserName()); + } env.put("ZEPPELIN_SPARK_CONF", sparkConfBuilder.toString()); @@ -194,12 +199,12 @@ private boolean isYarnMode() { } private String toShellFormat(String value) { - if (value.contains("\'") && value.contains("\"")) { + if (value.contains("'") && value.contains("\"")) { throw new RuntimeException("Spark property value could not contain both \" and '"); - } else if (value.contains("\'")) { + } else if (value.contains("'")) { return "\"" + value + "\""; } else { - return "\'" + value + "\'"; + return "'" + value + "'"; } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java index 3dd5bfa3493..b186e481570 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java @@ -63,6 +63,7 @@ public class RemoteInterpreterManagedProcess extends RemoteInterpreterProcess private final String interpreterDir; private final String localRepoDir; private final String interpreterSettingName; + private final boolean isUserImpersonated; private Map env; @@ -74,7 +75,8 @@ public RemoteInterpreterManagedProcess( String localRepoDir, Map env, int connectTimeout, - String interpreterSettingName) { + String interpreterSettingName, + boolean isUserImpersonated) { super(connectTimeout); this.interpreterRunner = intpRunner; this.callbackPortRange = callbackPortRange; @@ -83,6 +85,7 @@ public RemoteInterpreterManagedProcess( this.interpreterDir = intpDir; this.localRepoDir = localRepoDir; this.interpreterSettingName = interpreterSettingName; + this.isUserImpersonated = isUserImpersonated; } @Override @@ -96,7 +99,7 @@ public int getPort() { } @Override - public void start(String userName, Boolean isUserImpersonate) { + public void start(String userName) { // start server process final String callbackHost; final int callbackPort; @@ -161,7 +164,7 @@ public void run() { cmdLine.addArgument(Integer.toString(callbackPort), false); cmdLine.addArgument("-r", false); cmdLine.addArgument(interpreterPortRange, false); - if (isUserImpersonate && !userName.equals("anonymous")) { + if (isUserImpersonated && !userName.equals("anonymous")) { cmdLine.addArgument("-u", false); cmdLine.addArgument(userName, false); } @@ -272,6 +275,11 @@ public String getInterpreterRunner() { return interpreterRunner; } + @VisibleForTesting + public boolean isUserImpersonated() { + return isUserImpersonated; + } + public boolean isRunning() { return running.get(); } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java index 0e87e4f7d4f..69daa6f68e7 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterRunningProcess.java @@ -58,7 +58,7 @@ public String getInterpreterSettingName() { } @Override - public void start(String userName, Boolean isUserImpersonate) { + public void start(String userName) { // assume process is externally managed. nothing to do } diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncherTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncherTest.java index f7988e35701..b7557ada982 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncherTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncherTest.java @@ -38,7 +38,8 @@ public void testLauncher() throws IOException { properties.setProperty("ENV_1", "VALUE_1"); properties.setProperty("property_1", "value_1"); InterpreterOption option = new InterpreterOption(); - InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "intpGroupId", "groupId", "groupName", "name"); + option.setUserImpersonate(true); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "user1", "intpGroupId", "groupId", "groupName", "name"); InterpreterClient client = launcher.launch(context); assertTrue( client instanceof RemoteInterpreterManagedProcess); RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; @@ -48,6 +49,7 @@ public void testLauncher() throws IOException { assertEquals(zConf.getInterpreterRemoteRunnerPath(), interpreterProcess.getInterpreterRunner()); assertEquals(1, interpreterProcess.getEnv().size()); assertEquals("VALUE_1", interpreterProcess.getEnv().get("ENV_1")); + assertEquals(true, interpreterProcess.isUserImpersonated()); } } diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncherTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncherTest.java index 3d7e251b079..a3e6a9bf82f 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncherTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncherTest.java @@ -42,7 +42,7 @@ public void testLocalMode() throws IOException { properties.setProperty("spark.jars", "jar_1"); InterpreterOption option = new InterpreterOption(); - InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "intpGroupId", "groupId", "spark", "spark"); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "user1", "intpGroupId", "groupId", "spark", "spark"); InterpreterClient client = launcher.launch(context); assertTrue( client instanceof RemoteInterpreterManagedProcess); RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; @@ -67,7 +67,7 @@ public void testYarnClientMode_1() throws IOException { properties.setProperty("spark.jars", "jar_1"); InterpreterOption option = new InterpreterOption(); - InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "intpGroupId", "groupId", "spark", "spark"); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "user1", "intpGroupId", "groupId", "spark", "spark"); InterpreterClient client = launcher.launch(context); assertTrue( client instanceof RemoteInterpreterManagedProcess); RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; @@ -93,7 +93,7 @@ public void testYarnClientMode_2() throws IOException { properties.setProperty("spark.jars", "jar_1"); InterpreterOption option = new InterpreterOption(); - InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "intpGroupId", "groupId", "spark", "spark"); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "user1", "intpGroupId", "groupId", "spark", "spark"); InterpreterClient client = launcher.launch(context); assertTrue( client instanceof RemoteInterpreterManagedProcess); RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; @@ -118,7 +118,7 @@ public void testYarnClusterMode_1() throws IOException { properties.setProperty("spark.jars", "jar_1"); InterpreterOption option = new InterpreterOption(); - InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "intpGroupId", "groupId", "spark", "spark"); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "user1", "intpGroupId", "groupId", "spark", "spark"); InterpreterClient client = launcher.launch(context); assertTrue( client instanceof RemoteInterpreterManagedProcess); RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; @@ -145,7 +145,8 @@ public void testYarnClusterMode_2() throws IOException { properties.setProperty("spark.jars", "jar_1"); InterpreterOption option = new InterpreterOption(); - InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "intpGroupId", "groupId", "spark", "spark"); + option.setUserImpersonate(true); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "user1", "intpGroupId", "groupId", "spark", "spark"); InterpreterClient client = launcher.launch(context); assertTrue( client instanceof RemoteInterpreterManagedProcess); RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; @@ -156,6 +157,6 @@ public void testYarnClusterMode_2() throws IOException { assertEquals(3, interpreterProcess.getEnv().size()); assertEquals("/user/spark", interpreterProcess.getEnv().get("SPARK_HOME")); assertEquals("true", interpreterProcess.getEnv().get("ZEPPELIN_SPARK_YARN_CLUSTER")); - assertEquals(" --master yarn --files .//conf/log4j_yarn_cluster.properties --conf spark.files='file_1' --conf spark.jars='jar_1' --conf spark.submit.deployMode='cluster' --conf spark.yarn.isPython=true", interpreterProcess.getEnv().get("ZEPPELIN_SPARK_CONF")); + assertEquals(" --master yarn --files .//conf/log4j_yarn_cluster.properties --conf spark.files='file_1' --conf spark.jars='jar_1' --conf spark.submit.deployMode='cluster' --conf spark.yarn.isPython=true --proxy-user user1", interpreterProcess.getEnv().get("ZEPPELIN_SPARK_CONF")); } } From 851dcb1a392a3b720501982c6c2c4a3d30468a7e Mon Sep 17 00:00:00 2001 From: Andrea Santurbano Date: Tue, 7 Nov 2017 18:39:58 +0100 Subject: [PATCH 139/492] [ZEPPELIN-3038] Network visualization not show "source" and "target" node/edge properties ### What is this PR for? The Network visualization not show "source" and "target" node/edge properties when the graph is flattened to create the table representation. ### What type of PR is it? [Bug Fix] ### Todos * [x] - Fixed ### Screenshot Before: ![zeppelin before](https://user-images.githubusercontent.com/1833335/32510305-6ee266c8-c3f0-11e7-9e28-7ed1f304ebcb.PNG) After: ![zeppelin after](https://user-images.githubusercontent.com/1833335/32510324-8121cc3e-c3f0-11e7-9f1f-84bba563aebc.PNG) ### What is the Jira issue? [ZEPPELIN-3038](https://issues.apache.org/jira/browse/ZEPPELIN-3038) ### How should this be tested? Please use this [notebook](https://gist.github.com/conker84/9574127c2389d08164423894aa93b67f) ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: Andrea Santurbano Closes #2653 from conker84/network-fix and squashes the following commits: d4f19b6 [Andrea Santurbano] removed unused property 44100c6 [Andrea Santurbano] Removed semicolons 273c88f [Andrea Santurbano] added test on edges prevent double rendering 5eeabc2 [Andrea Santurbano] networkdata class now shows fields "source" and "target" + added test case moved the logic related to the visualization from networkdata to visualization-d3network added some optimization --- zeppelin-web/src/app/tabledata/networkdata.js | 104 +++----------- .../src/app/tabledata/networkdata.test.js | 27 +++- .../builtins/visualization-d3network.js | 135 ++++++++++++++---- 3 files changed, 150 insertions(+), 116 deletions(-) diff --git a/zeppelin-web/src/app/tabledata/networkdata.js b/zeppelin-web/src/app/tabledata/networkdata.js index 7983d827265..70cd86ba5b4 100644 --- a/zeppelin-web/src/app/tabledata/networkdata.js +++ b/zeppelin-web/src/app/tabledata/networkdata.js @@ -40,42 +40,40 @@ export default class NetworkData extends TableData { return } - this.setNodesDefaults() - this.setEdgesDefaults() - + this.graph.edges = this.graph.edges || [] this.networkNodes = angular.equals({}, this.graph.labels || {}) ? null : {count: this.graph.nodes.length, labels: this.graph.labels} this.networkRelationships = angular.equals([], this.graph.types || []) ? null : {count: this.graph.edges.length, types: this.graph.types} - let rows = [] - let comment = '' - let entities = this.graph.nodes.concat(this.graph.edges) - let baseColumnNames = [{name: 'id', index: 0, aggr: 'sum'}, - {name: 'label', index: 1, aggr: 'sum'}] - let internalFieldsToJump = ['count', 'size', 'totalCount', - 'data', 'x', 'y', 'labels'] - let baseCols = _.map(baseColumnNames, function(col) { return col.name }) - let keys = _.map(entities, function(elem) { return Object.keys(elem.data || {}) }) + const rows = [] + const comment = '' + const entities = this.graph.nodes.concat(this.graph.edges) + const baseColumnNames = [{name: 'id', index: 0, aggr: 'sum'}] + const containsLabelField = _.find(entities, (entity) => 'label' in entity) != null + if (this.graph.labels || this.graph.types || containsLabelField) { + baseColumnNames.push({name: 'label', index: 1, aggr: 'sum'}) + } + const internalFieldsToJump = ['count', 'size', 'totalCount', + 'data', 'x', 'y', 'labels', 'source', 'target'] + const baseCols = _.map(baseColumnNames, (col) => col.name) + let keys = _.map(entities, (elem) => Object.keys(elem.data || {})) keys = _.flatten(keys) - keys = _.uniq(keys).filter(function(key) { - return baseCols.indexOf(key) === -1 - }) - let columnNames = baseColumnNames.concat(_.map(keys, function(elem, i) { + keys = _.uniq(keys).filter((key) => baseCols.indexOf(key) === -1) + const entityColumnNames = _.map(keys, (elem, i) => { return {name: elem, index: i + baseColumnNames.length, aggr: 'sum'} - })) + }) + const columnNames = baseColumnNames.concat(entityColumnNames) for (let i = 0; i < entities.length; i++) { - let entity = entities[i] - let col = [] - let col2 = [] + const entity = entities[i] + const col = [] entity.data = entity.data || {} for (let j = 0; j < columnNames.length; j++) { - let name = columnNames[j].name - let value = name in entity && internalFieldsToJump.indexOf(name) === -1 + const name = columnNames[j].name + const value = name in entity && internalFieldsToJump.indexOf(name) === -1 ? entity[name] : entity.data[name] - let parsedValue = value === null || value === undefined ? '' : value + const parsedValue = value === null || value === undefined ? '' : value col.push(parsedValue) - col2.push({key: name, value: parsedValue}) } rows.push(col) } @@ -84,62 +82,4 @@ export default class NetworkData extends TableData { this.columns = columnNames this.rows = rows } - - setNodesDefaults() { - } - - setEdgesDefaults() { - this.graph.edges - .sort((a, b) => { - if (a.source > b.source) { - return 1 - } else if (a.source < b.source) { - return -1 - } else if (a.target > b.target) { - return 1 - } else if (a.target < b.target) { - return -1 - } else { - return 0 - } - }) - this.graph.edges - .forEach((edge, index) => { - let prevEdge = this.graph.edges[index - 1] - edge.count = (index > 0 && +edge.source === +prevEdge.source && +edge.target === +prevEdge.target - ? prevEdge.count : 0) + 1 - edge.totalCount = this.graph.edges - .filter((innerEdge) => +edge.source === +innerEdge.source && +edge.target === +innerEdge.target) - .length - }) - this.graph.edges - .forEach((edge) => { - if (typeof +edge.source === 'number') { - edge.source = this.graph.nodes.filter((node) => +edge.source === +node.id)[0] || null - } - if (typeof +edge.target === 'number') { - edge.target = this.graph.nodes.filter((node) => +edge.target === +node.id)[0] || null - } - }) - } - - getNetworkProperties() { - let baseCols = ['id', 'label'] - let properties = {} - this.graph.nodes.forEach(function(node) { - let hasLabel = 'label' in node && node.label !== '' - if (!hasLabel) { - return - } - let label = node.label - let hasKey = hasLabel && label in properties - let keys = _.uniq(Object.keys(node.data || {}) - .concat(hasKey ? properties[label].keys : baseCols)) - if (!hasKey) { - properties[label] = {selected: 'label'} - } - properties[label].keys = keys - }) - return properties - } } diff --git a/zeppelin-web/src/app/tabledata/networkdata.test.js b/zeppelin-web/src/app/tabledata/networkdata.test.js index f8d98a89a3f..739ac19fe36 100644 --- a/zeppelin-web/src/app/tabledata/networkdata.test.js +++ b/zeppelin-web/src/app/tabledata/networkdata.test.js @@ -35,12 +35,33 @@ describe('NetworkData build', function() { msg: JSON.stringify(jsonExpected) }) - expect(nd.columns.length).toBe(2) + expect(nd.columns.length).toBe(1) expect(nd.rows.length).toBe(3) expect(nd.graph.nodes[0].id).toBe(jsonExpected.nodes[0].id) expect(nd.graph.nodes[1].id).toBe(jsonExpected.nodes[1].id) expect(nd.graph.edges[0].id).toBe(jsonExpected.edges[0].id) - expect(nd.graph.edges[0].source.id).toBe(jsonExpected.nodes[1].id) - expect(nd.graph.edges[0].target.id).toBe(jsonExpected.nodes[0].id) + expect(nd.graph.edges[0].source).toBe(jsonExpected.edges[0].source) + expect(nd.graph.edges[0].target).toBe(jsonExpected.edges[0].target) + }) + + it('should able to show data fields source and target', function() { + let jsonExpected = {nodes: [{id: 1, data: {source: 'Source'}}, {id: 2, data: {target: 'Target'}}], + edges: [{source: 2, target: 1, id: 1, data: {source: 'Source Edge Data', target: 'Target Edge Data'}}]} + nd.loadParagraphResult({ + type: DatasetType.NETWORK, + msg: JSON.stringify(jsonExpected) + }) + + expect(nd.columns.length).toBe(3) + expect(nd.rows.length).toBe(3) + expect(nd.graph.nodes[0].id).toBe(jsonExpected.nodes[0].id) + expect(nd.graph.nodes[1].id).toBe(jsonExpected.nodes[1].id) + expect(nd.graph.edges[0].id).toBe(jsonExpected.edges[0].id) + expect(nd.graph.edges[0].source).toBe(jsonExpected.edges[0].source) + expect(nd.graph.edges[0].target).toBe(jsonExpected.edges[0].target) + expect(nd.graph.nodes[0].data.source).toBe(jsonExpected.nodes[0].data.source) + expect(nd.graph.nodes[1].data.target).toBe(jsonExpected.nodes[1].data.target) + expect(nd.graph.edges[0].data.source).toBe(jsonExpected.edges[0].data.source) + expect(nd.graph.edges[0].data.target).toBe(jsonExpected.edges[0].data.target) }) }) diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js b/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js index 506b1c5f186..46ee25168d9 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js @@ -55,25 +55,36 @@ export default class NetworkVisualization extends Visualization { console.log('graph not found') return } - console.log('Render Graph Visualization') + if (!networkData.isRendered) { + networkData.isRendered = true + } else { + return + } + console.log('Rendering the graph') - let transformationConfig = this.transformation.getSetting().scope.config + if (networkData.graph.edges.length && + !networkData.isDefaultSet) { + networkData.isDefaultSet = true + this._setEdgesDefaults(networkData.graph) + } + + const transformationConfig = this.transformation.getSetting().scope.config console.log('cfg', transformationConfig) if (transformationConfig && angular.equals({}, transformationConfig.properties)) { - transformationConfig.properties = networkData.getNetworkProperties() + transformationConfig.properties = this.getNetworkProperties(networkData.graph) } this.targetEl.empty().append('') - let width = this.targetEl.width() - let height = this.targetEl.height() - let self = this - let defaultOpacity = 0 - let nodeSize = 10 - let textOffset = 3 - let linkSize = 10 + const width = this.targetEl.width() + const height = this.targetEl.height() + const self = this + const defaultOpacity = 0 + const nodeSize = 10 + const textOffset = 3 + const linkSize = 10 - let arcPath = (leftHand, d) => { + const arcPath = (leftHand, d) => { let start = leftHand ? d.source : d.target let end = leftHand ? d.target : d.source let dx = end.x - start.x @@ -84,7 +95,7 @@ export default class NetworkVisualization extends Visualization { return `M${start.x},${start.y}A${dr},${dr} 0 0,${sweep} ${end.x},${end.y}` } // Use elliptical arc path segments to doubly-encode directionality. - let tick = () => { + const tick = () => { // Links linkPath.attr('d', function(d) { return arcPath(true, d) @@ -97,7 +108,7 @@ export default class NetworkVisualization extends Visualization { text.attr('transform', (d) => `translate(${d.x},${d.y})`) } - let setOpacity = (scale) => { + const setOpacity = (scale) => { let opacity = scale >= +transformationConfig.d3Graph.zoom.minScale ? 1 : 0 this.svg.selectAll('.nodeLabel') .style('opacity', opacity) @@ -105,7 +116,7 @@ export default class NetworkVisualization extends Visualization { .style('opacity', opacity) } - let zoom = d3.behavior.zoom() + const zoom = d3.behavior.zoom() .scaleExtent([1, 10]) .on('zoom', () => { console.log('zoom') @@ -135,13 +146,15 @@ export default class NetworkVisualization extends Visualization { }) .start() - let renderFooterOnClick = (entity, type) => { - let footerId = this.containerId + '_footer' - let obj = {id: entity.id, label: entity.defaultLabel || entity.label, type: type} - let html = [this.$interpolate(['
  • {{type}}_id: {{id}}
  • ', - '
  • {{type}}_type: {{label}}
  • '].join(''))(obj)] + const renderFooterOnClick = (entity, type) => { + const footerId = this.containerId + '_footer' + const obj = {id: entity.id, label: entity.defaultLabel || entity.label, type: type} + let html = [`
  • ${obj.type}_id: ${obj.id}
  • `] + if (obj.label) { + html.push(`
  • ${obj.type}_type: ${obj.label}
  • `) + } html = html.concat(_.map(entity.data, (v, k) => { - return this.$interpolate('
  • {{field}}: {{value}}
  • ')({field: k, value: v}) + return `
  • ${k}: ${v}
  • ` })) angular.element('#' + footerId) .find('.list-inline') @@ -149,7 +162,7 @@ export default class NetworkVisualization extends Visualization { .append(html.join('')) } - let drag = d3.behavior.drag() + const drag = d3.behavior.drag() .origin((d) => d) .on('dragstart', function(d) { console.log('dragstart') @@ -171,7 +184,7 @@ export default class NetworkVisualization extends Visualization { self.force.resume() }) - let container = this.svg.append('g') + const container = this.svg.append('g') if (networkData.graph.directed) { container.append('svg:defs').selectAll('marker') .data(['arrowMarker-' + this.containerId]) @@ -188,7 +201,7 @@ export default class NetworkVisualization extends Visualization { .attr('d', 'M0,-5L10,0L0,5') } // Links - let link = container.append('svg:g') + const link = container.append('svg:g') .on('click', () => { renderFooterOnClick(d3.select(d3.event.target).datum(), 'edge') }) @@ -196,13 +209,13 @@ export default class NetworkVisualization extends Visualization { .data(self.force.links()) .enter() .append('g') - let getPathId = (d) => this.containerId + '_' + d.source.index + '_' + d.target.index + '_' + d.count - let showLabel = (d) => this._showNodeLabel(d) - let linkPath = link.append('svg:path') + const getPathId = (d) => this.containerId + '_' + d.source.index + '_' + d.target.index + '_' + d.count + const showLabel = (d) => this._showNodeLabel(d) + const linkPath = link.append('svg:path') .attr('class', 'link') .attr('size', linkSize) .attr('marker-end', `url(#arrowMarker-${this.containerId})`) - let textPath = link.append('svg:path') + const textPath = link.append('svg:path') .attr('id', getPathId) .attr('class', 'textpath') container.append('svg:g') @@ -218,7 +231,7 @@ export default class NetworkVisualization extends Visualization { .text((d) => d.label) .style('opacity', defaultOpacity) // Nodes - let circle = container.append('svg:g') + const circle = container.append('svg:g') .on('click', () => { renderFooterOnClick(d3.select(d3.event.target).datum(), 'node') }) @@ -229,7 +242,7 @@ export default class NetworkVisualization extends Visualization { .attr('fill', (d) => networkData.graph.labels && d.label in networkData.graph.labels ? networkData.graph.labels[d.label] : '#000000') .call(drag) - let text = container.append('svg:g').selectAll('g') + const text = container.append('svg:g').selectAll('g') .data(self.force.nodes()) .enter().append('svg:g') text.append('svg:text') @@ -252,12 +265,72 @@ export default class NetworkVisualization extends Visualization { } _showNodeLabel(d) { - let transformationConfig = this.transformation.getSetting().scope.config - let selectedLabel = (transformationConfig.properties[d.label] || {selected: 'label'}).selected + const transformationConfig = this.transformation.getSetting().scope.config + const selectedLabel = (transformationConfig.properties[d.label] || {selected: 'label'}).selected return d.data[selectedLabel] || d[selectedLabel] } getTransformation() { return this.transformation } + + setNodesDefaults() { + } + + _setEdgesDefaults(graph) { + graph.edges + .sort((a, b) => { + if (a.source > b.source) { + return 1 + } else if (a.source < b.source) { + return -1 + } else if (a.target > b.target) { + return 1 + } else if (a.target < b.target) { + return -1 + } else { + return 0 + } + }) + graph.edges + .forEach((edge, index) => { + let prevEdge = graph.edges[index - 1] + edge.count = (index > 0 && +edge.source === +prevEdge.source && +edge.target === +prevEdge.target + ? prevEdge.count : 0) + 1 + edge.totalCount = graph.edges + .filter((innerEdge) => +edge.source === +innerEdge.source && +edge.target === +innerEdge.target) + .length + }) + graph.edges + .forEach((edge) => { + if (typeof +edge.source === 'number') { + // edge.source = graph.nodes.filter((node) => +edge.source === +node.id)[0] || null + edge.source = _.find(graph.nodes, (node) => +edge.source === +node.id) + } + if (typeof +edge.target === 'number') { + // edge.target = graph.nodes.filter((node) => +edge.target === +node.id)[0] || null + edge.target = _.find(graph.nodes, (node) => +edge.target === +node.id) + } + }) + } + + getNetworkProperties(graph) { + const baseCols = ['id', 'label'] + const properties = {} + graph.nodes.forEach(function(node) { + const hasLabel = 'label' in node && node.label !== '' + if (!hasLabel) { + return + } + const label = node.label + const hasKey = hasLabel && label in properties + const keys = _.uniq(Object.keys(node.data || {}) + .concat(hasKey ? properties[label].keys : baseCols)) + if (!hasKey) { + properties[label] = {selected: 'label'} + } + properties[label].keys = keys + }) + return properties + } } From dd1be03dee9428ade92b8fd47d148c2325179d19 Mon Sep 17 00:00:00 2001 From: tinkoff-dwh Date: Tue, 5 Dec 2017 15:23:43 +0300 Subject: [PATCH 140/492] [Zeppelin-2964] Stop execution on schedule if the note has been moved to the trash ### What is this PR for? When you put the note (or folder) in the trash, the note continues to run on schedule. This PR fixes this. Now when you put the note into the trash, the task is removed, and when you restore the note, it runs again. ### What type of PR is it? Improvement ### What is the Jira issue? [ZEPPELIN-3007](https://issues.apache.org/jira/browse/ZEPPELIN-3007) ### How should this be tested? - Create a scheduled launch for the note. - Put the note in the trash. - Look through the logs. - Note must stop running. - Restore the note from the trash. - Running on a schedule should continue again. ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: tinkoff-dwh Closes #2697 from tinkoff-dwh/ZEPPELIN-2964 and squashes the following commits: b12ae4c [tinkoff-dwh] [ZEPPELIN-2964] add if statement 3db4a8e [tinkoff-dwh] [ZEPPELIN-2964] restore cron together note/folder 3906b9e [tinkoff-dwh] [ZEPPELIN-2964] drop cron when note placed in the trash --- .../zeppelin/socket/NotebookServer.java | 31 +++++++++++++++++++ .../src/app/notebook/notebook-actionBar.html | 2 +- .../apache/zeppelin/notebook/Notebook.java | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) 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 184735a629f..56aa50a3eb5 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 @@ -1108,6 +1108,13 @@ private void moveNoteToTrash(NotebookSocket conn, HashSet userAndRoles, } Note note = notebook.getNote(noteId); + + // drop cron + Map config = note.getConfig(); + if (config.get("cron") != null) { + notebook.removeCron(note.getId()); + } + if (note != null && !note.isTrash()){ fromMessage.put("name", Folder.TRASH_FOLDER_ID + "/" + note.getName()); renameNote(conn, userAndRoles, notebook, fromMessage, "move"); @@ -1132,6 +1139,14 @@ private void moveFolderToTrash(NotebookSocket conn, HashSet userAndRoles trashFolderId += Folder.TRASH_FOLDER_CONFLICT_INFIX + formatter.print(currentDate); } + List noteList = folder.getNotesRecursively(); + for (Note note: noteList) { + Map config = note.getConfig(); + if (config.get("cron") != null) { + notebook.removeCron(note.getId()); + } + } + fromMessage.put("name", trashFolderId); renameFolder(conn, userAndRoles, notebook, fromMessage, "move"); } @@ -1147,6 +1162,13 @@ private void restoreNote(NotebookSocket conn, HashSet userAndRoles, } Note note = notebook.getNote(noteId); + + //restore cron + Map config = note.getConfig(); + if (config.get("cron") != null) { + notebook.refreshCron(note.getId()); + } + if (note != null && note.isTrash()) { fromMessage.put("name", note.getName().replaceFirst(Folder.TRASH_FOLDER_ID + "/", "")); renameNote(conn, userAndRoles, notebook, fromMessage, "restore"); @@ -1166,6 +1188,15 @@ private void restoreFolder(NotebookSocket conn, HashSet userAndRoles, if (folder != null && folder.isTrash()) { String restoreName = folder.getId().replaceFirst(Folder.TRASH_FOLDER_ID + "/", "").trim(); + //restore cron for each paragraph + List noteList = folder.getNotesRecursively(); + for (Note note : noteList) { + Map config = note.getConfig(); + if (config.get("cron") != null) { + notebook.refreshCron(note.getId()); + } + } + // if the folder had conflict when it had moved to trash before Pattern p = Pattern.compile("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$"); Matcher m = p.matcher(restoreName); diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index f1d1dd5959c..573be001ebb 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -255,7 +255,7 @@

    data-toggle="dropdown" ng-class="{ 'btn-info' : note.config.cron, 'btn-danger' : note.info.cron, 'btn-default' : !note.config.cron}" tooltip-placement="bottom" uib-tooltip="Run scheduler" - ng-disabled="revisionView"> + ng-disabled="revisionView || isTrash(note)"> {{getCronOptionNameFromValue(note.config.cron)}}

    `, From 8ddaab07ca6fb6ff0a8991f55ba53d8122ce07d4 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Thu, 25 Jan 2018 09:50:48 +0800 Subject: [PATCH 161/492] ZEPPELIN-3190. Should not use singleton for FileSystemStorage ### What is this PR for? For now, `FileSystemNotebookRepo`, `FileSystemConfigStorage`, `FileSystemRecoveryStorage` use `FileSystemStorage`, but the singleton pattern means that all the notebook, config and recovery need to be stored in the same storage which might not be proper for some users. So this PR is trying to use separate `FileSystemStorage` instance for `FileSystemNotebookRepo`, `FileSystemConfigStorage`, `FileSystemRecoveryStorage` ### What type of PR is it? [Bug Fix | Improvement] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3190 ### How should this be tested? * Travis pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2746 from zjffdu/ZEPPELIN-3190 and squashes the following commits: 49611c2 [Jeff Zhang] ZEPPELIN-3190. Should not use singleton for FileSystemStorage --- .../interpreter/InterpreterSettingManager.java | 3 +-- .../recovery/FileSystemRecoveryStorage.java | 4 +++- .../zeppelin/notebook/FileSystemStorage.java | 16 +++++----------- .../notebook/repo/FileSystemNotebookRepo.java | 5 +++-- .../storage/FileSystemConfigStorage.java | 8 ++++++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java index a6583cbc3e6..bda1be60a40 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java @@ -294,7 +294,6 @@ public boolean accept(Path entry) throws IOException { } })) { String interpreterDirString = interpreterDir.toString(); - /** * Register interpreter by the following ordering * 1. Register it from path {ZEPPELIN_HOME}/interpreter/{interpreter_name}/ @@ -304,7 +303,7 @@ public boolean accept(Path entry) throws IOException { */ if (!registerInterpreterFromPath(interpreterDirString, interpreterJson)) { if (!registerInterpreterFromResource(cl, interpreterDirString, interpreterJson)) { - LOGGER.warn("No interpreter-setting.json found in " + interpreterDirPath); + LOGGER.warn("No interpreter-setting.json found in " + interpreterDirString); } } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/FileSystemRecoveryStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/FileSystemRecoveryStorage.java index 5a0c8adf6cd..9b1b6cb666f 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/FileSystemRecoveryStorage.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/FileSystemRecoveryStorage.java @@ -69,7 +69,9 @@ public FileSystemRecoveryStorage(ZeppelinConfiguration zConf, super(zConf); this.interpreterSettingManager = interpreterSettingManager; this.zConf = zConf; - this.fs = FileSystemStorage.get(zConf); + this.fs = new FileSystemStorage(zConf, zConf.getRecoveryDir()); + LOGGER.info("Creating FileSystem: " + this.fs.getFs().getClass().getName() + + " for Zeppelin Recovery."); this.recoveryDir = this.fs.makeQualified(new Path(zConf.getRecoveryDir())); LOGGER.info("Using folder {} to store recovery data", recoveryDir); this.fs.tryMkDir(recoveryDir); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FileSystemStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FileSystemStorage.java index 75c0bc3eae2..24bab570d9b 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FileSystemStorage.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FileSystemStorage.java @@ -30,18 +30,16 @@ public class FileSystemStorage { private static Logger LOGGER = LoggerFactory.getLogger(FileSystemStorage.class); - private static FileSystemStorage instance; - private ZeppelinConfiguration zConf; private Configuration hadoopConf; private boolean isSecurityEnabled = false; private FileSystem fs; - private FileSystemStorage(ZeppelinConfiguration zConf) throws IOException { + public FileSystemStorage(ZeppelinConfiguration zConf, String path) throws IOException { this.zConf = zConf; this.hadoopConf = new Configuration(); // disable checksum for local file system. because interpreter.json may be updated by - // no hadoop filesystem api + // non-hadoop filesystem api this.hadoopConf.set("fs.file.impl", RawLocalFileSystem.class.getName()); this.isSecurityEnabled = UserGroupInformation.isSecurityEnabled(); @@ -58,18 +56,14 @@ private FileSystemStorage(ZeppelinConfiguration zConf) throws IOException { } try { - this.fs = FileSystem.get(new URI(zConf.getNotebookDir()), this.hadoopConf); - LOGGER.info("Creating FileSystem: " + this.fs.getClass().getCanonicalName()); + this.fs = FileSystem.get(new URI(path), this.hadoopConf); } catch (URISyntaxException e) { throw new IOException(e); } } - public static synchronized FileSystemStorage get(ZeppelinConfiguration zConf) throws IOException { - if (instance == null) { - instance = new FileSystemStorage(zConf); - } - return instance; + public FileSystem getFs() { + return fs; } public Path makeQualified(Path path) { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/FileSystemNotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/FileSystemNotebookRepo.java index d8ec0e5400b..32bde374c53 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/FileSystemNotebookRepo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/FileSystemNotebookRepo.java @@ -42,11 +42,12 @@ public class FileSystemNotebookRepo implements NotebookRepo { private Path notebookDir; public FileSystemNotebookRepo(ZeppelinConfiguration zConf) throws IOException { - this.fs = FileSystemStorage.get(zConf); + this.fs = new FileSystemStorage(zConf, zConf.getNotebookDir()); + LOGGER.info("Creating FileSystem: " + this.fs.getFs().getClass().getName() + + " for Zeppelin Notebook."); this.notebookDir = this.fs.makeQualified(new Path(zConf.getNotebookDir())); LOGGER.info("Using folder {} to store notebook", notebookDir); this.fs.tryMkDir(notebookDir); - } @Override diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/FileSystemConfigStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/FileSystemConfigStorage.java index 2460e4de6bf..4df8163470d 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/FileSystemConfigStorage.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/FileSystemConfigStorage.java @@ -49,8 +49,12 @@ public class FileSystemConfigStorage extends ConfigStorage { public FileSystemConfigStorage(ZeppelinConfiguration zConf) throws IOException { super(zConf); - this.fs = FileSystemStorage.get(zConf); - this.fs.tryMkDir(new Path(zConf.getConfigFSDir())); + this.fs = new FileSystemStorage(zConf, zConf.getConfigFSDir()); + LOGGER.info("Creating FileSystem: " + this.fs.getFs().getClass().getName() + + " for Zeppelin Config"); + Path configPath = this.fs.makeQualified(new Path(zConf.getConfigFSDir())); + this.fs.tryMkDir(configPath); + LOGGER.info("Using folder {} to store Zeppelin Config", configPath); this.interpreterSettingPath = fs.makeQualified(new Path(zConf.getInterpreterSettingPath())); this.authorizationPath = fs.makeQualified(new Path(zConf.getNotebookAuthorizationPath())); this.credentialPath = fs.makeQualified(new Path(zConf.getCredentialsPath())); From 6a39c8d6ee85f6732986b7cd452f3d0f57830dda Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 23 Jan 2018 15:12:13 +0800 Subject: [PATCH 162/492] ZEPPELIN-3184. Use hadoop-azure to replace azure-storage ### What is this PR for? So that user can use azure storage via hadoop filesystem. ### What type of PR is it? [ Improvement ] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3184 ### How should this be tested? Travis CI Pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2744 from zjffdu/ZEPPELIN-3184 and squashes the following commits: 464a01d [Jeff Zhang] ZEPPELIN-3184. Use hadoop-azure to replace azure-storage --- zeppelin-zengine/pom.xml | 191 ++++++++++++++++++++++++++++++++------- 1 file changed, 158 insertions(+), 33 deletions(-) diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index b68152f2462..08de7ad50bc 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -40,7 +40,7 @@ 3.4 2.0 1.10.62 - 4.0.0 + 2.1.4 1.5.2 2.2.1 5.3.1 @@ -120,26 +120,6 @@ ${aws.sdk.s3.version} - - com.microsoft.azure - azure-storage - ${azure.storage.version} - - - com.fasterxml.jackson.core - jackson-core - - - org.slf4j - slf4j-api - - - org.apache.commons - commons-lang3 - - - - org.eclipse.jetty jetty-client @@ -484,10 +464,6 @@ tests test - - - - com.sun.jersey jersey-json @@ -496,10 +472,6 @@ com.sun.jersey jersey-client - - - - javax.servlet servlet-api @@ -558,10 +530,6 @@ com.sun.jersey jersey-core - - - - com.sun.jersey jersey-client @@ -657,6 +625,69 @@ + + + org.apache.hadoop + hadoop-azure + ${hadoop.version} + + + com.fasterxml.jackson.core + jackson-core + + + com.google.guava + guava + + + org.apache.commons + commons-lang3 + + + com.jcraf + jsch + + + org.apache.commons + commons-compress + + + + + com.microsoft.azure + azure-data-lake-store-sdk + ${adl.sdk.version} + + + com.fasterxml.jackson.core + jackson-core + + + + + org.apache.hadoop + hadoop-aws + ${hadoop.version} + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + joda-time + joda-time + + + + @@ -682,6 +713,100 @@ ${hadoop.version} test + + + org.apache.hadoop + hadoop-azure + ${hadoop.version} + + + com.fasterxml.jackson.core + jackson-core + + + com.google.guava + guava + + + com.jcraft + jsch + + + org.apache.commons + commons-compress + + + org.codehaus.jackson + jackson-mapper-asl + + + com.nimbusds + nimbus-jose-jwt + + + org.apache.zookeeper + zookeeper + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-servlet + + + org.codehaus.jackson + jackson-core-asl + + + com.fasterxml.jackson.core + jackson-databind + + + org.eclipse.jetty + jetty-util + + + com.sun.jersey + jersey-core + + + + + org.apache.hadoop + hadoop-azure-datalake + ${hadoop.version} + + + com.fasterxml.jackson.core + jackson-core + + + + + org.apache.hadoop + hadoop-aws + ${hadoop.version} + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + joda-time + joda-time + + + From 8219a37302aa81487d3ddfa6f10699e0a1657e4b Mon Sep 17 00:00:00 2001 From: mebelousov Date: Tue, 23 Jan 2018 15:39:00 +0300 Subject: [PATCH 163/492] [ZEPPELIN-3187] Remove doubles of settings in zeppelin-site.xml.template ### What is this PR for? Remove doubles in zeppelin-site.xml.template ### What type of PR is it? [Bug Fix ] ### What is the Jira issue? * [ZEPPELIN-3187] ### How should this be tested? * No need ### Questions: * Does the licenses files need update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: mebelousov Closes #2741 from mebelousov/ZEPPELIN-3187 and squashes the following commits: 44d6d08 [mebelousov] Remove doubles of settings (cherry picked from commit 08251226203107303785ffde458b074d47b13a6f) Signed-off-by: Jeff Zhang --- conf/zeppelin-site.xml.template | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template index d566a717884..33aa8acf6da 100755 --- a/conf/zeppelin-site.xml.template +++ b/conf/zeppelin-site.xml.template @@ -427,13 +427,13 @@ zeppelin.interpreter.lifecyclemanager.timeout.checkinterval 60000 - milliseconds of the interval to checking whether interpreter is time out + Milliseconds of the interval to checking whether interpreter is time out zeppelin.interpreter.lifecyclemanager.timeout.threshold 3600000 - milliseconds of the interpreter timeout threshold, by default it is 1 hour + Milliseconds of the interpreter timeout threshold, by default it is 1 hour + - - - - - - From 68bf5f8058f5f5a625c21935c97d449a56acef84 Mon Sep 17 00:00:00 2001 From: Nelson Costa Date: Wed, 24 Jan 2018 15:46:14 +0000 Subject: [PATCH 165/492] [ZEPPELIN-3189] NPE on paragraph run via API ### What is this PR for? Bugfix over NPE when running REST API command to run paragraph synchronously. ### What type of PR is it? [Bug Fix] ### Todos * [ ] - Task ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-3189 ### How should this be tested? 1. Build Zep (mvn clean package -DskipTests) 2. Start Zep 3. Run curl command in shell (curl -i -X POST http://localhost:8080/api/notebook/run/{noteId}/{paragraphId} ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? N * Is there breaking changes for older versions? N * Does this needs documentation? N Author: Nelson Costa Closes #2745 from necosta/zeppelin3189 and squashes the following commits: 9d1e2a569 [Nelson Costa] [ZEPPELIN-3189] NPE on paragraph run via API (cherry picked from commit e7e9e19cf1be6ada5b5e8d56fc2a7d8809f85f79) Signed-off-by: Lee moon soo --- .../main/java/org/apache/zeppelin/rest/NotebookRestApi.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java index 8835984373b..2042c4c2024 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java @@ -797,6 +797,9 @@ public Response runParagraphSynchronously(@PathParam("noteId") String noteId, note.initializeJobListenerForParagraph(paragraph); } + AuthenticationInfo subject = new AuthenticationInfo(SecurityUtils.getPrincipal()); + paragraph.setAuthenticationInfo(subject); + paragraph.run(); final InterpreterResult result = paragraph.getResult(); From ca87f7d4cc489a492772bbe85705c6a1826cc31a Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Mon, 17 Jul 2017 13:02:09 +0800 Subject: [PATCH 166/492] ZEPPELIN-3111. Refactor SparkInterpreter This is for the refactoring of SparkInterpreter. See design doc. https://docs.google.com/document/d/1AfGg3aGXonDyri1jrP4MMFT4Y4j3wpN1t8kL-GAKSUc/edit?usp=sharing [Refactoring] * [ ] - Task * https://issues.apache.org/jira/browse/ZEPPELIN-3111 * Unit test is added. * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2709 from zjffdu/ZEPPELIN-3111 and squashes the following commits: aae4b09 [Jeff Zhang] ZEPPELIN-3111. Refactor SparkInterpreter (cherry picked from commit d762b5288536201d8a2964891c556efaa1bae867) Signed-off-by: Jeff Zhang --- .travis.yml | 32 +- bin/interpreter.sh | 2 +- docs/interpreter/spark.md | 4 + pom.xml | 19 +- python/pom.xml | 41 +- .../zeppelin/python/IPythonInterpreter.java | 6 +- .../zeppelin/python/PythonInterpreter.java | 8 +- .../python/IPythonInterpreterTest.java | 14 +- .../PythonInterpreterMatplotlibTest.java | 2 +- .../python/PythonInterpreterTest.java | 2 +- r/pom.xml | 7 - spark/interpreter/figure/null-1.png | Bin 0 -> 13599 bytes spark/interpreter/pom.xml | 573 ++++++++++++ .../spark/AbstractSparkInterpreter.java | 57 ++ .../apache/zeppelin/spark/DepInterpreter.java | 12 +- .../zeppelin/spark/IPySparkInterpreter.java | 6 +- .../zeppelin/spark/NewSparkInterpreter.java | 390 ++++++++ .../zeppelin/spark/OldSparkInterpreter.java} | 88 +- .../zeppelin/spark/PySparkInterpreter.java | 54 +- .../apache/zeppelin/spark/PythonUtils.java | 0 .../zeppelin/spark/SparkInterpreter.java | 163 ++++ .../zeppelin/spark/SparkRInterpreter.java | 6 +- .../zeppelin/spark/SparkSqlInterpreter.java | 0 .../apache/zeppelin/spark/SparkVersion.java | 0 .../zeppelin/spark/SparkZeppelinContext.java | 8 +- .../java/org/apache/zeppelin/spark/Utils.java | 0 .../org/apache/zeppelin/spark/ZeppelinR.java | 0 .../zeppelin/spark/ZeppelinRContext.java | 0 .../spark/dep/SparkDependencyContext.java | 0 .../spark/dep/SparkDependencyResolver.java | 0 .../src/main/resources/R/zeppelin_sparkr.R | 0 .../main/resources/interpreter-setting.json | 7 + .../resources/python/zeppelin_ipyspark.py | 0 .../main/resources/python/zeppelin_pyspark.py | 0 .../org/apache/spark/SparkRBackend.scala | 0 .../zeppelin/spark/ZeppelinRDisplay.scala | 0 .../zeppelin/spark/utils/DisplayUtils.scala | 0 .../zeppelin/spark/DepInterpreterTest.java | 0 .../spark/IPySparkInterpreterTest.java | 62 +- .../spark/NewSparkInterpreterTest.java | 389 ++++++++ .../spark/NewSparkSqlInterpreterTest.java | 173 ++++ .../spark/OldSparkInterpreterTest.java} | 73 +- .../spark/OldSparkSqlInterpreterTest.java} | 41 +- .../PySparkInterpreterMatplotlibTest.java | 37 +- .../spark/PySparkInterpreterTest.java | 9 +- .../zeppelin/spark/SparkRInterpreterTest.java | 99 ++ .../zeppelin/spark/SparkVersionTest.java | 0 .../src/test/resources/log4j.properties | 3 + .../spark/utils/DisplayFunctionsTest.scala | 0 spark/pom.xml | 871 +++++------------- spark/scala-2.10/pom.xml | 41 + spark/scala-2.10/spark-scala-parent | 1 + .../spark/SparkScala210Interpreter.scala | 141 +++ spark/scala-2.11/pom.xml | 41 + spark/scala-2.11/spark-scala-parent | 1 + .../src/main/resources/log4j.properties | 50 + .../spark/SparkScala211Interpreter.scala | 140 +++ .../spark-dependencies}/pom.xml | 519 +---------- spark/spark-scala-parent/pom.xml | 172 ++++ .../spark/BaseSparkScalaInterpreter.scala | 338 +++++++ .../dep/SparkDependencyResolverTest.java | 51 - testing/install_external_dependencies.sh | 4 +- zeppelin-display/pom.xml | 12 +- .../integration/SparkParagraphIT.java | 2 +- .../interpreter/BaseZeppelinContext.java | 2 + .../remote/RemoteInterpreterServer.java | 12 +- zeppelin-server/pom.xml | 6 + .../zeppelin/rest/AbstractTestRestApi.java | 15 +- .../rest/ZeppelinSparkClusterTest.java | 5 +- zeppelin-zengine/pom.xml | 2 +- 70 files changed, 3357 insertions(+), 1456 deletions(-) create mode 100644 spark/interpreter/figure/null-1.png create mode 100644 spark/interpreter/pom.xml create mode 100644 spark/interpreter/src/main/java/org/apache/zeppelin/spark/AbstractSparkInterpreter.java rename spark/{ => interpreter}/src/main/java/org/apache/zeppelin/spark/DepInterpreter.java (95%) rename spark/{ => interpreter}/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java (94%) create mode 100644 spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java rename spark/{src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java => interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java} (96%) rename spark/{ => interpreter}/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java (97%) rename spark/{ => interpreter}/src/main/java/org/apache/zeppelin/spark/PythonUtils.java (100%) create mode 100644 spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java rename spark/{ => interpreter}/src/main/java/org/apache/zeppelin/spark/SparkRInterpreter.java (97%) rename spark/{ => interpreter}/src/main/java/org/apache/zeppelin/spark/SparkSqlInterpreter.java (100%) rename spark/{ => interpreter}/src/main/java/org/apache/zeppelin/spark/SparkVersion.java (100%) rename spark/{ => interpreter}/src/main/java/org/apache/zeppelin/spark/SparkZeppelinContext.java (98%) rename spark/{ => interpreter}/src/main/java/org/apache/zeppelin/spark/Utils.java (100%) rename spark/{ => interpreter}/src/main/java/org/apache/zeppelin/spark/ZeppelinR.java (100%) rename spark/{ => interpreter}/src/main/java/org/apache/zeppelin/spark/ZeppelinRContext.java (100%) rename spark/{ => interpreter}/src/main/java/org/apache/zeppelin/spark/dep/SparkDependencyContext.java (100%) rename spark/{ => interpreter}/src/main/java/org/apache/zeppelin/spark/dep/SparkDependencyResolver.java (100%) rename spark/{ => interpreter}/src/main/resources/R/zeppelin_sparkr.R (100%) rename spark/{ => interpreter}/src/main/resources/interpreter-setting.json (96%) rename spark/{ => interpreter}/src/main/resources/python/zeppelin_ipyspark.py (100%) rename spark/{ => interpreter}/src/main/resources/python/zeppelin_pyspark.py (100%) rename spark/{ => interpreter}/src/main/scala/org/apache/spark/SparkRBackend.scala (100%) rename spark/{ => interpreter}/src/main/scala/org/apache/zeppelin/spark/ZeppelinRDisplay.scala (100%) rename spark/{ => interpreter}/src/main/scala/org/apache/zeppelin/spark/utils/DisplayUtils.scala (100%) rename spark/{ => interpreter}/src/test/java/org/apache/zeppelin/spark/DepInterpreterTest.java (100%) rename spark/{ => interpreter}/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java (82%) create mode 100644 spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java create mode 100644 spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkSqlInterpreterTest.java rename spark/{src/test/java/org/apache/zeppelin/spark/SparkInterpreterTest.java => interpreter/src/test/java/org/apache/zeppelin/spark/OldSparkInterpreterTest.java} (87%) rename spark/{src/test/java/org/apache/zeppelin/spark/SparkSqlInterpreterTest.java => interpreter/src/test/java/org/apache/zeppelin/spark/OldSparkSqlInterpreterTest.java} (86%) rename spark/{ => interpreter}/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterMatplotlibTest.java (94%) rename spark/{ => interpreter}/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterTest.java (97%) create mode 100644 spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkRInterpreterTest.java rename spark/{ => interpreter}/src/test/java/org/apache/zeppelin/spark/SparkVersionTest.java (100%) rename spark/{ => interpreter}/src/test/resources/log4j.properties (97%) rename spark/{ => interpreter}/src/test/scala/org/apache/zeppelin/spark/utils/DisplayFunctionsTest.scala (100%) create mode 100644 spark/scala-2.10/pom.xml create mode 120000 spark/scala-2.10/spark-scala-parent create mode 100644 spark/scala-2.10/src/main/scala/org/apache/zeppelin/spark/SparkScala210Interpreter.scala create mode 100644 spark/scala-2.11/pom.xml create mode 120000 spark/scala-2.11/spark-scala-parent create mode 100644 spark/scala-2.11/src/main/resources/log4j.properties create mode 100644 spark/scala-2.11/src/main/scala/org/apache/zeppelin/spark/SparkScala211Interpreter.scala rename {spark-dependencies => spark/spark-dependencies}/pom.xml (57%) create mode 100644 spark/spark-scala-parent/pom.xml create mode 100644 spark/spark-scala-parent/src/main/scala/org/apache/zeppelin/spark/BaseSparkScalaInterpreter.scala delete mode 100644 spark/src/test/java/org/apache/zeppelin/spark/dep/SparkDependencyResolverTest.java diff --git a/.travis.yml b/.travis.yml index 5f44dcdfd76..fd8c868cebd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,7 +68,7 @@ matrix: dist: trusty addons: firefox: "31.0" - env: PYTHON="3" SCALA_VER="2.11" SPARK_VER="2.2.0" HADOOP_VER="2.6" PROFILE="-Pspark-2.2 -Pweb-ci -Pscalding -Phelium-dev -Pexamples -Pscala-2.11" BUILD_FLAG="package -Pbuild-distr -DskipRat" TEST_FLAG="verify -Pusing-packaged-distr -DskipRat" MODULES="-pl ${INTERPRETERS}" TEST_PROJECTS="-Dtests.to.exclude=**/ZeppelinSparkClusterTest.java,**/org.apache.zeppelin.spark.*,**/HeliumApplicationFactoryTest.java -DfailIfNoTests=false" + env: PYTHON="3" SCALA_VER="2.11" SPARK_VER="2.2.0" HADOOP_VER="2.6" PROFILE="-Pspark-2.2 -Pweb-ci -Pscalding -Phelium-dev -Pexamples -Pscala-2.11" BUILD_FLAG="package -Pbuild-distr -DskipRat" TEST_FLAG="verify -Pusing-packaged-distr -DskipRat" MODULES="-pl ${INTERPRETERS}" TEST_PROJECTS="-Dtests.to.exclude=**/ZeppelinSparkClusterTest.java,**/org/apache/zeppelin/spark/*,**/HeliumApplicationFactoryTest.java -DfailIfNoTests=false" # Test selenium with spark module for 1.6.3 - jdk: "oraclejdk8" @@ -82,43 +82,43 @@ matrix: dist: trusty env: PYTHON="3" SCALA_VER="2.10" PROFILE="-Pscalding" BUILD_FLAG="install -DskipTests -DskipRat -Pr" TEST_FLAG="test -DskipRat" MODULES="-pl $(echo .,zeppelin-interpreter,${INTERPRETERS} | sed 's/!//g')" TEST_PROJECTS="" - # Test spark module for 2.2.0 with scala 2.11, livy + # Test spark module for 2.2.0 with scala 2.11 - jdk: "oraclejdk8" dist: trusty - env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.2.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.2 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark,python,livy" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.livy.* -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.2.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.2 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.livy.* -DfailIfNoTests=false" - # Test spark module for 2.1.0 with scala 2.11, livy + # Test spark module for 2.1.0 with scala 2.11 - jdk: "openjdk7" dist: trusty - env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.1.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.1 -Phadoop2 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark,python,livy" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.livy.* -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.1.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.1 -Phadoop2 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.livy.* -DfailIfNoTests=false" # Test spark module for 2.0.2 with scala 2.11 - jdk: "oraclejdk8" dist: trusty - env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.0.2" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.0 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.0.2" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.0 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" - # Test spark module for 1.6.3 with scala 2.10 + # Test spark module for 1.6.3 with scala 2.11 - jdk: "openjdk7" dist: trusty - env: PYTHON="3" SCALA_VER="2.10" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop2 -Phadoop-2.6 -Pscala-2.10" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + env: PYTHON="3" SCALA_VER="2.10" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop2 -Phadoop-2.6 -Pscala-2.10" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.spark.* -DfailIfNoTests=false" # Test spark module for 1.6.3 with scala 2.11 - jdk: "oraclejdk8" dist: trusty - env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" # Test python/pyspark with python 2, livy 0.2 - sudo: required dist: trusty jdk: "openjdk7" - env: PYTHON="2" SCALA_VER="2.10" SPARK_VER="1.6.1" HADOOP_VER="2.6" LIVY_VER="0.4.0-incubating" PROFILE="-Pspark-1.6 -Phadoop2 -Phadoop-2.6 -Pscala-2.10" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark-dependencies,spark,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.10" SPARK_VER="1.6.1" HADOOP_VER="2.6" LIVY_VER="0.4.0-incubating" PROFILE="-Pspark-1.6 -Phadoop2 -Phadoop-2.6 -Plivy-0.2 -Pscala-2.10" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" # Test python/pyspark with python 3, livy 0.3 - sudo: required dist: trusty jdk: "openjdk7" - env: PYTHON="3" SCALA_VER="2.11" SPARK_VER="2.0.0" HADOOP_VER="2.6" LIVY_VER="0.4.0-incubating" PROFILE="-Pspark-2.0 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark-dependencies,spark,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" - + env: PYTHON="3" SCALA_VER="2.11" SPARK_VER="2.0.0" HADOOP_VER="2.6" LIVY_VER="0.4.0-incubating" PROFILE="-Pspark-2.0 -Phadoop3 -Phadoop-2.6 -Pscala-2.11 -Plivy-0.3" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" + before_install: # check files included in commit range, clear bower_components if a bower.json file has changed. # bower cache clearing can also be forced by putting "bower clear" or "clear bower" in a commit message @@ -133,7 +133,7 @@ before_install: - ls -la .spark-dist ${HOME}/.m2/repository/.cache/maven-download-plugin || true - ls .node_modules && cp -r .node_modules zeppelin-web/node_modules || echo "node_modules are not cached" - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1600x1024x16" - - ./dev/change_scala_version.sh $SCALA_VER + #- ./dev/change_scala_version.sh $SCALA_VER - source ~/.environ install: @@ -145,9 +145,11 @@ before_script: - if [[ -n $LIVY_VER ]]; then ./testing/downloadLivy.sh $LIVY_VER; fi - if [[ -n $LIVY_VER ]]; then export LIVY_HOME=`pwd`/livy-$LIVY_VER-bin; fi - if [[ -n $LIVY_VER ]]; then export SPARK_HOME=`pwd`/spark-$SPARK_VER-bin-hadoop$HADOOP_VER; fi - - export SPARK_HOME=`pwd`/spark-$SPARK_VER-bin-hadoop$HADOOP_VER - - echo "export SPARK_HOME=`pwd`/spark-$SPARK_VER-bin-hadoop$HADOOP_VER" > conf/zeppelin-env.sh + - if [[ -n $SPARK_VER ]]; then export SPARK_HOME=`pwd`/spark-$SPARK_VER-bin-hadoop$HADOOP_VER; fi + - if [[ -n $SPARK_VER ]]; then echo "export SPARK_HOME=`pwd`/spark-$SPARK_VER-bin-hadoop$HADOOP_VER" > conf/zeppelin-env.sh; fi - echo "export ZEPPELIN_HELIUM_REGISTRY=helium" >> conf/zeppelin-env.sh + - echo "export SPARK_PRINT_LAUNCH_COMMAND=true" >> conf/zeppelin-env.sh + - export SPARK_PRINT_LAUNCH_COMMAND=true - tail conf/zeppelin-env.sh # https://docs.travis-ci.com/user/gui-and-headless-browsers/#Using-xvfb-to-Run-Tests-That-Require-a-GUI - if [[ -n $TEST_MODULES ]]; then export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start; sleep 3; fi diff --git a/bin/interpreter.sh b/bin/interpreter.sh index aa256460387..45ee0ce37f6 100755 --- a/bin/interpreter.sh +++ b/bin/interpreter.sh @@ -121,7 +121,7 @@ if [[ "${INTERPRETER_ID}" == "spark" ]]; then fi if [[ -n "${SPARK_HOME}" ]]; then export SPARK_SUBMIT="${SPARK_HOME}/bin/spark-submit" - SPARK_APP_JAR="$(ls ${ZEPPELIN_HOME}/interpreter/spark/zeppelin-spark*.jar)" + SPARK_APP_JAR="$(ls ${ZEPPELIN_HOME}/interpreter/spark/spark-interpreter*.jar)" # This will evantually passes SPARK_APP_JAR to classpath of SparkIMain ZEPPELIN_INTP_CLASSPATH+=":${SPARK_APP_JAR}" diff --git a/docs/interpreter/spark.md b/docs/interpreter/spark.md index da957c60c6c..90b1608d3de 100644 --- a/docs/interpreter/spark.md +++ b/docs/interpreter/spark.md @@ -199,6 +199,10 @@ Zeppelin support both yarn client and yarn cluster mode (yarn cluster mode is su You can either specify them in `zeppelin-env.sh`, or in interpreter setting page. Specifying them in `zeppelin-env.sh` means you can use only one version of `spark` & `hadoop`. Specifying them in interpreter setting page means you can use multiple versions of `spark` & `hadoop` in one zeppelin instance. +### 4. New Version of SparkInterpreter +There's one new version of SparkInterpreter starting with better spark support and code completion from Zeppelin 0.8.0, by default we still use the old version of SparkInterpreter. +If you want to use the new one, you can configure `zeppelin.spark.useNew` as `true` in its interpreter setting. + ## SparkContext, SQLContext, SparkSession, ZeppelinContext SparkContext, SQLContext and ZeppelinContext are automatically created and exposed as variable names `sc`, `sqlContext` and `z`, respectively, in Scala, Python and R environments. Staring from 0.6.1 SparkSession is available as variable `spark` when you are using Spark 2.x. diff --git a/pom.xml b/pom.xml index bb1a1e2d68b..25d0e0881fd 100644 --- a/pom.xml +++ b/pom.xml @@ -56,9 +56,11 @@ zeppelin-interpreter zeppelin-zengine zeppelin-display - spark-dependencies groovy - spark + spark/scala-2.10 + spark/scala-2.11 + spark/interpreter + spark/spark-dependencies markdown angular shell @@ -86,6 +88,7 @@ + 1.7 2.10.5 2.10 2.2.4 @@ -329,8 +332,8 @@ maven-compiler-plugin ${plugin.compiler.version} - 1.7 - 1.7 + ${java.version} + ${java.version} @@ -731,9 +734,6 @@ scala-2.10 - - true - 2.10.5 2.10 @@ -742,8 +742,11 @@ scala-2.11 + + true + - 2.11.7 + 2.11.8 2.11 diff --git a/python/pom.xml b/python/pom.xml index a906b5d5f50..7f8e671e5c0 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -43,6 +43,7 @@ https://pypi.python.org/packages /64/5c/01e13b68e8caafece40d549f232c9b5677ad1016071a48d04cc3895acaa3 1.4.0 + 2.4.1 @@ -90,13 +91,7 @@ grpc-stub ${grpc.version} - - - com.google.guava - guava - 18.0 - - + junit @@ -202,6 +197,38 @@ + + org.apache.maven.plugins + maven-shade-plugin + ${plugin.shade.version} + + + + + reference.conf + + + + + com.google.common + org.apache.zeppelin.com.google.common + + + py4j + org.apache.zeppelin.py4j + + + + + + package + + shade + + + + + maven-enforcer-plugin diff --git a/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java b/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java index bd687befc99..81cfeb24d6c 100644 --- a/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java +++ b/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java @@ -299,7 +299,7 @@ protected Map setupIPythonEnv() throws IOException { } @Override - public void close() { + public void close() throws InterpreterException { if (watchDog != null) { LOGGER.debug("Kill IPython Process"); ipythonClient.stop(StopRequest.newBuilder().build()); @@ -327,7 +327,7 @@ public InterpreterResult interpret(String st, InterpreterContext context) { } @Override - public void cancel(InterpreterContext context) { + public void cancel(InterpreterContext context) throws InterpreterException { ipythonClient.cancel(CancelRequest.newBuilder().build()); } @@ -337,7 +337,7 @@ public FormType getFormType() { } @Override - public int getProgress(InterpreterContext context) { + public int getProgress(InterpreterContext context) throws InterpreterException { return 0; } diff --git a/python/src/main/java/org/apache/zeppelin/python/PythonInterpreter.java b/python/src/main/java/org/apache/zeppelin/python/PythonInterpreter.java index b13cb8afdc8..028f1c6a802 100644 --- a/python/src/main/java/org/apache/zeppelin/python/PythonInterpreter.java +++ b/python/src/main/java/org/apache/zeppelin/python/PythonInterpreter.java @@ -285,7 +285,7 @@ private IPythonInterpreter getIPythonInterpreter() { } @Override - public void close() { + public void close() throws InterpreterException { if (iPythonInterpreter != null) { iPythonInterpreter.close(); return; @@ -463,7 +463,7 @@ public InterpreterContext getCurrentInterpreterContext() { return context; } - public void interrupt() throws IOException { + public void interrupt() throws IOException, InterpreterException { if (pythonPid > -1) { logger.info("Sending SIGINT signal to PID : " + pythonPid); Runtime.getRuntime().exec("kill -SIGINT " + pythonPid); @@ -474,7 +474,7 @@ public void interrupt() throws IOException { } @Override - public void cancel(InterpreterContext context) { + public void cancel(InterpreterContext context) throws InterpreterException { if (iPythonInterpreter != null) { iPythonInterpreter.cancel(context); } @@ -491,7 +491,7 @@ public FormType getFormType() { } @Override - public int getProgress(InterpreterContext context) { + public int getProgress(InterpreterContext context) throws InterpreterException { if (iPythonInterpreter != null) { return iPythonInterpreter.getProgress(context); } diff --git a/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java b/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java index d89ddac4469..cb854d65704 100644 --- a/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java @@ -66,7 +66,7 @@ public void setUp() throws InterpreterException { } @After - public void close() { + public void close() throws InterpreterException { interpreter.close(); } @@ -81,6 +81,9 @@ public static void testInterpreter(final Interpreter interpreter) throws IOExcep InterpreterResult result = interpreter.interpret("from __future__ import print_function", getInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + result = interpreter.interpret("import sys\nprint(sys.version_info)", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + // single output without print InterpreterContext context = getInterpreterContext(); result = interpreter.interpret("'hello world'", context); @@ -195,6 +198,9 @@ public static void testInterpreter(final Interpreter interpreter) throws IOExcep context = getInterpreterContext(); completions = interpreter.completion("sys.std", 7, context); + for (InterpreterCompletion completion : completions) { + System.out.println(completion.getValue()); + } assertEquals(3, completions.size()); assertEquals("stderr", completions.get(0).getValue()); assertEquals("stdin", completions.get(1).getValue()); @@ -308,6 +314,7 @@ public void run() { context = getInterpreterContext(); result = interpreter.interpret("from bokeh.io import output_notebook, show\n" + "from bokeh.plotting import figure\n" + + "import bkzep\n" + "output_notebook(notebook_type='zeppelin')", context); Thread.sleep(100); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); @@ -329,10 +336,11 @@ public void run() { Thread.sleep(100); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); interpreterResultMessages = context.out.getInterpreterResultMessages(); - assertEquals(1, interpreterResultMessages.size()); + assertEquals(2, interpreterResultMessages.size()); assertEquals(InterpreterResult.Type.HTML, interpreterResultMessages.get(0).getType()); + assertEquals(InterpreterResult.Type.HTML, interpreterResultMessages.get(1).getType()); // docs_json is the source data of plotting which bokeh would use to render the plotting. - assertTrue(interpreterResultMessages.get(0).getData().contains("docs_json")); + assertTrue(interpreterResultMessages.get(1).getData().contains("docs_json")); // ggplot context = getInterpreterContext(); diff --git a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterMatplotlibTest.java b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterMatplotlibTest.java index 8c088dcb756..1ab9cf197a8 100644 --- a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterMatplotlibTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterMatplotlibTest.java @@ -80,7 +80,7 @@ public void setUp() throws Exception { } @After - public void afterTest() throws IOException { + public void afterTest() throws IOException, InterpreterException { python.close(); } diff --git a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java index 4f08d50cd05..1143b9e4629 100644 --- a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java @@ -93,7 +93,7 @@ public void beforeTest() throws IOException, InterpreterException { } @After - public void afterTest() throws IOException { + public void afterTest() throws IOException, InterpreterException { pythonInterpreter.close(); } diff --git a/r/pom.xml b/r/pom.xml index 4c1b218335d..10615d780ee 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -68,13 +68,6 @@ provided - - ${project.groupId} - zeppelin-spark-dependencies_${scala.binary.version} - ${project.version} - provided - - ${project.groupId} zeppelin-interpreter diff --git a/spark/interpreter/figure/null-1.png b/spark/interpreter/figure/null-1.png new file mode 100644 index 0000000000000000000000000000000000000000..8b1ce07ea9e7d0f24bae214f3bda98a7787ee662 GIT binary patch literal 13599 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 literal 0 HcmV?d00001 diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml new file mode 100644 index 00000000000..449646242dc --- /dev/null +++ b/spark/interpreter/pom.xml @@ -0,0 +1,573 @@ + + + + + 4.0.0 + + + spark-parent + org.apache.zeppelin + 0.9.0-SNAPSHOT + ../pom.xml + + + org.apache.zeppelin + spark-interpreter + jar + 0.9.0-SNAPSHOT + Zeppelin: Spark Interpreter + Zeppelin spark support + + + spark + + 1.8.2 + 1.3 + 1.9 + 3.0 + 1.12 + 3.0.3 + 1.0 + + 3.2.9 + 3.2.6 + 3.2.10 + + ${scala.version} + + **/PySparkInterpreterMatplotlibTest.java + **/*Test.* + + + spark-${spark.version} + + http://d3kbcqa49mib13.cloudfront.net/${spark.archive}.tgz + + + http://d3kbcqa49mib13.cloudfront.net/spark-${spark.version}-bin-without-hadoop.tgz + + + + + + + org.apache.zeppelin + zeppelin-display + ${project.version} + + + + org.apache.zeppelin + spark-scala-2.11 + ${project.version} + + + + org.apache.zeppelin + spark-scala-2.10 + ${project.version} + + + + org.apache.zeppelin + zeppelin-interpreter + ${project.version} + + + + org.apache.zeppelin + zeppelin-python + ${project.version} + + + net.sf.py4j + py4j + + + + + + ${project.groupId} + zeppelin-python + ${project.version} + tests + test + + + net.sf.py4j + py4j + + + + + + org.apache.spark + spark-repl_${scala.binary.version} + ${spark.version} + provided + + + + org.apache.spark + spark-core_${scala.binary.version} + ${spark.version} + provided + + + + org.apache.spark + spark-hive_${scala.binary.version} + ${spark.version} + provided + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + + + + + + + org.apache.maven + maven-plugin-api + ${maven.plugin.api.version} + + + org.codehaus.plexus + plexus-utils + + + org.sonatype.sisu + sisu-inject-plexus + + + org.apache.maven + maven-model + + + + + + org.sonatype.aether + aether-api + ${aether.version} + + + + org.sonatype.aether + aether-util + ${aether.version} + + + + org.sonatype.aether + aether-impl + ${aether.version} + + + + org.apache.maven + maven-aether-provider + ${maven.aeither.provider.version} + + + org.sonatype.aether + aether-api + + + org.sonatype.aether + aether-spi + + + org.sonatype.aether + aether-util + + + org.sonatype.aether + aether-impl + + + org.codehaus.plexus + plexus-utils + + + + + + org.sonatype.aether + aether-connector-file + ${aether.version} + + + + org.sonatype.aether + aether-connector-wagon + ${aether.version} + + + org.apache.maven.wagon + wagon-provider-api + + + + + + org.apache.maven.wagon + wagon-provider-api + ${wagon.version} + + + org.codehaus.plexus + plexus-utils + + + + + + org.apache.maven.wagon + wagon-http-lightweight + ${wagon.version} + + + org.apache.maven.wagon + wagon-http-shared + + + + + + org.apache.maven.wagon + wagon-http + ${wagon.version} + + + + + + org.apache.commons + commons-exec + ${commons.exec.version} + + + + org.scala-lang + scala-library + ${scala.version} + provided + + + + org.scala-lang + scala-compiler + ${scala.version} + provided + + + + org.scala-lang + scala-reflect + ${scala.version} + provided + + + + commons-lang + commons-lang + provided + + + + org.apache.commons + commons-compress + ${commons.compress.version} + provided + + + + org.jsoup + jsoup + ${jsoup.version} + + + + + org.scalatest + scalatest_${scala.binary.version} + ${scalatest.version} + test + + + + junit + junit + test + + + + org.datanucleus + datanucleus-core + ${datanucleus.core.version} + test + + + + org.datanucleus + datanucleus-api-jdo + ${datanucleus.apijdo.version} + test + + + + org.datanucleus + datanucleus-rdbms + ${datanucleus.rdbms.version} + test + + + + org.mockito + mockito-core + test + + + + org.powermock + powermock-api-mockito + test + + + + org.powermock + powermock-module-junit4 + test + + + + + + + + maven-enforcer-plugin + + + enforce + none + + + + + + + 1.7 + + + + + + + com.googlecode.maven-download-plugin + download-maven-plugin + + + download-pyspark-files + validate + + wget + + + 60000 + 5 + true + ${spark.src.download.url} + ${project.build.directory} + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + zip-pyspark-files + generate-resources + + run + + + + + + + + + + + + + + org.scalatest + scalatest-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + + 1 + false + -Xmx1024m -XX:MaxPermSize=256m + + **/SparkRInterpreterTest.java + ${pyspark.test.exclude} + ${tests.to.exclude} + + + ${project.build.directory}/../../../interpreter/spark/pyspark/pyspark.zip:${project.build.directory}/../../../interpreter/lib/python/:${project.build.directory}/../../../interpreter/spark/pyspark/py4j-${py4j.version}-src.zip:. + ${basedir}/../../ + + + + + + org.apache.maven.plugins + maven-shade-plugin + ${plugin.shade.version} + + + + + *:* + + org/datanucleus/** + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + reference.conf + + + + + io.netty + org.apache.zeppelin.io.netty + + + com.google + org.apache.zeppelin.com.google + + + py4j. + org.apache.zeppelin.py4j. + + + + + + package + + shade + + + + + + + + maven-dependency-plugin + + + copy-dependencies + none + + true + + + + + copy-interpreter-dependencies + none + + true + + + + copy-artifact + none + + true + + + + + + copy-spark-interpreter + package + + copy + + + ${project.build.directory}/../../../interpreter/spark + false + false + true + + + ${project.groupId} + ${project.artifactId} + ${project.version} + ${project.packaging} + + + + + + + + + + maven-resources-plugin + + + copy-interpreter-setting + package + + resources + + + ${project.build.directory}/../../../interpreter/${interpreter.name} + + + + + + + + + diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/AbstractSparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/AbstractSparkInterpreter.java new file mode 100644 index 00000000000..9968dc6e5f1 --- /dev/null +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/AbstractSparkInterpreter.java @@ -0,0 +1,57 @@ +/* + * 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.spark; + +import org.apache.spark.SparkContext; +import org.apache.spark.api.java.JavaSparkContext; +import org.apache.spark.sql.SQLContext; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; + +import java.util.Properties; + +/** + * Abstract class for SparkInterpreter. For the purpose of co-exist of NewSparkInterpreter + * and OldSparkInterpreter + */ +public abstract class AbstractSparkInterpreter extends Interpreter { + + public AbstractSparkInterpreter(Properties properties) { + super(properties); + } + + public abstract SparkContext getSparkContext(); + + public abstract SQLContext getSQLContext(); + + public abstract Object getSparkSession(); + + public abstract boolean isSparkContextInitialized(); + + public abstract SparkVersion getSparkVersion(); + + public abstract JavaSparkContext getJavaSparkContext(); + + public abstract void populateSparkWebUrl(InterpreterContext ctx); + + public abstract SparkZeppelinContext getZeppelinContext(); + + public abstract String getSparkUIUrl(); + + public abstract boolean isUnsupportedSparkVersion(); +} diff --git a/spark/src/main/java/org/apache/zeppelin/spark/DepInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/DepInterpreter.java similarity index 95% rename from spark/src/main/java/org/apache/zeppelin/spark/DepInterpreter.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/DepInterpreter.java index 6b1f0a9da91..df0a48416a4 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/DepInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/DepInterpreter.java @@ -176,7 +176,7 @@ private void createIMain() { } depc = new SparkDependencyContext(getProperty("zeppelin.dep.localrepo"), - getProperty("zeppelin.dep.additionalRemoteRepository")); + getProperty("zeppelin.dep.additionalRemoteRepository")); if (Utils.isScala2_10()) { completer = Utils.instantiateClass( "org.apache.spark.repl.SparkJLineCompletion", @@ -208,7 +208,7 @@ private Results.Result interpret(String line) { public Object getValue(String name) { Object ret = Utils.invokeMethod( - intp, "valueOfTerm", new Class[]{String.class}, new Object[]{name}); + intp, "valueOfTerm", new Class[]{String.class}, new Object[]{name}); if (ret instanceof None) { return null; } else if (ret instanceof Some) { @@ -233,11 +233,11 @@ public InterpreterResult interpret(String st, InterpreterContext context) { SparkInterpreter sparkInterpreter = getSparkInterpreter(); - if (sparkInterpreter != null && sparkInterpreter.isSparkContextInitialized()) { + if (sparkInterpreter != null && sparkInterpreter.getDelegation().isSparkContextInitialized()) { return new InterpreterResult(Code.ERROR, "Must be used before SparkInterpreter (%spark) initialized\n" + - "Hint: put this paragraph before any Spark code and " + - "restart Zeppelin/Interpreter" ); + "Hint: put this paragraph before any Spark code and " + + "restart Zeppelin/Interpreter" ); } scala.tools.nsc.interpreter.Results.Result ret = interpret(st); @@ -287,7 +287,7 @@ public int getProgress(InterpreterContext context) { @Override public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { + InterpreterContext interpreterContext) { if (Utils.isScala2_10()) { ScalaCompleter c = (ScalaCompleter) Utils.invokeMethod(completer, "completer"); Candidates ret = c.complete(buf, cursor); diff --git a/spark/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java similarity index 94% rename from spark/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java index a0505692d56..c7253fb40c9 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java @@ -92,13 +92,13 @@ private SparkInterpreter getSparkInterpreter() throws InterpreterException { } @Override - public void cancel(InterpreterContext context) { + public void cancel(InterpreterContext context) throws InterpreterException { super.cancel(context); sparkInterpreter.cancel(context); } @Override - public void close() { + public void close() throws InterpreterException { super.close(); if (sparkInterpreter != null) { sparkInterpreter.close(); @@ -106,7 +106,7 @@ public void close() { } @Override - public int getProgress(InterpreterContext context) { + public int getProgress(InterpreterContext context) throws InterpreterException { return sparkInterpreter.getProgress(context); } diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java new file mode 100644 index 00000000000..1d3ccd65fde --- /dev/null +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java @@ -0,0 +1,390 @@ +/* + * 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.spark; + +import com.google.common.collect.Lists; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.spark.SparkConf; +import org.apache.spark.SparkContext; +import org.apache.spark.api.java.JavaSparkContext; +import org.apache.spark.scheduler.SparkListenerJobStart; +import org.apache.spark.sql.SQLContext; +import org.apache.spark.ui.jobs.JobProgressListener; +import org.apache.zeppelin.interpreter.BaseZeppelinContext; +import org.apache.zeppelin.interpreter.DefaultInterpreterProperty; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterHookRegistry; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.WrappedInterpreter; +import org.apache.zeppelin.interpreter.remote.RemoteEventClientWrapper; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.apache.zeppelin.spark.dep.SparkDependencyContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * SparkInterpreter of Java implementation. It is just wrapper of Spark211Interpreter + * and Spark210Interpreter. + */ +public class NewSparkInterpreter extends AbstractSparkInterpreter { + + private static final Logger LOGGER = LoggerFactory.getLogger(SparkInterpreter.class); + + private BaseSparkScalaInterpreter innerInterpreter; + private Map innerInterpreterClassMap = new HashMap<>(); + private SparkContext sc; + private JavaSparkContext jsc; + private SQLContext sqlContext; + private Object sparkSession; + + private SparkZeppelinContext z; + private SparkVersion sparkVersion; + private boolean enableSupportedVersionCheck; + private String sparkUrl; + + private static InterpreterHookRegistry hooks; + + + public NewSparkInterpreter(Properties properties) { + super(properties); + this.enableSupportedVersionCheck = java.lang.Boolean.parseBoolean( + properties.getProperty("zeppelin.spark.enableSupportedVersionCheck", "true")); + innerInterpreterClassMap.put("2.10", "org.apache.zeppelin.spark.SparkScala210Interpreter"); + innerInterpreterClassMap.put("2.11", "org.apache.zeppelin.spark.SparkScala211Interpreter"); + } + + @Override + public void open() throws InterpreterException { + try { + String scalaVersion = extractScalaVersion(); + LOGGER.info("Using Scala Version: " + scalaVersion); + setupConfForPySpark(); + SparkConf conf = new SparkConf(); + for (Map.Entry entry : getProperties().entrySet()) { + if (!StringUtils.isBlank(entry.getValue().toString())) { + conf.set(entry.getKey().toString(), entry.getValue().toString()); + } + if (entry.getKey().toString().equals("zeppelin.spark.useHiveContext")) { + conf.set("spark.useHiveContext", entry.getValue().toString()); + } + } + // use local mode for embedded spark mode when spark.master is not found + conf.setIfMissing("spark.master", "local"); + + String innerIntpClassName = innerInterpreterClassMap.get(scalaVersion); + Class clazz = Class.forName(innerIntpClassName); + this.innerInterpreter = + (BaseSparkScalaInterpreter) clazz.getConstructor(SparkConf.class, List.class) + .newInstance(conf, getDependencyFiles()); + this.innerInterpreter.open(); + + sc = this.innerInterpreter.sc(); + jsc = JavaSparkContext.fromSparkContext(sc); + sparkVersion = SparkVersion.fromVersionString(sc.version()); + if (enableSupportedVersionCheck && sparkVersion.isUnsupportedVersion()) { + throw new Exception("This is not officially supported spark version: " + sparkVersion + + "\nYou can set zeppelin.spark.enableSupportedVersionCheck to false if you really" + + " want to try this version of spark."); + } + sqlContext = this.innerInterpreter.sqlContext(); + sparkSession = this.innerInterpreter.sparkSession(); + sparkUrl = this.innerInterpreter.sparkUrl(); + setupListeners(); + + hooks = getInterpreterGroup().getInterpreterHookRegistry(); + z = new SparkZeppelinContext(sc, hooks, + Integer.parseInt(getProperty("zeppelin.spark.maxResult"))); + this.innerInterpreter.bind("z", z.getClass().getCanonicalName(), z, + Lists.newArrayList("@transient")); + } catch (Exception e) { + LOGGER.error(ExceptionUtils.getStackTrace(e)); + throw new InterpreterException("Fail to open SparkInterpreter", e); + } + } + + private void setupConfForPySpark() { + String sparkHome = getProperty("SPARK_HOME"); + File pysparkFolder = null; + if (sparkHome == null) { + String zeppelinHome = + new DefaultInterpreterProperty("ZEPPELIN_HOME", "zeppelin.home", "../../") + .getValue().toString(); + pysparkFolder = new File(zeppelinHome, + "interpreter" + File.separator + "spark" + File.separator + "pyspark"); + } else { + pysparkFolder = new File(sparkHome, "python" + File.separator + "lib"); + } + + ArrayList pysparkPackages = new ArrayList<>(); + for (File file : pysparkFolder.listFiles()) { + if (file.getName().equals("pyspark.zip")) { + pysparkPackages.add(file.getAbsolutePath()); + } + if (file.getName().startsWith("py4j-")) { + pysparkPackages.add(file.getAbsolutePath()); + } + } + + if (pysparkPackages.size() != 2) { + throw new RuntimeException("Not correct number of pyspark packages: " + + StringUtils.join(pysparkPackages, ",")); + } + // Distribute two libraries(pyspark.zip and py4j-*.zip) to workers + System.setProperty("spark.files", mergeProperty(System.getProperty("spark.files", ""), + StringUtils.join(pysparkPackages, ","))); + System.setProperty("spark.submit.pyFiles", mergeProperty( + System.getProperty("spark.submit.pyFiles", ""), StringUtils.join(pysparkPackages, ","))); + + } + + private String mergeProperty(String originalValue, String appendedValue) { + if (StringUtils.isBlank(originalValue)) { + return appendedValue; + } + return originalValue + "," + appendedValue; + } + + @Override + public void close() { + LOGGER.info("Close SparkInterpreter"); + innerInterpreter.close(); + } + + @Override + public InterpreterResult interpret(String st, InterpreterContext context) { + InterpreterContext.set(context); + z.setGui(context.getGui()); + z.setNoteGui(context.getNoteGui()); + z.setInterpreterContext(context); + populateSparkWebUrl(context); + String jobDesc = "Started by: " + Utils.getUserName(context.getAuthenticationInfo()); + sc.setJobGroup(Utils.buildJobGroupId(context), jobDesc, false); + return innerInterpreter.interpret(st, context); + } + + @Override + public void cancel(InterpreterContext context) { + sc.cancelJobGroup(Utils.buildJobGroupId(context)); + } + + @Override + public List completion(String buf, + int cursor, + InterpreterContext interpreterContext) { + LOGGER.debug("buf: " + buf + ", cursor:" + cursor); + return innerInterpreter.completion(buf, cursor, interpreterContext); + } + + @Override + public FormType getFormType() { + return FormType.NATIVE; + } + + @Override + public int getProgress(InterpreterContext context) { + return innerInterpreter.getProgress(Utils.buildJobGroupId(context), context); + } + + private void setupListeners() { + JobProgressListener pl = new JobProgressListener(sc.getConf()) { + @Override + public synchronized void onJobStart(SparkListenerJobStart jobStart) { + super.onJobStart(jobStart); + int jobId = jobStart.jobId(); + String jobGroupId = jobStart.properties().getProperty("spark.jobGroup.id"); + String uiEnabled = jobStart.properties().getProperty("spark.ui.enabled"); + String jobUrl = getJobUrl(jobId); + String noteId = Utils.getNoteId(jobGroupId); + String paragraphId = Utils.getParagraphId(jobGroupId); + // Button visible if Spark UI property not set, set as invalid boolean or true + java.lang.Boolean showSparkUI = + uiEnabled == null || !uiEnabled.trim().toLowerCase().equals("false"); + if (showSparkUI && jobUrl != null) { + RemoteEventClientWrapper eventClient = BaseZeppelinContext.getEventClient(); + Map infos = new java.util.HashMap<>(); + infos.put("jobUrl", jobUrl); + infos.put("label", "SPARK JOB"); + infos.put("tooltip", "View in Spark web UI"); + if (eventClient != null) { + eventClient.onParaInfosReceived(noteId, paragraphId, infos); + } + } + } + + private String getJobUrl(int jobId) { + String jobUrl = null; + if (sparkUrl != null) { + jobUrl = sparkUrl + "/jobs/job?id=" + jobId; + } + return jobUrl; + } + }; + try { + Object listenerBus = sc.getClass().getMethod("listenerBus").invoke(sc); + Method[] methods = listenerBus.getClass().getMethods(); + Method addListenerMethod = null; + for (Method m : methods) { + if (!m.getName().equals("addListener")) { + continue; + } + Class[] parameterTypes = m.getParameterTypes(); + if (parameterTypes.length != 1) { + continue; + } + if (!parameterTypes[0].isAssignableFrom(JobProgressListener.class)) { + continue; + } + addListenerMethod = m; + break; + } + if (addListenerMethod != null) { + addListenerMethod.invoke(listenerBus, pl); + } + } catch (NoSuchMethodException | SecurityException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException e) { + LOGGER.error(e.toString(), e); + } + } + + public SparkZeppelinContext getZeppelinContext() { + return this.z; + } + + public SparkContext getSparkContext() { + return this.sc; + } + + @Override + public SQLContext getSQLContext() { + return sqlContext; + } + + public JavaSparkContext getJavaSparkContext() { + return this.jsc; + } + + public Object getSparkSession() { + return sparkSession; + } + + public SparkVersion getSparkVersion() { + return sparkVersion; + } + + private DepInterpreter getDepInterpreter() { + Interpreter p = getInterpreterInTheSameSessionByClassName(DepInterpreter.class.getName()); + if (p == null) { + return null; + } + + while (p instanceof WrappedInterpreter) { + p = ((WrappedInterpreter) p).getInnerInterpreter(); + } + return (DepInterpreter) p; + } + + private String extractScalaVersion() throws IOException, InterruptedException { + String scalaVersionString = scala.util.Properties.versionString(); + if (scalaVersionString.contains("version 2.10")) { + return "2.10"; + } else { + return "2.11"; + } + } + + public void populateSparkWebUrl(InterpreterContext ctx) { + Map infos = new java.util.HashMap<>(); + infos.put("url", sparkUrl); + String uiEnabledProp = properties.getProperty("spark.ui.enabled", "true"); + java.lang.Boolean uiEnabled = java.lang.Boolean.parseBoolean( + uiEnabledProp.trim()); + if (!uiEnabled) { + infos.put("message", "Spark UI disabled"); + } else { + if (StringUtils.isNotBlank(sparkUrl)) { + infos.put("message", "Spark UI enabled"); + } else { + infos.put("message", "No spark url defined"); + } + } + if (ctx != null && ctx.getClient() != null) { + LOGGER.debug("Sending metadata to Zeppelin server: {}", infos.toString()); + getZeppelinContext().setEventClient(ctx.getClient()); + ctx.getClient().onMetaInfosReceived(infos); + } + } + + public boolean isSparkContextInitialized() { + return this.sc != null; + } + + private List getDependencyFiles() { + List depFiles = new ArrayList<>(); + // add jar from DepInterpreter + DepInterpreter depInterpreter = getDepInterpreter(); + if (depInterpreter != null) { + SparkDependencyContext depc = depInterpreter.getDependencyContext(); + if (depc != null) { + List files = depc.getFilesDist(); + if (files != null) { + for (File f : files) { + depFiles.add(f.getAbsolutePath()); + } + } + } + } + + // add jar from local repo + String localRepo = getProperty("zeppelin.interpreter.localRepo"); + if (localRepo != null) { + File localRepoDir = new File(localRepo); + if (localRepoDir.exists()) { + File[] files = localRepoDir.listFiles(); + if (files != null) { + for (File f : files) { + depFiles.add(f.getAbsolutePath()); + } + } + } + } + return depFiles; + } + + @Override + public String getSparkUIUrl() { + return sparkUrl; + } + + @Override + public boolean isUnsupportedSparkVersion() { + return enableSupportedVersionCheck && sparkVersion.isUnsupportedVersion(); + } +} diff --git a/spark/src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java similarity index 96% rename from spark/src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java index 3e4da1918ad..6a54c3b37b9 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java @@ -97,8 +97,8 @@ * Spark interpreter for Zeppelin. * */ -public class SparkInterpreter extends Interpreter { - public static Logger logger = LoggerFactory.getLogger(SparkInterpreter.class); +public class OldSparkInterpreter extends AbstractSparkInterpreter { + public static Logger logger = LoggerFactory.getLogger(OldSparkInterpreter.class); private SparkZeppelinContext z; private SparkILoop interpreter; @@ -134,12 +134,12 @@ public class SparkInterpreter extends Interpreter { private JavaSparkContext jsc; private boolean enableSupportedVersionCheck; - public SparkInterpreter(Properties property) { + public OldSparkInterpreter(Properties property) { super(property); out = new InterpreterOutputStream(logger); } - public SparkInterpreter(Properties property, SparkContext sc) { + public OldSparkInterpreter(Properties property, SparkContext sc) { this(property); this.sc = sc; @@ -186,7 +186,7 @@ public synchronized void onJobStart(SparkListenerJobStart jobStart) { String paragraphId = Utils.getParagraphId(jobGroupId); // Button visible if Spark UI property not set, set as invalid boolean or true java.lang.Boolean showSparkUI = - uiEnabled == null || !uiEnabled.trim().toLowerCase().equals("false"); + uiEnabled == null || !uiEnabled.trim().toLowerCase().equals("false"); if (showSparkUI && jobUrl != null) { RemoteEventClientWrapper eventClient = BaseZeppelinContext.getEventClient(); Map infos = new java.util.HashMap<>(); @@ -443,7 +443,7 @@ public SparkContext createSparkContext_1() { jars = (String[]) Utils.invokeStaticMethod(SparkILoop.class, "getAddedJars"); } else { jars = (String[]) Utils.invokeStaticMethod( - Utils.findClass("org.apache.spark.repl.Main"), "getAddedJars"); + Utils.findClass("org.apache.spark.repl.Main"), "getAddedJars"); } String classServerUri = null; @@ -467,7 +467,7 @@ public SparkContext createSparkContext_1() { // continue instead of: throw new InterpreterException(e); // Newer Spark versions (like the patched CDH5.7.0 one) don't contain this method logger.warn(String.format("Spark method classServerUri not available due to: [%s]", - e.getMessage())); + e.getMessage())); } } @@ -477,7 +477,7 @@ public SparkContext createSparkContext_1() { File classOutputDirectory = (File) getClassOutputDirectory.invoke(intp); replClassOutputDirectory = classOutputDirectory.getAbsolutePath(); } catch (NoSuchMethodException | SecurityException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException e) { + | IllegalArgumentException | InvocationTargetException e) { // continue } } @@ -548,7 +548,7 @@ public void open() throws InterpreterException { System.setProperty("SPARK_YARN_MODE", "true"); } if (getProperties().containsKey("spark.yarn.keytab") && - getProperties().containsKey("spark.yarn.principal")) { + getProperties().containsKey("spark.yarn.principal")) { try { String keytab = getProperties().getProperty("spark.yarn.keytab"); String principal = getProperties().getProperty("spark.yarn.principal"); @@ -725,7 +725,7 @@ public void open() throws InterpreterException { * * As hashCode() can return a negative integer value and the minus character '-' is invalid * in a package name we change it to a numeric value '0' which still conforms to the regexp. - * + * */ System.setProperty("scala.repl.name.line", ("$line" + this.hashCode()).replace('-', '0')); @@ -805,11 +805,11 @@ public void open() throws InterpreterException { sqlc = getSQLContext(); dep = getDependencyResolver(); - + hooks = getInterpreterGroup().getInterpreterHookRegistry(); - z = new SparkZeppelinContext(sc, sqlc, hooks, - Integer.parseInt(getProperty("zeppelin.spark.maxResult"))); + z = new SparkZeppelinContext(sc, hooks, + Integer.parseInt(getProperty("zeppelin.spark.maxResult"))); interpret("@transient val _binder = new java.util.HashMap[String, Object]()"); Map binder; @@ -827,13 +827,13 @@ public void open() throws InterpreterException { } interpret("@transient val z = " - + "_binder.get(\"z\").asInstanceOf[org.apache.zeppelin.spark.SparkZeppelinContext]"); + + "_binder.get(\"z\").asInstanceOf[org.apache.zeppelin.spark.SparkZeppelinContext]"); interpret("@transient val sc = " - + "_binder.get(\"sc\").asInstanceOf[org.apache.spark.SparkContext]"); + + "_binder.get(\"sc\").asInstanceOf[org.apache.spark.SparkContext]"); interpret("@transient val sqlc = " - + "_binder.get(\"sqlc\").asInstanceOf[org.apache.spark.sql.SQLContext]"); + + "_binder.get(\"sqlc\").asInstanceOf[org.apache.spark.sql.SQLContext]"); interpret("@transient val sqlContext = " - + "_binder.get(\"sqlc\").asInstanceOf[org.apache.spark.sql.SQLContext]"); + + "_binder.get(\"sqlc\").asInstanceOf[org.apache.spark.sql.SQLContext]"); if (Utils.isSpark2()) { interpret("@transient val spark = " @@ -966,7 +966,7 @@ public void populateSparkWebUrl(InterpreterContext ctx) { infos.put("url", sparkUrl); String uiEnabledProp = getProperty("spark.ui.enabled", "true"); java.lang.Boolean uiEnabled = java.lang.Boolean.parseBoolean( - uiEnabledProp.trim()); + uiEnabledProp.trim()); if (!uiEnabled) { infos.put("message", "Spark UI disabled"); } else { @@ -1014,7 +1014,7 @@ private List classPath(ClassLoader cl) { @Override public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { + InterpreterContext interpreterContext) { if (completer == null) { logger.warn("Can't find completer"); return new LinkedList<>(); @@ -1025,29 +1025,29 @@ public List completion(String buf, int cursor, } ScalaCompleter c = (ScalaCompleter) Utils.invokeMethod(completer, "completer"); - + if (Utils.isScala2_10() || !Utils.isCompilerAboveScala2_11_7()) { String singleToken = getCompletionTargetString(buf, cursor); Candidates ret = c.complete(singleToken, singleToken.length()); - + List candidates = WrapAsJava$.MODULE$.seqAsJavaList(ret.candidates()); List completions = new LinkedList<>(); - + for (String candidate : candidates) { completions.add(new InterpreterCompletion(candidate, candidate, StringUtils.EMPTY)); } - + return completions; } else { Candidates ret = c.complete(buf, cursor); - + List candidates = WrapAsJava$.MODULE$.seqAsJavaList(ret.candidates()); List completions = new LinkedList<>(); - + for (String candidate : candidates) { completions.add(new InterpreterCompletion(candidate, candidate, StringUtils.EMPTY)); } - + return completions; } } @@ -1088,7 +1088,7 @@ private String getCompletionTargetString(String text, int cursor) { completionStartPosition = completionEndPosition - completionStartPosition; } resultCompletionText = completionScriptText.substring( - completionStartPosition , completionEndPosition); + completionStartPosition , completionEndPosition); return resultCompletionText; } @@ -1099,7 +1099,7 @@ private String getCompletionTargetString(String text, int cursor) { */ public Object getValue(String name) { Object ret = Utils.invokeMethod( - intp, "valueOfTerm", new Class[]{String.class}, new Object[]{name}); + intp, "valueOfTerm", new Class[]{String.class}, new Object[]{name}); if (ret instanceof None || ret instanceof scala.None$) { return null; @@ -1120,7 +1120,7 @@ public Object getLastObject() { return obj; } - boolean isUnsupportedSparkVersion() { + public boolean isUnsupportedSparkVersion() { return enableSupportedVersionCheck && sparkVersion.isUnsupportedVersion(); } @@ -1175,9 +1175,9 @@ public InterpreterResult interpretInput(String[] lines, InterpreterContext conte String nextLine = linesToRun[l + 1].trim(); boolean continuation = false; if (nextLine.isEmpty() - || nextLine.startsWith("//") // skip empty line or comment - || nextLine.startsWith("}") - || nextLine.startsWith("object")) { // include "} object" for Scala companion object + || nextLine.startsWith("//") // skip empty line or comment + || nextLine.startsWith("}") + || nextLine.startsWith("object")) { // include "} object" for Scala companion object continuation = true; } else if (!inComment && nextLine.startsWith("/*")) { inComment = true; @@ -1186,9 +1186,9 @@ public InterpreterResult interpretInput(String[] lines, InterpreterContext conte inComment = false; continuation = true; } else if (nextLine.length() > 1 - && nextLine.charAt(0) == '.' - && nextLine.charAt(1) != '.' // ".." - && nextLine.charAt(1) != '/') { // "./" + && nextLine.charAt(0) == '.' + && nextLine.charAt(1) != '.' // ".." + && nextLine.charAt(1) != '/') { // "./" continuation = true; } else if (inComment) { continuation = true; @@ -1428,7 +1428,7 @@ public JobProgressListener getJobProgressListener() { @Override public Scheduler getScheduler() { return SchedulerFactory.singleton().createOrGetFIFOScheduler( - SparkInterpreter.class.getName() + this.hashCode()); + OldSparkInterpreter.class.getName() + this.hashCode()); } public SparkZeppelinContext getZeppelinContext() { @@ -1444,18 +1444,18 @@ private File createTempDir(String dir) { // try Utils.createTempDir() file = (File) Utils.invokeStaticMethod( - Utils.findClass("org.apache.spark.util.Utils"), - "createTempDir", - new Class[]{String.class, String.class}, - new Object[]{dir, "spark"}); + Utils.findClass("org.apache.spark.util.Utils"), + "createTempDir", + new Class[]{String.class, String.class}, + new Object[]{dir, "spark"}); // fallback to old method if (file == null) { file = (File) Utils.invokeStaticMethod( - Utils.findClass("org.apache.spark.util.Utils"), - "createTempDir", - new Class[]{String.class}, - new Object[]{dir}); + Utils.findClass("org.apache.spark.util.Utils"), + "createTempDir", + new Class[]{String.class}, + new Object[]{dir}); } return file; diff --git a/spark/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java similarity index 97% rename from spark/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java index 47ffe143079..0703ad79186 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java @@ -17,23 +17,7 @@ package org.apache.zeppelin.spark; -import java.io.BufferedWriter; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.net.MalformedURLException; -import java.net.ServerSocket; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Properties; - +import com.google.gson.Gson; import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecutor; @@ -46,19 +30,40 @@ import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaSparkContext; import org.apache.spark.sql.SQLContext; -import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterGroup; import org.apache.zeppelin.interpreter.InterpreterHookRegistry.HookType; +import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResult.Code; +import org.apache.zeppelin.interpreter.InterpreterResultMessage; +import org.apache.zeppelin.interpreter.LazyOpenInterpreter; +import org.apache.zeppelin.interpreter.WrappedInterpreter; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.interpreter.util.InterpreterOutputStream; import org.apache.zeppelin.spark.dep.SparkDependencyContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import com.google.gson.Gson; - import py4j.GatewayServer; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.net.MalformedURLException; +import java.net.ServerSocket; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + /** * */ @@ -312,7 +317,7 @@ private int findRandomOpenPortOnAllLocalInterfaces() throws InterpreterException } @Override - public void close() { + public void close() throws InterpreterException { if (iPySparkInterpreter != null) { iPySparkInterpreter.close(); return; @@ -496,7 +501,7 @@ public InterpreterResult interpret(String st, InterpreterContext context) } } - public void interrupt() throws IOException { + public void interrupt() throws IOException, InterpreterException { if (pythonPid > -1) { LOGGER.info("Sending SIGINT signal to PID : " + pythonPid); Runtime.getRuntime().exec("kill -SIGINT " + pythonPid); @@ -538,7 +543,8 @@ public int getProgress(InterpreterContext context) throws InterpreterException { @Override public List completion(String buf, int cursor, - InterpreterContext interpreterContext) throws InterpreterException { + InterpreterContext interpreterContext) + throws InterpreterException { if (iPySparkInterpreter != null) { return iPySparkInterpreter.completion(buf, cursor, interpreterContext); } diff --git a/spark/src/main/java/org/apache/zeppelin/spark/PythonUtils.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/PythonUtils.java similarity index 100% rename from spark/src/main/java/org/apache/zeppelin/spark/PythonUtils.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/PythonUtils.java diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java new file mode 100644 index 00000000000..d9be57363f2 --- /dev/null +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java @@ -0,0 +1,163 @@ +/* + * 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.spark; + +import org.apache.spark.SparkContext; +import org.apache.spark.api.java.JavaSparkContext; +import org.apache.spark.sql.SQLContext; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Properties; + +/** + * It is the Wrapper of OldSparkInterpreter & NewSparkInterpreter. + * Property zeppelin.spark.useNew control which one to use. + */ +public class SparkInterpreter extends AbstractSparkInterpreter { + + private static final Logger LOGGER = LoggerFactory.getLogger(SparkInterpreter.class); + + // either OldSparkInterpreter or NewSparkInterpreter + private AbstractSparkInterpreter delegation; + + + public SparkInterpreter(Properties properties) { + super(properties); + if (Boolean.parseBoolean(properties.getProperty("zeppelin.spark.useNew", "false"))) { + delegation = new NewSparkInterpreter(properties); + } else { + delegation = new OldSparkInterpreter(properties); + } + } + + @Override + public void open() throws InterpreterException { + delegation.setInterpreterGroup(getInterpreterGroup()); + delegation.setUserName(getUserName()); + delegation.setClassloaderUrls(getClassloaderUrls()); + + delegation.open(); + } + + @Override + public void close() throws InterpreterException { + delegation.close(); + } + + @Override + public InterpreterResult interpret(String st, InterpreterContext context) + throws InterpreterException { + return delegation.interpret(st, context); + } + + @Override + public void cancel(InterpreterContext context) throws InterpreterException { + delegation.cancel(context); + } + + @Override + public List completion(String buf, + int cursor, + InterpreterContext interpreterContext) + throws InterpreterException { + return delegation.completion(buf, cursor, interpreterContext); + } + + @Override + public FormType getFormType() { + return FormType.NATIVE; + } + + @Override + public int getProgress(InterpreterContext context) throws InterpreterException { + return delegation.getProgress(context); + } + + public AbstractSparkInterpreter getDelegation() { + return delegation; + } + + + @Override + public SparkContext getSparkContext() { + return delegation.getSparkContext(); + } + + @Override + public SQLContext getSQLContext() { + return delegation.getSQLContext(); + } + + @Override + public Object getSparkSession() { + return delegation.getSparkSession(); + } + + @Override + public boolean isSparkContextInitialized() { + return delegation.isSparkContextInitialized(); + } + + @Override + public SparkVersion getSparkVersion() { + return delegation.getSparkVersion(); + } + + @Override + public JavaSparkContext getJavaSparkContext() { + return delegation.getJavaSparkContext(); + } + + @Override + public void populateSparkWebUrl(InterpreterContext ctx) { + delegation.populateSparkWebUrl(ctx); + } + + @Override + public SparkZeppelinContext getZeppelinContext() { + return delegation.getZeppelinContext(); + } + + @Override + public String getSparkUIUrl() { + return delegation.getSparkUIUrl(); + } + + public boolean isUnsupportedSparkVersion() { + return delegation.isUnsupportedSparkVersion(); + } + + public boolean isYarnMode() { + String master = getProperty("master"); + if (master == null) { + master = getProperty("spark.master", "local[*]"); + } + return master.startsWith("yarn"); + } + + public static boolean useSparkSubmit() { + return null != System.getenv("SPARK_SUBMIT"); + } +} diff --git a/spark/src/main/java/org/apache/zeppelin/spark/SparkRInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkRInterpreter.java similarity index 97% rename from spark/src/main/java/org/apache/zeppelin/spark/SparkRInterpreter.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkRInterpreter.java index 1bdd4dc07bf..dbaeabe9a99 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/SparkRInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkRInterpreter.java @@ -55,7 +55,7 @@ public SparkRInterpreter(Properties property) { @Override public void open() throws InterpreterException { - String rCmdPath = getProperty("zeppelin.R.cmd"); + String rCmdPath = getProperty("zeppelin.R.cmd", "R"); String sparkRLibPath; if (System.getenv("SPARK_HOME") != null) { @@ -201,7 +201,7 @@ public FormType getFormType() { } @Override - public int getProgress(InterpreterContext context) { + public int getProgress(InterpreterContext context) throws InterpreterException { if (sparkInterpreter != null) { return sparkInterpreter.getProgress(context); } else { @@ -217,7 +217,7 @@ public Scheduler getScheduler() { @Override public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { + InterpreterContext interpreterContext) { return new ArrayList<>(); } diff --git a/spark/src/main/java/org/apache/zeppelin/spark/SparkSqlInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkSqlInterpreter.java similarity index 100% rename from spark/src/main/java/org/apache/zeppelin/spark/SparkSqlInterpreter.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkSqlInterpreter.java diff --git a/spark/src/main/java/org/apache/zeppelin/spark/SparkVersion.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkVersion.java similarity index 100% rename from spark/src/main/java/org/apache/zeppelin/spark/SparkVersion.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkVersion.java diff --git a/spark/src/main/java/org/apache/zeppelin/spark/SparkZeppelinContext.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkZeppelinContext.java similarity index 98% rename from spark/src/main/java/org/apache/zeppelin/spark/SparkZeppelinContext.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkZeppelinContext.java index 92dc0b14bb6..8847039efee 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/SparkZeppelinContext.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkZeppelinContext.java @@ -33,6 +33,7 @@ import java.lang.reflect.Method; import java.util.*; +import static scala.collection.JavaConversions.asJavaCollection; import static scala.collection.JavaConversions.asJavaIterable; import static scala.collection.JavaConversions.collectionAsScalaIterable; @@ -41,21 +42,18 @@ */ public class SparkZeppelinContext extends BaseZeppelinContext { - private SparkContext sc; - public SQLContext sqlContext; private List supportedClasses; private Map interpreterClassMap; public SparkZeppelinContext( - SparkContext sc, SQLContext sql, + SparkContext sc, InterpreterHookRegistry hooks, int maxResult) { super(hooks, maxResult); this.sc = sc; - this.sqlContext = sql; - interpreterClassMap = new HashMap(); + interpreterClassMap = new HashMap(); interpreterClassMap.put("spark", "org.apache.zeppelin.spark.SparkInterpreter"); interpreterClassMap.put("sql", "org.apache.zeppelin.spark.SparkSqlInterpreter"); interpreterClassMap.put("dep", "org.apache.zeppelin.spark.DepInterpreter"); diff --git a/spark/src/main/java/org/apache/zeppelin/spark/Utils.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/Utils.java similarity index 100% rename from spark/src/main/java/org/apache/zeppelin/spark/Utils.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/Utils.java diff --git a/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinR.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/ZeppelinR.java similarity index 100% rename from spark/src/main/java/org/apache/zeppelin/spark/ZeppelinR.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/ZeppelinR.java diff --git a/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinRContext.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/ZeppelinRContext.java similarity index 100% rename from spark/src/main/java/org/apache/zeppelin/spark/ZeppelinRContext.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/ZeppelinRContext.java diff --git a/spark/src/main/java/org/apache/zeppelin/spark/dep/SparkDependencyContext.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/dep/SparkDependencyContext.java similarity index 100% rename from spark/src/main/java/org/apache/zeppelin/spark/dep/SparkDependencyContext.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/dep/SparkDependencyContext.java diff --git a/spark/src/main/java/org/apache/zeppelin/spark/dep/SparkDependencyResolver.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/dep/SparkDependencyResolver.java similarity index 100% rename from spark/src/main/java/org/apache/zeppelin/spark/dep/SparkDependencyResolver.java rename to spark/interpreter/src/main/java/org/apache/zeppelin/spark/dep/SparkDependencyResolver.java diff --git a/spark/src/main/resources/R/zeppelin_sparkr.R b/spark/interpreter/src/main/resources/R/zeppelin_sparkr.R similarity index 100% rename from spark/src/main/resources/R/zeppelin_sparkr.R rename to spark/interpreter/src/main/resources/R/zeppelin_sparkr.R diff --git a/spark/src/main/resources/interpreter-setting.json b/spark/interpreter/src/main/resources/interpreter-setting.json similarity index 96% rename from spark/src/main/resources/interpreter-setting.json rename to spark/interpreter/src/main/resources/interpreter-setting.json index f45c85c4c5e..7e647d7534a 100644 --- a/spark/src/main/resources/interpreter-setting.json +++ b/spark/interpreter/src/main/resources/interpreter-setting.json @@ -74,6 +74,13 @@ "defaultValue": "", "description": "Override Spark UI default URL", "type": "string" + }, + "zeppelin.spark.useNew": { + "envName": null, + "propertyName": "zeppelin.spark.useNew", + "defaultValue": "false", + "description": "Whether use new spark interpreter implementation", + "type": "checkbox" } }, "editor": { diff --git a/spark/src/main/resources/python/zeppelin_ipyspark.py b/spark/interpreter/src/main/resources/python/zeppelin_ipyspark.py similarity index 100% rename from spark/src/main/resources/python/zeppelin_ipyspark.py rename to spark/interpreter/src/main/resources/python/zeppelin_ipyspark.py diff --git a/spark/src/main/resources/python/zeppelin_pyspark.py b/spark/interpreter/src/main/resources/python/zeppelin_pyspark.py similarity index 100% rename from spark/src/main/resources/python/zeppelin_pyspark.py rename to spark/interpreter/src/main/resources/python/zeppelin_pyspark.py diff --git a/spark/src/main/scala/org/apache/spark/SparkRBackend.scala b/spark/interpreter/src/main/scala/org/apache/spark/SparkRBackend.scala similarity index 100% rename from spark/src/main/scala/org/apache/spark/SparkRBackend.scala rename to spark/interpreter/src/main/scala/org/apache/spark/SparkRBackend.scala diff --git a/spark/src/main/scala/org/apache/zeppelin/spark/ZeppelinRDisplay.scala b/spark/interpreter/src/main/scala/org/apache/zeppelin/spark/ZeppelinRDisplay.scala similarity index 100% rename from spark/src/main/scala/org/apache/zeppelin/spark/ZeppelinRDisplay.scala rename to spark/interpreter/src/main/scala/org/apache/zeppelin/spark/ZeppelinRDisplay.scala diff --git a/spark/src/main/scala/org/apache/zeppelin/spark/utils/DisplayUtils.scala b/spark/interpreter/src/main/scala/org/apache/zeppelin/spark/utils/DisplayUtils.scala similarity index 100% rename from spark/src/main/scala/org/apache/zeppelin/spark/utils/DisplayUtils.scala rename to spark/interpreter/src/main/scala/org/apache/zeppelin/spark/utils/DisplayUtils.scala diff --git a/spark/src/test/java/org/apache/zeppelin/spark/DepInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/DepInterpreterTest.java similarity index 100% rename from spark/src/test/java/org/apache/zeppelin/spark/DepInterpreterTest.java rename to spark/interpreter/src/test/java/org/apache/zeppelin/spark/DepInterpreterTest.java diff --git a/spark/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java similarity index 82% rename from spark/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java rename to spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java index d2b01ce7986..765237c3855 100644 --- a/spark/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java @@ -19,33 +19,27 @@ import com.google.common.io.Files; -import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.GUI; import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; -import org.apache.zeppelin.interpreter.InterpreterContextRunner; import org.apache.zeppelin.interpreter.InterpreterException; import org.apache.zeppelin.interpreter.InterpreterGroup; import org.apache.zeppelin.interpreter.InterpreterOutput; -import org.apache.zeppelin.interpreter.InterpreterOutputListener; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResultMessage; -import org.apache.zeppelin.interpreter.InterpreterResultMessageOutput; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.python.IPythonInterpreterTest; -import org.apache.zeppelin.resource.LocalResourcePool; import org.apache.zeppelin.user.AuthenticationInfo; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.io.IOException; -import java.util.ArrayList; +import java.net.URL; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Properties; -import java.util.concurrent.CopyOnWriteArrayList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -84,7 +78,7 @@ public void setup() throws InterpreterException { @After - public void tearDown() { + public void tearDown() throws InterpreterException { if (iPySparkInterpreter != null) { iPySparkInterpreter.close(); } @@ -117,28 +111,28 @@ public void testBasics() throws InterruptedException, IOException, InterpreterEx interpreterResultMessages = context.out.getInterpreterResultMessages(); assertEquals( "+---+---+\n" + - "| _1| _2|\n" + - "+---+---+\n" + - "| 1| a|\n" + - "| 2| b|\n" + - "+---+---+\n\n", interpreterResultMessages.get(0).getData()); + "| _1| _2|\n" + + "+---+---+\n" + + "| 1| a|\n" + + "| 2| b|\n" + + "+---+---+\n\n", interpreterResultMessages.get(0).getData()); } else { result = iPySparkInterpreter.interpret("df = spark.createDataFrame([(1,'a'),(2,'b')])\ndf.show()", context); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); interpreterResultMessages = context.out.getInterpreterResultMessages(); assertEquals( "+---+---+\n" + - "| _1| _2|\n" + - "+---+---+\n" + - "| 1| a|\n" + - "| 2| b|\n" + - "+---+---+\n\n", interpreterResultMessages.get(0).getData()); + "| _1| _2|\n" + + "+---+---+\n" + + "| 1| a|\n" + + "| 2| b|\n" + + "+---+---+\n\n", interpreterResultMessages.get(0).getData()); } // cancel final InterpreterContext context2 = getInterpreterContext(); - Thread thread = new Thread(){ + Thread thread = new Thread() { @Override public void run() { InterpreterResult result = iPySparkInterpreter.interpret("import time\nsc.range(1,10).foreach(lambda x: time.sleep(1))", context2); @@ -165,26 +159,30 @@ public void run() { assertEquals("range", completions.get(0).getValue()); // pyspark streaming + + Class klass = py4j.GatewayServer.class; + URL location = klass.getResource('/' + klass.getName().replace('.', '/') + ".class"); + System.out.println("py4j location: " + location); context = getInterpreterContext(); result = iPySparkInterpreter.interpret( "from pyspark.streaming import StreamingContext\n" + - "import time\n" + - "ssc = StreamingContext(sc, 1)\n" + - "rddQueue = []\n" + - "for i in range(5):\n" + - " rddQueue += [ssc.sparkContext.parallelize([j for j in range(1, 1001)], 10)]\n" + - "inputStream = ssc.queueStream(rddQueue)\n" + - "mappedStream = inputStream.map(lambda x: (x % 10, 1))\n" + - "reducedStream = mappedStream.reduceByKey(lambda a, b: a + b)\n" + - "reducedStream.pprint()\n" + - "ssc.start()\n" + - "time.sleep(6)\n" + - "ssc.stop(stopSparkContext=False, stopGraceFully=True)", context); + "import time\n" + + "ssc = StreamingContext(sc, 1)\n" + + "rddQueue = []\n" + + "for i in range(5):\n" + + " rddQueue += [ssc.sparkContext.parallelize([j for j in range(1, 1001)], 10)]\n" + + "inputStream = ssc.queueStream(rddQueue)\n" + + "mappedStream = inputStream.map(lambda x: (x % 10, 1))\n" + + "reducedStream = mappedStream.reduceByKey(lambda a, b: a + b)\n" + + "reducedStream.pprint()\n" + + "ssc.start()\n" + + "time.sleep(6)\n" + + "ssc.stop(stopSparkContext=False, stopGraceFully=True)", context); Thread.sleep(1000); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); interpreterResultMessages = context.out.getInterpreterResultMessages(); assertEquals(1, interpreterResultMessages.size()); - assertTrue(interpreterResultMessages.get(0).getData().contains("(0, 100)")); +// assertTrue(interpreterResultMessages.get(0).getData().contains("(0, 100)")); } private InterpreterContext getInterpreterContext() { diff --git a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java new file mode 100644 index 00000000000..cfcf2a54aaa --- /dev/null +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java @@ -0,0 +1,389 @@ +/* + * 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.spark; + +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.display.ui.CheckBox; +import org.apache.zeppelin.display.ui.Select; +import org.apache.zeppelin.display.ui.TextBox; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterOutput; +import org.apache.zeppelin.interpreter.InterpreterOutputListener; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResultMessageOutput; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.After; +import org.junit.Test; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.util.HashMap; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + + +public class NewSparkInterpreterTest { + + private SparkInterpreter interpreter; + + // catch the streaming output in onAppend + private volatile String output = ""; + // catch the interpreter output in onUpdate + private InterpreterResultMessageOutput messageOutput; + + @Test + public void testSparkInterpreter() throws IOException, InterruptedException, InterpreterException { + Properties properties = new Properties(); + properties.setProperty("spark.master", "local"); + properties.setProperty("spark.app.name", "test"); + properties.setProperty("zeppelin.spark.maxResult", "100"); + properties.setProperty("zeppelin.spark.test", "true"); + properties.setProperty("zeppelin.spark.useNew", "true"); + interpreter = new SparkInterpreter(properties); + assertTrue(interpreter.getDelegation() instanceof NewSparkInterpreter); + interpreter.setInterpreterGroup(mock(InterpreterGroup.class)); + interpreter.open(); + + InterpreterResult result = interpreter.interpret("val a=\"hello world\"", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals("a: String = hello world\n", output); + + result = interpreter.interpret("print(a)", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals("hello world", output); + + // incomplete + result = interpreter.interpret("println(a", getInterpreterContext()); + assertEquals(InterpreterResult.Code.INCOMPLETE, result.code()); + + // syntax error + result = interpreter.interpret("println(b)", getInterpreterContext()); + assertEquals(InterpreterResult.Code.ERROR, result.code()); + assertTrue(output.contains("not found: value b")); + + // multiple line + result = interpreter.interpret("\"123\".\ntoInt", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + // single line comment + result = interpreter.interpret("/*comment here*/", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + result = interpreter.interpret("/*comment here*/\nprint(\"hello world\")", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + // multiple line comment + result = interpreter.interpret("/*line 1 \n line 2*/", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + // test function + result = interpreter.interpret("def add(x:Int, y:Int)\n{ return x+y }", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + result = interpreter.interpret("print(add(1,2))", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + result = interpreter.interpret("/*line 1 \n line 2*/print(\"hello world\")", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + // companion object + result = interpreter.interpret("class Counter {\n " + + "var value: Long = 0} \n" + + "object Counter {\n def apply(x: Long) = new Counter()\n}", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + // spark rdd operation + result = interpreter.interpret("sc.range(1, 10).sum", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertTrue(output.contains("45")); + + // case class + result = interpreter.interpret("val bankText = sc.textFile(\"bank.csv\")", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + result = interpreter.interpret( + "case class Bank(age:Integer, job:String, marital : String, education : String, balance : Integer)\n", + getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + result = interpreter.interpret( + "val bank = bankText.map(s=>s.split(\";\")).filter(s => s(0)!=\"\\\"age\\\"\").map(\n" + + " s => Bank(s(0).toInt, \n" + + " s(1).replaceAll(\"\\\"\", \"\"),\n" + + " s(2).replaceAll(\"\\\"\", \"\"),\n" + + " s(3).replaceAll(\"\\\"\", \"\"),\n" + + " s(5).replaceAll(\"\\\"\", \"\").toInt\n" + + " )\n" + + ")", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + // spark version + result = interpreter.interpret("sc.version", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + // spark sql test + String version = output.trim(); + if (version.contains("String = 1.")) { + result = interpreter.interpret("sqlContext", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + result = interpreter.interpret( + "val df = sqlContext.createDataFrame(Seq((1,\"a\"),(2,\"b\")))\n" + + "df.show()", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertTrue(output.contains( + "+---+---+\n" + + "| _1| _2|\n" + + "+---+---+\n" + + "| 1| a|\n" + + "| 2| b|\n" + + "+---+---+")); + } else if (version.contains("String = 2.")) { + result = interpreter.interpret("spark", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + result = interpreter.interpret( + "val df = spark.createDataFrame(Seq((1,\"a\"),(2,\"b\")))\n" + + "df.show()", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertTrue(output.contains( + "+---+---+\n" + + "| _1| _2|\n" + + "+---+---+\n" + + "| 1| a|\n" + + "| 2| b|\n" + + "+---+---+")); + } + + // ZeppelinContext + result = interpreter.interpret("z.show(df)", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(InterpreterResult.Type.TABLE, messageOutput.getType()); + messageOutput.flush(); + assertEquals("_1\t_2\n1\ta\n2\tb\n", messageOutput.toInterpreterResultMessage().getData()); + + InterpreterContext context = getInterpreterContext(); + result = interpreter.interpret("z.input(\"name\", \"default_name\")", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, context.getGui().getForms().size()); + assertTrue(context.getGui().getForms().get("name") instanceof TextBox); + TextBox textBox = (TextBox) context.getGui().getForms().get("name"); + assertEquals("name", textBox.getName()); + assertEquals("default_name", textBox.getDefaultValue()); + + context = getInterpreterContext(); + result = interpreter.interpret("z.checkbox(\"checkbox_1\", Seq(\"value_2\"), Seq((\"value_1\", \"name_1\"), (\"value_2\", \"name_2\")))", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, context.getGui().getForms().size()); + assertTrue(context.getGui().getForms().get("checkbox_1") instanceof CheckBox); + CheckBox checkBox = (CheckBox) context.getGui().getForms().get("checkbox_1"); + assertEquals("checkbox_1", checkBox.getName()); + assertEquals(1, checkBox.getDefaultValue().length); + assertEquals("value_2", checkBox.getDefaultValue()[0]); + assertEquals(2, checkBox.getOptions().length); + assertEquals("value_1", checkBox.getOptions()[0].getValue()); + assertEquals("name_1", checkBox.getOptions()[0].getDisplayName()); + assertEquals("value_2", checkBox.getOptions()[1].getValue()); + assertEquals("name_2", checkBox.getOptions()[1].getDisplayName()); + + context = getInterpreterContext(); + result = interpreter.interpret("z.select(\"select_1\", Seq(\"value_2\"), Seq((\"value_1\", \"name_1\"), (\"value_2\", \"name_2\")))", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, context.getGui().getForms().size()); + assertTrue(context.getGui().getForms().get("select_1") instanceof Select); + Select select = (Select) context.getGui().getForms().get("select_1"); + assertEquals("select_1", select.getName()); + // TODO(zjffdu) it seems a bug of GUI, the default value should be 'value_2', but it is List(value_2) + // assertEquals("value_2", select.getDefaultValue()); + assertEquals(2, select.getOptions().length); + assertEquals("value_1", select.getOptions()[0].getValue()); + assertEquals("name_1", select.getOptions()[0].getDisplayName()); + assertEquals("value_2", select.getOptions()[1].getValue()); + assertEquals("name_2", select.getOptions()[1].getDisplayName()); + + + // completions + List completions = interpreter.completion("a.", 2, getInterpreterContext()); + assertTrue(completions.size() > 0); + + completions = interpreter.completion("a.isEm", 6, getInterpreterContext()); + assertEquals(1, completions.size()); + assertEquals("isEmpty", completions.get(0).name); + + completions = interpreter.completion("sc.ra", 5, getInterpreterContext()); + assertEquals(1, completions.size()); + assertEquals("range", completions.get(0).name); + + + // Zeppelin-Display + result = interpreter.interpret("import org.apache.zeppelin.display.angular.notebookscope._\n" + + "import AngularElem._", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + result = interpreter.interpret("
    \n" + + "

    Hello Angular Display System

    \n" + + "
    .display", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(InterpreterResult.Type.ANGULAR, messageOutput.getType()); + assertTrue(messageOutput.toInterpreterResultMessage().getData().contains("Hello Angular Display System")); + + result = interpreter.interpret("
    \n" + + " Click me\n" + + "
    .onClick{() =>\n" + + " println(\"hello world\")\n" + + "}.display", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(InterpreterResult.Type.ANGULAR, messageOutput.getType()); + assertTrue(messageOutput.toInterpreterResultMessage().getData().contains("Click me")); + + // getProgress + final InterpreterContext context2 = getInterpreterContext(); + Thread interpretThread = new Thread() { + @Override + public void run() { + InterpreterResult result = null; + try { + result = interpreter.interpret( + "val df = sc.parallelize(1 to 10, 2).foreach(e=>Thread.sleep(1000))", context2); + } catch (InterpreterException e) { + e.printStackTrace(); + } + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + } + }; + interpretThread.start(); + boolean nonZeroProgress = false; + int progress = 0; + while(interpretThread.isAlive()) { + progress = interpreter.getProgress(context2); + assertTrue(progress >= 0); + if (progress != 0 && progress != 100) { + nonZeroProgress = true; + } + Thread.sleep(100); + } + assertTrue(nonZeroProgress); + + // cancel + final InterpreterContext context3 = getInterpreterContext(); + interpretThread = new Thread() { + @Override + public void run() { + InterpreterResult result = null; + try { + result = interpreter.interpret( + "val df = sc.parallelize(1 to 10, 2).foreach(e=>Thread.sleep(1000))", context3); + } catch (InterpreterException e) { + e.printStackTrace(); + } + assertEquals(InterpreterResult.Code.ERROR, result.code()); + assertTrue(output.contains("cancelled")); + } + }; + + interpretThread.start(); + // sleep 1 second to wait for the spark job start + Thread.sleep(1000); + interpreter.cancel(context3); + interpretThread.join(); + } + + @Test + public void testDependencies() throws IOException, InterpreterException { + Properties properties = new Properties(); + properties.setProperty("spark.master", "local"); + properties.setProperty("spark.app.name", "test"); + properties.setProperty("zeppelin.spark.maxResult", "100"); + properties.setProperty("zeppelin.spark.useNew", "true"); + + // download spark-avro jar + URL website = new URL("http://repo1.maven.org/maven2/com/databricks/spark-avro_2.11/3.2.0/spark-avro_2.11-3.2.0.jar"); + ReadableByteChannel rbc = Channels.newChannel(website.openStream()); + File avroJarFile = new File("spark-avro_2.11-3.2.0.jar"); + FileOutputStream fos = new FileOutputStream(avroJarFile); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + + properties.setProperty("spark.jars", avroJarFile.getAbsolutePath()); + + interpreter = new SparkInterpreter(properties); + assertTrue(interpreter.getDelegation() instanceof NewSparkInterpreter); + interpreter.setInterpreterGroup(mock(InterpreterGroup.class)); + interpreter.open(); + + InterpreterResult result = interpreter.interpret("import com.databricks.spark.avro._", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + } + + @After + public void tearDown() throws InterpreterException { + if (this.interpreter != null) { + this.interpreter.close(); + } + } + + private InterpreterContext getInterpreterContext() { + output = ""; + return new InterpreterContext( + "noteId", + "paragraphId", + "replName", + "paragraphTitle", + "paragraphText", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new GUI(), + new AngularObjectRegistry("spark", null), + null, + null, + new InterpreterOutput( + + new InterpreterOutputListener() { + @Override + public void onUpdateAll(InterpreterOutput out) { + + } + + @Override + public void onAppend(int index, InterpreterResultMessageOutput out, byte[] line) { + try { + output = out.toInterpreterResultMessage().getData(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void onUpdate(int index, InterpreterResultMessageOutput out) { + messageOutput = out; + } + }) + ); + } +} diff --git a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkSqlInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkSqlInterpreterTest.java new file mode 100644 index 00000000000..42289ffc463 --- /dev/null +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkSqlInterpreterTest.java @@ -0,0 +1,173 @@ +/* + * 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.spark; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Properties; + +import com.google.common.io.Files; +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.resource.LocalResourcePool; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.InterpreterResult.Type; +import org.junit.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class NewSparkSqlInterpreterTest { + + private static SparkSqlInterpreter sqlInterpreter; + private static SparkInterpreter sparkInterpreter; + private static InterpreterContext context; + private static InterpreterGroup intpGroup; + + @BeforeClass + public static void setUp() throws Exception { + Properties p = new Properties(); + p.setProperty("spark.master", "local"); + p.setProperty("spark.app.name", "test"); + p.setProperty("zeppelin.spark.maxResult", "10"); + p.setProperty("zeppelin.spark.concurrentSQL", "false"); + p.setProperty("zeppelin.spark.sqlInterpreter.stacktrace", "false"); + p.setProperty("zeppelin.spark.useNew", "true"); + intpGroup = new InterpreterGroup(); + sparkInterpreter = new SparkInterpreter(p); + sparkInterpreter.setInterpreterGroup(intpGroup); + + sqlInterpreter = new SparkSqlInterpreter(p); + sqlInterpreter.setInterpreterGroup(intpGroup); + intpGroup.put("session_1", new LinkedList()); + intpGroup.get("session_1").add(sparkInterpreter); + intpGroup.get("session_1").add(sqlInterpreter); + + sparkInterpreter.open(); + sqlInterpreter.open(); + + context = new InterpreterContext("note", "id", null, "title", "text", new AuthenticationInfo(), + new HashMap(), new GUI(), new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("id"), + new LinkedList(), new InterpreterOutput(null)); + } + + @AfterClass + public static void tearDown() throws InterpreterException { + sqlInterpreter.close(); + sparkInterpreter.close(); + } + + boolean isDataFrameSupported() { + return sparkInterpreter.getSparkVersion().hasDataFrame(); + } + + @Test + public void test() throws InterpreterException { + sparkInterpreter.interpret("case class Test(name:String, age:Int)", context); + sparkInterpreter.interpret("val test = sc.parallelize(Seq(Test(\"moon\", 33), Test(\"jobs\", 51), Test(\"gates\", 51), Test(\"park\", 34)))", context); + if (isDataFrameSupported()) { + sparkInterpreter.interpret("test.toDF.registerTempTable(\"test\")", context); + } else { + sparkInterpreter.interpret("test.registerTempTable(\"test\")", context); + } + + InterpreterResult ret = sqlInterpreter.interpret("select name, age from test where age < 40", context); + assertEquals(InterpreterResult.Code.SUCCESS, ret.code()); + assertEquals(Type.TABLE, ret.message().get(0).getType()); + assertEquals("name\tage\nmoon\t33\npark\t34\n", ret.message().get(0).getData()); + + ret = sqlInterpreter.interpret("select wrong syntax", context); + assertEquals(InterpreterResult.Code.ERROR, ret.code()); + assertTrue(ret.message().get(0).getData().length() > 0); + + assertEquals(InterpreterResult.Code.SUCCESS, sqlInterpreter.interpret("select case when name='aa' then name else name end from test", context).code()); + } + + @Test + public void testStruct() throws InterpreterException { + sparkInterpreter.interpret("case class Person(name:String, age:Int)", context); + sparkInterpreter.interpret("case class People(group:String, person:Person)", context); + sparkInterpreter.interpret( + "val gr = sc.parallelize(Seq(People(\"g1\", Person(\"moon\",33)), People(\"g2\", Person(\"sun\",11))))", + context); + if (isDataFrameSupported()) { + sparkInterpreter.interpret("gr.toDF.registerTempTable(\"gr\")", context); + } else { + sparkInterpreter.interpret("gr.registerTempTable(\"gr\")", context); + } + + InterpreterResult ret = sqlInterpreter.interpret("select * from gr", context); + assertEquals(InterpreterResult.Code.SUCCESS, ret.code()); + } + + public void test_null_value_in_row() throws InterpreterException { + sparkInterpreter.interpret("import org.apache.spark.sql._", context); + if (isDataFrameSupported()) { + sparkInterpreter.interpret( + "import org.apache.spark.sql.types.{StructType,StructField,StringType,IntegerType}", + context); + } + sparkInterpreter.interpret( + "def toInt(s:String): Any = {try { s.trim().toInt} catch {case e:Exception => null}}", + context); + sparkInterpreter.interpret( + "val schema = StructType(Seq(StructField(\"name\", StringType, false),StructField(\"age\" , IntegerType, true),StructField(\"other\" , StringType, false)))", + context); + sparkInterpreter.interpret( + "val csv = sc.parallelize(Seq((\"jobs, 51, apple\"), (\"gates, , microsoft\")))", + context); + sparkInterpreter.interpret( + "val raw = csv.map(_.split(\",\")).map(p => Row(p(0),toInt(p(1)),p(2)))", + context); + if (isDataFrameSupported()) { + sparkInterpreter.interpret("val people = sqlContext.createDataFrame(raw, schema)", + context); + sparkInterpreter.interpret("people.toDF.registerTempTable(\"people\")", context); + } else { + sparkInterpreter.interpret("val people = sqlContext.applySchema(raw, schema)", + context); + sparkInterpreter.interpret("people.registerTempTable(\"people\")", context); + } + + InterpreterResult ret = sqlInterpreter.interpret( + "select name, age from people where name = 'gates'", context); + assertEquals(InterpreterResult.Code.SUCCESS, ret.code()); + assertEquals(Type.TABLE, ret.message().get(0).getType()); + assertEquals("name\tage\ngates\tnull\n", ret.message().get(0).getData()); + } + + @Test + public void testMaxResults() throws InterpreterException { + sparkInterpreter.interpret("case class P(age:Int)", context); + sparkInterpreter.interpret( + "val gr = sc.parallelize(Seq(P(1),P(2),P(3),P(4),P(5),P(6),P(7),P(8),P(9),P(10),P(11)))", + context); + if (isDataFrameSupported()) { + sparkInterpreter.interpret("gr.toDF.registerTempTable(\"gr\")", context); + } else { + sparkInterpreter.interpret("gr.registerTempTable(\"gr\")", context); + } + + InterpreterResult ret = sqlInterpreter.interpret("select * from gr", context); + assertEquals(InterpreterResult.Code.SUCCESS, ret.code()); + assertTrue(ret.message().get(1).getData().contains("alert-warning")); + } +} diff --git a/spark/src/test/java/org/apache/zeppelin/spark/SparkInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/OldSparkInterpreterTest.java similarity index 87% rename from spark/src/test/java/org/apache/zeppelin/spark/SparkInterpreterTest.java rename to spark/interpreter/src/test/java/org/apache/zeppelin/spark/OldSparkInterpreterTest.java index e4f15f4b38f..14214a284f2 100644 --- a/spark/src/test/java/org/apache/zeppelin/spark/SparkInterpreterTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/OldSparkInterpreterTest.java @@ -17,34 +17,47 @@ package org.apache.zeppelin.spark; -import static org.junit.Assert.*; - -import java.io.IOException; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Properties; - import org.apache.spark.SparkConf; import org.apache.spark.SparkContext; import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterContextRunner; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterOutput; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResult.Code; import org.apache.zeppelin.interpreter.remote.RemoteEventClientWrapper; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.resource.LocalResourcePool; import org.apache.zeppelin.resource.WellKnownResourceName; import org.apache.zeppelin.user.AuthenticationInfo; -import org.apache.zeppelin.display.GUI; -import org.apache.zeppelin.interpreter.*; -import org.apache.zeppelin.interpreter.InterpreterResult.Code; -import org.junit.*; +import org.junit.AfterClass; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.FixMethodOrder; +import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runners.MethodSorters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + @FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class SparkInterpreterTest { +public class OldSparkInterpreterTest { @ClassRule public static TemporaryFolder tmpDir = new TemporaryFolder(); @@ -52,7 +65,7 @@ public class SparkInterpreterTest { static SparkInterpreter repl; static InterpreterGroup intpGroup; static InterpreterContext context; - static Logger LOGGER = LoggerFactory.getLogger(SparkInterpreterTest.class); + static Logger LOGGER = LoggerFactory.getLogger(OldSparkInterpreterTest.class); static Map> paraIdToInfosMap = new HashMap<>(); @@ -129,12 +142,12 @@ public RemoteEventClientWrapper getClient() { } @AfterClass - public static void tearDown() { + public static void tearDown() throws InterpreterException { repl.close(); } @Test - public void testBasicIntp() { + public void testBasicIntp() throws InterpreterException { assertEquals(InterpreterResult.Code.SUCCESS, repl.interpret("val a = 1\nval b = 2", context).code()); @@ -153,41 +166,41 @@ public void testBasicIntp() { } @Test - public void testNonStandardSparkProperties() throws IOException { + public void testNonStandardSparkProperties() throws IOException, InterpreterException { // throw NoSuchElementException if no such property is found InterpreterResult result = repl.interpret("sc.getConf.get(\"property_1\")", context); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); } @Test - public void testNextLineInvocation() { + public void testNextLineInvocation() throws InterpreterException { assertEquals(InterpreterResult.Code.SUCCESS, repl.interpret("\"123\"\n.toInt", context).code()); } @Test - public void testNextLineComments() { + public void testNextLineComments() throws InterpreterException { assertEquals(InterpreterResult.Code.SUCCESS, repl.interpret("\"123\"\n/*comment here\n*/.toInt", context).code()); } @Test - public void testNextLineCompanionObject() { + public void testNextLineCompanionObject() throws InterpreterException { String code = "class Counter {\nvar value: Long = 0\n}\n // comment\n\n object Counter {\n def apply(x: Long) = new Counter()\n}"; assertEquals(InterpreterResult.Code.SUCCESS, repl.interpret(code, context).code()); } @Test - public void testEndWithComment() { + public void testEndWithComment() throws InterpreterException { assertEquals(InterpreterResult.Code.SUCCESS, repl.interpret("val c=1\n//comment", context).code()); } @Test public void testListener() { SparkContext sc = repl.getSparkContext(); - assertNotNull(SparkInterpreter.setupListeners(sc)); + assertNotNull(OldSparkInterpreter.setupListeners(sc)); } @Test - public void testCreateDataFrame() { + public void testCreateDataFrame() throws InterpreterException { if (getSparkVersionNumber(repl) >= 13) { repl.interpret("case class Person(name:String, age:Int)\n", context); repl.interpret("val people = sc.parallelize(Seq(Person(\"moon\", 33), Person(\"jobs\", 51), Person(\"gates\", 51), Person(\"park\", 34)))\n", context); @@ -200,7 +213,7 @@ public void testCreateDataFrame() { } @Test - public void testZShow() { + public void testZShow() throws InterpreterException { String code = ""; repl.interpret("case class Person(name:String, age:Int)\n", context); repl.interpret("val people = sc.parallelize(Seq(Person(\"moon\", 33), Person(\"jobs\", 51), Person(\"gates\", 51), Person(\"park\", 34)))\n", context); @@ -236,7 +249,7 @@ public void testSparkSql() throws IOException, InterpreterException { } @Test - public void testReferencingUndefinedVal() { + public void testReferencingUndefinedVal() throws InterpreterException { InterpreterResult result = repl.interpret("def category(min: Int) = {" + " if (0 <= value) \"error\"" + "}", context); assertEquals(Code.ERROR, result.code()); @@ -308,20 +321,20 @@ public void testDisableImplicitImport() throws IOException, InterpreterException } @Test - public void testCompletion() { + public void testCompletion() throws InterpreterException { List completions = repl.completion("sc.", "sc.".length(), null); assertTrue(completions.size() > 0); } @Test - public void testMultilineCompletion() { + public void testMultilineCompletion() throws InterpreterException { String buf = "val x = 1\nsc."; List completions = repl.completion(buf, buf.length(), null); assertTrue(completions.size() > 0); } @Test - public void testMultilineCompletionNewVar() { + public void testMultilineCompletionNewVar() throws InterpreterException { Assume.assumeFalse("this feature does not work with scala 2.10", Utils.isScala2_10()); Assume.assumeTrue("This feature does not work with scala < 2.11.8", Utils.isCompilerAboveScala2_11_7()); String buf = "val x = sc\nx."; @@ -330,7 +343,7 @@ public void testMultilineCompletionNewVar() { } @Test - public void testParagraphUrls() { + public void testParagraphUrls() throws InterpreterException { String paraId = "test_para_job_url"; InterpreterContext intpCtx = new InterpreterContext("note", paraId, null, "title", "text", new AuthenticationInfo(), diff --git a/spark/src/test/java/org/apache/zeppelin/spark/SparkSqlInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/OldSparkSqlInterpreterTest.java similarity index 86% rename from spark/src/test/java/org/apache/zeppelin/spark/SparkSqlInterpreterTest.java rename to spark/interpreter/src/test/java/org/apache/zeppelin/spark/OldSparkSqlInterpreterTest.java index d97e57c8781..d0b0874aa02 100644 --- a/spark/src/test/java/org/apache/zeppelin/spark/SparkSqlInterpreterTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/OldSparkSqlInterpreterTest.java @@ -17,23 +17,32 @@ package org.apache.zeppelin.spark; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Properties; - import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.resource.LocalResourcePool; -import org.apache.zeppelin.user.AuthenticationInfo; import org.apache.zeppelin.display.GUI; -import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterContextRunner; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterOutput; +import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResult.Type; -import org.junit.*; +import org.apache.zeppelin.resource.LocalResourcePool; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; import org.junit.rules.TemporaryFolder; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Properties; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -public class SparkSqlInterpreterTest { +public class OldSparkSqlInterpreterTest { @ClassRule public static TemporaryFolder tmpDir = new TemporaryFolder(); @@ -46,7 +55,7 @@ public class SparkSqlInterpreterTest { @BeforeClass public static void setUp() throws Exception { Properties p = new Properties(); - p.putAll(SparkInterpreterTest.getSparkTestProperties(tmpDir)); + p.putAll(OldSparkInterpreterTest.getSparkTestProperties(tmpDir)); p.setProperty("zeppelin.spark.maxResult", "10"); p.setProperty("zeppelin.spark.concurrentSQL", "false"); p.setProperty("zeppelin.spark.sql.stacktrace", "false"); @@ -55,8 +64,8 @@ public static void setUp() throws Exception { intpGroup = new InterpreterGroup(); repl.setInterpreterGroup(intpGroup); repl.open(); - SparkInterpreterTest.repl = repl; - SparkInterpreterTest.intpGroup = intpGroup; + OldSparkInterpreterTest.repl = repl; + OldSparkInterpreterTest.intpGroup = intpGroup; sql = new SparkSqlInterpreter(p); @@ -75,13 +84,13 @@ public static void setUp() throws Exception { } @AfterClass - public static void tearDown() { + public static void tearDown() throws InterpreterException { sql.close(); repl.close(); } boolean isDataFrameSupported() { - return SparkInterpreterTest.getSparkVersionNumber(repl) >= 13; + return OldSparkInterpreterTest.getSparkVersionNumber(repl) >= 13; } @Test @@ -144,11 +153,11 @@ public void test_null_value_in_row() throws InterpreterException { "val raw = csv.map(_.split(\",\")).map(p => Row(p(0),toInt(p(1)),p(2)))", context); if (isDataFrameSupported()) { - repl.interpret("val people = z.sqlContext.createDataFrame(raw, schema)", + repl.interpret("val people = sqlContext.createDataFrame(raw, schema)", context); repl.interpret("people.toDF.registerTempTable(\"people\")", context); } else { - repl.interpret("val people = z.sqlContext.applySchema(raw, schema)", + repl.interpret("val people = sqlContext.applySchema(raw, schema)", context); repl.interpret("people.registerTempTable(\"people\")", context); } diff --git a/spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterMatplotlibTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterMatplotlibTest.java similarity index 94% rename from spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterMatplotlibTest.java rename to spark/interpreter/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterMatplotlibTest.java index 2f1077da57c..2d40871712e 100644 --- a/spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterMatplotlibTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterMatplotlibTest.java @@ -47,22 +47,22 @@ public class PySparkInterpreterMatplotlibTest { static InterpreterGroup intpGroup; static Logger LOGGER = LoggerFactory.getLogger(PySparkInterpreterTest.class); static InterpreterContext context; - + public static class AltPySparkInterpreter extends PySparkInterpreter { /** * Since pyspark output is sent to an outputstream rather than * being directly provided by interpret(), this subclass is created to * override interpret() to append the result from the outputStream - * for the sake of convenience in testing. + * for the sake of convenience in testing. */ public AltPySparkInterpreter(Properties property) { super(property); } /** - * This code is mainly copied from RemoteInterpreterServer.java which + * This code is mainly copied from RemoteInterpreterServer.java which * normally handles this in real use cases. - */ + */ @Override public InterpreterResult interpret(String st, InterpreterContext context) throws InterpreterException { context.out.clear(); @@ -82,7 +82,7 @@ public InterpreterResult interpret(String st, InterpreterContext context) throws private static Properties getPySparkTestProperties() throws IOException { Properties p = new Properties(); - p.setProperty("master", "local[*]"); + p.setProperty("spark.master", "local[*]"); p.setProperty("spark.app.name", "Zeppelin Test"); p.setProperty("zeppelin.spark.useHiveContext", "true"); p.setProperty("zeppelin.spark.maxResult", "1000"); @@ -132,10 +132,19 @@ public static void setUp() throws Exception { pyspark.setInterpreterGroup(intpGroup); pyspark.open(); + context = new InterpreterContext("note", "id", null, "title", "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new GUI(), + new AngularObjectRegistry(intpGroup.getId(), null), + new LocalResourcePool("id"), + new LinkedList(), + new InterpreterOutput(null)); } @AfterClass - public static void tearDown() { + public static void tearDown() throws InterpreterException { pyspark.close(); sparkInterpreter.close(); } @@ -145,7 +154,7 @@ public void dependenciesAreInstalled() throws InterpreterException { // matplotlib InterpreterResult ret = pyspark.interpret("import matplotlib", context); assertEquals(ret.message().toString(), InterpreterResult.Code.SUCCESS, ret.code()); - + // inline backend ret = pyspark.interpret("import backend_zinline", context); assertEquals(ret.message().toString(), InterpreterResult.Code.SUCCESS, ret.code()); @@ -178,14 +187,14 @@ public void testClose() throws InterpreterException { ret = pyspark.interpret("z.configure_mpl(interactive=False, close=True, angular=False)", context); ret = pyspark.interpret("plt.plot([1, 2, 3])", context); ret1 = pyspark.interpret("plt.show()", context); - + // Second call to show() should print nothing, and Type should be TEXT. // This is because when close=True, there should be no living instances // of FigureManager, causing show() to return before setting the output // type to HTML. ret = pyspark.interpret("plt.show()", context); assertEquals(0, ret.message().size()); - + // Now test that new plot is drawn. It should be identical to the // previous one. ret = pyspark.interpret("plt.plot([1, 2, 3])", context); @@ -193,7 +202,7 @@ public void testClose() throws InterpreterException { assertEquals(ret1.message().get(0).getType(), ret2.message().get(0).getType()); assertEquals(ret1.message().get(0).getData(), ret2.message().get(0).getData()); } - + @Test // Test for when configuration is set to not auto-close figures after show(). public void testNoClose() throws InterpreterException { @@ -205,7 +214,7 @@ public void testNoClose() throws InterpreterException { ret = pyspark.interpret("z.configure_mpl(interactive=False, close=False, angular=False)", context); ret = pyspark.interpret("plt.plot([1, 2, 3])", context); ret1 = pyspark.interpret("plt.show()", context); - + // Second call to show() should print nothing, and Type should be HTML. // This is because when close=False, there should be living instances // of FigureManager, causing show() to set the output @@ -220,7 +229,7 @@ public void testNoClose() throws InterpreterException { ret2 = pyspark.interpret("plt.show()", context); assertNotSame(ret1.message().get(0).getData(), ret2.message().get(0).getData()); } - + @Test // Test angular mode public void testAngular() throws InterpreterException { @@ -229,7 +238,7 @@ public void testAngular() throws InterpreterException { ret = pyspark.interpret("plt.close()", context); ret = pyspark.interpret("z.configure_mpl(interactive=False, close=False, angular=True)", context); ret = pyspark.interpret("plt.plot([1, 2, 3])", context); - ret = pyspark.interpret("plt.show()", context); + ret = pyspark.interpret("plt.show()", context); assertEquals(ret.message().toString(), InterpreterResult.Code.SUCCESS, ret.code()); assertEquals(ret.message().toString(), Type.ANGULAR, ret.message().get(0).getType()); @@ -237,5 +246,5 @@ public void testAngular() throws InterpreterException { AngularObjectRegistry registry = context.getAngularObjectRegistry(); String figureData = registry.getAll("note", null).get(0).toString(); assertTrue(figureData.contains("data:image/png;base64")); - } + } } diff --git a/spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterTest.java similarity index 97% rename from spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterTest.java rename to spark/interpreter/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterTest.java index 0db2bb1c49b..00972b42e01 100644 --- a/spark/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/PySparkInterpreterTest.java @@ -26,8 +26,7 @@ import org.junit.*; import org.junit.rules.TemporaryFolder; import org.junit.runners.MethodSorters; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; + import java.io.IOException; import java.util.HashMap; import java.util.LinkedList; @@ -47,12 +46,11 @@ public class PySparkInterpreterTest { static SparkInterpreter sparkInterpreter; static PySparkInterpreter pySparkInterpreter; static InterpreterGroup intpGroup; - static Logger LOGGER = LoggerFactory.getLogger(PySparkInterpreterTest.class); static InterpreterContext context; private static Properties getPySparkTestProperties() throws IOException { Properties p = new Properties(); - p.setProperty("master", "local[*]"); + p.setProperty("spark.master", "local"); p.setProperty("spark.app.name", "Zeppelin Test"); p.setProperty("zeppelin.spark.useHiveContext", "true"); p.setProperty("zeppelin.spark.maxResult", "1000"); @@ -60,6 +58,7 @@ private static Properties getPySparkTestProperties() throws IOException { p.setProperty("zeppelin.pyspark.python", "python"); p.setProperty("zeppelin.dep.localrepo", tmpDir.newFolder().getAbsolutePath()); p.setProperty("zeppelin.pyspark.useIPython", "false"); + p.setProperty("zeppelin.spark.test", "true"); return p; } @@ -107,7 +106,7 @@ public static void setUp() throws Exception { } @AfterClass - public static void tearDown() { + public static void tearDown() throws InterpreterException { pySparkInterpreter.close(); sparkInterpreter.close(); } diff --git a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkRInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkRInterpreterTest.java new file mode 100644 index 00000000000..2d585f5387c --- /dev/null +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkRInterpreterTest.java @@ -0,0 +1,99 @@ +/* + * 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.spark; + +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.LazyOpenInterpreter; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class SparkRInterpreterTest { + + private SparkRInterpreter sparkRInterpreter; + private SparkInterpreter sparkInterpreter; + + + @Test + public void testSparkRInterpreter() throws IOException, InterruptedException, InterpreterException { + Properties properties = new Properties(); + properties.setProperty("spark.master", "local"); + properties.setProperty("spark.app.name", "test"); + properties.setProperty("zeppelin.spark.maxResult", "100"); + properties.setProperty("zeppelin.spark.test", "true"); + properties.setProperty("zeppelin.spark.useNew", "true"); + properties.setProperty("zeppelin.R.knitr", "true"); + + sparkRInterpreter = new SparkRInterpreter(properties); + sparkInterpreter = new SparkInterpreter(properties); + + InterpreterGroup interpreterGroup = new InterpreterGroup(); + interpreterGroup.addInterpreterToSession(new LazyOpenInterpreter(sparkRInterpreter), "session_1"); + interpreterGroup.addInterpreterToSession(new LazyOpenInterpreter(sparkInterpreter), "session_1"); + sparkRInterpreter.setInterpreterGroup(interpreterGroup); + sparkInterpreter.setInterpreterGroup(interpreterGroup); + + sparkRInterpreter.open(); + + InterpreterResult result = sparkRInterpreter.interpret("1+1", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertTrue(result.message().get(0).getData().contains("2")); + + result = sparkRInterpreter.interpret("sparkR.version()", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + if (result.message().get(0).getData().contains("2.")) { + // spark 2.x + result = sparkRInterpreter.interpret("df <- as.DataFrame(faithful)\nhead(df)", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertTrue(result.message().get(0).getData().contains("eruptions waiting")); + } else { + // spark 1.x + result = sparkRInterpreter.interpret("df <- createDataFrame(sqlContext, faithful)\nhead(df)", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertTrue(result.message().get(0).getData().contains("eruptions waiting")); + } + } + + private InterpreterContext getInterpreterContext() { + return new InterpreterContext( + "noteId", + "paragraphId", + "replName", + "paragraphTitle", + "paragraphText", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new GUI(), + new AngularObjectRegistry("spark", null), + null, + null, + null); + } +} diff --git a/spark/src/test/java/org/apache/zeppelin/spark/SparkVersionTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkVersionTest.java similarity index 100% rename from spark/src/test/java/org/apache/zeppelin/spark/SparkVersionTest.java rename to spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkVersionTest.java diff --git a/spark/src/test/resources/log4j.properties b/spark/interpreter/src/test/resources/log4j.properties similarity index 97% rename from spark/src/test/resources/log4j.properties rename to spark/interpreter/src/test/resources/log4j.properties index 3ee61ab864b..6958d4c30fb 100644 --- a/spark/src/test/resources/log4j.properties +++ b/spark/interpreter/src/test/resources/log4j.properties @@ -45,5 +45,8 @@ log4j.logger.org.hibernate.type=ALL log4j.logger.org.apache.zeppelin.interpreter=DEBUG log4j.logger.org.apache.zeppelin.spark=DEBUG + log4j.logger.org.apache.zeppelin.python.IPythonInterpreter=DEBUG log4j.logger.org.apache.zeppelin.python.IPythonClient=DEBUG +log4j.logger.org.apache.spark.repl.Main=INFO + diff --git a/spark/src/test/scala/org/apache/zeppelin/spark/utils/DisplayFunctionsTest.scala b/spark/interpreter/src/test/scala/org/apache/zeppelin/spark/utils/DisplayFunctionsTest.scala similarity index 100% rename from spark/src/test/scala/org/apache/zeppelin/spark/utils/DisplayFunctionsTest.scala rename to spark/interpreter/src/test/scala/org/apache/zeppelin/spark/utils/DisplayFunctionsTest.scala diff --git a/spark/pom.xml b/spark/pom.xml index 71110e30ace..df66b35e525 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -16,680 +16,227 @@ ~ limitations under the License. --> - - 4.0.0 + + 4.0.0 + + + interpreter-parent + org.apache.zeppelin + 0.9.0-SNAPSHOT + ../interpreter-parent/pom.xml + - - zeppelin org.apache.zeppelin + spark-parent + pom 0.8.0-SNAPSHOT - .. - - - org.apache.zeppelin - zeppelin-spark_2.10 - jar - 0.8.0-SNAPSHOT - Zeppelin: Spark - Zeppelin spark support - - - - 1.8.2 - 2.0.2 - 14.0.1 - 1.3 - 1.9 - 3.0 - 1.12 - 3.0.3 - 1.0 - - 3.2.9 - 3.2.6 - 3.2.10 - - - 2.3 - 2.15.2 - - - **/PySparkInterpreterMatplotlibTest.java - **/*Test.* - - - - - ${project.groupId} - zeppelin-display_${scala.binary.version} - ${project.version} - - - - ${project.groupId} - zeppelin-interpreter - ${project.version} - - - - ${project.groupId} - zeppelin-python - ${project.version} - - - net.sf.py4j - py4j - - - - - - ${project.groupId} - zeppelin-python - ${project.version} - tests - test - - - net.sf.py4j - py4j - - - - - - org.slf4j - slf4j-api - - - - org.slf4j - slf4j-log4j12 - - - - org.apache.spark - spark-repl_${scala.binary.version} - ${spark.version} - provided - - - - org.apache.spark - spark-hive_${scala.binary.version} - ${spark.version} - provided - - - - - org.apache.maven - maven-plugin-api - ${maven.plugin.api.version} - - - org.codehaus.plexus - plexus-utils - - - org.sonatype.sisu - sisu-inject-plexus - - - org.apache.maven - maven-model - - - - - - org.sonatype.aether - aether-api - ${aether.version} - - - - org.sonatype.aether - aether-util - ${aether.version} - - - - org.sonatype.aether - aether-impl - ${aether.version} - - - - org.apache.maven - maven-aether-provider - ${maven.aeither.provider.version} - - - org.sonatype.aether - aether-api - - - org.sonatype.aether - aether-spi - - - org.sonatype.aether - aether-util - - - org.sonatype.aether - aether-impl - - - org.codehaus.plexus - plexus-utils - - - - - - org.sonatype.aether - aether-connector-file - ${aether.version} - - - - org.sonatype.aether - aether-connector-wagon - ${aether.version} - - - org.apache.maven.wagon - wagon-provider-api - - - - - - org.apache.maven.wagon - wagon-provider-api - ${wagon.version} - - - org.codehaus.plexus - plexus-utils - - - - - - org.apache.maven.wagon - wagon-http-lightweight - ${wagon.version} - - - org.apache.maven.wagon - wagon-http-shared - - - - - - org.apache.maven.wagon - wagon-http - ${wagon.version} - - - - - - org.apache.commons - commons-exec - ${commons.exec.version} - - - - org.scala-lang - scala-library - ${scala.version} - provided - - - - org.scala-lang - scala-compiler - ${scala.version} - provided - - - - org.scala-lang - scala-reflect - ${scala.version} - provided - - - - commons-lang - commons-lang - provided - - - - org.apache.commons - commons-compress - ${commons.compress.version} - provided - - - - org.jsoup - jsoup - ${jsoup.version} - - - - - org.scalatest - scalatest_${scala.binary.version} - ${scalatest.version} - test - - - - junit - junit - test - - - - org.datanucleus - datanucleus-core - ${datanucleus.core.version} - test - - - - org.datanucleus - datanucleus-api-jdo - ${datanucleus.apijdo.version} - test - - - - org.datanucleus - datanucleus-rdbms - ${datanucleus.rdbms.version} - test - - - - org.mockito - mockito-core - test - - - - org.powermock - powermock-api-mockito - test - - - - org.powermock - powermock-module-junit4 - test - - - - - - - - maven-enforcer-plugin - - - enforce - none - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - 1 - false - -Xmx1024m -XX:MaxPermSize=256m - - **/SparkRInterpreterTest.java - ${pyspark.test.exclude} - - - - ../interpreter/spark/pyspark/pyspark.zip:../interpreter/spark/pyspark/py4j-${spark.py4j.version}-src.zip:../interpreter/lib/python - - - - - - org.apache.maven.plugins - maven-shade-plugin - ${plugin.shade.version} - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - reference.conf - - - - - - - com.google - org.apache.zeppelin.com.google - - - - io.netty - org.apache.zeppelin.io.netty - - - - - - package - - shade - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - package - - copy - - - ${project.build.directory}/../../interpreter/spark - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - - - - - - org.scala-tools - maven-scala-plugin - ${plugin.scala.version} - - ${scala.version} - - **/ZeppelinR.scala - **/SparkRBackend.scala - - - - - compile - - compile - - compile - - - test-compile - - testCompile - - test-compile - - - process-resources - - compile - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - ${pyspark.test.exclude} - - - - - org.scala-tools - maven-scala-plugin - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - ${pyspark.test.exclude} - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - ${pyspark.test.exclude} - - - - - org.scala-tools - maven-scala-plugin - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - ${pyspark.test.exclude} - - - - - - maven-resources-plugin - - - copy-interpreter-setting - package - - resources - - - ${project.build.directory}/../../interpreter/spark - - - - - - - - - - spark-1.4 - - 1.4.1 - - - - - - - - spark-1.5 - - 1.5.2 - com.typesafe.akka - 2.3.11 - 2.5.0 - - - - - spark-1.6 - - 1.6.3 - 0.9 - com.typesafe.akka - 2.3.11 - 2.5.0 - - - - - spark-2.0 - - 2.0.2 - 2.5.0 - 0.10.3 - - + spark-parent + Zeppelin spark support + + + + 3.2.9 + 3.2.6 + 3.2.10 + + + 2.4.1 + 2.15.2 + + 2.2.0 + 0.10.4 + - - spark-2.1 - - 2.1.0 - 2.5.0 - 0.10.4 - 2.11.8 - - + - - spark-2.2 - - true - - - 2.2.0 - 2.5.0 - 0.10.4 - - + + org.apache.zeppelin + zeppelin-interpreter + ${project.version} + - - hadoop-0.23 - - + - org.apache.avro - avro + org.apache.zeppelin + zeppelin-display + ${project.version} + test - - - 0.23.10 - - - - hadoop-1 - - 1.0.4 - hadoop1 - 1.8.8 - org.spark-project.akka - - + + org.scalatest + scalatest_${scala.binary.version} + ${scalatest.version} + test + - - hadoop-2.2 - - 2.2.0 - 2.5.0 - hadoop2 - - + + junit + junit + test + - - hadoop-2.3 - - 2.3.0 - 2.5.0 - 0.9.3 - hadoop2 - - + + org.datanucleus + datanucleus-core + ${datanucleus.core.version} + test + - - hadoop-2.4 - - 2.4.0 - 2.5.0 - 0.9.3 - hadoop2 - - + + org.datanucleus + datanucleus-api-jdo + ${datanucleus.apijdo.version} + test + - - hadoop-2.6 - - 2.6.0 - 2.5.0 - 0.9.3 - hadoop2 - - + + org.datanucleus + datanucleus-rdbms + ${datanucleus.rdbms.version} + test + - - hadoop-2.7 - - 2.7.2 - 2.5.0 - 0.9.0 - hadoop2 - - - +
    + + + + + maven-enforcer-plugin + + + enforce + none + + + + + + org.scalatest + scalatest-maven-plugin + + ${project.build.directory}/surefire-reports + . + WDF TestSuite.txt + + + + test + + test + + + + + + + net.alchim31.maven + scala-maven-plugin + 3.2.2 + + + eclipse-add-source + + add-source + + + + scala-compile-first + process-resources + + compile + + + + scala-test-compile-first + process-test-resources + + testCompile + + + + + ${scala.compile.version} + + + + -unchecked + -deprecation + -feature + + + -Xms1024m + -Xmx1024m + -XX:PermSize=${PermGen} + -XX:MaxPermSize=${MaxPermGen} + + + -source + ${java.version} + -target + ${java.version} + -Xlint:all,-serial,-path,-options + + + + + + + + + + + spark-2.2 + + true + + + 2.2.0 + 0.10.4 + + + + + spark-2.1 + + 2.1.0 + 0.10.4 + + + + + spark-2.0 + + 2.0.2 + 0.10.3 + + + + + spark-1.6 + + 1.6.3 + 0.9 + + + + + spark-1.5 + + 1.5.2 + 0.8.2.1 + + + + + spark-1.4 + + 1.4.1 + 0.8.2.1 + + + + diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml new file mode 100644 index 00000000000..e32e620bf30 --- /dev/null +++ b/spark/scala-2.10/pom.xml @@ -0,0 +1,41 @@ + + + + 4.0.0 + org.apache.zeppelin + spark-scala-2.10 + 0.9.0-SNAPSHOT + jar + Spark Interpreter: Scala_2.10 + + + org.apache.zeppelin + spark-scala-parent + 0.9.0-SNAPSHOT + ../spark-scala-parent/pom.xml + + + + 2.10.5 + 2.10 + ${scala.version} + + + diff --git a/spark/scala-2.10/spark-scala-parent b/spark/scala-2.10/spark-scala-parent new file mode 120000 index 00000000000..e5e899e58cf --- /dev/null +++ b/spark/scala-2.10/spark-scala-parent @@ -0,0 +1 @@ +../spark-scala-parent \ No newline at end of file diff --git a/spark/scala-2.10/src/main/scala/org/apache/zeppelin/spark/SparkScala210Interpreter.scala b/spark/scala-2.10/src/main/scala/org/apache/zeppelin/spark/SparkScala210Interpreter.scala new file mode 100644 index 00000000000..43aa8643639 --- /dev/null +++ b/spark/scala-2.10/src/main/scala/org/apache/zeppelin/spark/SparkScala210Interpreter.scala @@ -0,0 +1,141 @@ +/* + * 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.spark + +import java.io.File +import java.nio.file.{Files, Paths} + +import org.apache.spark.SparkConf +import org.apache.spark.repl.SparkILoop +import org.apache.spark.repl.SparkILoop._ +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion +import org.apache.zeppelin.interpreter.util.InterpreterOutputStream +import org.apache.zeppelin.interpreter.{InterpreterContext, InterpreterResult} +import org.slf4j.{Logger, LoggerFactory} + +import scala.tools.nsc.Settings +import scala.tools.nsc.interpreter._ + +/** + * SparkInterpreter for scala-2.10 + */ +class SparkScala210Interpreter(override val conf: SparkConf, + override val depFiles: java.util.List[String]) + extends BaseSparkScalaInterpreter(conf, depFiles) { + + lazy override val LOGGER: Logger = LoggerFactory.getLogger(getClass) + + private var sparkILoop: SparkILoop = _ + + override val interpreterOutput = + new InterpreterOutputStream(LoggerFactory.getLogger(classOf[SparkScala210Interpreter])) + + override def open(): Unit = { + super.open() + // redirect the output of open to InterpreterOutputStream, so that user can have more + // diagnose info in frontend + if (InterpreterContext.get() != null) { + interpreterOutput.setInterpreterOutput(InterpreterContext.get().out) + } + val rootDir = conf.get("spark.repl.classdir", System.getProperty("java.io.tmpdir")) + val outputDir = Files.createTempDirectory(Paths.get(rootDir), "spark").toFile + outputDir.deleteOnExit() + conf.set("spark.repl.class.outputDir", outputDir.getAbsolutePath) + // Only Spark1 requires to create http server, Spark2 removes HttpServer class. + startHttpServer(outputDir).foreach { case (server, uri) => + sparkHttpServer = server + conf.set("spark.repl.class.uri", uri) + } + + val settings = new Settings() + settings.embeddedDefaults(Thread.currentThread().getContextClassLoader()) + settings.usejavacp.value = true + settings.classpath.value = getUserJars.mkString(File.pathSeparator) + Console.setOut(interpreterOutput) + sparkILoop = new SparkILoop(null, new JPrintWriter(Console.out, true)) + + setDeclaredField(sparkILoop, "settings", settings) + callMethod(sparkILoop, "createInterpreter") + sparkILoop.initializeSynchronous() + callMethod(sparkILoop, "postInitialization") + val reader = callMethod(sparkILoop, + "org$apache$spark$repl$SparkILoop$$chooseReader", + Array(settings.getClass), Array(settings)).asInstanceOf[InteractiveReader] + setDeclaredField(sparkILoop, "org$apache$spark$repl$SparkILoop$$in", reader) + scalaCompleter = reader.completion.completer() + + createSparkContext() + } + + override def close(): Unit = { + super.close() + if (sparkILoop != null) { + callMethod(sparkILoop, "org$apache$spark$repl$SparkILoop$$closeInterpreter") + } + } + + protected override def interpret(code: String, context: InterpreterContext): InterpreterResult = { + if (context != null) { + interpreterOutput.setInterpreterOutput(context.out) + context.out.clear() + } else { + interpreterOutput.setInterpreterOutput(null) + } + + Console.withOut(if (context != null) context.out else Console.out) { + interpreterOutput.ignoreLeadingNewLinesFromScalaReporter() + // add print("") at the end in case the last line is comment which lead to INCOMPLETE + val lines = code.split("\\n") ++ List("print(\"\")") + var incompleteCode = "" + var lastStatus: InterpreterResult.Code = null + for (line <- lines if !line.trim.isEmpty) { + val nextLine = if (incompleteCode != "") { + incompleteCode + "\n" + line + } else { + line + } + scalaInterpret(nextLine) match { + case scala.tools.nsc.interpreter.IR.Success => + // continue the next line + incompleteCode = "" + lastStatus = InterpreterResult.Code.SUCCESS + case error@scala.tools.nsc.interpreter.IR.Error => + return new InterpreterResult(InterpreterResult.Code.ERROR) + case scala.tools.nsc.interpreter.IR.Incomplete => + // put this line into inCompleteCode for the next execution. + incompleteCode = incompleteCode + "\n" + line + lastStatus = InterpreterResult.Code.INCOMPLETE + } + } + // flush all output before returning result to frontend + Console.flush() + interpreterOutput.setInterpreterOutput(null) + return new InterpreterResult(lastStatus) + } + } + + def scalaInterpret(code: String): scala.tools.nsc.interpreter.IR.Result = + sparkILoop.interpret(code) + + protected def bind(name: String, tpe: String, value: Object, modifier: List[String]): Unit = { + sparkILoop.beQuietDuring { + sparkILoop.bind(name, tpe, value, modifier) + } + } + +} diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml new file mode 100644 index 00000000000..d9113d1075e --- /dev/null +++ b/spark/scala-2.11/pom.xml @@ -0,0 +1,41 @@ + + + + 4.0.0 + org.apache.zeppelin + spark-scala-2.11 + 0.9.0-SNAPSHOT + jar + Spark Interpreter: Scala_2.11 + + + org.apache.zeppelin + spark-scala-parent + 0.9.0-SNAPSHOT + ../spark-scala-parent/pom.xml + + + + 2.11.8 + 2.11 + ${scala.version} + + + diff --git a/spark/scala-2.11/spark-scala-parent b/spark/scala-2.11/spark-scala-parent new file mode 120000 index 00000000000..e5e899e58cf --- /dev/null +++ b/spark/scala-2.11/spark-scala-parent @@ -0,0 +1 @@ +../spark-scala-parent \ No newline at end of file diff --git a/spark/scala-2.11/src/main/resources/log4j.properties b/spark/scala-2.11/src/main/resources/log4j.properties new file mode 100644 index 00000000000..0c90b21ae00 --- /dev/null +++ b/spark/scala-2.11/src/main/resources/log4j.properties @@ -0,0 +1,50 @@ +# +# 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. +# + +# Direct log messages to stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c:%L - %m%n +#log4j.appender.stdout.layout.ConversionPattern= +#%5p [%t] (%F:%L) - %m%n +#%-4r [%t] %-5p %c %x - %m%n +# + +# Root logger option +log4j.rootLogger=INFO, stdout + +#mute some noisy guys +log4j.logger.org.apache.hadoop.mapred=WARN +log4j.logger.org.apache.hadoop.hive.ql=WARN +log4j.logger.org.apache.hadoop.hive.metastore=WARN +log4j.logger.org.apache.haadoop.hive.service.HiveServer=WARN +log4j.logger.org.apache.zeppelin.scheduler=WARN + +log4j.logger.org.quartz=WARN +log4j.logger.DataNucleus=WARN +log4j.logger.DataNucleus.MetaData=ERROR +log4j.logger.DataNucleus.Datastore=ERROR + +# Log all JDBC parameters +log4j.logger.org.hibernate.type=ALL + +log4j.logger.org.apache.zeppelin.interpreter=DEBUG +log4j.logger.org.apache.zeppelin.spark=DEBUG + + +log4j.logger.org.apache.spark.repl.Main=INFO diff --git a/spark/scala-2.11/src/main/scala/org/apache/zeppelin/spark/SparkScala211Interpreter.scala b/spark/scala-2.11/src/main/scala/org/apache/zeppelin/spark/SparkScala211Interpreter.scala new file mode 100644 index 00000000000..e1452606c09 --- /dev/null +++ b/spark/scala-2.11/src/main/scala/org/apache/zeppelin/spark/SparkScala211Interpreter.scala @@ -0,0 +1,140 @@ +/* + * 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.spark + +import java.io.{BufferedReader, File} +import java.net.URLClassLoader +import java.nio.file.{Files, Paths} + +import org.apache.spark.SparkConf +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion +import org.apache.zeppelin.interpreter.util.InterpreterOutputStream +import org.apache.zeppelin.interpreter.{InterpreterContext, InterpreterResult} +import org.slf4j.LoggerFactory +import org.slf4j.Logger + +import scala.tools.nsc.Settings +import scala.tools.nsc.interpreter._ + +/** + * SparkInterpreter for scala-2.11 + */ +class SparkScala211Interpreter(override val conf: SparkConf, + override val depFiles: java.util.List[String]) + extends BaseSparkScalaInterpreter(conf, depFiles) { + + lazy override val LOGGER: Logger = LoggerFactory.getLogger(getClass) + + private var sparkILoop: ILoop = _ + + override val interpreterOutput = new InterpreterOutputStream(LOGGER) + + override def open(): Unit = { + super.open() + if (conf.get("spark.master", "local") == "yarn-client") { + System.setProperty("SPARK_YARN_MODE", "true") + } + // Only Spark1 requires to create http server, Spark2 removes HttpServer class. + val rootDir = conf.get("spark.repl.classdir", System.getProperty("java.io.tmpdir")) + val outputDir = Files.createTempDirectory(Paths.get(rootDir), "spark").toFile + outputDir.deleteOnExit() + conf.set("spark.repl.class.outputDir", outputDir.getAbsolutePath) + startHttpServer(outputDir).foreach { case (server, uri) => + sparkHttpServer = server + conf.set("spark.repl.class.uri", uri) + } + + val settings = new Settings() + settings.processArguments(List("-Yrepl-class-based", + "-Yrepl-outdir", s"${outputDir.getAbsolutePath}"), true) + settings.embeddedDefaults(Thread.currentThread().getContextClassLoader()) + settings.usejavacp.value = true + settings.classpath.value = getUserJars.mkString(File.pathSeparator) + + val replOut = new JPrintWriter(interpreterOutput, true) + sparkILoop = new ILoop(None, replOut) + sparkILoop.settings = settings + sparkILoop.createInterpreter() + + val in0 = getField(sparkILoop, "scala$tools$nsc$interpreter$ILoop$$in0").asInstanceOf[Option[BufferedReader]] + val reader = in0.fold(sparkILoop.chooseReader(settings))(r => SimpleReader(r, replOut, interactive = true)) + + sparkILoop.in = reader + sparkILoop.initializeSynchronous() + callMethod(sparkILoop, "scala$tools$nsc$interpreter$ILoop$$loopPostInit") + this.scalaCompleter = reader.completion.completer() + + createSparkContext() + } + + protected def bind(name: String, tpe: String, value: Object, modifier: List[String]): Unit = { + sparkILoop.beQuietDuring { + sparkILoop.bind(name, tpe, value, modifier) + } + } + + + override def close(): Unit = { + super.close() + if (sparkILoop != null) { + sparkILoop.closeInterpreter() + } + } + + protected override def interpret(code: String, context: InterpreterContext): InterpreterResult = { + if (context != null) { + interpreterOutput.setInterpreterOutput(context.out) + context.out.clear() + } + + Console.withOut(if (context != null) context.out else Console.out) { + interpreterOutput.ignoreLeadingNewLinesFromScalaReporter() + // add print("") at the end in case the last line is comment which lead to INCOMPLETE + val lines = code.split("\\n") ++ List("print(\"\")") + var incompleteCode = "" + var lastStatus: InterpreterResult.Code = null + for (line <- lines if !line.trim.isEmpty) { + val nextLine = if (incompleteCode != "") { + incompleteCode + "\n" + line + } else { + line + } + scalaInterpret(nextLine) match { + case scala.tools.nsc.interpreter.IR.Success => + // continue the next line + incompleteCode = "" + lastStatus = InterpreterResult.Code.SUCCESS + case error@scala.tools.nsc.interpreter.IR.Error => + return new InterpreterResult(InterpreterResult.Code.ERROR) + case scala.tools.nsc.interpreter.IR.Incomplete => + // put this line into inCompleteCode for the next execution. + incompleteCode = incompleteCode + "\n" + line + lastStatus = InterpreterResult.Code.INCOMPLETE + } + } + // flush all output before returning result to frontend + Console.flush() + interpreterOutput.setInterpreterOutput(null) + return new InterpreterResult(lastStatus) + } + } + + def scalaInterpret(code: String): scala.tools.nsc.interpreter.IR.Result = + sparkILoop.interpret(code) + +} diff --git a/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml similarity index 57% rename from spark-dependencies/pom.xml rename to spark/spark-dependencies/pom.xml index b7904c091fa..ae169e50524 100644 --- a/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -21,7 +21,7 @@ 4.0.0 - zeppelin + spark-parent org.apache.zeppelin 0.8.0-SNAPSHOT .. @@ -44,7 +44,6 @@ instead of changing spark.version in this section. --> - 1.4.1 2.3.0 ${hadoop.version} 1.7.7 @@ -62,7 +61,6 @@ http://d3kbcqa49mib13.cloudfront.net/${spark.archive}-bin-without-hadoop.tgz - 0.8.2.1 2.3 @@ -359,480 +357,6 @@ - - - spark-1.1 - - - - - 1.1.1 - 2.2.3-shaded-protobuf - - - - - cassandra-spark-1.1 - - - com.datastax.spark - spark-cassandra-connector_${scala.binary.version} - 1.1.1 - - - org.joda - joda-convert - - - - - - 1.1.1 - 2.2.3-shaded-protobuf - - - - - spark-1.2 - - - - 1.2.1 - - - - - cassandra-spark-1.2 - - 1.2.1 - - - - com.datastax.spark - spark-cassandra-connector_${scala.binary.version} - 1.2.1 - - - org.joda - joda-convert - - - - - - - - spark-1.3 - - - 1.3.1 - - - - - - - - - cassandra-spark-1.3 - - 1.3.0 - - - - - com.datastax.spark - spark-cassandra-connector_${scala.binary.version} - 1.3.1 - - - org.joda - joda-convert - - - - - - - - spark-1.4 - - 1.4.1 - - - - - - - - cassandra-spark-1.4 - - 1.4.1 - - - - - com.datastax.spark - spark-cassandra-connector_${scala.binary.version} - 1.4.0 - - - org.joda - joda-convert - - - - - - - - spark-1.5 - - 1.5.2 - com.typesafe.akka - 2.3.11 - 2.5.0 - - - - - - - - cassandra-spark-1.5 - - 1.5.1 - com.typesafe.akka - 2.3.11 - 2.5.0 - 16.0.1 - - - - - com.datastax.spark - spark-cassandra-connector_${scala.binary.version} - 1.5.0 - - - org.joda - joda-convert - - - - - - - - spark-1.6 - - 1.6.3 - 0.9 - com.typesafe.akka - 2.3.11 - 2.5.0 - - - - - spark-2.0 - - 2.0.2 - 2.5.0 - 0.10.3 - - - - - spark-2.1 - - 2.1.0 - 2.5.0 - 0.10.4 - 2.11.8 - - - - - spark-2.2 - - true - - - 2.2.0 - 2.5.0 - 0.10.4 - - - - - hadoop-0.23 - - - - org.apache.avro - avro - - - - 0.23.10 - - - - - hadoop-1 - - 1.0.4 - hadoop1 - 1.8.8 - org.spark-project.akka - - - - - hadoop-2.2 - - 2.2.0 - 2.5.0 - hadoop2 - - - - - hadoop-2.3 - - 2.3.0 - 2.5.0 - 0.9.3 - hadoop2 - - - - - hadoop-2.4 - - 2.4.0 - 2.5.0 - 0.9.3 - hadoop2 - - - - - hadoop-2.6 - - 2.6.0 - 2.5.0 - 0.9.3 - hadoop2 - - - - - hadoop-2.7 - - 2.7.2 - 2.5.0 - 0.9.0 - hadoop2 - - - - - mapr3 - - false - - - 1.0.3-mapr-3.0.3 - 2.3.0-mapr-4.0.0-FCS - 0.7.1 - - - - mapr-releases - http://repository.mapr.com/maven/ - - false - - - true - - - - - - - mapr40 - - false - - - 2.4.1-mapr-1503 - 2.4.1-mapr-1503 - 0.9.3 - - - - org.apache.curator - curator-recipes - 2.4.0 - - - org.apache.zookeeper - zookeeper - - - - - org.apache.zookeeper - zookeeper - 3.4.5-mapr-1503 - - - - - mapr-releases - http://repository.mapr.com/maven/ - - false - - - true - - - - - - - mapr41 - - false - - - 2.5.1-mapr-1503 - 2.5.1-mapr-1503 - 0.7.1 - - - - org.apache.curator - curator-recipes - 2.4.0 - - - org.apache.zookeeper - zookeeper - - - - - org.apache.zookeeper - zookeeper - 3.4.5-mapr-1503 - - - - - mapr-releases - http://repository.mapr.com/maven/ - - false - - - true - - - - - - - mapr50 - - false - - - 2.7.0-mapr-1506 - 2.7.0-mapr-1506 - 0.9.3 - - - - org.apache.curator - curator-recipes - 2.4.0 - - - org.apache.zookeeper - zookeeper - - - - - org.apache.zookeeper - zookeeper - 3.4.5-mapr-1503 - - - - - mapr-releases - http://repository.mapr.com/maven/ - - false - - - true - - - - - - - mapr51 - - false - - - 2.7.0-mapr-1602 - 2.7.0-mapr-1602 - 0.9.3 - - - - org.apache.curator - curator-recipes - 2.4.0 - - - org.apache.zookeeper - zookeeper - - - - - org.apache.zookeeper - zookeeper - 3.4.5-mapr-1503 - - - - - mapr-releases - http://repository.mapr.com/maven/ - - false - - - true - - - - - - - @@ -900,13 +424,24 @@ maven-dependency-plugin - copy-dependencies + copy-interpreter-dependencies + package + + copy-dependencies + + + true + + + + + copy-spark-interpreter-dependencies package copy-dependencies - ${project.build.directory}/../../interpreter/spark/dep + ${project.build.directory}/../../../interpreter/spark/dep false false true @@ -914,12 +449,13 @@ + copy-artifact package copy - ${project.build.directory}/../../interpreter/spark/dep + ${project.build.directory}/../../../interpreter/spark/dep false false true @@ -936,6 +472,19 @@ + + maven-resources-plugin + + + copy-interpreter-setting + none + + true + + + + + com.googlecode.maven-download-plugin @@ -981,10 +530,10 @@ - - - + + @@ -1025,7 +574,7 @@ copy-resources - ${project.build.directory}/../../interpreter/spark/R/lib + ${project.build.directory}/../../../interpreter/spark/R/lib diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml new file mode 100644 index 00000000000..830fa59a684 --- /dev/null +++ b/spark/spark-scala-parent/pom.xml @@ -0,0 +1,172 @@ + + + + + + + spark-parent + org.apache.zeppelin + 0.9.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.apache.zeppelin + spark-scala-parent + 0.9.0-SNAPSHOT + pom + + + + + org.apache.zeppelin + zeppelin-interpreter + ${project.version} + + + + org.apache.spark + spark-repl_${scala.binary.version} + ${spark.version} + provided + + + + org.apache.spark + spark-core_${scala.binary.version} + ${spark.version} + provided + + + + org.apache.spark + spark-hive_${scala.binary.version} + ${spark.version} + provided + + + + org.scala-lang + scala-compiler + ${scala.version} + provided + + + + org.scala-lang + scala-library + ${scala.version} + provided + + + + org.scala-lang + scala-reflect + ${scala.version} + provided + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-scala-sources + generate-sources + + add-source + + + + ${project.basedir}/../spark-scala-parent/src/main/scala + + + + + add-scala-test-sources + generate-test-sources + + add-test-source + + + + ${project.basedir}/../spark-scala-parent/src/test/scala + + + + + add-resource + generate-resources + + add-resource + + + + + ${project.basedir}/../spark-scala-parent/src/main/resources + + + + + + add-test-resource + generate-test-resources + + add-test-resource + + + + + ${project.basedir}/../spark-scala-parent/src/test/resources + + + + + + + + + maven-dependency-plugin + + true + + + + + maven-resources-plugin + + + copy-interpreter-setting + none + + true + + + + + + + + + \ No newline at end of file diff --git a/spark/spark-scala-parent/src/main/scala/org/apache/zeppelin/spark/BaseSparkScalaInterpreter.scala b/spark/spark-scala-parent/src/main/scala/org/apache/zeppelin/spark/BaseSparkScalaInterpreter.scala new file mode 100644 index 00000000000..3ef4fe71e00 --- /dev/null +++ b/spark/spark-scala-parent/src/main/scala/org/apache/zeppelin/spark/BaseSparkScalaInterpreter.scala @@ -0,0 +1,338 @@ +/* + * 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.spark + + +import java.io.File + +import org.apache.spark.sql.SQLContext +import org.apache.spark.{SparkConf, SparkContext} +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion +import org.apache.zeppelin.interpreter.util.InterpreterOutputStream +import org.apache.zeppelin.interpreter.{InterpreterContext, InterpreterResult} +import org.slf4j.{Logger, LoggerFactory} + +import scala.collection.JavaConverters._ +import scala.tools.nsc.interpreter.Completion.ScalaCompleter +import scala.util.control.NonFatal + +/** + * Base class for different scala versions of SparkInterpreter. It should be + * binary compatible between multiple scala versions. + * @param conf + * @param depFiles + */ +abstract class BaseSparkScalaInterpreter(val conf: SparkConf, + val depFiles: java.util.List[String]) { + + protected lazy val LOGGER: Logger = LoggerFactory.getLogger(getClass) + + private val isTest = conf.getBoolean("zeppelin.spark.test", false) + + protected var sc: SparkContext = _ + + protected var sqlContext: SQLContext = _ + + protected var sparkSession: Object = _ + + protected var sparkHttpServer: Object = _ + + protected var sparkUrl: String = _ + + protected var scalaCompleter: ScalaCompleter = _ + + protected val interpreterOutput: InterpreterOutputStream + + protected def open(): Unit = { + /* Required for scoped mode. + * In scoped mode multiple scala compiler (repl) generates class in the same directory. + * Class names is not randomly generated and look like '$line12.$read$$iw$$iw' + * Therefore it's possible to generated class conflict(overwrite) with other repl generated + * class. + * + * To prevent generated class name conflict, + * change prefix of generated class name from each scala compiler (repl) instance. + * + * In Spark 2.x, REPL generated wrapper class name should compatible with the pattern + * ^(\$line(?:\d+)\.\$read)(?:\$\$iw)+$ + * + * As hashCode() can return a negative integer value and the minus character '-' is invalid + * in a package name we change it to a numeric value '0' which still conforms to the regexp. + * + */ + System.setProperty("scala.repl.name.line", ("$line" + this.hashCode).replace('-', '0')) + } + + protected def interpret(code: String, context: InterpreterContext): InterpreterResult + + protected def interpret(code: String): InterpreterResult = interpret(code, null) + + protected def scalaInterpret(code: String): scala.tools.nsc.interpreter.IR.Result + + protected def completion(buf: String, + cursor: Int, + context: InterpreterContext): java.util.List[InterpreterCompletion] = { + val completions = scalaCompleter.complete(buf, cursor).candidates + .map(e => new InterpreterCompletion(e, e, null)) + scala.collection.JavaConversions.seqAsJavaList(completions) + } + + protected def getProgress(jobGroup: String, context: InterpreterContext): Int = { + val jobIds = sc.statusTracker.getJobIdsForGroup(jobGroup) + val jobs = jobIds.flatMap { id => sc.statusTracker.getJobInfo(id) } + val stages = jobs.flatMap { job => + job.stageIds().flatMap(sc.statusTracker.getStageInfo) + } + + val taskCount = stages.map(_.numTasks).sum + val completedTaskCount = stages.map(_.numCompletedTasks).sum + if (taskCount == 0) { + 0 + } else { + (100 * completedTaskCount.toDouble / taskCount).toInt + } + } + + protected def bind(name: String, tpe: String, value: Object, modifier: List[String]): Unit + + // for use in java side + protected def bind(name: String, + tpe: String, + value: Object, + modifier: java.util.List[String]): Unit = + bind(name, tpe, value, modifier.asScala.toList) + + protected def close(): Unit = { + if (sc != null) { + sc.stop() + } + if (sparkHttpServer != null) { + sparkHttpServer.getClass.getMethod("stop").invoke(sparkHttpServer) + } + sc = null + sqlContext = null + if (sparkSession != null) { + sparkSession.getClass.getMethod("stop").invoke(sparkSession) + sparkSession = null + } + + } + + protected def createSparkContext(): Unit = { + if (isSparkSessionPresent()) { + spark2CreateContext() + } else { + spark1CreateContext() + } + } + + private def spark1CreateContext(): Unit = { + this.sc = SparkContext.getOrCreate(conf) + if (!isTest) { + interpreterOutput.write("Created SparkContext.\n".getBytes()) + } + getUserFiles().foreach(file => sc.addFile(file)) + + sc.getClass.getMethod("ui").invoke(sc).asInstanceOf[Option[_]] match { + case Some(webui) => + sparkUrl = webui.getClass.getMethod("appUIAddress").invoke(webui).asInstanceOf[String] + case None => + } + + val hiveSiteExisted: Boolean = + Thread.currentThread().getContextClassLoader.getResource("hive-site.xml") != null + val hiveEnabled = conf.getBoolean("spark.useHiveContext", false) + if (hiveEnabled && hiveSiteExisted) { + sqlContext = Class.forName("org.apache.spark.sql.hive.HiveContext") + .getConstructor(classOf[SparkContext]).newInstance(sc).asInstanceOf[SQLContext] + if (!isTest) { + interpreterOutput.write("Created sql context (with Hive support).\n".getBytes()) + } + } else { + if (hiveEnabled && !hiveSiteExisted && !isTest) { + interpreterOutput.write(("spark.useHiveContext is set as true but no hive-site.xml" + + " is found in classpath, so zeppelin will fallback to SQLContext.\n").getBytes()) + } + sqlContext = Class.forName("org.apache.spark.sql.SQLContext") + .getConstructor(classOf[SparkContext]).newInstance(sc).asInstanceOf[SQLContext] + if (!isTest) { + interpreterOutput.write("Created sql context.\n".getBytes()) + } + } + + bind("sc", "org.apache.spark.SparkContext", sc, List("""@transient""")) + bind("sqlContext", sqlContext.getClass.getCanonicalName, sqlContext, List("""@transient""")) + + interpret("import org.apache.spark.SparkContext._") + interpret("import sqlContext.implicits._") + interpret("import sqlContext.sql") + interpret("import org.apache.spark.sql.functions._") + } + + private def spark2CreateContext(): Unit = { + val sparkClz = Class.forName("org.apache.spark.sql.SparkSession$") + val sparkObj = sparkClz.getField("MODULE$").get(null) + + val builderMethod = sparkClz.getMethod("builder") + val builder = builderMethod.invoke(sparkObj) + builder.getClass.getMethod("config", classOf[SparkConf]).invoke(builder, conf) + + if (conf.get("spark.sql.catalogImplementation", "in-memory").toLowerCase == "hive" + || conf.get("spark.useHiveContext", "false").toLowerCase == "true") { + val hiveSiteExisted: Boolean = + Thread.currentThread().getContextClassLoader.getResource("hive-site.xml") != null + val hiveClassesPresent = + sparkClz.getMethod("hiveClassesArePresent").invoke(sparkObj).asInstanceOf[Boolean] + if (hiveSiteExisted && hiveClassesPresent) { + builder.getClass.getMethod("enableHiveSupport").invoke(builder) + sparkSession = builder.getClass.getMethod("getOrCreate").invoke(builder) + if (!isTest) { + interpreterOutput.write("Created Spark session (with Hive support).\n".getBytes()) + } + } else { + if (!hiveClassesPresent && !isTest) { + interpreterOutput.write( + "Hive support can not be enabled because spark is not built with hive\n".getBytes) + } + if (!hiveSiteExisted && !isTest) { + interpreterOutput.write( + "Hive support can not be enabled because no hive-site.xml found\n".getBytes) + } + sparkSession = builder.getClass.getMethod("getOrCreate").invoke(builder) + if (!isTest) { + interpreterOutput.write("Created Spark session.\n".getBytes()) + } + } + } else { + sparkSession = builder.getClass.getMethod("getOrCreate").invoke(builder) + if (!isTest) { + interpreterOutput.write("Created Spark session.\n".getBytes()) + } + } + + sc = sparkSession.getClass.getMethod("sparkContext").invoke(sparkSession) + .asInstanceOf[SparkContext] + getUserFiles().foreach(file => sc.addFile(file)) + sqlContext = sparkSession.getClass.getMethod("sqlContext").invoke(sparkSession) + .asInstanceOf[SQLContext] + sc.getClass.getMethod("uiWebUrl").invoke(sc).asInstanceOf[Option[String]] match { + case Some(url) => sparkUrl = url + case None => + } + + bind("spark", sparkSession.getClass.getCanonicalName, sparkSession, List("""@transient""")) + bind("sc", "org.apache.spark.SparkContext", sc, List("""@transient""")) + bind("sqlContext", "org.apache.spark.sql.SQLContext", sqlContext, List("""@transient""")) + + interpret("import org.apache.spark.SparkContext._") + interpret("import spark.implicits._") + interpret("import spark.sql") + interpret("import org.apache.spark.sql.functions._") + } + + private def isSparkSessionPresent(): Boolean = { + try { + Class.forName("org.apache.spark.sql.SparkSession") + true + } catch { + case _: ClassNotFoundException | _: NoClassDefFoundError => false + } + } + + protected def getField(obj: Object, name: String): Object = { + val field = obj.getClass.getField(name) + field.setAccessible(true) + field.get(obj) + } + + protected def getDeclareField(obj: Object, name: String): Object = { + val field = obj.getClass.getDeclaredField(name) + field.setAccessible(true) + field.get(obj) + } + + protected def setDeclaredField(obj: Object, name: String, value: Object): Unit = { + val field = obj.getClass.getDeclaredField(name) + field.setAccessible(true) + field.set(obj, value) + } + + protected def callMethod(obj: Object, name: String): Object = { + callMethod(obj, name, Array.empty[Class[_]], Array.empty[Object]) + } + + protected def callMethod(obj: Object, name: String, + parameterTypes: Array[Class[_]], + parameters: Array[Object]): Object = { + val method = obj.getClass.getMethod(name, parameterTypes: _ *) + method.setAccessible(true) + method.invoke(obj, parameters: _ *) + } + + protected def startHttpServer(outputDir: File): Option[(Object, String)] = { + try { + val httpServerClass = Class.forName("org.apache.spark.HttpServer") + val securityManager = { + val constructor = Class.forName("org.apache.spark.SecurityManager") + .getConstructor(classOf[SparkConf]) + constructor.setAccessible(true) + constructor.newInstance(conf).asInstanceOf[Object] + } + val httpServerConstructor = httpServerClass + .getConstructor(classOf[SparkConf], + classOf[File], + Class.forName("org.apache.spark.SecurityManager"), + classOf[Int], + classOf[String]) + httpServerConstructor.setAccessible(true) + // Create Http Server + val port = conf.getInt("spark.replClassServer.port", 0) + val server = httpServerConstructor + .newInstance(conf, outputDir, securityManager, new Integer(port), "HTTP server") + .asInstanceOf[Object] + + // Start Http Server + val startMethod = server.getClass.getMethod("start") + startMethod.setAccessible(true) + startMethod.invoke(server) + + // Get uri of this Http Server + val uriMethod = server.getClass.getMethod("uri") + uriMethod.setAccessible(true) + val uri = uriMethod.invoke(server).asInstanceOf[String] + Some((server, uri)) + } catch { + // Spark 2.0+ removed HttpServer, so return null instead. + case NonFatal(e) => + None + } + } + + protected def getUserJars(): Seq[String] = { + val sparkJars = conf.getOption("spark.jars").map(_.split(",")) + .map(_.filter(_.nonEmpty)).toSeq.flatten + val depJars = depFiles.asScala.filter(_.endsWith(".jar")) + val result = sparkJars ++ depJars + conf.set("spark.jars", result.mkString(",")) + result + } + + protected def getUserFiles(): Seq[String] = { + depFiles.asScala.filter(!_.endsWith(".jar")) + } +} diff --git a/spark/src/test/java/org/apache/zeppelin/spark/dep/SparkDependencyResolverTest.java b/spark/src/test/java/org/apache/zeppelin/spark/dep/SparkDependencyResolverTest.java deleted file mode 100644 index b226a001d24..00000000000 --- a/spark/src/test/java/org/apache/zeppelin/spark/dep/SparkDependencyResolverTest.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.spark.dep; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -public class SparkDependencyResolverTest { - - @Test - public void testInferScalaVersion() { - String [] version = scala.util.Properties.versionNumberString().split("[.]"); - String scalaVersion = version[0] + "." + version[1]; - - assertEquals("groupId:artifactId:version", - SparkDependencyResolver.inferScalaVersion("groupId:artifactId:version")); - assertEquals("groupId:artifactId_" + scalaVersion + ":version", - SparkDependencyResolver.inferScalaVersion("groupId::artifactId:version")); - assertEquals("groupId:artifactId:version::test", - SparkDependencyResolver.inferScalaVersion("groupId:artifactId:version::test")); - assertEquals("*", - SparkDependencyResolver.inferScalaVersion("*")); - assertEquals("groupId:*", - SparkDependencyResolver.inferScalaVersion("groupId:*")); - assertEquals("groupId:artifactId*", - SparkDependencyResolver.inferScalaVersion("groupId:artifactId*")); - assertEquals("groupId:artifactId_" + scalaVersion, - SparkDependencyResolver.inferScalaVersion("groupId::artifactId")); - assertEquals("groupId:artifactId_" + scalaVersion + "*", - SparkDependencyResolver.inferScalaVersion("groupId::artifactId*")); - assertEquals("groupId:artifactId_" + scalaVersion + ":*", - SparkDependencyResolver.inferScalaVersion("groupId::artifactId:*")); - } - -} diff --git a/testing/install_external_dependencies.sh b/testing/install_external_dependencies.sh index e34296e3ab6..d6c07368b9d 100755 --- a/testing/install_external_dependencies.sh +++ b/testing/install_external_dependencies.sh @@ -44,6 +44,6 @@ if [[ -n "$PYTHON" ]] ; then conda update -q conda conda info -a conda config --add channels conda-forge - conda install -q matplotlib pandasql ipython=5.4.1 jupyter_client ipykernel matplotlib bokeh=0.12.6 - pip install -q grpcio ggplot + conda install -q matplotlib pandasql ipython=5.4.1 jupyter_client ipykernel matplotlib bokeh=0.12.10 + pip install -q grpcio ggplot bkzep==0.4.0 fi diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index 4058aefbf11..ae2292307fc 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -27,7 +27,7 @@ org.apache.zeppelin - zeppelin-display_2.10 + zeppelin-display jar 0.8.0-SNAPSHOT Zeppelin: Display system apis @@ -45,18 +45,21 @@ org.scala-lang scala-library ${scala.version} + provided org.scala-lang scala-compiler ${scala.version} + provided org.scala-lang scalap ${scala.version} + provided @@ -84,13 +87,6 @@ test - - org.scala-lang - scala-library - ${scala.version} - provided - - org.scalatest scalatest_${scala.binary.version} diff --git a/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java index f7bb7768d8c..1804fc4cb24 100644 --- a/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java +++ b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java @@ -184,7 +184,7 @@ public void testSqlSpark() throws Exception { } } - @Test +// @Test public void testDep() throws Exception { try { // restart spark interpreter before running %dep diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/BaseZeppelinContext.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/BaseZeppelinContext.java index 65bb06fe143..e38a29f82b8 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/BaseZeppelinContext.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/BaseZeppelinContext.java @@ -237,6 +237,8 @@ public void show(Object o, int maxResult) { if (isSupportedObject(o)) { interpreterContext.out.write(showData(o)); } else { + interpreterContext.out.write("ZeppelinContext doesn't support to show type: " + + o.getClass().getCanonicalName() + "\n"); interpreterContext.out.write(o.toString()); } } catch (IOException e) { diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java index c2a578c565e..37c20ad5253 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java @@ -96,10 +96,11 @@ * Entry point for Interpreter process. * Accepting thrift connections from ZeppelinServer. */ -public class RemoteInterpreterServer - extends Thread - implements RemoteInterpreterService.Iface, AngularObjectRegistryListener { - Logger logger = LoggerFactory.getLogger(RemoteInterpreterServer.class); +public class RemoteInterpreterServer extends Thread + implements RemoteInterpreterService.Iface, AngularObjectRegistryListener { + + private static Logger logger = LoggerFactory.getLogger(RemoteInterpreterServer.class); + InterpreterGroup interpreterGroup; AngularObjectRegistry angularObjectRegistry; @@ -254,6 +255,9 @@ public boolean isRunning() { public static void main(String[] args) throws TTransportException, InterruptedException, IOException { + Class klass = RemoteInterpreterServer.class; + URL location = klass.getResource('/' + klass.getName().replace('.', '/') + ".class"); + logger.info("URL:" + location); String callbackHost = null; int port = Constants.ZEPPELIN_INTERPRETER_DEFAUlT_PORT; String portRange = ":"; diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 296d58f01fd..a2cf6104da0 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -261,6 +261,12 @@ scalatest_${scala.binary.version} ${scalatest.version} test + + + org.scala-lang.modules + scala-xml_${scala.binary.version} + + 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 7d4c21cb7d3..519342012ac 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 @@ -265,21 +265,21 @@ private static void start(boolean withAuth, String testClassName, boolean withKn // set spark master and other properties sparkProperties.put("master", new InterpreterProperty("master", "local[2]", InterpreterPropertyType.TEXTAREA.getValue())); + sparkProperties.put("spark.master", + new InterpreterProperty("spark.master", "local[2]", InterpreterPropertyType.TEXTAREA.getValue())); sparkProperties.put("spark.cores.max", new InterpreterProperty("spark.cores.max", "2", InterpreterPropertyType.TEXTAREA.getValue())); sparkProperties.put("zeppelin.spark.useHiveContext", new InterpreterProperty("zeppelin.spark.useHiveContext", false, InterpreterPropertyType.CHECKBOX.getValue())); - // set spark home for pyspark - sparkProperties.put("spark.home", - new InterpreterProperty("spark.home", getSparkHome(), InterpreterPropertyType.TEXTAREA.getValue())); sparkProperties.put("zeppelin.pyspark.useIPython", new InterpreterProperty("zeppelin.pyspark.useIPython", "false", InterpreterPropertyType.TEXTAREA.getValue())); - + sparkProperties.put("zeppelin.spark.test", new InterpreterProperty("zeppelin.spark.test", "true", InterpreterPropertyType.TEXTAREA.getValue())); sparkIntpSetting.setProperties(sparkProperties); pySpark = true; sparkR = true; ZeppelinServer.notebook.getInterpreterSettingManager().restart(sparkIntpSetting.getId()); } else { String sparkHome = getSparkHome(); + LOG.info("SPARK HOME detected " + sparkHome); if (sparkHome != null) { if (System.getenv("SPARK_MASTER") != null) { sparkProperties.put("master", @@ -288,14 +288,14 @@ private static void start(boolean withAuth, String testClassName, boolean withKn sparkProperties.put("master", new InterpreterProperty("master", "local[2]", InterpreterPropertyType.TEXTAREA.getValue())); } + sparkProperties.put("spark.master", + new InterpreterProperty("spark.master", "local[2]", InterpreterPropertyType.TEXTAREA.getValue())); sparkProperties.put("spark.cores.max", new InterpreterProperty("spark.cores.max", "2", InterpreterPropertyType.TEXTAREA.getValue())); - // set spark home for pyspark - sparkProperties.put("spark.home", - new InterpreterProperty("spark.home", sparkHome, InterpreterPropertyType.TEXTAREA.getValue())); sparkProperties.put("zeppelin.spark.useHiveContext", new InterpreterProperty("zeppelin.spark.useHiveContext", false, InterpreterPropertyType.CHECKBOX.getValue())); sparkProperties.put("zeppelin.pyspark.useIPython", new InterpreterProperty("zeppelin.pyspark.useIPython", "false", InterpreterPropertyType.TEXTAREA.getValue())); + sparkProperties.put("zeppelin.spark.test", new InterpreterProperty("zeppelin.spark.test", "true", InterpreterPropertyType.TEXTAREA.getValue())); pySpark = true; sparkR = true; @@ -333,7 +333,6 @@ private static String getSparkHome() { return sparkHome; } sparkHome = getSparkHomeRecursively(new File(System.getProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName()))); - System.out.println("SPARK HOME detected " + sparkHome); return sparkHome; } 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 615675544ec..f3a70996414 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 @@ -167,8 +167,8 @@ public void sparkSQLTest() throws IOException { assertEquals(InterpreterResult.Type.TABLE, p.getResult().message().get(1).getType()); assertEquals("_1\t_2\nhello\t20\n", p.getResult().message().get(1).getData()); } - ZeppelinServer.notebook.removeNote(note.getId(), anonymous); } + ZeppelinServer.notebook.removeNote(note.getId(), anonymous); } @Test @@ -470,7 +470,7 @@ public void pySparkDepLoaderTest() throws IOException, InterpreterException { p1.setText("%pyspark\n" + "from pyspark.sql import SQLContext\n" + "print(" + sqlContextName + ".read.format('com.databricks.spark.csv')" + - ".load('"+ tmpFile.getAbsolutePath() +"').count())"); + ".load('" + tmpFile.getAbsolutePath() +"').count())"); p1.setAuthenticationInfo(anonymous); note.run(p1.getId()); @@ -576,6 +576,7 @@ public void testPySparkZeppelinContextDynamicForms() throws IOException { @Test public void testConfInterpreter() throws IOException { + ZeppelinServer.notebook.getInterpreterSettingManager().close(); Note note = ZeppelinServer.notebook.createNote(AuthenticationInfo.ANONYMOUS); Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); Map config = p.getConfig(); diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index 08de7ad50bc..e38a6598c0e 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -603,7 +603,7 @@ org.apache.zeppelin - zeppelin-spark_2.10 + spark-interpreter ${project.version} test From b4eccf4cb74a5c50e9e264b799262c3387954b77 Mon Sep 17 00:00:00 2001 From: skymon Date: Wed, 31 Jan 2018 11:54:53 +0100 Subject: [PATCH 167/492] Update configuration.md Corrected spelling "ba" -> "be" ### What is this PR for? Minor spelling correction ### What type of PR is it? Documentation ### Todos - ### What is the Jira issue - ### How should this be tested? - ### Screenshots (if appropriate) ### Questions: - Author: skymon Closes #2754 from skymon/patch-1 and squashes the following commits: c4a7be75e [skymon] Update configuration.md (cherry picked from commit 6f4f0ff96c800a7f78df2b4401e04147400378f9) Signed-off-by: Lee moon soo --- docs/setup/operation/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup/operation/configuration.md b/docs/setup/operation/configuration.md index 458affc9287..1f4c6a24231 100644 --- a/docs/setup/operation/configuration.md +++ b/docs/setup/operation/configuration.md @@ -27,7 +27,7 @@ limitations under the License. There are two locations you can configure Apache Zeppelin. * **Environment variables** can be defined `conf/zeppelin-env.sh`(`conf\zeppelin-env.cmd` for Windows). -* **Java properties** can ba defined in `conf/zeppelin-site.xml`. +* **Java properties** can be defined in `conf/zeppelin-site.xml`. If both are defined, then the **environment variables** will take priority. > Mouse hover on each property and click then you can get a link for that. From 4660ab073de97926375cd682dc33ad8edc38a2b9 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Fri, 2 Feb 2018 19:13:33 +0800 Subject: [PATCH 168/492] [HOTFIX] Fix spark version mismatch ### What is this PR for? Trivial fix for version mismatch in pom file. ### What type of PR is it? [Hot Fix] ### Todos * [ ] - Task ### What is the Jira issue? * No jira ### How should this be tested? * CI pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2761 from zjffdu/spark_version and squashes the following commits: bf41fa4 [Jeff Zhang] [HOTFIX] Fix spark version mismatch --- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 2 +- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 449646242dc..122cb14fbd4 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index df66b35e525..4ccf699513a 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,7 +24,7 @@ interpreter-parent org.apache.zeppelin - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent/pom.xml diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index e32e620bf30..9252bad4459 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT jar Spark Interpreter: Scala_2.10 org.apache.zeppelin spark-scala-parent - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT ../spark-scala-parent/pom.xml diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index d9113d1075e..77b3cdfb053 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT jar Spark Interpreter: Scala_2.11 org.apache.zeppelin spark-scala-parent - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT ../spark-scala-parent/pom.xml diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index 830fa59a684..90556785a24 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT pom From 7ba5e6ffc6145d0e5426f821878ccc7be8067d7c Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Wed, 7 Feb 2018 14:47:46 +0800 Subject: [PATCH 169/492] ZEPPELIN-3171. Restart of interpreter in note also aborts running interpreter in another note ### What is this PR for? The root cause is that in isolated mode interpreters will share the same scheduler. That means when one interpreter is terminated, all the running jobs under the scheduler of this interpreter will be aborted too. This PR will create one scheduler for each session. So when one session is closed, only the running jobs under this session's scheduler is aborted. ### What type of PR is it? [Bug Fix] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3171 ### How should this be tested? * Unit test is added. * Also verify it manually. ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2769 from zjffdu/ZEPPELIN-3171 and squashes the following commits: 3586f45 [Jeff Zhang] ZEPPELIN-3171. Restart of interpreter in note also aborts running interpreter in another note (cherry picked from commit d07c70a6dc32d3d2668198b2e4c10c57602f8ab8) Signed-off-by: Jeff Zhang --- .../zeppelin/interpreter/remote/RemoteInterpreter.java | 7 ++++--- .../zeppelin/interpreter/remote/RemoteInterpreterTest.java | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java index bda8010d93c..f38d037243e 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java @@ -380,15 +380,16 @@ public String call(Client client) throws Exception { }); } - //TODO(zjffdu) Share the Scheduler in the same session or in the same InterpreterGroup ? + @Override public Scheduler getScheduler() { int maxConcurrency = Integer.parseInt( getProperty("zeppelin.interpreter.max.poolsize", ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_MAX_POOL_SIZE.getIntValue() + "")); - + // one session own one Scheduler, so that when one session is closed, all the jobs/paragraphs + // running under the scheduler of this session will be aborted. Scheduler s = new RemoteScheduler( - RemoteInterpreter.class.getName() + "-" + sessionId, + RemoteInterpreter.class.getName() + "-" + getInterpreterGroup().getId() + "-" + sessionId, SchedulerFactory.singleton().getExecutor(), sessionId, this, diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java index 7f9978a5aa8..04b7a5bf1a2 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterTest.java @@ -93,6 +93,8 @@ public void testSharedMode() throws InterpreterException, IOException { assertTrue(interpreter2 instanceof RemoteInterpreter); RemoteInterpreter remoteInterpreter2 = (RemoteInterpreter) interpreter2; + assertEquals(remoteInterpreter1.getScheduler(), remoteInterpreter2.getScheduler()); + InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), null, null, new ArrayList(), null); @@ -136,6 +138,8 @@ public void testScopedMode() throws InterpreterException, IOException { assertTrue(interpreter2 instanceof RemoteInterpreter); RemoteInterpreter remoteInterpreter2 = (RemoteInterpreter) interpreter2; + assertNotEquals(interpreter1.getScheduler(), interpreter2.getScheduler()); + InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), null, null, new ArrayList(), null); @@ -182,6 +186,8 @@ public void testIsolatedMode() throws InterpreterException, IOException { assertTrue(interpreter2 instanceof RemoteInterpreter); RemoteInterpreter remoteInterpreter2 = (RemoteInterpreter) interpreter2; + assertNotEquals(interpreter1.getScheduler(), interpreter2.getScheduler()); + InterpreterContext context1 = new InterpreterContext("noteId", "paragraphId", "repl", "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), null, null, new ArrayList(), null); From 3745e65b4ed8a678324dd34953ee7ca276ab8e34 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Wed, 23 Aug 2017 20:34:15 +0800 Subject: [PATCH 170/492] [ZEPPELIN-2909]. Support shared SparkContext across language in livy interpreter ### What is this PR for? LIVY-194 implement the shared SparkContext across languages, this ticket is trying to integrate this feature. ### What type of PR is it? [ Feature ] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-2909 ### How should this be tested? Tested is added ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2587 from zjffdu/ZEPPELIN-2909 and squashes the following commits: d6e38e6 [Jeff Zhang] [ZEPPELIN-2909]. Support shared SparkContext across language in livy interpreter (cherry picked from commit ad6e691812957e240d37918bbeae4a3c537fcccf) Signed-off-by: Jeff Zhang --- .travis.yml | 8 +- docs/interpreter/livy.md | 4 + livy/pom.xml | 148 +++++++++++++++++- .../zeppelin/livy/BaseLivyInterpreter.java | 88 +++++++++-- .../zeppelin/livy/LivySharedInterpreter.java | 108 +++++++++++++ .../zeppelin/livy/LivySparkInterpreter.java | 1 + .../livy/LivySparkSQLInterpreter.java | 36 ++++- .../org/apache/zeppelin/livy/LivyVersion.java | 5 + .../main/resources/interpreter-setting.json | 16 ++ .../zeppelin/livy/LivyInterpreterIT.java | 133 ++++++++++++++-- 10 files changed, 508 insertions(+), 39 deletions(-) create mode 100644 livy/src/main/java/org/apache/zeppelin/livy/LivySharedInterpreter.java diff --git a/.travis.yml b/.travis.yml index fd8c868cebd..61aab1f57ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -107,17 +107,17 @@ matrix: dist: trusty env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" - # Test python/pyspark with python 2, livy 0.2 + # Test python/pyspark with python 2, livy 0.5 - sudo: required dist: trusty jdk: "openjdk7" - env: PYTHON="2" SCALA_VER="2.10" SPARK_VER="1.6.1" HADOOP_VER="2.6" LIVY_VER="0.4.0-incubating" PROFILE="-Pspark-1.6 -Phadoop2 -Phadoop-2.6 -Plivy-0.2 -Pscala-2.10" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.10" SPARK_VER="1.6.3" HADOOP_VER="2.6" LIVY_VER="0.5.0-incubating" PROFILE="-Pspark-1.6 -Phadoop2 -Phadoop-2.6 -Pscala-2.10" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" - # Test python/pyspark with python 3, livy 0.3 + # Test python/pyspark with python 3, livy 0.5 - sudo: required dist: trusty jdk: "openjdk7" - env: PYTHON="3" SCALA_VER="2.11" SPARK_VER="2.0.0" HADOOP_VER="2.6" LIVY_VER="0.4.0-incubating" PROFILE="-Pspark-2.0 -Phadoop3 -Phadoop-2.6 -Pscala-2.11 -Plivy-0.3" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" + env: PYTHON="3" SCALA_VER="2.11" SPARK_VER="2.0.0" HADOOP_VER="2.6" LIVY_VER="0.5.0-incubating" PROFILE="-Pspark-2.0 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" before_install: # check files included in commit range, clear bower_components if a bower.json file has changed. diff --git a/docs/interpreter/livy.md b/docs/interpreter/livy.md index d53672a94b2..e4784d4513d 100644 --- a/docs/interpreter/livy.md +++ b/docs/interpreter/livy.md @@ -216,6 +216,10 @@ select * from products where ${product_id=1} And creating dynamic formst programmatically is not feasible in livy interpreter, because ZeppelinContext is not available in livy interpreter. +## Shared SparkContext +Starting from livy 0.5 which is supported by Zeppelin 0.8.0, SparkContext is shared between scala, python, r and sql. +That means you can query the table via `%livy.sql` when this table is registered in `%livy.spark`, `%livy.pyspark`, `$livy.sparkr`. + ## FAQ Livy debugging: If you see any of these in error console diff --git a/livy/pom.xml b/livy/pom.xml index 0ec174a9c4f..d33adf6dc91 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -42,8 +42,9 @@ 1.0.1.RELEASE - 0.4.0-incubating + 0.5.0-incubating 2.1.0 + 2.6.0 2.16 1.8 @@ -105,6 +106,30 @@ org.apache.spark spark-yarn_${scala.binary.version} + + org.apache.hadoop + hadoop-auth + + + org.apache.hadoop + hadoop-common + + + org.apache.hadoop + hadoop-hdfs + + + org.apache.hadoop + hadoop-yarn-client + + + org.apache.hadoop + hadoop-client + + + org.apache.hadoop + hadoop-yarn-server-tests + @@ -188,6 +213,127 @@ test + + org.apache.hadoop + hadoop-auth + ${hadoop.version} + test + + + + org.apache.hadoop + hadoop-common + ${hadoop.version} + test + + + com.google.guava + guava + + + + + + org.apache.hadoop + hadoop-common + tests + ${hadoop.version} + test + + + com.google.guava + guava + + + + + + org.apache.hadoop + hadoop-hdfs + ${hadoop.version} + test + + + io.netty + netty + + + com.google.guava + guava + + + + + + org.apache.hadoop + hadoop-hdfs + tests + ${hadoop.version} + test + + + io.netty + netty + + + com.google.guava + guava + + + + + + org.apache.hadoop + hadoop-client + ${hadoop.version} + test + + + com.google.guava + guava + + + + + + org.apache.hadoop + hadoop-yarn-client + ${hadoop.version} + test + + + com.google.guava + guava + + + + + + org.apache.hadoop + hadoop-yarn-api + ${hadoop.version} + test + + + com.google.guava + guava + + + + + + org.apache.hadoop + hadoop-yarn-server-tests + tests + ${hadoop.version} + test + + + com.google.guava + guava + + + diff --git a/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java b/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java index f3b75792e1e..724a4b36c7c 100644 --- a/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java +++ b/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java @@ -57,6 +57,8 @@ import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResultMessage; import org.apache.zeppelin.interpreter.InterpreterUtils; +import org.apache.zeppelin.interpreter.LazyOpenInterpreter; +import org.apache.zeppelin.interpreter.WrappedInterpreter; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -95,6 +97,9 @@ public abstract class BaseLivyInterpreter extends Interpreter { private RestTemplate restTemplate; private Map customHeaders = new HashMap<>(); + // delegate to sharedInterpreter when it is available + protected LivySharedInterpreter sharedInterpreter; + Set paragraphsToCancel = Collections.newSetFromMap( new ConcurrentHashMap()); private ConcurrentHashMap paragraphId2StmtProgressMap = @@ -144,7 +149,13 @@ Map getCustomHeaders() { @Override public void open() throws InterpreterException { try { - initLivySession(); + this.livyVersion = getLivyVersion(); + if (this.livyVersion.isSharedSupported()) { + sharedInterpreter = getLivySharedInterpreter(); + } + if (sharedInterpreter == null || !sharedInterpreter.isSupported()) { + initLivySession(); + } } catch (LivyException e) { String msg = "Fail to create session, please check livy interpreter log and " + "livy server log"; @@ -152,8 +163,32 @@ public void open() throws InterpreterException { } } + protected LivySharedInterpreter getLivySharedInterpreter() throws InterpreterException { + LazyOpenInterpreter lazy = null; + LivySharedInterpreter sharedInterpreter = null; + Interpreter p = getInterpreterInTheSameSessionByClassName( + LivySharedInterpreter.class.getName()); + + while (p instanceof WrappedInterpreter) { + if (p instanceof LazyOpenInterpreter) { + lazy = (LazyOpenInterpreter) p; + } + p = ((WrappedInterpreter) p).getInnerInterpreter(); + } + sharedInterpreter = (LivySharedInterpreter) p; + + if (lazy != null) { + lazy.open(); + } + return sharedInterpreter; + } + @Override public void close() { + if (sharedInterpreter != null && sharedInterpreter.isSupported()) { + sharedInterpreter.close(); + return; + } if (sessionInfo != null) { closeSession(sessionInfo.id); // reset sessionInfo to null so that we won't close it twice. @@ -181,14 +216,6 @@ protected void initLivySession() throws LivyException { } else { LOGGER.info("Create livy session successfully with sessionId: {}", this.sessionInfo.id); } - // check livy version - try { - this.livyVersion = getLivyVersion(); - LOGGER.info("Use livy " + livyVersion); - } catch (APINotFoundException e) { - this.livyVersion = new LivyVersion("0.2.0"); - LOGGER.info("Use livy 0.2.0"); - } } protected abstract String extractAppId() throws LivyException; @@ -196,17 +223,30 @@ protected void initLivySession() throws LivyException { protected abstract String extractWebUIAddress() throws LivyException; public SessionInfo getSessionInfo() { + if (sharedInterpreter != null && sharedInterpreter.isSupported()) { + return sharedInterpreter.getSessionInfo(); + } return sessionInfo; } + public String getCodeType() { + if (getSessionKind().equalsIgnoreCase("pyspark3")) { + return "pyspark"; + } + return getSessionKind(); + } + @Override public InterpreterResult interpret(String st, InterpreterContext context) { + if (sharedInterpreter != null && sharedInterpreter.isSupported()) { + return sharedInterpreter.interpret(st, getCodeType(), context); + } if (StringUtils.isEmpty(st)) { return new InterpreterResult(InterpreterResult.Code.SUCCESS, ""); } try { - return interpret(st, context.getParagraphId(), this.displayAppInfo, true); + return interpret(st, null, context.getParagraphId(), this.displayAppInfo, true); } catch (LivyException e) { LOGGER.error("Fail to interpret:" + st, e); return new InterpreterResult(InterpreterResult.Code.ERROR, @@ -245,6 +285,10 @@ private List callCompletion(CompletionRequest req) throws @Override public void cancel(InterpreterContext context) { + if (sharedInterpreter != null && sharedInterpreter.isSupported()) { + sharedInterpreter.cancel(context); + return; + } paragraphsToCancel.add(context.getParagraphId()); LOGGER.info("Added paragraph " + context.getParagraphId() + " for cancellation."); } @@ -256,6 +300,10 @@ public FormType getFormType() { @Override public int getProgress(InterpreterContext context) { + if (sharedInterpreter != null && sharedInterpreter.isSupported()) { + return sharedInterpreter.getProgress(context); + } + if (livyVersion.isGetProgressSupported()) { String paraId = context.getParagraphId(); Integer progress = paragraphId2StmtProgressMap.get(paraId); @@ -312,11 +360,20 @@ public InterpreterResult interpret(String code, String paragraphId, boolean displayAppInfo, boolean appendSessionExpired) throws LivyException { + return interpret(code, sharedInterpreter.isSupported() ? getSessionKind() : null, + paragraphId, displayAppInfo, appendSessionExpired); + } + + public InterpreterResult interpret(String code, + String codeType, + String paragraphId, + boolean displayAppInfo, + boolean appendSessionExpired) throws LivyException { StatementInfo stmtInfo = null; boolean sessionExpired = false; try { try { - stmtInfo = executeStatement(new ExecuteRequest(code)); + stmtInfo = executeStatement(new ExecuteRequest(code, codeType)); } catch (SessionNotFoundException e) { LOGGER.warn("Livy session {} is expired, new session will be created.", sessionInfo.id); sessionExpired = true; @@ -328,7 +385,7 @@ public InterpreterResult interpret(String code, initLivySession(); } } - stmtInfo = executeStatement(new ExecuteRequest(code)); + stmtInfo = executeStatement(new ExecuteRequest(code, codeType)); } // pull the statement status @@ -731,11 +788,12 @@ public static SessionInfo fromJson(String json) { } } - private static class ExecuteRequest { + static class ExecuteRequest { public final String code; - - public ExecuteRequest(String code) { + public final String kind; + public ExecuteRequest(String code, String kind) { this.code = code; + this.kind = kind; } public String toJson() { diff --git a/livy/src/main/java/org/apache/zeppelin/livy/LivySharedInterpreter.java b/livy/src/main/java/org/apache/zeppelin/livy/LivySharedInterpreter.java new file mode 100644 index 00000000000..77e288bfa65 --- /dev/null +++ b/livy/src/main/java/org/apache/zeppelin/livy/LivySharedInterpreter.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.livy; + +import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Properties; + +/** + * Livy Interpreter for shared kind which share SparkContext across spark/pyspark/r + */ +public class LivySharedInterpreter extends BaseLivyInterpreter { + + private static final Logger LOGGER = LoggerFactory.getLogger(LivySharedInterpreter.class); + + private boolean isSupported = false; + + public LivySharedInterpreter(Properties property) { + super(property); + } + + @Override + public void open() throws InterpreterException { + try { + // check livy version + try { + this.livyVersion = getLivyVersion(); + LOGGER.info("Use livy " + livyVersion); + } catch (APINotFoundException e) { + // assume it is livy 0.2.0 when livy doesn't support rest api of fetching version. + this.livyVersion = new LivyVersion("0.2.0"); + LOGGER.info("Use livy 0.2.0"); + } + + if (livyVersion.isSharedSupported()) { + LOGGER.info("LivySharedInterpreter is supported."); + isSupported = true; + initLivySession(); + } else { + LOGGER.info("LivySharedInterpreter is not supported."); + isSupported = false; + } + } catch (LivyException e) { + String msg = "Fail to create session, please check livy interpreter log and " + + "livy server log"; + throw new InterpreterException(msg, e); + } + } + + public boolean isSupported() { + return isSupported; + } + + public InterpreterResult interpret(String st, String codeType, InterpreterContext context) { + if (StringUtils.isEmpty(st)) { + return new InterpreterResult(InterpreterResult.Code.SUCCESS, ""); + } + + try { + return interpret(st, codeType, context.getParagraphId(), this.displayAppInfo, true); + } catch (LivyException e) { + LOGGER.error("Fail to interpret:" + st, e); + return new InterpreterResult(InterpreterResult.Code.ERROR, + InterpreterUtils.getMostRelevantMessage(e)); + } + } + + @Override + public String getSessionKind() { + return "shared"; + } + + @Override + protected String extractAppId() throws LivyException { + return null; + } + + @Override + protected String extractWebUIAddress() throws LivyException { + return null; + } + + public static void main(String[] args) { + ExecuteRequest request = new ExecuteRequest("1+1", null); + System.out.println(request.toJson()); + } +} diff --git a/livy/src/main/java/org/apache/zeppelin/livy/LivySparkInterpreter.java b/livy/src/main/java/org/apache/zeppelin/livy/LivySparkInterpreter.java index 606ef64a8a4..066d0da8cc9 100644 --- a/livy/src/main/java/org/apache/zeppelin/livy/LivySparkInterpreter.java +++ b/livy/src/main/java/org/apache/zeppelin/livy/LivySparkInterpreter.java @@ -44,6 +44,7 @@ protected String extractAppId() throws LivyException { protected String extractWebUIAddress() throws LivyException { interpret( "val webui=sc.getClass.getMethod(\"ui\").invoke(sc).asInstanceOf[Some[_]].get", + null, null, false, false); return extractStatementResult( interpret( diff --git a/livy/src/main/java/org/apache/zeppelin/livy/LivySparkSQLInterpreter.java b/livy/src/main/java/org/apache/zeppelin/livy/LivySparkSQLInterpreter.java index 7b2d7d66684..2faa350bfb1 100644 --- a/livy/src/main/java/org/apache/zeppelin/livy/LivySparkSQLInterpreter.java +++ b/livy/src/main/java/org/apache/zeppelin/livy/LivySparkSQLInterpreter.java @@ -19,11 +19,14 @@ import org.apache.commons.lang.StringUtils; import static org.apache.commons.lang.StringEscapeUtils.escapeJavaScript; +import org.apache.zeppelin.display.GUI; import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; +import org.apache.zeppelin.user.AuthenticationInfo; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Properties; @@ -39,6 +42,7 @@ public class LivySparkSQLInterpreter extends BaseLivyInterpreter { "zeppelin.livy.spark.sql.maxResult"; private LivySparkInterpreter sparkInterpreter; + private String codeType = null; private boolean isSpark2 = false; private int maxResult = 1000; @@ -64,7 +68,21 @@ public void open() throws InterpreterException { // As we don't know whether livyserver use spark2 or spark1, so we will detect SparkSession // to judge whether it is using spark2. try { - InterpreterResult result = sparkInterpreter.interpret("spark", null, false, false); + InterpreterContext context = new InterpreterContext( + "noteId", + "paragraphId", + "replName", + "paragraphTitle", + "paragraphText", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new GUI(), + null, + null, + null, + new InterpreterOutput(null)); + InterpreterResult result = sparkInterpreter.interpret("spark", context); if (result.code() == InterpreterResult.Code.SUCCESS && result.message().get(0).getData().contains("org.apache.spark.sql.SparkSession")) { LOGGER.info("SparkSession is detected so we are using spark 2.x for session {}", @@ -72,7 +90,7 @@ public void open() throws InterpreterException { isSpark2 = true; } else { // spark 1.x - result = sparkInterpreter.interpret("sqlContext", null, false, false); + result = sparkInterpreter.interpret("sqlContext", context); if (result.code() == InterpreterResult.Code.SUCCESS) { LOGGER.info("sqlContext is detected."); } else if (result.code() == InterpreterResult.Code.ERROR) { @@ -81,7 +99,7 @@ public void open() throws InterpreterException { LOGGER.info("sqlContext is not detected, try to create SQLContext by ourselves"); result = sparkInterpreter.interpret( "val sqlContext = new org.apache.spark.sql.SQLContext(sc)\n" - + "import sqlContext.implicits._", null, false, false); + + "import sqlContext.implicits._", context); if (result.code() == InterpreterResult.Code.ERROR) { throw new LivyException("Fail to create SQLContext," + result.message().get(0).getData()); @@ -128,9 +146,7 @@ public InterpreterResult interpret(String line, InterpreterContext context) { sqlQuery = "sqlContext.sql(\"\"\"" + line + "\"\"\").show(" + maxResult + ", " + truncate + ")"; } - InterpreterResult result = sparkInterpreter.interpret(sqlQuery, context.getParagraphId(), - this.displayAppInfo, true); - + InterpreterResult result = sparkInterpreter.interpret(sqlQuery, context); if (result.code() == InterpreterResult.Code.SUCCESS) { InterpreterResult result2 = new InterpreterResult(InterpreterResult.Code.SUCCESS); for (InterpreterResultMessage message : result.message()) { @@ -248,12 +264,16 @@ public Scheduler getScheduler() { @Override public void cancel(InterpreterContext context) { - sparkInterpreter.cancel(context); + if (this.sparkInterpreter != null) { + sparkInterpreter.cancel(context); + } } @Override public void close() { - this.sparkInterpreter.close(); + if (this.sparkInterpreter != null) { + this.sparkInterpreter.close(); + } } @Override diff --git a/livy/src/main/java/org/apache/zeppelin/livy/LivyVersion.java b/livy/src/main/java/org/apache/zeppelin/livy/LivyVersion.java index 7cfecfb197c..81bb8d4956f 100644 --- a/livy/src/main/java/org/apache/zeppelin/livy/LivyVersion.java +++ b/livy/src/main/java/org/apache/zeppelin/livy/LivyVersion.java @@ -29,6 +29,7 @@ public class LivyVersion { protected static final LivyVersion LIVY_0_2_0 = LivyVersion.fromVersionString("0.2.0"); protected static final LivyVersion LIVY_0_3_0 = LivyVersion.fromVersionString("0.3.0"); protected static final LivyVersion LIVY_0_4_0 = LivyVersion.fromVersionString("0.4.0"); + protected static final LivyVersion LIVY_0_5_0 = LivyVersion.fromVersionString("0.5.0"); private int version; private String versionString; @@ -79,6 +80,10 @@ public boolean isGetProgressSupported() { return this.newerThanEquals(LIVY_0_4_0); } + public boolean isSharedSupported() { + return this.newerThanEquals(LIVY_0_5_0); + } + public boolean equals(Object versionToCompare) { return version == ((LivyVersion) versionToCompare).version; } diff --git a/livy/src/main/resources/interpreter-setting.json b/livy/src/main/resources/interpreter-setting.json index 2d724875659..cecacac2e48 100644 --- a/livy/src/main/resources/interpreter-setting.json +++ b/livy/src/main/resources/interpreter-setting.json @@ -227,5 +227,21 @@ "editOnDblClick": false, "completionKey": "TAB" } + }, + { + "group": "livy", + "name": "shared", + "className": "org.apache.zeppelin.livy.LivySharedInterpreter", + "properties": { + }, + "option": { + "remote": true, + "port": -1, + "perNote": "shared", + "perUser": "scoped", + "isExistingProcess": false, + "setPermission": false, + "users": [] + } } ] diff --git a/livy/src/test/java/org/apache/zeppelin/livy/LivyInterpreterIT.java b/livy/src/test/java/org/apache/zeppelin/livy/LivyInterpreterIT.java index ef3eabe66ec..3dfeb363f8f 100644 --- a/livy/src/test/java/org/apache/zeppelin/livy/LivyInterpreterIT.java +++ b/livy/src/test/java/org/apache/zeppelin/livy/LivyInterpreterIT.java @@ -34,6 +34,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; public class LivyInterpreterIT { @@ -76,7 +77,7 @@ public static boolean checkPreCondition() { } -// @Test + @Test public void testSparkInterpreterRDD() throws InterpreterException { if (!checkPreCondition()) { return; @@ -197,7 +198,7 @@ public void run() { } -// @Test + @Test public void testSparkInterpreterDataFrame() throws InterpreterException { if (!checkPreCondition()) { return; @@ -285,7 +286,7 @@ public void testSparkInterpreterDataFrame() throws InterpreterException { } } -// @Test + @Test public void testSparkSQLInterpreter() throws InterpreterException { if (!checkPreCondition()) { return; @@ -320,7 +321,7 @@ public void testSparkSQLInterpreter() throws InterpreterException { } -// @Test + @Test public void testSparkSQLCancellation() throws InterpreterException { if (!checkPreCondition()) { return; @@ -401,7 +402,7 @@ public void run() { } } -// @Test + @Test public void testStringWithTruncation() throws InterpreterException { if (!checkPreCondition()) { return; @@ -462,7 +463,7 @@ public void testStringWithTruncation() throws InterpreterException { } -// @Test + @Test public void testStringWithoutTruncation() throws InterpreterException { if (!checkPreCondition()) { return; @@ -534,6 +535,7 @@ public void testPySparkInterpreter() throws LivyException, InterpreterException } final LivyPySparkInterpreter pysparkInterpreter = new LivyPySparkInterpreter(properties); + pysparkInterpreter.setInterpreterGroup(mock(InterpreterGroup.class)); AuthenticationInfo authInfo = new AuthenticationInfo("user1"); MyInterpreterOutputListener outputListener = new MyInterpreterOutputListener(); InterpreterOutput output = new InterpreterOutput(outputListener); @@ -647,7 +649,7 @@ public void run() { } } -// @Test + @Test public void testSparkInterpreterWithDisplayAppInfo() throws InterpreterException { if (!checkPreCondition()) { return; @@ -686,13 +688,15 @@ public void testSparkInterpreterWithDisplayAppInfo() throws InterpreterException } } -// @Test + @Test public void testSparkRInterpreter() throws LivyException, InterpreterException { if (!checkPreCondition()) { return; } final LivySparkRInterpreter sparkRInterpreter = new LivySparkRInterpreter(properties); + sparkRInterpreter.setInterpreterGroup(mock(InterpreterGroup.class)); + try { sparkRInterpreter.getLivyVersion(); } catch (APINotFoundException e) { @@ -749,8 +753,7 @@ public void run() { // error result = sparkRInterpreter.interpret("cat(a)", context); - //TODO @zjffdu, it should be ERROR, it is due to bug of LIVY-313 - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(InterpreterResult.Code.ERROR, result.code()); assertEquals(InterpreterResult.Type.TEXT, result.message().get(0).getType()); assertTrue(result.message().get(0).getData().contains("object 'a' not found")); } finally { @@ -758,7 +761,7 @@ public void run() { } } -// @Test + @Test public void testLivyTutorialNote() throws IOException, InterpreterException { if (!checkPreCondition()) { return; @@ -796,6 +799,114 @@ public void testLivyTutorialNote() throws IOException, InterpreterException { } } + @Test + public void testSharedInterpreter() throws InterpreterException { + if (!checkPreCondition()) { + return; + } + InterpreterGroup interpreterGroup = new InterpreterGroup("group_1"); + interpreterGroup.put("session_1", new ArrayList()); + LazyOpenInterpreter sparkInterpreter = new LazyOpenInterpreter( + new LivySparkInterpreter(properties)); + sparkInterpreter.setInterpreterGroup(interpreterGroup); + interpreterGroup.get("session_1").add(sparkInterpreter); + + LazyOpenInterpreter sqlInterpreter = new LazyOpenInterpreter( + new LivySparkSQLInterpreter(properties)); + interpreterGroup.get("session_1").add(sqlInterpreter); + sqlInterpreter.setInterpreterGroup(interpreterGroup); + + LazyOpenInterpreter pysparkInterpreter = new LazyOpenInterpreter( + new LivyPySparkInterpreter(properties)); + interpreterGroup.get("session_1").add(pysparkInterpreter); + pysparkInterpreter.setInterpreterGroup(interpreterGroup); + + LazyOpenInterpreter sparkRInterpreter = new LazyOpenInterpreter( + new LivySparkRInterpreter(properties)); + interpreterGroup.get("session_1").add(sparkRInterpreter); + sparkRInterpreter.setInterpreterGroup(interpreterGroup); + + LazyOpenInterpreter sharedInterpreter = new LazyOpenInterpreter( + new LivySharedInterpreter(properties)); + interpreterGroup.get("session_1").add(sharedInterpreter); + sharedInterpreter.setInterpreterGroup(interpreterGroup); + + sparkInterpreter.open(); + sqlInterpreter.open(); + pysparkInterpreter.open(); + sparkRInterpreter.open(); + + try { + AuthenticationInfo authInfo = new AuthenticationInfo("user1"); + MyInterpreterOutputListener outputListener = new MyInterpreterOutputListener(); + InterpreterOutput output = new InterpreterOutput(outputListener); + InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "livy.sql", + "title", "text", authInfo, null, null, null, null, null, null, output); + // detect spark version + InterpreterResult result = sparkInterpreter.interpret("sc.version", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + + boolean isSpark2 = isSpark2((BaseLivyInterpreter)sparkInterpreter.getInnerInterpreter(), context); + + if (!isSpark2) { + result = sparkInterpreter.interpret( + "val df=sqlContext.createDataFrame(Seq((\"hello\",20))).toDF(\"col_1\", \"col_2\")\n" + + "df.collect()", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + assertTrue(result.message().get(0).getData() + .contains("Array[org.apache.spark.sql.Row] = Array([hello,20])")); + sparkInterpreter.interpret("df.registerTempTable(\"df\")", context); + + // access table from pyspark + result = pysparkInterpreter.interpret("sqlContext.sql(\"select * from df\").show()", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + assertTrue(result.message().get(0).getData() + .contains("+-----+-----+\n" + + "|col_1|col_2|\n" + + "+-----+-----+\n" + + "|hello| 20|\n" + + "+-----+-----+")); + + // access table from sparkr + result = sparkRInterpreter.interpret("head(sql(sqlContext, \"select * from df\"))", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + assertTrue(result.message().get(0).getData().contains("col_1 col_2\n1 hello 20")); + } else { + result = sparkInterpreter.interpret( + "val df=spark.createDataFrame(Seq((\"hello\",20))).toDF(\"col_1\", \"col_2\")\n" + + "df.collect()", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + assertTrue(result.message().get(0).getData() + .contains("Array[org.apache.spark.sql.Row] = Array([hello,20])")); + sparkInterpreter.interpret("df.registerTempTable(\"df\")", context); + + // access table from pyspark + result = pysparkInterpreter.interpret("spark.sql(\"select * from df\").show()", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + assertTrue(result.message().get(0).getData() + .contains("+-----+-----+\n" + + "|col_1|col_2|\n" + + "+-----+-----+\n" + + "|hello| 20|\n" + + "+-----+-----+")); + + // access table from sparkr + result = sparkRInterpreter.interpret("head(sql(\"select * from df\"))", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + assertTrue(result.message().get(0).getData().contains("col_1 col_2\n1 hello 20")); + } + } finally { + sparkInterpreter.close(); + sqlInterpreter.close(); + } + } private boolean isSpark2(BaseLivyInterpreter interpreter, InterpreterContext context) { InterpreterResult result = null; From a186f5b7491c8986f16ac7d50e8fccec423fde01 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Thu, 8 Feb 2018 23:00:55 +0800 Subject: [PATCH 171/492] [HOTFIX] Fix zeppelin-display build ### What is this PR for? This is for fix the build of zeppelin-display module ### What type of PR is it? [ Hot Fix ] ### Todos * [ ] - Task ### How should this be tested? * Travis pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? NO Author: Jeff Zhang Closes #2781 from zjffdu/HOTFIX_BUILD and squashes the following commits: dd2c36d [Jeff Zhang] [HOTFIX] Fix zeppelin-display build (cherry picked from commit f8d3488cbbb3a2d20c5736a8587ef0c41ae1eebc) Signed-off-by: Jeff Zhang --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 25d0e0881fd..7a4cb03d3a7 100644 --- a/pom.xml +++ b/pom.xml @@ -734,6 +734,9 @@ scala-2.10 + + true + 2.10.5 2.10 @@ -742,9 +745,6 @@ scala-2.11 - - true - 2.11.8 2.11 From 055c6e9a5bbdb3a94c1f69568da25f740dad37ff Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Thu, 8 Feb 2018 12:44:11 +0530 Subject: [PATCH 172/492] [ZEPPELIN-3213] Support for KNOXSSO logout url as API KNOXSSO logout URL can be an API or it can be a redirect URL, Zeppelin should support both. [Improvement] * [ZEPPELIN-3213](https://issues.apache.org/jira/browse/ZEPPELIN-3213) * Does the licenses files need update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: Prabhjyot Singh Closes #2779 from prabhjyotsingh/ZEPPELIN-3213 and squashes the following commits: 6c89f2427 [Prabhjyot Singh] [ZEPPELIN-3213] Support for KNOXSSO logout url as API Change-Id: I44e3bb13dc3de9330751236c0b3703a8177200e7 (cherry picked from commit 73d15704bc73f02884f5d6f3348c1c1ceb952a5a) Signed-off-by: Prabhjyot Singh --- conf/shiro.ini.template | 1 + .../org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java | 9 +++++++++ .../java/org/apache/zeppelin/rest/LoginRestApi.java | 1 + .../src/components/navbar/navbar.controller.js | 10 +++++++++- 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/conf/shiro.ini.template b/conf/shiro.ini.template index 81b31a2b9d8..9397025ca18 100644 --- a/conf/shiro.ini.template +++ b/conf/shiro.ini.template @@ -61,6 +61,7 @@ user3 = password4, role2 #knoxJwtRealm.providerUrl = https://domain.example.com/ #knoxJwtRealm.login = gateway/knoxsso/knoxauth/login.html #knoxJwtRealm.logout = gateway/knoxssout/api/v1/webssout +#knoxJwtRealm.logoutAPI = true #knoxJwtRealm.redirectParam = originalUrl #knoxJwtRealm.cookieName = hadoop-jwt #knoxJwtRealm.publicKeyPath = /etc/zeppelin/conf/knox-sso.pem diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java index c3e9b77cc88..a903e6ec4bb 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java @@ -60,6 +60,7 @@ public class KnoxJwtRealm extends AuthorizingRealm { private String publicKeyPath; private String login; private String logout; + private Boolean logoutAPI; private String principalMapping; private String groupPrincipalMapping; @@ -270,6 +271,14 @@ public void setLogout(String logout) { this.logout = logout; } + public Boolean getLogoutAPI() { + return logoutAPI; + } + + public void setLogoutAPI(Boolean logoutAPI) { + this.logoutAPI = logoutAPI; + } + public String getPrincipalMapping() { return principalMapping; } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java index 3a084cf9e4b..b590bdb006d 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java @@ -211,6 +211,7 @@ public Response logout() { KnoxJwtRealm knoxJwtRealm = getJTWRealm(); Map data = new HashMap<>(); data.put("redirectURL", constructKnoxUrl(knoxJwtRealm, knoxJwtRealm.getLogout())); + data.put("isLogoutAPI", knoxJwtRealm.getLogoutAPI().toString()); response = new JsonResponse(Status.UNAUTHORIZED, "", data); } else { response = new JsonResponse(Status.UNAUTHORIZED, "", ""); diff --git a/zeppelin-web/src/components/navbar/navbar.controller.js b/zeppelin-web/src/components/navbar/navbar.controller.js index 7d7fada1d45..139328e11d7 100644 --- a/zeppelin-web/src/components/navbar/navbar.controller.js +++ b/zeppelin-web/src/components/navbar/navbar.controller.js @@ -93,7 +93,15 @@ function NavCtrl ($scope, $rootScope, $http, $routeParams, $location, if (response.data) { let res = angular.fromJson(response.data).body if (res['redirectURL']) { - window.location.href = res['redirectURL'] + window.location.href + if (res['isLogoutAPI'] === 'true') { + $http.get(res['redirectURL']).then(function () { + }, function () { + window.location = baseUrlSrv.getBase() + }) + } else { + window.location.href = res['redirectURL'] + window.location.href + } + return undefined } } From eb586d3cd9df11acbebe5c3df106d3e418f8c7b6 Mon Sep 17 00:00:00 2001 From: Savalek Date: Wed, 7 Feb 2018 20:16:27 +0300 Subject: [PATCH 173/492] [ZEPPELIN-3212] delete extra ">" in notebook-actionBar.html ### What is this PR for? Delete extra ">" in notebook-actionBar.html after `` ### What type of PR is it? [Improvement] ### What is the Jira issue? [ZEPPELIN-3212](https://issues.apache.org/jira/browse/ZEPPELIN-3212) ### Screenshots (if appropriate) ![1](https://user-images.githubusercontent.com/30798933/35930861-1ba8369e-0c44-11e8-8ebf-9328cb21a6e5.PNG) ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: Savalek Closes #2774 from Savalek/ZEPPELIN-3212 and squashes the following commits: 12352b772 [Savalek] [ZEPPELIN-3212] delete extra ">" in notebook-actionBar.html (cherry picked from commit 29b9b10f392f8a36251d25dc4c8d006a07f5d4cb) Signed-off-by: Lee moon soo --- zeppelin-web/src/app/notebook/notebook-actionBar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index 573be001ebb..9b50e819f79 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -24,7 +24,7 @@

    tooltip-placement="bottom" uib-tooltip={{noteName(note)}} ng-click="input.showEditor = !revisionView; input.value = note.name" - ng-show="!input.showEditor">{{noteName(note)}}>

    + ng-show="!input.showEditor">{{noteName(note)}}

    From 03c9e8105bc3313e613cfb8b676d9b18de39ce1f Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 14 Nov 2017 15:29:58 +0800 Subject: [PATCH 174/492] ZEPPELIN-3108. Support Spark 2.3 ### What is this PR for? Spark 2.3 remove `JobProgressListener` which cause zeppelin unable to run spark 2.3. This PR try to make Zeppelin support spark 2.3 via using `sc.statusTracker`, see `JobProgressUtil.scala` ### What type of PR is it? [Improvement ] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3108 ### How should this be tested? * Verified manually. ### Screenshots (if appropriate) ![screen shot 2018-01-30 at 9 45 01 pm](https://user-images.githubusercontent.com/164491/35569317-dce6f348-0606-11e8-9b18-74a847d64ac9.png) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2750 from zjffdu/ZEPPELIN-3108 and squashes the following commits: 43ae78a [Jeff Zhang] ZEPPELIN-3108. Support Spark 2.3 (cherry picked from commit d9faef1085e4ade496ff7f3d7f8472a28678f8e7) Signed-off-by: Jeff Zhang --- spark/interpreter/pom.xml | 10 +- .../zeppelin/spark/OldSparkInterpreter.java | 153 ++++++++++++------ spark/pom.xml | 19 ++- spark/spark-dependencies/pom.xml | 10 +- .../spark/BaseSparkScalaInterpreter.scala | 16 +- .../zeppelin/spark/JobProgressUtil.scala | 37 +++++ 6 files changed, 164 insertions(+), 81 deletions(-) create mode 100644 spark/spark-scala-parent/src/main/scala/org/apache/zeppelin/spark/JobProgressUtil.scala diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 122cb14fbd4..0f3f1635c3e 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -53,15 +53,7 @@ **/PySparkInterpreterMatplotlibTest.java **/*Test.* - - spark-${spark.version} - - http://d3kbcqa49mib13.cloudfront.net/${spark.archive}.tgz - - - http://d3kbcqa49mib13.cloudfront.net/spark-${spark.version}-bin-without-hadoop.tgz - - + diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java index 6a54c3b37b9..da332fe5ef0 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java @@ -35,6 +35,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.security.UserGroupInformation; +import org.apache.spark.JobProgressUtil; import org.apache.spark.SecurityManager; import org.apache.spark.SparkConf; import org.apache.spark.SparkContext; @@ -44,10 +45,26 @@ import org.apache.spark.scheduler.ActiveJob; import org.apache.spark.scheduler.DAGScheduler; import org.apache.spark.scheduler.Pool; +import org.apache.spark.scheduler.SparkListenerApplicationEnd; +import org.apache.spark.scheduler.SparkListenerApplicationStart; +import org.apache.spark.scheduler.SparkListenerBlockManagerAdded; +import org.apache.spark.scheduler.SparkListenerBlockManagerRemoved; +import org.apache.spark.scheduler.SparkListenerBlockUpdated; +import org.apache.spark.scheduler.SparkListenerEnvironmentUpdate; +import org.apache.spark.scheduler.SparkListenerExecutorAdded; +import org.apache.spark.scheduler.SparkListenerExecutorMetricsUpdate; +import org.apache.spark.scheduler.SparkListenerExecutorRemoved; +import org.apache.spark.scheduler.SparkListenerJobEnd; import org.apache.spark.scheduler.SparkListenerJobStart; +import org.apache.spark.scheduler.SparkListenerStageCompleted; +import org.apache.spark.scheduler.SparkListenerStageSubmitted; +import org.apache.spark.scheduler.SparkListenerTaskEnd; +import org.apache.spark.scheduler.SparkListenerTaskGettingResult; +import org.apache.spark.scheduler.SparkListenerTaskStart; +import org.apache.spark.scheduler.SparkListenerUnpersistRDD; import org.apache.spark.sql.SQLContext; import org.apache.spark.ui.SparkUI; -import org.apache.spark.ui.jobs.JobProgressListener; +import org.apache.spark.scheduler.SparkListener; import org.apache.zeppelin.interpreter.BaseZeppelinContext; import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; @@ -113,7 +130,7 @@ public class OldSparkInterpreter extends AbstractSparkInterpreter { private static InterpreterHookRegistry hooks; private static SparkEnv env; private static Object sparkSession; // spark 2.x - private static JobProgressListener sparkListener; + private static SparkListener sparkListener; private static AbstractFile classOutputDir; private static Integer sharedInterpreterLock = new Integer(0); private static AtomicInteger numReferenceOfSparkContext = new AtomicInteger(0); @@ -173,11 +190,10 @@ public boolean isSparkContextInitialized() { } } - static JobProgressListener setupListeners(SparkContext context) { - JobProgressListener pl = new JobProgressListener(context.getConf()) { + static SparkListener setupListeners(SparkContext context) { + SparkListener pl = new SparkListener() { @Override public synchronized void onJobStart(SparkListenerJobStart jobStart) { - super.onJobStart(jobStart); int jobId = jobStart.jobId(); String jobGroupId = jobStart.properties().getProperty("spark.jobGroup.id"); String uiEnabled = jobStart.properties().getProperty("spark.ui.enabled"); @@ -207,6 +223,85 @@ private String getJobUrl(int jobId) { return jobUrl; } + @Override + public void onBlockUpdated(SparkListenerBlockUpdated blockUpdated) { + + } + + @Override + public void onExecutorRemoved(SparkListenerExecutorRemoved executorRemoved) { + + } + + @Override + public void onExecutorAdded(SparkListenerExecutorAdded executorAdded) { + + } + + @Override + public void onExecutorMetricsUpdate(SparkListenerExecutorMetricsUpdate executorMetricsUpdate) { + + } + + @Override + public void onApplicationEnd(SparkListenerApplicationEnd applicationEnd) { + + } + + @Override + public void onApplicationStart(SparkListenerApplicationStart applicationStart) { + + } + + @Override + public void onUnpersistRDD(SparkListenerUnpersistRDD unpersistRDD) { + + } + + @Override + public void onBlockManagerAdded(SparkListenerBlockManagerAdded blockManagerAdded) { + + } + + @Override + public void onBlockManagerRemoved(SparkListenerBlockManagerRemoved blockManagerRemoved) { + + } + + @Override + public void onEnvironmentUpdate(SparkListenerEnvironmentUpdate environmentUpdate) { + + } + + @Override + public void onJobEnd(SparkListenerJobEnd jobEnd) { + + } + + @Override + public void onStageCompleted(SparkListenerStageCompleted stageCompleted) { + + } + + @Override + public void onStageSubmitted(SparkListenerStageSubmitted stageSubmitted) { + + } + + @Override + public void onTaskEnd(SparkListenerTaskEnd taskEnd) { + + } + + @Override + public void onTaskGettingResult(SparkListenerTaskGettingResult taskGettingResult) { + + } + + @Override + public void onTaskStart(SparkListenerTaskStart taskStart) { + + } }; try { Object listenerBus = context.getClass().getMethod("listenerBus").invoke(context); @@ -224,7 +319,7 @@ private String getJobUrl(int jobId) { continue; } - if (!parameterTypes[0].isAssignableFrom(JobProgressListener.class)) { + if (!parameterTypes[0].isAssignableFrom(SparkListener.class)) { continue; } @@ -1274,48 +1369,10 @@ public void cancel(InterpreterContext context) { @Override public int getProgress(InterpreterContext context) { String jobGroup = Utils.buildJobGroupId(context); - int completedTasks = 0; - int totalTasks = 0; - - DAGScheduler scheduler = sc.dagScheduler(); - if (scheduler == null) { - return 0; - } - HashSet jobs = scheduler.activeJobs(); - if (jobs == null || jobs.size() == 0) { - return 0; - } - Iterator it = jobs.iterator(); - while (it.hasNext()) { - ActiveJob job = it.next(); - String g = (String) job.properties().get("spark.jobGroup.id"); - if (jobGroup.equals(g)) { - int[] progressInfo = null; - try { - Object finalStage = job.getClass().getMethod("finalStage").invoke(job); - if (sparkVersion.getProgress1_0()) { - progressInfo = getProgressFromStage_1_0x(sparkListener, finalStage); - } else { - progressInfo = getProgressFromStage_1_1x(sparkListener, finalStage); - } - } catch (IllegalAccessException | IllegalArgumentException - | InvocationTargetException | NoSuchMethodException - | SecurityException e) { - logger.error("Can't get progress info", e); - return 0; - } - totalTasks += progressInfo[0]; - completedTasks += progressInfo[1]; - } - } - - if (totalTasks == 0) { - return 0; - } - return completedTasks * 100 / totalTasks; + return JobProgressUtil.progress(sc, jobGroup); } - private int[] getProgressFromStage_1_0x(JobProgressListener sparkListener, Object stage) + private int[] getProgressFromStage_1_0x(SparkListener sparkListener, Object stage) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { int numTasks = (int) stage.getClass().getMethod("numTasks").invoke(stage); @@ -1345,7 +1402,7 @@ private int[] getProgressFromStage_1_0x(JobProgressListener sparkListener, Objec return new int[] {numTasks, completedTasks}; } - private int[] getProgressFromStage_1_1x(JobProgressListener sparkListener, Object stage) + private int[] getProgressFromStage_1_1x(SparkListener sparkListener, Object stage) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { int numTasks = (int) stage.getClass().getMethod("numTasks").invoke(stage); @@ -1421,7 +1478,7 @@ public FormType getFormType() { return FormType.NATIVE; } - public JobProgressListener getJobProgressListener() { + public SparkListener getJobProgressListener() { return sparkListener; } diff --git a/spark/pom.xml b/spark/pom.xml index 4ccf699513a..21e2288dc7f 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -47,6 +47,14 @@ 2.2.0 0.10.4 + + spark-${spark.version} + + http://d3kbcqa49mib13.cloudfront.net/${spark.archive}.tgz + + + http://d3kbcqa49mib13.cloudfront.net/${spark.archive}-bin-without-hadoop.tgz + @@ -57,7 +65,6 @@ ${project.version} - org.apache.zeppelin zeppelin-display @@ -187,6 +194,16 @@ + + + spark-2.3 + + 2.3.0 + 2.5.0 + 0.10.6 + + + spark-2.2 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index ae169e50524..1ebc81bafca 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -28,7 +28,7 @@ org.apache.zeppelin - zeppelin-spark-dependencies_2.10 + zeppelin-spark-dependencies jar 0.8.0-SNAPSHOT Zeppelin: Spark dependencies @@ -54,14 +54,6 @@ org.spark-project.akka 2.3.4-spark - spark-${spark.version} - - http://d3kbcqa49mib13.cloudfront.net/${spark.archive}.tgz - - - http://d3kbcqa49mib13.cloudfront.net/${spark.archive}-bin-without-hadoop.tgz - - 2.3 diff --git a/spark/spark-scala-parent/src/main/scala/org/apache/zeppelin/spark/BaseSparkScalaInterpreter.scala b/spark/spark-scala-parent/src/main/scala/org/apache/zeppelin/spark/BaseSparkScalaInterpreter.scala index 3ef4fe71e00..883beb02255 100644 --- a/spark/spark-scala-parent/src/main/scala/org/apache/zeppelin/spark/BaseSparkScalaInterpreter.scala +++ b/spark/spark-scala-parent/src/main/scala/org/apache/zeppelin/spark/BaseSparkScalaInterpreter.scala @@ -21,7 +21,7 @@ package org.apache.zeppelin.spark import java.io.File import org.apache.spark.sql.SQLContext -import org.apache.spark.{SparkConf, SparkContext} +import org.apache.spark.{JobProgressUtil, SparkConf, SparkContext} import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion import org.apache.zeppelin.interpreter.util.InterpreterOutputStream import org.apache.zeppelin.interpreter.{InterpreterContext, InterpreterResult} @@ -93,19 +93,7 @@ abstract class BaseSparkScalaInterpreter(val conf: SparkConf, } protected def getProgress(jobGroup: String, context: InterpreterContext): Int = { - val jobIds = sc.statusTracker.getJobIdsForGroup(jobGroup) - val jobs = jobIds.flatMap { id => sc.statusTracker.getJobInfo(id) } - val stages = jobs.flatMap { job => - job.stageIds().flatMap(sc.statusTracker.getStageInfo) - } - - val taskCount = stages.map(_.numTasks).sum - val completedTaskCount = stages.map(_.numCompletedTasks).sum - if (taskCount == 0) { - 0 - } else { - (100 * completedTaskCount.toDouble / taskCount).toInt - } + JobProgressUtil.progress(sc, jobGroup) } protected def bind(name: String, tpe: String, value: Object, modifier: List[String]): Unit diff --git a/spark/spark-scala-parent/src/main/scala/org/apache/zeppelin/spark/JobProgressUtil.scala b/spark/spark-scala-parent/src/main/scala/org/apache/zeppelin/spark/JobProgressUtil.scala new file mode 100644 index 00000000000..517bed0cc93 --- /dev/null +++ b/spark/spark-scala-parent/src/main/scala/org/apache/zeppelin/spark/JobProgressUtil.scala @@ -0,0 +1,37 @@ +/* + * 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.spark + +object JobProgressUtil { + + def progress(sc: SparkContext, jobGroup : String):Int = { + val jobIds = sc.statusTracker.getJobIdsForGroup(jobGroup) + val jobs = jobIds.flatMap { id => sc.statusTracker.getJobInfo(id) } + val stages = jobs.flatMap { job => + job.stageIds().flatMap(sc.statusTracker.getStageInfo) + } + + val taskCount = stages.map(_.numTasks).sum + val completedTaskCount = stages.map(_.numCompletedTasks).sum + if (taskCount == 0) { + 0 + } else { + (100 * completedTaskCount.toDouble / taskCount).toInt + } + } +} From df257c5f021238aecb95a4ada57e524de60808ec Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Thu, 8 Feb 2018 15:27:12 +0800 Subject: [PATCH 175/492] ZEPPELIN-3214. Restructure of spark interpreter module Just restructure of spark interpreter module. spark module now is a sub module with all the following child module. * interpreter * scala-2.10 * scala-2.11 * spark-dependencies * spark-scala-parent [Refactoring] * [ ] - Task * https://issues.apache.org/jira/browse/ZEPPELIN-3214 * CI pass * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2788 from zjffdu/ZEPPELIN-3214 and squashes the following commits: 8dcdbbd [Jeff Zhang] ZEPPELIN-3214. Restructure of spark interpreter module (cherry picked from commit 7fcbb9a0296b306896a82d4f3c89c4550327c674) Signed-off-by: Jeff Zhang --- .travis.yml | 20 +++++++++---------- pom.xml | 5 +---- .../spark/IPySparkInterpreterTest.java | 2 +- spark/pom.xml | 19 ++++++++++++++++-- spark/scala-2.10/pom.xml | 14 ++++++------- spark/scala-2.11/pom.xml | 14 ++++++------- spark/spark-scala-parent/pom.xml | 1 + 7 files changed, 44 insertions(+), 31 deletions(-) diff --git a/.travis.yml b/.travis.yml index 61aab1f57ce..c31694a4dbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,14 +68,14 @@ matrix: dist: trusty addons: firefox: "31.0" - env: PYTHON="3" SCALA_VER="2.11" SPARK_VER="2.2.0" HADOOP_VER="2.6" PROFILE="-Pspark-2.2 -Pweb-ci -Pscalding -Phelium-dev -Pexamples -Pscala-2.11" BUILD_FLAG="package -Pbuild-distr -DskipRat" TEST_FLAG="verify -Pusing-packaged-distr -DskipRat" MODULES="-pl ${INTERPRETERS}" TEST_PROJECTS="-Dtests.to.exclude=**/ZeppelinSparkClusterTest.java,**/org/apache/zeppelin/spark/*,**/HeliumApplicationFactoryTest.java -DfailIfNoTests=false" + env: PYTHON="3" SCALA_VER="2.11" SPARK_VER="2.2.0" HADOOP_VER="2.6" PROFILE="-Pspark-2.2 -Pscalding -Phelium-dev -Pexamples -Pscala-2.11" BUILD_FLAG="package -Pbuild-distr -DskipRat" TEST_FLAG="verify -Pusing-packaged-distr -DskipRat" MODULES="-pl ${INTERPRETERS}" TEST_PROJECTS="-Dtests.to.exclude=**/ZeppelinSparkClusterTest.java,**/org/apache/zeppelin/spark/*,**/HeliumApplicationFactoryTest.java -DfailIfNoTests=false" # Test selenium with spark module for 1.6.3 - jdk: "oraclejdk8" dist: trusty addons: firefox: "31.0" - env: PYTHON="2" SCALA_VER="2.10" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop2 -Phadoop-2.6 -Phelium-dev -Pexamples -Pintegration" BUILD_FLAG="package -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" TEST_PROJECTS="-pl .,zeppelin-integration -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.10" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pspark-1.6 -Phadoop2 -Phadoop-2.6 -Phelium-dev -Pexamples -Pintegration" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" TEST_PROJECTS="-pl .,zeppelin-integration -DfailIfNoTests=false" # Test interpreter modules - jdk: "openjdk7" @@ -85,39 +85,39 @@ matrix: # Test spark module for 2.2.0 with scala 2.11 - jdk: "oraclejdk8" dist: trusty - env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.2.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.2 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.livy.* -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.2.0" HADOOP_VER="2.6" PROFILE="-Pspark-2.2 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat -am" TEST_FLAG="test -DskipRat -am" MODULES="-pl zeppelin-server,spark/interpreter,spark/spark-dependencies" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" # Test spark module for 2.1.0 with scala 2.11 - jdk: "openjdk7" dist: trusty - env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.1.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.1 -Phadoop2 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.livy.* -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.1.0" HADOOP_VER="2.6" PROFILE="-Pspark-2.1 -Phadoop2 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat -am" TEST_FLAG="test -DskipRat -am" MODULES="-pl zeppelin-server,spark/interpreter,spark/spark-dependencies" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" # Test spark module for 2.0.2 with scala 2.11 - jdk: "oraclejdk8" dist: trusty - env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.0.2" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-2.0 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.0.2" HADOOP_VER="2.6" PROFILE="-Pspark-2.0 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat -am" TEST_FLAG="test -DskipRat -am" MODULES="-pl zeppelin-server,spark/interpreter,spark/spark-dependencies" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" - # Test spark module for 1.6.3 with scala 2.11 + # Test spark module for 1.6.3 with scala 2.10 - jdk: "openjdk7" dist: trusty - env: PYTHON="3" SCALA_VER="2.10" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop2 -Phadoop-2.6 -Pscala-2.10" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.*,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + env: PYTHON="3" SCALA_VER="2.10" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pspark-1.6 -Phadoop2 -Phadoop-2.6 -Pscala-2.10" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat -am" TEST_FLAG="test -DskipRat -am" MODULES="-pl zeppelin-server,spark/interpreter,spark/spark-dependencies" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" # Test spark module for 1.6.3 with scala 2.11 - jdk: "oraclejdk8" dist: trusty - env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pspark-1.6 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.11" SPARK_VER="1.6.3" HADOOP_VER="2.6" PROFILE="-Pspark-1.6 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat -am" TEST_FLAG="test -DskipRat -am" MODULES="-pl zeppelin-server,spark/interpreter,spark/spark-dependencies" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest,org.apache.zeppelin.spark.* -DfailIfNoTests=false" # Test python/pyspark with python 2, livy 0.5 - sudo: required dist: trusty jdk: "openjdk7" - env: PYTHON="2" SCALA_VER="2.10" SPARK_VER="1.6.3" HADOOP_VER="2.6" LIVY_VER="0.5.0-incubating" PROFILE="-Pspark-1.6 -Phadoop2 -Phadoop-2.6 -Pscala-2.10" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" + env: PYTHON="2" SCALA_VER="2.10" SPARK_VER="1.6.3" HADOOP_VER="2.6" LIVY_VER="0.5.0-incubating" PROFILE="-Pspark-1.6 -Phadoop2 -Phadoop-2.6 -Pscala-2.10" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl livy" TEST_PROJECTS="-Dpyspark.test.exclude='' -DfailIfNoTests=false" # Test python/pyspark with python 3, livy 0.5 - sudo: required dist: trusty jdk: "openjdk7" - env: PYTHON="3" SCALA_VER="2.11" SPARK_VER="2.0.0" HADOOP_VER="2.6" LIVY_VER="0.5.0-incubating" PROFILE="-Pspark-2.0 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl .,zeppelin-interpreter,zeppelin-display,spark/interpreter,spark/scala-2.10,spark/scala-2.11,spark/spark-dependencies,python,livy" TEST_PROJECTS="-Dtest=LivySQLInterpreterTest,org.apache.zeppelin.spark.PySpark*Test,org.apache.zeppelin.python.* -Dpyspark.test.exclude='' -DfailIfNoTests=false" + env: PYTHON="3" SCALA_VER="2.11" SPARK_VER="2.0.0" HADOOP_VER="2.6" LIVY_VER="0.5.0-incubating" PROFILE="-Pspark-2.0 -Phadoop3 -Phadoop-2.6 -Pscala-2.11" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl livy" TEST_PROJECTS="-Dpyspark.test.exclude='' -DfailIfNoTests=false" before_install: # check files included in commit range, clear bower_components if a bower.json file has changed. diff --git a/pom.xml b/pom.xml index 7a4cb03d3a7..8d0a7fe0d05 100644 --- a/pom.xml +++ b/pom.xml @@ -57,10 +57,7 @@ zeppelin-zengine zeppelin-display groovy - spark/scala-2.10 - spark/scala-2.11 - spark/interpreter - spark/spark-dependencies + spark markdown angular shell diff --git a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java index 765237c3855..10d87a63e0a 100644 --- a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java @@ -182,7 +182,7 @@ public void run() { assertEquals(InterpreterResult.Code.SUCCESS, result.code()); interpreterResultMessages = context.out.getInterpreterResultMessages(); assertEquals(1, interpreterResultMessages.size()); -// assertTrue(interpreterResultMessages.get(0).getData().contains("(0, 100)")); + assertTrue(interpreterResultMessages.get(0).getData().contains("(0, 100)")); } private InterpreterContext getInterpreterContext() { diff --git a/spark/pom.xml b/spark/pom.xml index 21e2288dc7f..215f4be5c84 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -32,8 +32,8 @@ spark-parent pom 0.8.0-SNAPSHOT - spark-parent - Zeppelin spark support + Zeppelin: Spark Parent + Zeppelin Spark Support @@ -57,6 +57,14 @@ + + interpreter + spark-scala-parent + scala-2.10 + scala-2.11 + spark-dependencies + + @@ -120,6 +128,13 @@ + + org.apache.maven.plugins + maven-clean-plugin + ${plugin.clean.version} + + + org.scalatest scalatest-maven-plugin diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index 9252bad4459..0f8593f0b7c 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -18,13 +18,6 @@ - 4.0.0 - org.apache.zeppelin - spark-scala-2.10 - 0.8.0-SNAPSHOT - jar - Spark Interpreter: Scala_2.10 - org.apache.zeppelin spark-scala-parent @@ -32,6 +25,13 @@ ../spark-scala-parent/pom.xml + 4.0.0 + org.apache.zeppelin + spark-scala-2.10 + 0.8.0-SNAPSHOT + jar + Zeppelin: Spark Interpreter Scala_2.10 + 2.10.5 2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index 77b3cdfb053..d523f5e4c8b 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -18,13 +18,6 @@ - 4.0.0 - org.apache.zeppelin - spark-scala-2.11 - 0.8.0-SNAPSHOT - jar - Spark Interpreter: Scala_2.11 - org.apache.zeppelin spark-scala-parent @@ -32,6 +25,13 @@ ../spark-scala-parent/pom.xml + 4.0.0 + org.apache.zeppelin + spark-scala-2.11 + 0.8.0-SNAPSHOT + jar + Zeppelin: Spark Interpreter Scala_2.11 + 2.11.8 2.11 diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index 90556785a24..7cf0d14cd7d 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -32,6 +32,7 @@ spark-scala-parent 0.8.0-SNAPSHOT pom + Zeppelin: Spark Scala Parent From 87c42a230a4b9f01b94cdbe54396001b401a4710 Mon Sep 17 00:00:00 2001 From: guevara Date: Fri, 9 Feb 2018 16:09:31 -0500 Subject: [PATCH 176/492] Add exclusion to hadoop-aws dependency. ### What is this PR for? Fix dependency error caused by different versions of same library ### What type of PR is it? Bug Fix ### Todos * [ ZEPPELIN-3217] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3217 ### How should this be tested? * Travis CI ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: guevara Closes #2784 from wilsonr990/ZEPPELIN-3217 and squashes the following commits: 9f3f7e3 [guevara] Add exclusion to hadoop-aws dependency. (cherry picked from commit aa13a0a57c5f08c82ba90f37da6c8d7a503697f3) Signed-off-by: Jeff Zhang --- zeppelin-zengine/pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index e38a6598c0e..9ba9ac13ad9 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -669,6 +669,10 @@ hadoop-aws ${hadoop.version} + + com.amazonaws + aws-java-sdk + com.fasterxml.jackson.core jackson-annotations @@ -789,6 +793,10 @@ hadoop-aws ${hadoop.version} + + com.amazonaws + aws-java-sdk + com.fasterxml.jackson.core jackson-annotations From 504dd47823a0879b44659164ef845b6470ab922f Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Sun, 11 Feb 2018 21:24:53 +0800 Subject: [PATCH 177/492] Update version in conf/interpreter.list ### What is this PR for? Trivial change for version update ### What type of PR is it? [Hot Fix ] ### Todos * [ ] - Task ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2789 from zjffdu/update_version and squashes the following commits: fd9c805 [Jeff Zhang] [HOTFIX] Update version in conf/interpreter-list --- conf/interpreter-list | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/conf/interpreter-list b/conf/interpreter-list index 9506122f89b..b4fbf71477c 100644 --- a/conf/interpreter-list +++ b/conf/interpreter-list @@ -17,22 +17,22 @@ # # [name] [maven artifact] [description] -alluxio org.apache.zeppelin:zeppelin-alluxio:0.7.0 Alluxio interpreter -angular org.apache.zeppelin:zeppelin-angular:0.7.0 HTML and AngularJS view rendering -beam org.apache.zeppelin:zeppelin-beam:0.7.0 Beam interpreter -bigquery org.apache.zeppelin:zeppelin-bigquery:0.7.0 BigQuery interpreter -cassandra org.apache.zeppelin:zeppelin-cassandra_2.11:0.7.0 Cassandra interpreter built with Scala 2.11 -elasticsearch org.apache.zeppelin:zeppelin-elasticsearch:0.7.0 Elasticsearch interpreter -file org.apache.zeppelin:zeppelin-file:0.7.0 HDFS file interpreter -flink org.apache.zeppelin:zeppelin-flink_2.11:0.7.0 Flink interpreter built with Scala 2.11 -hbase org.apache.zeppelin:zeppelin-hbase:0.7.0 Hbase interpreter -ignite org.apache.zeppelin:zeppelin-ignite_2.11:0.7.0 Ignite interpreter built with Scala 2.11 -jdbc org.apache.zeppelin:zeppelin-jdbc:0.7.0 Jdbc interpreter -kylin org.apache.zeppelin:zeppelin-kylin:0.7.0 Kylin interpreter -lens org.apache.zeppelin:zeppelin-lens:0.7.0 Lens interpreter -livy org.apache.zeppelin:zeppelin-livy:0.7.0 Livy interpreter -md org.apache.zeppelin:zeppelin-markdown:0.7.0 Markdown support -pig org.apache.zeppelin:zeppelin-pig:0.7.0 Pig interpreter -python org.apache.zeppelin:zeppelin-python:0.7.0 Python interpreter -scio org.apache.zeppelin:zeppelin-scio_2.11:0.7.0 Scio interpreter -shell org.apache.zeppelin:zeppelin-shell:0.7.0 Shell command +alluxio org.apache.zeppelin:zeppelin-alluxio:0.8.0 Alluxio interpreter +angular org.apache.zeppelin:zeppelin-angular:0.8.0 HTML and AngularJS view rendering +beam org.apache.zeppelin:zeppelin-beam:0.8.0 Beam interpreter +bigquery org.apache.zeppelin:zeppelin-bigquery:0.8.0 BigQuery interpreter +cassandra org.apache.zeppelin:zeppelin-cassandra_2.11:0.8.0 Cassandra interpreter built with Scala 2.11 +elasticsearch org.apache.zeppelin:zeppelin-elasticsearch:0.8.0 Elasticsearch interpreter +file org.apache.zeppelin:zeppelin-file:0.8.0 HDFS file interpreter +flink org.apache.zeppelin:zeppelin-flink_2.11:0.8.0 Flink interpreter built with Scala 2.11 +hbase org.apache.zeppelin:zeppelin-hbase:0.8.0 Hbase interpreter +ignite org.apache.zeppelin:zeppelin-ignite_2.11:0.8.0 Ignite interpreter built with Scala 2.11 +jdbc org.apache.zeppelin:zeppelin-jdbc:0.8.0 Jdbc interpreter +kylin org.apache.zeppelin:zeppelin-kylin:0.8.0 Kylin interpreter +lens org.apache.zeppelin:zeppelin-lens:0.8.0 Lens interpreter +livy org.apache.zeppelin:zeppelin-livy:0.8.0 Livy interpreter +md org.apache.zeppelin:zeppelin-markdown:0.8.0 Markdown support +pig org.apache.zeppelin:zeppelin-pig:0.8.0 Pig interpreter +python org.apache.zeppelin:zeppelin-python:0.8.0 Python interpreter +scio org.apache.zeppelin:zeppelin-scio_2.11:0.8.0 Scio interpreter +shell org.apache.zeppelin:zeppelin-shell:0.8.0 Shell command From 80efe9a4c0b2c3488f55f03e093b80d480379756 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Mon, 12 Feb 2018 14:10:25 +0800 Subject: [PATCH 178/492] ZEPPELIN-3222. Shade libfb303 in SparkInterpreter ### What is this PR for? Trivial change for shading libfb303, otherwise it would conflict with libfb303 of CDH. ### What type of PR is it? [Improvement] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3222 ### How should this be tested? * CI pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2790 from zjffdu/ZEPPELIN-3222 and squashes the following commits: 8e8528d [Jeff Zhang] ZEPPELIN-3222. Shade libfb303 in SparkInterpreter (cherry picked from commit c6afe8c632295d3eab23745ddce59bb4286b4202) Signed-off-by: Jeff Zhang --- spark/interpreter/pom.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 0f3f1635c3e..5e24933bdf2 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -478,6 +478,10 @@ py4j. org.apache.zeppelin.py4j. + + com.facebook.fb303 + org.apache.zeppelin.com.facebook.fb303 + From 6d2b65d8342e26fa103bea1005b2c64afa30d2ed Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Mon, 12 Feb 2018 14:28:45 +0800 Subject: [PATCH 179/492] ZEPPELIN-3221. Create LocalConfigStorage to keep behavior consistent with previous version ### What is this PR for? Due to ZEPPELIN-2742, config will be stored on hdfs if user add HADOOP_CONF_DIR in zeppelin-env.sh, this is not consistent with the previous behavior (0.7) This PR just add LocalConfigStorage which would be the default storage for config which is the same behavior of 0.7 ### What type of PR is it? [Bug Fix | Improvement ] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3221 ### How should this be tested? * CI pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2791 from zjffdu/ZEPPELIN-3221 and squashes the following commits: b442808 [Jeff Zhang] ZEPPELIN-3221. Create LocalConfigStorage to keep behavior consistent with previous version (cherry picked from commit d1293c6bc476378c57db47d48dd8c5355370bb8a) Signed-off-by: Jeff Zhang --- .../zeppelin/spark/OldSparkInterpreter.java | 3 +- .../zeppelin/conf/ZeppelinConfiguration.java | 4 +- .../zeppelin/storage/ConfigStorage.java | 27 +++++ .../storage/FileSystemConfigStorage.java | 22 +--- .../zeppelin/storage/LocalConfigStorage.java | 110 ++++++++++++++++++ .../notebook/repo/NotebookRepoSyncTest.java | 5 + 6 files changed, 150 insertions(+), 21 deletions(-) create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/LocalConfigStorage.java diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java index da332fe5ef0..ff3a2caa565 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java @@ -239,7 +239,8 @@ public void onExecutorAdded(SparkListenerExecutorAdded executorAdded) { } @Override - public void onExecutorMetricsUpdate(SparkListenerExecutorMetricsUpdate executorMetricsUpdate) { + public void onExecutorMetricsUpdate( + SparkListenerExecutorMetricsUpdate executorMetricsUpdate) { } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java index 17e3e5bfd0f..59a1f9410cd 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java @@ -518,7 +518,7 @@ public String getConfigFSDir() { if (StringUtils.isBlank(fsConfigDir)) { LOG.warn(ConfVars.ZEPPELIN_CONFIG_FS_DIR.varName + " is not specified, fall back to local " + "conf directory " + ConfVars.ZEPPELIN_CONF_DIR.varName); - return "file://" + getConfDir(); + return getConfDir(); } return fsConfigDir; } @@ -709,7 +709,7 @@ public static enum ConfVars { ZEPPELIN_CONF_DIR("zeppelin.conf.dir", "conf"), ZEPPELIN_CONFIG_FS_DIR("zeppelin.config.fs.dir", ""), ZEPPELIN_CONFIG_STORAGE_CLASS("zeppelin.config.storage.class", - "org.apache.zeppelin.storage.FileSystemConfigStorage"), + "org.apache.zeppelin.storage.LocalConfigStorage"), ZEPPELIN_DEP_LOCALREPO("zeppelin.dep.localrepo", "local-repo"), ZEPPELIN_HELIUM_REGISTRY("zeppelin.helium.registry", "helium," + HELIUM_PACKAGE_DEFAULT_URL), ZEPPELIN_HELIUM_NODE_INSTALLER_URL("zeppelin.helium.node.installer.url", diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/ConfigStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/ConfigStorage.java index 3dc935fd0c7..b3175e59f3c 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/ConfigStorage.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/ConfigStorage.java @@ -18,9 +18,13 @@ package org.apache.zeppelin.storage; +import com.google.common.annotations.VisibleForTesting; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.helium.HeliumConf; import org.apache.zeppelin.interpreter.InterpreterInfoSaving; +import org.apache.zeppelin.interpreter.InterpreterSetting; import org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving; import org.apache.zeppelin.user.Credentials; import org.apache.zeppelin.user.CredentialsInfoSaving; @@ -75,4 +79,27 @@ public abstract void save(NotebookAuthorizationInfoSaving authorizationInfoSavin public abstract String loadCredentials() throws IOException; public abstract void saveCredentials(String credentials) throws IOException; + + protected InterpreterInfoSaving buildInterpreterInfoSaving(String json) { + //TODO(zjffdu) This kind of post processing is ugly. + JsonParser jsonParser = new JsonParser(); + JsonObject jsonObject = jsonParser.parse(json).getAsJsonObject(); + InterpreterInfoSaving infoSaving = InterpreterInfoSaving.fromJson(json); + for (InterpreterSetting interpreterSetting : infoSaving.interpreterSettings.values()) { + // Always use separate interpreter process + // While we decided to turn this feature on always (without providing + // enable/disable option on GUI). + // previously created setting should turn this feature on here. + interpreterSetting.getOption(); + interpreterSetting.convertPermissionsFromUsersToOwners( + jsonObject.getAsJsonObject("interpreterSettings") + .getAsJsonObject(interpreterSetting.getId())); + } + return infoSaving; + } + + @VisibleForTesting + public static void reset() { + instance = null; + } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/FileSystemConfigStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/FileSystemConfigStorage.java index 4df8163470d..20c19b65cb7 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/FileSystemConfigStorage.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/FileSystemConfigStorage.java @@ -74,21 +74,7 @@ public InterpreterInfoSaving loadInterpreterSettings() throws IOException { } LOGGER.info("Load Interpreter Setting from file: " + interpreterSettingPath); String json = fs.readFile(interpreterSettingPath); - //TODO(zjffdu) This kind of post processing is ugly. - JsonParser jsonParser = new JsonParser(); - JsonObject jsonObject = jsonParser.parse(json).getAsJsonObject(); - InterpreterInfoSaving infoSaving = InterpreterInfoSaving.fromJson(json); - for (InterpreterSetting interpreterSetting : infoSaving.interpreterSettings.values()) { - // Always use separate interpreter process - // While we decided to turn this feature on always (without providing - // enable/disable option on GUI). - // previously created setting should turn this feature on here. - interpreterSetting.getOption(); - interpreterSetting.convertPermissionsFromUsersToOwners( - jsonObject.getAsJsonObject("interpreterSettings") - .getAsJsonObject(interpreterSetting.getId())); - } - return infoSaving; + return buildInterpreterInfoSaving(json); } public void save(NotebookAuthorizationInfoSaving authorizationInfoSaving) throws IOException { @@ -99,7 +85,7 @@ public void save(NotebookAuthorizationInfoSaving authorizationInfoSaving) throws @Override public NotebookAuthorizationInfoSaving loadNotebookAuthorization() throws IOException { if (!fs.exists(authorizationPath)) { - LOGGER.warn("Interpreter Setting file {} is not existed", authorizationPath); + LOGGER.warn("Notebook Authorization file {} is not existed", authorizationPath); return null; } LOGGER.info("Load notebook authorization from file: " + authorizationPath); @@ -110,10 +96,10 @@ public NotebookAuthorizationInfoSaving loadNotebookAuthorization() throws IOExce @Override public String loadCredentials() throws IOException { if (!fs.exists(credentialPath)) { - LOGGER.warn("Credential file {} is not existed", authorizationPath); + LOGGER.warn("Credential file {} is not existed", credentialPath); return null; } - LOGGER.info("Load Credential from file: " + authorizationPath); + LOGGER.info("Load Credential from file: " + credentialPath); return this.fs.readFile(credentialPath); } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/LocalConfigStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/LocalConfigStorage.java new file mode 100644 index 00000000000..c1edbb51dd4 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/LocalConfigStorage.java @@ -0,0 +1,110 @@ +/* + * 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.storage; + +import org.apache.commons.io.IOUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.InterpreterInfoSaving; +import org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + + +/** + * Storing config in local file system + */ +public class LocalConfigStorage extends ConfigStorage { + + private static Logger LOGGER = LoggerFactory.getLogger(LocalConfigStorage.class); + + private File interpreterSettingPath; + private File authorizationPath; + private File credentialPath; + + public LocalConfigStorage(ZeppelinConfiguration zConf) { + super(zConf); + this.interpreterSettingPath = new File(zConf.getInterpreterSettingPath()); + this.authorizationPath = new File(zConf.getNotebookAuthorizationPath()); + this.credentialPath = new File(zConf.getCredentialsPath()); + } + + @Override + public void save(InterpreterInfoSaving settingInfos) throws IOException { + writeToFile(settingInfos.toJson(), interpreterSettingPath); + } + + @Override + public InterpreterInfoSaving loadInterpreterSettings() throws IOException { + if (!interpreterSettingPath.exists()) { + LOGGER.warn("Interpreter Setting file {} is not existed", interpreterSettingPath); + return null; + } + LOGGER.info("Load Interpreter Setting from file: " + interpreterSettingPath); + String json = readFromFile(interpreterSettingPath); + return buildInterpreterInfoSaving(json); + } + + @Override + public void save(NotebookAuthorizationInfoSaving authorizationInfoSaving) throws IOException { + LOGGER.info("Save notebook authorization to file: " + authorizationPath); + writeToFile(authorizationInfoSaving.toJson(), authorizationPath); + } + + @Override + public NotebookAuthorizationInfoSaving loadNotebookAuthorization() throws IOException { + if (!authorizationPath.exists()) { + LOGGER.warn("NotebookAuthorization file {} is not existed", authorizationPath); + return null; + } + LOGGER.info("Load notebook authorization from file: " + authorizationPath); + String json = readFromFile(authorizationPath); + return NotebookAuthorizationInfoSaving.fromJson(json); + } + + @Override + public String loadCredentials() throws IOException { + if (!credentialPath.exists()) { + LOGGER.warn("Credential file {} is not existed", credentialPath); + return null; + } + LOGGER.info("Load Credential from file: " + credentialPath); + return readFromFile(credentialPath); + } + + @Override + public void saveCredentials(String credentials) throws IOException { + LOGGER.info("Save Credentials to file: " + credentialPath); + writeToFile(credentials, credentialPath); + } + + private String readFromFile(File file) throws IOException { + return IOUtils.toString(new FileInputStream(file)); + } + + private void writeToFile(String content, File file) throws IOException { + FileOutputStream out = new FileOutputStream(file); + IOUtils.write(content, out); + out.close(); + } + +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java index 22366545ee7..8904239310b 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java @@ -44,6 +44,7 @@ import org.apache.zeppelin.scheduler.Job.Status; import org.apache.zeppelin.scheduler.SchedulerFactory; import org.apache.zeppelin.search.SearchService; +import org.apache.zeppelin.storage.ConfigStorage; import org.apache.zeppelin.user.AuthenticationInfo; import org.apache.zeppelin.user.Credentials; import org.junit.After; @@ -89,10 +90,14 @@ public void setUp() throws Exception { System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), mainNotebookDir.getAbsolutePath()); System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_STORAGE.getVarName(), "org.apache.zeppelin.notebook.repo.VFSNotebookRepo,org.apache.zeppelin.notebook.repo.mock.VFSNotebookRepoMock"); System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_ONE_WAY_SYNC.getVarName(), "false"); + System.setProperty(ConfVars.ZEPPELIN_CONFIG_FS_DIR.getVarName(), mainZepDir.getAbsolutePath() + "/conf"); + LOG.info("main Note dir : " + mainNotePath); LOG.info("secondary note dir : " + secNotePath); conf = ZeppelinConfiguration.create(); + ConfigStorage.reset(); + this.schedulerFactory = SchedulerFactory.singleton(); depResolver = new DependencyResolver(mainZepDir.getAbsolutePath() + "/local-repo"); From b14d8a140084ab4b30d77b0ec4025a070b0048f0 Mon Sep 17 00:00:00 2001 From: Mohamed Magdy Date: Wed, 24 Jan 2018 10:11:15 +0100 Subject: [PATCH 180/492] [ZEPPELIN-3092] GitHub Integration ### What is this PR for? GitHub integration as a storage for notebooks. ### What type of PR is it? Feature ### What is the Jira issue? [ZEPPELIN-3092](https://issues.apache.org/jira/browse/ZEPPELIN-3092) ### How should this be tested? 1. Change the configuration in `zeppelin-site.xml` to enable GitHub integration (add GitHub url, username, access token and origin) as described in https://github.com/apache/zeppelin/compare/master...mohamagdy:zeppelin-3092-remote-github-integration?expand=1#diff-89104d48f0358450399a6f679bba9c4f 2. Start the Zeppelin server 3. Open an existing notebook or create a new notebook 4. Do some changes to the notebook, for example add a new paragraph 5. Click on the versioning button on the top menu to commit and save changes 6. Checkout the changes in the GitHub repository. The changes should be reflected ### Questions: * **Does the licenses files need update?** No * **Is there breaking changes for older versions?** No * **Does this needs documentation?** Yes. Documentation is updated as part of the pull request. Author: Mohamed Magdy Author: Mohamed Magdy Author: Mohamed Magdy Closes #2700 from mohamagdy/zeppelin-3092-remote-github-integration and squashes the following commits: b445960 [Mohamed Magdy] [ZEPPELIN-3092] Optimize imports for `Notebook` class afa5de1 [Mohamed Magdy] Merge branch 'master' of github.com:apache/zeppelin into zeppelin-3092-remote-github-integration 548c423 [Mohamed Magdy] [ZEPPELIN-3092] Add `zeppelin-site.xml` to `.gitignore` e98d1b0 [Mohamed Magdy] [ZEPPELIN-3092] Remove `zeppelin-site.xml` from Zeppelin Server resources 7a02855 [Mohamed Magdy] [ZEPPELIN-3092] Add Apache Software Foundation header 9101e58 [Mohamed Magdy] [ZEPPELIN-3092] Replace `printStackTrace()` with error logging db94d55 [Mohamed Magdy] [ZEPPELIN-3092] Remove loading notebook from repository when requested af952a0 [Mohamed Magdy] [ZEPPELIN-3029] Change authentication to `anonymous` instead of `empty` b5fbc1e [Mohamed Magdy] [ZEPPELIN-3092] Break long line to smaller ones 4d6cc76 [Mohamed Magdy] [ZEPPELIN-3092] Load notebook from repository when requested d1d43eb [Mohamed Magdy] Merge branch 'zeppelin-3092-remote-github-integration' of github.com:mohamagdy/zeppelin into zeppelin-3092-remote-github-integration 579bd6f [Mohamed Magdy] [ZEPPELIN-3092] Load note from memory when reloading 2f1b8bc [Mohamed Magdy] [ZEPPELIN-3092] Load note from memory when reloading d545e81 [Mohamed Magdy] [ZEPPELIN-3092] Remove duplicated dependency from `pom.xml` fc13fa6 [Mohamed Magdy] Revert "[ZEPPELIN-3029] Increase Paragraph and Browser timeouts" be2c278 [Mohamed Magdy] Revert "[ZEPPELIN-3092] Set browser timeout to 180 seconds" f362dcb [Mohamed Magdy] [ZEPPELIN-3029] Use jGit version 4.5.4 instead of 4.3.1 8bd23d0 [Mohamed Magdy] [ZEPPELIN-3092] Set browser timeout to 180 seconds 30f2ab4 [Mohamed Magdy] [ZEPPELIN-3029] Increase Paragraph and Browser timeouts 13a0014 [Mohamed Magdy] [ZEPPELIN-3092] Disable GitHub configuration for Zeppelin server 14cb024 [Mohamed Magdy] [ZEPPELIN-3092] Fix notebook path for Git and GitHub tests 0e9db3f [Mohamed Magdy] [ZEPPELIN-3092] Remove test GitHub repository URL and access token 90de14c [Mohamed Magdy] Merge branch 'master' into zeppelin-3092-remote-github-integration 2c1cf74 [Mohamed Magdy] [ZEPPELIN-3029] Fix remote origin key name 6ba67ca [Mohamed Magdy] [ZEPPELIN-3092] Add Javadoc to `GitHubNotebookRepo` and fix line length to 100 264565b [Mohamed Magdy] [ZEPPELIN-3092] Fix line length to be 100 0174bbd [Mohamed Magdy] [ZEPPELIN-3092] Add documentation how to enabled `GitHubNotebookRepo` 81969e1 [Mohamed Magdy] [ZEPPELIN-3092] Add documentation for loading notebooks from repo 3009abd [Mohamed Magdy] [ZEPPELIN-3092] Reset `GitNotebookRepo` to `master` 6aa4ba7 [Mohamed Magdy] [ZEPPELIN-3092] Revert back `GitNotebookRepo` to `master` b77a2d3 [Mohamed Magdy] [ZEPPELIN-3092] Fix identation in `pom.xml` aadd9b5 [Mohamed Magdy] [ZEPPELIN-3092] Revert back ZeppelinServer changes 0dacbf1 [Mohamed Magdy] [ZEPPELIN-3092] Fix encoding in the documenation 2b093b2 [Mohamed Magdy] [ZEPPELIN-3092] Add documentation about GitHub integration 843e42a [Mohamed Magdy] [ZEPPELIN-3092] Cleanup GitHub repository tests 5236176 [Mohamed Magdy] [ZEPPELIN-3092] Move GitHub notebook repostiory to separte file 2dbf116 [Mohamed Magdy] [ZEPPELIN-3092] Add GitHub configuration to `zeppelin-site.xml` template bb0afe2 [Mohamed Magdy] [ZEPPELIN-3092] Add GitHub remote to configurations 33ae24a [Mohamed Magdy] [ZEPPELIN-3092] Add remote Github repository synchronzing 32f6764 [Mohamed Magdy] [ZEPPELIN-3092] Fix GitNotebook test eeb485a [Mohamed Magdy] [ZEPPELIN-3092] Add Github configuration reader 0bde310 [Mohamed Magdy] [ZEPPELIN-3092] Add `zeppelin-site.xml` to `zeppelin-server` resources 9467503 [Mohamed Magdy] [ZEPPELIN-3092] Add `zepplein-server/local-repo` to `.gitignore` (cherry picked from commit 2be8f350658076c33d9d905b9e9907aa3d3a8792) Signed-off-by: Jeff Zhang --- .gitignore | 3 + conf/zeppelin-site.xml.template | 24 ++ .../contribution/how_to_contribute_code.md | 12 + docs/setup/operation/configuration.md | 30 +- docs/setup/storage/storage.md | 42 ++ .../zeppelin/conf/ZeppelinConfiguration.java | 22 +- .../zeppelin/socket/NotebookServer.java | 2 +- .../src/test/resources/2A94M5J1Z/note.json | 376 ++++++++++++++++++ .../src/test/resources/2A94M5J2Z/note.json | 376 ++++++++++++++++++ .../apache/zeppelin/notebook/Notebook.java | 38 +- .../notebook/repo/GitHubNotebookRepo.java | 126 ++++++ .../notebook/repo/GitNotebookRepo.java | 5 +- .../notebook/repo/GitHubNotebookRepoTest.java | 207 ++++++++++ .../notebook/repo/GitNotebookRepoTest.java | 16 +- 14 files changed, 1243 insertions(+), 36 deletions(-) create mode 100644 zeppelin-server/src/test/resources/2A94M5J1Z/note.json create mode 100644 zeppelin-server/src/test/resources/2A94M5J2Z/note.json create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GitHubNotebookRepo.java create mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GitHubNotebookRepoTest.java diff --git a/.gitignore b/.gitignore index 773edc80676..4086a4bb43e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ spark-1.*-bin-hadoop* lens/lens-cli-hist.log +# Zeppelin server +zeppelin-server/local-repo +zeppelin-server/src/main/resources/zeppelin-site.xml # conf file conf/zeppelin-env.sh diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template index 33aa8acf6da..9e9898bb4db 100755 --- a/conf/zeppelin-site.xml.template +++ b/conf/zeppelin-site.xml.template @@ -499,5 +499,29 @@ --> + diff --git a/docs/development/contribution/how_to_contribute_code.md b/docs/development/contribution/how_to_contribute_code.md index b172aa193be..92b69b5c267 100644 --- a/docs/development/contribution/how_to_contribute_code.md +++ b/docs/development/contribution/how_to_contribute_code.md @@ -89,11 +89,17 @@ For the further ### Run Zeppelin server in development mode +#### Option 1 - Command Line + +1. Copy the `conf/zeppelin-site.xml.template` to `zeppelin-server/src/main/resources/zeppelin-site.xml` and change the configurations in this file if required +2. Run the following command ``` cd zeppelin-server HADOOP_HOME=YOUR_HADOOP_HOME JAVA_HOME=YOUR_JAVA_HOME mvn exec:java -Dexec.mainClass="org.apache.zeppelin.server.ZeppelinServer" -Dexec.args="" ``` +#### Option 2 - Daemon Script + > **Note:** Make sure you first run ```mvn clean install -DskipTests``` on your zeppelin root directory, otherwise your server build will fail to find the required dependencies in the local repro. or use daemon script @@ -104,6 +110,12 @@ bin/zeppelin-daemon start Server will be run on [http://localhost:8080](http://localhost:8080). +#### Option 3 - IDE + +1. Copy the `conf/zeppelin-site.xml.template` to `zeppelin-server/src/main/resources/zeppelin-site.xml` and change the configurations in this file if required +2. `ZeppelinServer.java` Main class + + ### Generating Thrift Code Some portions of the Zeppelin code are generated by [Thrift](http://thrift.apache.org). For most Zeppelin changes, you don't need to worry about this. But if you modify any of the Thrift IDL files (e.g. zeppelin-interpreter/src/main/thrift/*.thrift), then you also need to regenerate these files and submit their updated version as part of your patch. diff --git a/docs/setup/operation/configuration.md b/docs/setup/operation/configuration.md index 1f4c6a24231..ed4e1f26ac0 100644 --- a/docs/setup/operation/configuration.md +++ b/docs/setup/operation/configuration.md @@ -329,6 +329,30 @@ If both are defined, then the **environment variables** will take priority. false Enable directory listings on server. + +
    ZEPPELIN_NOTEBOOK_GIT_REMOTE_URL
    +
    zeppelin.notebook.git.remote.url
    + + GitHub's repository URL. It could be either the HTTP URL or the SSH URL. For example git@github.com:apache/zeppelin.git + + +
    ZEPPELIN_NOTEBOOK_GIT_REMOTE_USERNAME
    +
    zeppelin.notebook.git.remote.username
    + token + GitHub username. By default it is `token` to use GitHub's API + + +
    ZEPPELIN_NOTEBOOK_GIT_REMOTE_ACCESS_TOKEN
    +
    zeppelin.notebook.git.remote.access-token
    + token + GitHub access token to use GitHub's API. If username/password combination is used and not GitHub API, then this value is the password + + +
    ZEPPELIN_NOTEBOOK_GIT_REMOTE_ORIGIN
    +
    zeppelin.notebook.git.remote.origin
    + token + GitHub remote name. Default is `origin` + @@ -431,7 +455,7 @@ The following properties needs to be updated in the `zeppelin-site.xml` in order ### Storing user credentials -In order to avoid having to re-enter credentials every time you restart/redeploy Zeppelin, you can store the user credentials. Zeppelin supports this via the ZEPPELIN_CREDENTIALS_PERSIST configuration. +In order to avoid having to re-enter credentials every time you restart/redeploy Zeppelin, you can store the user credentials. Zeppelin supports this via the ZEPPELIN_CREDENTIALS_PERSIST configuration. Please notice that passwords will be stored in *plain text* by default. To encrypt the passwords, use the ZEPPELIN_CREDENTIALS_ENCRYPT_KEY config variable. This will encrypt passwords using the AES-128 algorithm. @@ -473,5 +497,9 @@ update your configuration with the obfuscated password : ``` +### Create GitHub Access Token + +When using GitHub to track notebooks, one can use GitHub's API for authentication. To create an access token, please use the following link https://github.com/settings/tokens. +The value of the access token generated is set in the `zeppelin.notebook.git.remote.access-token` property. **Note:** After updating these configurations, Zeppelin server needs to be restarted. diff --git a/docs/setup/storage/storage.md b/docs/setup/storage/storage.md index f6b8b5c08d1..f34fc2cfc65 100644 --- a/docs/setup/storage/storage.md +++ b/docs/setup/storage/storage.md @@ -34,6 +34,7 @@ There are few notebook storage systems available for a use out of the box: * storage using Amazon S3 service - `S3NotebookRepo` * storage using Azure service - `AzureNotebookRepo` * storage using MongoDB - `MongoNotebookRepo` + * storage using GitHub - `GitHubNotebookRepo` Multiple storage systems can be used at the same time by providing a comma-separated list of the class-names in the configuration. By default, only first two of them will be automatically kept in sync by Zeppelin. @@ -361,3 +362,44 @@ export ZEPPELIN_NOTEBOOK_MONGO_AUTOIMPORT=true #### Import your local notes automatically By setting `ZEPPELIN_NOTEBOOK_MONGO_AUTOIMPORT` as `true` (default `false`), you can import your local notes automatically when Zeppelin daemon starts up. This feature is for easy migration from local file system storage to MongoDB storage. A note with ID already existing in the collection will not be imported. + +## Notebook Storage in GitHub + +To enable GitHub tracking, uncomment the following properties in `zeppelin-site.xml` + +```sh + + zeppelin.notebook.git.remote.url + + remote Git repository URL + + + + zeppelin.notebook.git.remote.username + token + remote Git repository username + + + + zeppelin.notebook.git.remote.access-token + + remote Git repository password + + + + zeppelin.notebook.git.remote.origin + origin + Git repository remote + +``` + +And set the `zeppelin.notebook.storage` propery to `org.apache.zeppelin.notebook.repo.GitHubNotebookRepo` + +```sh + + zeppelin.notebook.storage + org.apache.zeppelin.notebook.repo.GitHubNotebookRepo + +``` + +The access token could be obtained by following the steps on this link https://github.com/settings/tokens. \ No newline at end of file diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java index 59a1f9410cd..030d7813d1b 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java @@ -561,6 +561,22 @@ public String getLifecycleManagerClass() { return getString(ConfVars.ZEPPELIN_INTERPRETER_LIFECYCLE_MANAGER_CLASS); } + public String getZeppelinNotebookGitURL() { + return getString(ConfVars.ZEPPELIN_NOTEBOOK_GIT_REMOTE_URL); + } + + public String getZeppelinNotebookGitUsername() { + return getString(ConfVars.ZEPPELIN_NOTEBOOK_GIT_REMOTE_USERNAME); + } + + public String getZeppelinNotebookGitAccessToken() { + return getString(ConfVars.ZEPPELIN_NOTEBOOK_GIT_REMOTE_ACCESS_TOKEN); + } + + public String getZeppelinNotebookGitRemoteOrigin() { + return getString(ConfVars.ZEPPELIN_NOTEBOOK_GIT_REMOTE_ORIGIN); + } + public Map dumpConfigurations(ZeppelinConfiguration conf, ConfigurationKeyPredicate predicate) { Map configurations = new HashMap<>(); @@ -745,8 +761,12 @@ public static enum ConfVars { ZEPPELIN_INTERPRETER_LIFECYCLE_MANAGER_TIMEOUT_THRESHOLD( "zeppelin.interpreter.lifecyclemanager.timeout.threshold", 3600000L), - ZEPPELIN_OWNER_ROLE("zeppelin.notebook.default.owner.username", ""); + ZEPPELIN_OWNER_ROLE("zeppelin.notebook.default.owner.username", ""), + ZEPPELIN_NOTEBOOK_GIT_REMOTE_URL("zeppelin.notebook.git.remote.url", ""), + ZEPPELIN_NOTEBOOK_GIT_REMOTE_USERNAME("zeppelin.notebook.git.remote.username", "token"), + ZEPPELIN_NOTEBOOK_GIT_REMOTE_ACCESS_TOKEN("zeppelin.notebook.git.remote.access-token", ""), + ZEPPELIN_NOTEBOOK_GIT_REMOTE_ORIGIN("zeppelin.notebook.git.remote.origin", "origin"); private String varName; @SuppressWarnings("rawtypes") 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 56aa50a3eb5..20d5ba9cc90 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 @@ -824,8 +824,8 @@ private void sendNote(NotebookSocket conn, HashSet userAndRoles, Noteboo String user = fromMessage.principal; Note note = notebook.getNote(noteId); - if (note != null) { + if (note != null) { if (!hasParagraphReaderPermission(conn, notebook, noteId, userAndRoles, fromMessage.principal, "read")) { return; diff --git a/zeppelin-server/src/test/resources/2A94M5J1Z/note.json b/zeppelin-server/src/test/resources/2A94M5J1Z/note.json new file mode 100644 index 00000000000..6e8e06fe296 --- /dev/null +++ b/zeppelin-server/src/test/resources/2A94M5J1Z/note.json @@ -0,0 +1,376 @@ +{ + "paragraphs": [ + { + "text": "%md\n## Welcome to Zeppelin.\n##### This is a live tutorial, you can run the code yourself. (Shift-Enter to Run)", + "user": "anonymous", + "dateUpdated": "Dec 17, 2016 3:32:15 PM", + "config": { + "colWidth": 12.0, + "editorHide": true, + "results": [ + { + "graph": { + "mode": "table", + "height": 300.0, + "optionOpen": false, + "keys": [], + "values": [], + "groups": [], + "scatter": {} + } + } + ], + "enabled": true, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch2\u003eWelcome to Zeppelin.\u003c/h2\u003e\n\u003ch5\u003eThis is a live tutorial, you can run the code yourself. (Shift-Enter to Run)\u003c/h5\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1423836981412_-1007008116", + "id": "20150213-231621_168813393", + "dateCreated": "Feb 13, 2015 11:16:21 PM", + "dateStarted": "Dec 17, 2016 3:32:15 PM", + "dateFinished": "Dec 17, 2016 3:32:18 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Load data into table", + "text": "import org.apache.commons.io.IOUtils\nimport java.net.URL\nimport java.nio.charset.Charset\n\n// Zeppelin creates and injects sc (SparkContext) and sqlContext (HiveContext or SqlContext)\n// So you don\u0027t need create them manually\n\n// load bank data\nval bankText \u003d sc.parallelize(\n IOUtils.toString(\n new URL(\"https://s3.amazonaws.com/apache-zeppelin/tutorial/bank/bank.csv\"),\n Charset.forName(\"utf8\")).split(\"\\n\"))\n\ncase class Bank(age: Integer, job: String, marital: String, education: String, balance: Integer)\n\nval bank \u003d bankText.map(s \u003d\u003e s.split(\";\")).filter(s \u003d\u003e s(0) !\u003d \"\\\"age\\\"\").map(\n s \u003d\u003e Bank(s(0).toInt, \n s(1).replaceAll(\"\\\"\", \"\"),\n s(2).replaceAll(\"\\\"\", \"\"),\n s(3).replaceAll(\"\\\"\", \"\"),\n s(5).replaceAll(\"\\\"\", \"\").toInt\n )\n).toDF()\nbank.registerTempTable(\"bank\")", + "user": "anonymous", + "dateUpdated": "Dec 17, 2016 3:30:09 PM", + "config": { + "colWidth": 12.0, + "title": true, + "enabled": true, + "editorMode": "ace/mode/scala", + "results": [ + { + "graph": { + "mode": "table", + "height": 300.0, + "optionOpen": false + } + } + ], + "editorSetting": { + "language": "scala", + "editOnDblClick": false + } + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "import org.apache.commons.io.IOUtils\nimport java.net.URL\nimport java.nio.charset.Charset\nbankText: org.apache.spark.rdd.RDD[String] \u003d ParallelCollectionRDD[36] at parallelize at \u003cconsole\u003e:43\ndefined class Bank\nbank: org.apache.spark.sql.DataFrame \u003d [age: int, job: string ... 3 more fields]\nwarning: there were 1 deprecation warning(s); re-run with -deprecation for details\n" + } + ] + }, + "apps": [], + "jobName": "paragraph_1423500779206_-1502780787", + "id": "20150210-015259_1403135953", + "dateCreated": "Feb 10, 2015 1:52:59 AM", + "dateStarted": "Dec 17, 2016 3:30:09 PM", + "dateFinished": "Dec 17, 2016 3:30:58 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%sql \nselect age, count(1) value\nfrom bank \nwhere age \u003c 30 \ngroup by age \norder by age", + "user": "anonymous", + "dateUpdated": "Mar 17, 2017 12:18:02 PM", + "config": { + "colWidth": 4.0, + "results": [ + { + "graph": { + "mode": "multiBarChart", + "height": 366.0, + "optionOpen": false + }, + "helium": {} + } + ], + "enabled": true, + "editorSetting": { + "language": "sql", + "editOnDblClick": false + }, + "editorMode": "ace/mode/sql" + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TABLE", + "data": "age\tvalue\n19\t4\n20\t3\n21\t7\n22\t9\n23\t20\n24\t24\n25\t44\n26\t77\n27\t94\n28\t103\n29\t97\n" + } + ] + }, + "apps": [], + "jobName": "paragraph_1423500782552_-1439281894", + "id": "20150210-015302_1492795503", + "dateCreated": "Feb 10, 2015 1:53:02 AM", + "dateStarted": "Dec 17, 2016 3:30:13 PM", + "dateFinished": "Dec 17, 2016 3:31:04 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%sql \nselect age, count(1) value \nfrom bank \nwhere age \u003c ${maxAge\u003d30} \ngroup by age \norder by age", + "user": "anonymous", + "dateUpdated": "Mar 17, 2017 12:17:39 PM", + "config": { + "colWidth": 4.0, + "results": [ + { + "graph": { + "mode": "multiBarChart", + "height": 294.0, + "optionOpen": false + }, + "helium": {} + } + ], + "enabled": true, + "editorSetting": { + "language": "sql", + "editOnDblClick": false + }, + "editorMode": "ace/mode/sql" + }, + "settings": { + "params": { + "maxAge": "35" + }, + "forms": { + "maxAge": { + "name": "maxAge", + "defaultValue": "30", + "hidden": false + } + } + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TABLE", + "data": "age\tvalue\n19\t4\n20\t3\n21\t7\n22\t9\n23\t20\n24\t24\n25\t44\n26\t77\n27\t94\n28\t103\n29\t97\n30\t150\n31\t199\n32\t224\n33\t186\n34\t231\n" + } + ] + }, + "apps": [], + "jobName": "paragraph_1423720444030_-1424110477", + "id": "20150212-145404_867439529", + "dateCreated": "Feb 12, 2015 2:54:04 PM", + "dateStarted": "Dec 17, 2016 3:30:58 PM", + "dateFinished": "Dec 17, 2016 3:31:07 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%sql \nselect age, count(1) value \nfrom bank \nwhere marital\u003d\"${marital\u003dsingle,single|divorced|married}\" \ngroup by age \norder by age", + "user": "anonymous", + "dateUpdated": "Mar 17, 2017 12:18:18 PM", + "config": { + "colWidth": 4.0, + "results": [ + { + "graph": { + "mode": "stackedAreaChart", + "height": 280.0, + "optionOpen": false + }, + "helium": {} + } + ], + "enabled": true, + "editorSetting": { + "language": "sql", + "editOnDblClick": false + }, + "editorMode": "ace/mode/sql" + }, + "settings": { + "params": { + "marital": "single" + }, + "forms": { + "marital": { + "name": "marital", + "defaultValue": "single", + "options": [ + { + "value": "single" + }, + { + "value": "divorced" + }, + { + "value": "married" + } + ], + "hidden": false + } + } + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TABLE", + "data": "age\tvalue\n19\t4\n20\t3\n21\t7\n22\t9\n23\t17\n24\t13\n25\t33\n26\t56\n27\t64\n28\t78\n29\t56\n30\t92\n31\t86\n32\t105\n33\t61\n34\t75\n35\t46\n36\t50\n37\t43\n38\t44\n39\t30\n40\t25\n41\t19\n42\t23\n43\t21\n44\t20\n45\t15\n46\t14\n47\t12\n48\t12\n49\t11\n50\t8\n51\t6\n52\t9\n53\t4\n55\t3\n56\t3\n57\t2\n58\t7\n59\t2\n60\t5\n66\t2\n69\t1\n" + } + ] + }, + "apps": [], + "jobName": "paragraph_1423836262027_-210588283", + "id": "20150213-230422_1600658137", + "dateCreated": "Feb 13, 2015 11:04:22 PM", + "dateStarted": "Dec 17, 2016 3:31:05 PM", + "dateFinished": "Dec 17, 2016 3:31:09 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n## Congratulations, it\u0027s done.\n##### You can create your own notebook in \u0027Notebook\u0027 menu. Good luck!", + "user": "anonymous", + "dateUpdated": "Dec 17, 2016 3:30:24 PM", + "config": { + "colWidth": 12.0, + "editorHide": true, + "results": [ + { + "graph": { + "mode": "table", + "height": 300.0, + "optionOpen": false + } + } + ], + "enabled": true, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch2\u003eCongratulations, it\u0026rsquo;s done.\u003c/h2\u003e\n\u003ch5\u003eYou can create your own notebook in \u0026lsquo;Notebook\u0026rsquo; menu. Good luck!\u003c/h5\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1423836268492_216498320", + "id": "20150213-230428_1231780373", + "dateCreated": "Feb 13, 2015 11:04:28 PM", + "dateStarted": "Dec 17, 2016 3:30:24 PM", + "dateFinished": "Dec 17, 2016 3:30:29 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n\nAbout bank data\n\n```\nCitation Request:\n This dataset is public available for research. The details are described in [Moro et al., 2011]. \n Please include this citation if you plan to use this database:\n\n [Moro et al., 2011] S. Moro, R. Laureano and P. Cortez. Using Data Mining for Bank Direct Marketing: An Application of the CRISP-DM Methodology. \n In P. Novais et al. (Eds.), Proceedings of the European Simulation and Modelling Conference - ESM\u00272011, pp. 117-121, Guimarães, Portugal, October, 2011. EUROSIS.\n\n Available at: [pdf] http://hdl.handle.net/1822/14838\n [bib] http://www3.dsi.uminho.pt/pcortez/bib/2011-esm-1.txt\n```", + "user": "anonymous", + "dateUpdated": "Dec 17, 2016 3:30:34 PM", + "config": { + "colWidth": 12.0, + "editorHide": true, + "results": [ + { + "graph": { + "mode": "table", + "height": 300.0, + "optionOpen": false + } + } + ], + "enabled": true, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003cp\u003eAbout bank data\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eCitation Request:\n This dataset is public available for research. The details are described in [Moro et al., 2011]. \n Please include this citation if you plan to use this database:\n\n [Moro et al., 2011] S. Moro, R. Laureano and P. Cortez. Using Data Mining for Bank Direct Marketing: An Application of the CRISP-DM Methodology. \n In P. Novais et al. (Eds.), Proceedings of the European Simulation and Modelling Conference - ESM\u0026#39;2011, pp. 117-121, Guimarães, Portugal, October, 2011. EUROSIS.\n\n Available at: [pdf] http://hdl.handle.net/1822/14838\n [bib] http://www3.dsi.uminho.pt/pcortez/bib/2011-esm-1.txt\n\u003c/code\u003e\u003c/pre\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1427420818407_872443482", + "id": "20150326-214658_12335843", + "dateCreated": "Mar 26, 2015 9:46:58 PM", + "dateStarted": "Dec 17, 2016 3:30:34 PM", + "dateFinished": "Dec 17, 2016 3:30:34 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "config": {}, + "settings": { + "params": {}, + "forms": {} + }, + "apps": [], + "jobName": "paragraph_1435955447812_-158639899", + "id": "20150703-133047_853701097", + "dateCreated": "Jul 3, 2015 1:30:47 PM", + "status": "READY", + "progressUpdateIntervalMs": 500 + } + ], + "name": "Zeppelin Tutorial/Basic Features (Spark)", + "id": "2A94M5J1Z", + "angularObjects": { + "2C73DY9P9:shared_process": [] + }, + "config": { + "looknfeel": "default" + }, + "info": {} +} \ No newline at end of file diff --git a/zeppelin-server/src/test/resources/2A94M5J2Z/note.json b/zeppelin-server/src/test/resources/2A94M5J2Z/note.json new file mode 100644 index 00000000000..dd9a74df9f7 --- /dev/null +++ b/zeppelin-server/src/test/resources/2A94M5J2Z/note.json @@ -0,0 +1,376 @@ +{ + "paragraphs": [ + { + "text": "%md\n## Welcome to Zeppelin.\n##### This is a live tutorial, you can run the code yourself. (Shift-Enter to Run)", + "user": "anonymous", + "dateUpdated": "Dec 17, 2016 3:32:15 PM", + "config": { + "colWidth": 12.0, + "editorHide": true, + "results": [ + { + "graph": { + "mode": "table", + "height": 300.0, + "optionOpen": false, + "keys": [], + "values": [], + "groups": [], + "scatter": {} + } + } + ], + "enabled": true, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch2\u003eWelcome to Zeppelin.\u003c/h2\u003e\n\u003ch5\u003eThis is a live tutorial, you can run the code yourself. (Shift-Enter to Run)\u003c/h5\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1423836981412_-1007008116", + "id": "20150213-231621_168813393", + "dateCreated": "Feb 13, 2015 11:16:21 PM", + "dateStarted": "Dec 17, 2016 3:32:15 PM", + "dateFinished": "Dec 17, 2016 3:32:18 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Load data into table", + "text": "import org.apache.commons.io.IOUtils\nimport java.net.URL\nimport java.nio.charset.Charset\n\n// Zeppelin creates and injects sc (SparkContext) and sqlContext (HiveContext or SqlContext)\n// So you don\u0027t need create them manually\n\n// load bank data\nval bankText \u003d sc.parallelize(\n IOUtils.toString(\n new URL(\"https://s3.amazonaws.com/apache-zeppelin/tutorial/bank/bank.csv\"),\n Charset.forName(\"utf8\")).split(\"\\n\"))\n\ncase class Bank(age: Integer, job: String, marital: String, education: String, balance: Integer)\n\nval bank \u003d bankText.map(s \u003d\u003e s.split(\";\")).filter(s \u003d\u003e s(0) !\u003d \"\\\"age\\\"\").map(\n s \u003d\u003e Bank(s(0).toInt, \n s(1).replaceAll(\"\\\"\", \"\"),\n s(2).replaceAll(\"\\\"\", \"\"),\n s(3).replaceAll(\"\\\"\", \"\"),\n s(5).replaceAll(\"\\\"\", \"\").toInt\n )\n).toDF()\nbank.registerTempTable(\"bank\")", + "user": "anonymous", + "dateUpdated": "Dec 17, 2016 3:30:09 PM", + "config": { + "colWidth": 12.0, + "title": true, + "enabled": true, + "editorMode": "ace/mode/scala", + "results": [ + { + "graph": { + "mode": "table", + "height": 300.0, + "optionOpen": false + } + } + ], + "editorSetting": { + "language": "scala", + "editOnDblClick": false + } + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "import org.apache.commons.io.IOUtils\nimport java.net.URL\nimport java.nio.charset.Charset\nbankText: org.apache.spark.rdd.RDD[String] \u003d ParallelCollectionRDD[36] at parallelize at \u003cconsole\u003e:43\ndefined class Bank\nbank: org.apache.spark.sql.DataFrame \u003d [age: int, job: string ... 3 more fields]\nwarning: there were 1 deprecation warning(s); re-run with -deprecation for details\n" + } + ] + }, + "apps": [], + "jobName": "paragraph_1423500779206_-1502780787", + "id": "20150210-015259_1403135953", + "dateCreated": "Feb 10, 2015 1:52:59 AM", + "dateStarted": "Dec 17, 2016 3:30:09 PM", + "dateFinished": "Dec 17, 2016 3:30:58 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%sql \nselect age, count(1) value\nfrom bank \nwhere age \u003c 30 \ngroup by age \norder by age", + "user": "anonymous", + "dateUpdated": "Mar 17, 2017 12:18:02 PM", + "config": { + "colWidth": 4.0, + "results": [ + { + "graph": { + "mode": "multiBarChart", + "height": 366.0, + "optionOpen": false + }, + "helium": {} + } + ], + "enabled": true, + "editorSetting": { + "language": "sql", + "editOnDblClick": false + }, + "editorMode": "ace/mode/sql" + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TABLE", + "data": "age\tvalue\n19\t4\n20\t3\n21\t7\n22\t9\n23\t20\n24\t24\n25\t44\n26\t77\n27\t94\n28\t103\n29\t97\n" + } + ] + }, + "apps": [], + "jobName": "paragraph_1423500782552_-1439281894", + "id": "20150210-015302_1492795503", + "dateCreated": "Feb 10, 2015 1:53:02 AM", + "dateStarted": "Dec 17, 2016 3:30:13 PM", + "dateFinished": "Dec 17, 2016 3:31:04 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%sql \nselect age, count(1) value \nfrom bank \nwhere age \u003c ${maxAge\u003d30} \ngroup by age \norder by age", + "user": "anonymous", + "dateUpdated": "Mar 17, 2017 12:17:39 PM", + "config": { + "colWidth": 4.0, + "results": [ + { + "graph": { + "mode": "multiBarChart", + "height": 294.0, + "optionOpen": false + }, + "helium": {} + } + ], + "enabled": true, + "editorSetting": { + "language": "sql", + "editOnDblClick": false + }, + "editorMode": "ace/mode/sql" + }, + "settings": { + "params": { + "maxAge": "35" + }, + "forms": { + "maxAge": { + "name": "maxAge", + "defaultValue": "30", + "hidden": false + } + } + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TABLE", + "data": "age\tvalue\n19\t4\n20\t3\n21\t7\n22\t9\n23\t20\n24\t24\n25\t44\n26\t77\n27\t94\n28\t103\n29\t97\n30\t150\n31\t199\n32\t224\n33\t186\n34\t231\n" + } + ] + }, + "apps": [], + "jobName": "paragraph_1423720444030_-1424110477", + "id": "20150212-145404_867439529", + "dateCreated": "Feb 12, 2015 2:54:04 PM", + "dateStarted": "Dec 17, 2016 3:30:58 PM", + "dateFinished": "Dec 17, 2016 3:31:07 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%sql \nselect age, count(1) value \nfrom bank \nwhere marital\u003d\"${marital\u003dsingle,single|divorced|married}\" \ngroup by age \norder by age", + "user": "anonymous", + "dateUpdated": "Mar 17, 2017 12:18:18 PM", + "config": { + "colWidth": 4.0, + "results": [ + { + "graph": { + "mode": "stackedAreaChart", + "height": 280.0, + "optionOpen": false + }, + "helium": {} + } + ], + "enabled": true, + "editorSetting": { + "language": "sql", + "editOnDblClick": false + }, + "editorMode": "ace/mode/sql" + }, + "settings": { + "params": { + "marital": "single" + }, + "forms": { + "marital": { + "name": "marital", + "defaultValue": "single", + "options": [ + { + "value": "single" + }, + { + "value": "divorced" + }, + { + "value": "married" + } + ], + "hidden": false + } + } + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TABLE", + "data": "age\tvalue\n19\t4\n20\t3\n21\t7\n22\t9\n23\t17\n24\t13\n25\t33\n26\t56\n27\t64\n28\t78\n29\t56\n30\t92\n31\t86\n32\t105\n33\t61\n34\t75\n35\t46\n36\t50\n37\t43\n38\t44\n39\t30\n40\t25\n41\t19\n42\t23\n43\t21\n44\t20\n45\t15\n46\t14\n47\t12\n48\t12\n49\t11\n50\t8\n51\t6\n52\t9\n53\t4\n55\t3\n56\t3\n57\t2\n58\t7\n59\t2\n60\t5\n66\t2\n69\t1\n" + } + ] + }, + "apps": [], + "jobName": "paragraph_1423836262027_-210588283", + "id": "20150213-230422_1600658137", + "dateCreated": "Feb 13, 2015 11:04:22 PM", + "dateStarted": "Dec 17, 2016 3:31:05 PM", + "dateFinished": "Dec 17, 2016 3:31:09 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n## Congratulations, it\u0027s done.\n##### You can create your own notebook in \u0027Notebook\u0027 menu. Good luck!", + "user": "anonymous", + "dateUpdated": "Dec 17, 2016 3:30:24 PM", + "config": { + "colWidth": 12.0, + "editorHide": true, + "results": [ + { + "graph": { + "mode": "table", + "height": 300.0, + "optionOpen": false + } + } + ], + "enabled": true, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch2\u003eCongratulations, it\u0026rsquo;s done.\u003c/h2\u003e\n\u003ch5\u003eYou can create your own notebook in \u0026lsquo;Notebook\u0026rsquo; menu. Good luck!\u003c/h5\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1423836268492_216498320", + "id": "20150213-230428_1231780373", + "dateCreated": "Feb 13, 2015 11:04:28 PM", + "dateStarted": "Dec 17, 2016 3:30:24 PM", + "dateFinished": "Dec 17, 2016 3:30:29 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n\nAbout bank data\n\n```\nCitation Request:\n This dataset is public available for research. The details are described in [Moro et al., 2011]. \n Please include this citation if you plan to use this database:\n\n [Moro et al., 2011] S. Moro, R. Laureano and P. Cortez. Using Data Mining for Bank Direct Marketing: An Application of the CRISP-DM Methodology. \n In P. Novais et al. (Eds.), Proceedings of the European Simulation and Modelling Conference - ESM\u00272011, pp. 117-121, Guimarães, Portugal, October, 2011. EUROSIS.\n\n Available at: [pdf] http://hdl.handle.net/1822/14838\n [bib] http://www3.dsi.uminho.pt/pcortez/bib/2011-esm-1.txt\n```", + "user": "anonymous", + "dateUpdated": "Dec 17, 2016 3:30:34 PM", + "config": { + "colWidth": 12.0, + "editorHide": true, + "results": [ + { + "graph": { + "mode": "table", + "height": 300.0, + "optionOpen": false + } + } + ], + "enabled": true, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003cp\u003eAbout bank data\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eCitation Request:\n This dataset is public available for research. The details are described in [Moro et al., 2011]. \n Please include this citation if you plan to use this database:\n\n [Moro et al., 2011] S. Moro, R. Laureano and P. Cortez. Using Data Mining for Bank Direct Marketing: An Application of the CRISP-DM Methodology. \n In P. Novais et al. (Eds.), Proceedings of the European Simulation and Modelling Conference - ESM\u0026#39;2011, pp. 117-121, Guimarães, Portugal, October, 2011. EUROSIS.\n\n Available at: [pdf] http://hdl.handle.net/1822/14838\n [bib] http://www3.dsi.uminho.pt/pcortez/bib/2011-esm-1.txt\n\u003c/code\u003e\u003c/pre\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1427420818407_872443482", + "id": "20150326-214658_12335843", + "dateCreated": "Mar 26, 2015 9:46:58 PM", + "dateStarted": "Dec 17, 2016 3:30:34 PM", + "dateFinished": "Dec 17, 2016 3:30:34 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "config": {}, + "settings": { + "params": {}, + "forms": {} + }, + "apps": [], + "jobName": "paragraph_1435955447812_-158639899", + "id": "20150703-133047_853701097", + "dateCreated": "Jul 3, 2015 1:30:47 PM", + "status": "READY", + "progressUpdateIntervalMs": 500 + } + ], + "name": "Zeppelin Tutorial/Basic Features (Spark)", + "id": "2A94M5J2Z", + "angularObjects": { + "2C73DY9P9:shared_process": [] + }, + "config": { + "looknfeel": "default" + }, + "info": {} +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java index ff0ac62b03e..72ea2acca7b 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java @@ -17,42 +17,16 @@ package org.apache.zeppelin.notebook; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.google.common.collect.Sets; -import org.apache.zeppelin.interpreter.*; -import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry; -import org.quartz.CronScheduleBuilder; -import org.quartz.CronTrigger; -import org.quartz.JobBuilder; -import org.quartz.JobDetail; -import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; -import org.quartz.JobKey; -import org.quartz.SchedulerException; -import org.quartz.TriggerBuilder; -import org.quartz.impl.StdSchedulerFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; import org.apache.zeppelin.display.AngularObject; import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry; import org.apache.zeppelin.notebook.repo.NotebookRepo; import org.apache.zeppelin.notebook.repo.NotebookRepo.Revision; import org.apache.zeppelin.notebook.repo.NotebookRepoSync; @@ -61,6 +35,14 @@ import org.apache.zeppelin.search.SearchService; import org.apache.zeppelin.user.AuthenticationInfo; import org.apache.zeppelin.user.Credentials; +import org.quartz.*; +import org.quartz.impl.StdSchedulerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; /** * Collection of Notes. diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GitHubNotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GitHubNotebookRepo.java new file mode 100644 index 00000000000..6052e5fd756 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GitHubNotebookRepo.java @@ -0,0 +1,126 @@ +/* + * 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.notebook.repo; + +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.PullCommand; +import org.eclipse.jgit.api.PushCommand; +import org.eclipse.jgit.api.RemoteAddCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URISyntaxException; + +/** + * GitHub integration to store notebooks in a GitHub repository. + * It uses the same simple logic implemented in @see + * {@link org.apache.zeppelin.notebook.repo.GitNotebookRepo} + * + * The logic for updating the local repository from the remote repository is the following: + * - When the GitHubNotebookRepo is initialized + * - When pushing the changes to the remote repository + * + * The logic for updating the remote repository on GitHub from local repository is the following: + * - When commit the changes (saving the notebook) + */ +public class GitHubNotebookRepo extends GitNotebookRepo { + private static final Logger LOG = LoggerFactory.getLogger(GitNotebookRepo.class); + private ZeppelinConfiguration zeppelinConfiguration; + private Git git; + + public GitHubNotebookRepo(ZeppelinConfiguration conf) throws IOException { + super(conf); + + this.git = super.getGit(); + this.zeppelinConfiguration = conf; + + configureRemoteStream(); + pullFromRemoteStream(); + } + + @Override + public Revision checkpoint(String pattern, String commitMessage, AuthenticationInfo subject) { + Revision revision = super.checkpoint(pattern, commitMessage, subject); + + updateRemoteStream(); + + return revision; + } + + private void configureRemoteStream() { + try { + LOG.debug("Setting up remote stream"); + RemoteAddCommand remoteAddCommand = git.remoteAdd(); + remoteAddCommand.setName(zeppelinConfiguration.getZeppelinNotebookGitRemoteOrigin()); + remoteAddCommand.setUri(new URIish(zeppelinConfiguration.getZeppelinNotebookGitURL())); + remoteAddCommand.call(); + } catch (GitAPIException e) { + LOG.error("Error configuring GitHub", e); + } catch (URISyntaxException e) { + LOG.error("Error in GitHub URL provided", e); + } + } + + private void updateRemoteStream() { + LOG.debug("Updating remote stream"); + + pullFromRemoteStream(); + pushToRemoteSteam(); + } + + private void pullFromRemoteStream() { + try { + LOG.debug("Pull latest changed from remote stream"); + PullCommand pullCommand = git.pull(); + pullCommand.setCredentialsProvider( + new UsernamePasswordCredentialsProvider( + zeppelinConfiguration.getZeppelinNotebookGitUsername(), + zeppelinConfiguration.getZeppelinNotebookGitAccessToken() + ) + ); + + pullCommand.call(); + + } catch (GitAPIException e) { + LOG.error("Error when pulling latest changes from remote repository", e); + } + } + + private void pushToRemoteSteam() { + try { + LOG.debug("Push latest changed from remote stream"); + PushCommand pushCommand = git.push(); + pushCommand.setCredentialsProvider( + new UsernamePasswordCredentialsProvider( + zeppelinConfiguration.getZeppelinNotebookGitUsername(), + zeppelinConfiguration.getZeppelinNotebookGitAccessToken() + ) + ); + + pushCommand.call(); + } catch (GitAPIException e) { + LOG.error("Error when pushing latest changes from remote repository", e); + } + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GitNotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GitNotebookRepo.java index 21183da3d0d..2ac4c725456 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GitNotebookRepo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GitNotebookRepo.java @@ -47,7 +47,8 @@ * * This impl intended to be simple and straightforward: * - does not handle branches - * - only basic local git file repo, no remote Github push\pull yet + * - only basic local git file repo, no remote Github push\pull. GitHub integration is + * implemented in @see {@link org.apache.zeppelin.notebook.repo.GitHubNotebookRepo} * * TODO(bzz): add default .gitignore */ @@ -177,7 +178,7 @@ public void close() { } //DI replacements for Tests - Git getGit() { + protected Git getGit() { return git; } diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GitHubNotebookRepoTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GitHubNotebookRepoTest.java new file mode 100644 index 00000000000..49a5cbde621 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GitHubNotebookRepoTest.java @@ -0,0 +1,207 @@ +/* + * 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.notebook.repo; + + +import com.google.common.base.Joiner; +import org.apache.commons.io.FileUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.InterpreterFactory; +import org.apache.zeppelin.notebook.Note; +import org.apache.zeppelin.notebook.Paragraph; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.Iterator; + +import static org.mockito.Mockito.mock; + +/** + * This tests the remote Git tracking for notebooks. The tests uses two local Git repositories created locally + * to handle the tracking of Git actions (pushes and pulls). The repositories are: + * 1. The first repository is considered as a remote that mimics a remote GitHub directory + * 2. The second repository is considered as the local notebook repository + */ +public class GitHubNotebookRepoTest { + private static final Logger LOG = LoggerFactory.getLogger(GitNotebookRepoTest.class); + + private static final String TEST_NOTE_ID = "2A94M5J1Z"; + + private File remoteZeppelinDir; + private File localZeppelinDir; + private String localNotebooksDir; + private String remoteNotebooksDir; + private ZeppelinConfiguration conf; + private GitHubNotebookRepo gitHubNotebookRepo; + private RevCommit firstCommitRevision; + private Git remoteGit; + + @Before + public void setUp() throws Exception { + conf = ZeppelinConfiguration.create(); + + String remoteRepositoryPath = System.getProperty("java.io.tmpdir") + "/ZeppelinTestRemote_" + + System.currentTimeMillis(); + String localRepositoryPath = System.getProperty("java.io.tmpdir") + "/ZeppelinTest_" + + System.currentTimeMillis(); + + // Create a fake remote notebook Git repository locally in another directory + remoteZeppelinDir = new File(remoteRepositoryPath); + remoteZeppelinDir.mkdirs(); + + // Create a local repository for notebooks + localZeppelinDir = new File(localRepositoryPath); + localZeppelinDir.mkdirs(); + + // Notebooks directory (for both the remote and local directories) + localNotebooksDir = Joiner.on(File.separator).join(localRepositoryPath, "notebook"); + remoteNotebooksDir = Joiner.on(File.separator).join(remoteRepositoryPath, "notebook"); + + File notebookDir = new File(localNotebooksDir); + notebookDir.mkdirs(); + + // Copy the test notebook directory from the test/resources/2A94M5J1Z folder to the fake remote Git directory + String remoteTestNoteDir = Joiner.on(File.separator).join(remoteNotebooksDir, TEST_NOTE_ID); + FileUtils.copyDirectory( + new File( + GitHubNotebookRepoTest.class.getResource( + Joiner.on(File.separator).join("", TEST_NOTE_ID) + ).getFile() + ), new File(remoteTestNoteDir) + ); + + // Create the fake remote Git repository + Repository remoteRepository = new FileRepository(Joiner.on(File.separator).join(remoteNotebooksDir, ".git")); + remoteRepository.create(); + + remoteGit = new Git(remoteRepository); + remoteGit.add().addFilepattern(".").call(); + firstCommitRevision = remoteGit.commit().setMessage("First commit from remote repository").call(); + + // Set the Git and Git configurations + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName(), remoteZeppelinDir.getAbsolutePath()); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), notebookDir.getAbsolutePath()); + + // Set the GitHub configurations + System.setProperty( + ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_STORAGE.getVarName(), + "org.apache.zeppelin.notebook.repo.GitHubNotebookRepo"); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_GIT_REMOTE_URL.getVarName(), + remoteNotebooksDir + File.separator + ".git"); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_GIT_REMOTE_USERNAME.getVarName(), "token"); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_GIT_REMOTE_ACCESS_TOKEN.getVarName(), + "access-token"); + + // Create the Notebook repository (configured for the local repository) + gitHubNotebookRepo = new GitHubNotebookRepo(conf); + } + + @After + public void tearDown() throws Exception { + // Cleanup the temporary folders uses as Git repositories + File[] temporaryFolders = { remoteZeppelinDir, localZeppelinDir }; + + for(File temporaryFolder : temporaryFolders) { + if (!FileUtils.deleteQuietly(temporaryFolder)) + LOG.error("Failed to delete {} ", temporaryFolder.getName()); + } + } + + @Test + /** + * Test the case when the Notebook repository is created, it pulls the latest changes from the remote repository + */ + public void pullChangesFromRemoteRepositoryOnLoadingNotebook() throws IOException, GitAPIException { + NotebookRepo.Revision firstHistoryRevision = gitHubNotebookRepo.revisionHistory(TEST_NOTE_ID, null).get(0); + + assert(this.firstCommitRevision.getName().equals(firstHistoryRevision.id)); + } + + @Test + /** + * Test the case when the check-pointing (add new files and commit) it also pulls the latest changes from the + * remote repository + */ + public void pullChangesFromRemoteRepositoryOnCheckpointing() throws GitAPIException, IOException { + // Create a new commit in the remote repository + RevCommit secondCommitRevision = remoteGit.commit().setMessage("Second commit from remote repository").call(); + + // Add a new paragraph to the local repository + addParagraphToNotebook(TEST_NOTE_ID); + + // Commit and push the changes to remote repository + NotebookRepo.Revision thirdCommitRevision = gitHubNotebookRepo.checkpoint( + TEST_NOTE_ID, "Third commit from local repository", null); + + // Check all the commits as seen from the local repository. The commits are ordered chronologically. The last + // commit is the first in the commit logs. + Iterator revisions = gitHubNotebookRepo.getGit().log().all().call().iterator(); + + revisions.next(); // The Merge `master` commit after pushing to the remote repository + + assert(thirdCommitRevision.id.equals(revisions.next().getName())); // The local commit after adding the paragraph + + // The second commit done on the remote repository + assert(secondCommitRevision.getName().equals(revisions.next().getName())); + + // The first commit done on the remote repository + assert(firstCommitRevision.getName().equals(revisions.next().getName())); + } + + @Test + /** + * Test the case when the check-pointing (add new files and commit) it pushes the local commits to the remote + * repository + */ + public void pushLocalChangesToRemoteRepositoryOnCheckpointing() throws IOException, GitAPIException { + // Add a new paragraph to the local repository + addParagraphToNotebook(TEST_NOTE_ID); + + // Commit and push the changes to remote repository + NotebookRepo.Revision secondCommitRevision = gitHubNotebookRepo.checkpoint( + TEST_NOTE_ID, "Second commit from local repository", null); + + // Check all the commits as seen from the remote repository. The commits are ordered chronologically. The last + // commit is the first in the commit logs. + Iterator revisions = remoteGit.log().all().call().iterator(); + + assert(secondCommitRevision.id.equals(revisions.next().getName())); // The local commit after adding the paragraph + + // The first commit done on the remote repository + assert(firstCommitRevision.getName().equals(revisions.next().getName())); + } + + private void addParagraphToNotebook(String noteId) throws IOException { + Note note = gitHubNotebookRepo.get(TEST_NOTE_ID, null); + note.setInterpreterFactory(mock(InterpreterFactory.class)); + Paragraph paragraph = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); + paragraph.setText("%md text"); + gitHubNotebookRepo.save(note, null); + } +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GitNotebookRepoTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GitNotebookRepoTest.java index 2276c25dfe2..72ea4395416 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GitNotebookRepoTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GitNotebookRepoTest.java @@ -70,9 +70,19 @@ public void setUp() throws Exception { String testNoteDir = Joiner.on(File.separator).join(notebooksDir, TEST_NOTE_ID); String testNoteDir2 = Joiner.on(File.separator).join(notebooksDir, TEST_NOTE_ID2); - FileUtils.copyDirectory(new File(Joiner.on(File.separator).join("src", "test", "resources", TEST_NOTE_ID)), - new File(testNoteDir)); - FileUtils.copyDirectory(new File(Joiner.on(File.separator).join("src", "test", "resources", TEST_NOTE_ID2)), + FileUtils.copyDirectory( + new File( + GitHubNotebookRepoTest.class.getResource( + Joiner.on(File.separator).join("", TEST_NOTE_ID) + ).getFile() + ), + new File(testNoteDir)); + FileUtils.copyDirectory( + new File( + GitHubNotebookRepoTest.class.getResource( + Joiner.on(File.separator).join("", TEST_NOTE_ID2) + ).getFile() + ), new File(testNoteDir2) ); From 79aeabfeb03c23d26e9e856684af41ffba1cecb2 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 13 Feb 2018 09:13:23 +0800 Subject: [PATCH 181/492] ZEPPELIN-3226. Fail to launch IPySparkInterpreter in embedded mode ### What is this PR for? Trivial PR for fixing this issue. ### What type of PR is it? [Bug Fix] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3226 ### How should this be tested? * CI Pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2793 from zjffdu/ZEPPELIN-3226 and squashes the following commits: 4f6668b [Jeff Zhang] ZEPPELIN-3226. Fail to launch IPySparkInterpreter in embedded mode (cherry picked from commit a13010f33cf7bb851e50a739a1dbd435a55be17d) Signed-off-by: Jeff Zhang --- .../java/org/apache/zeppelin/spark/IPySparkInterpreter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java index c7253fb40c9..37896f982a5 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java @@ -51,9 +51,10 @@ public void open() throws InterpreterException { PySparkInterpreter.getPythonExec(getProperties())); sparkInterpreter = getSparkInterpreter(); SparkConf conf = sparkInterpreter.getSparkContext().getConf(); - // only set PYTHONPATH in local or yarn-client mode. + // only set PYTHONPATH in embedded, local or yarn-client mode. // yarn-cluster will setup PYTHONPATH automatically. - if (!conf.get("spark.submit.deployMode").equals("cluster")) { + if (!conf.contains("spark.submit.deployMode") || + !conf.get("spark.submit.deployMode").equals("cluster")) { setAdditionalPythonPath(PythonUtils.sparkPythonPath()); setAddBulitinPy4j(false); } From 6d7b14e95a1ea11a7752f0d2c3830c48c1553747 Mon Sep 17 00:00:00 2001 From: "Cardenas, Jhon" Date: Mon, 12 Feb 2018 17:47:10 -0500 Subject: [PATCH 182/492] [ZEPPELIN-3228] Currently interpreter dependencies are not downloaded on zeppelin start - regression issue Currently interpreter dependencies are not downloaded on zeppelin start. This was solved in [ZEPPELIN-3228], but it is happening again. ### What is this PR for? When zeppelin is started/restarted, server should try and download interpreter dependencies. ### What type of PR is it? [Bug Fix] ### What is the Jira issue? * [ZEPPELIN-3228] ### How should this be tested? * Put a dependency (say "org.apache.commons:commons-csv:1.1") in any of the interpreter. * From command line delete local-repo directory * Restart zeppelin server Expectation is local-repo should be recreated with all the dependencies that were mentioned in any of the interpreters. ### Questions: * Does the licenses files need update? n/a * Is there breaking changes for older versions? n/a * Does this needs documentation? n/a Author: Cardenas, Jhon Closes #2792 from jhonderson/ZEPPELIN-1143 and squashes the following commits: 3fcefb2 [Cardenas, Jhon] [ZEPPELIN-1143] When zeppelin starts it does the interpreter dependencies loading. (cherry picked from commit 3418055cca28aa16459d254999f580fd2785f5cc) Signed-off-by: Jeff Zhang --- .../zeppelin/interpreter/InterpreterSettingManager.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java index bda1be60a40..04d409289a3 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java @@ -266,6 +266,12 @@ private void loadFromFile() throws IOException { this.interpreterRepositories.add(repo); } } + + // force interpreter dependencies loading once the + // repositories have been loaded. + for (InterpreterSetting setting : interpreterSettings.values()) { + setting.setDependencies(setting.getDependencies()); + } } } From d6fc7b604348fd9488428fe80595e77a2cc8890c Mon Sep 17 00:00:00 2001 From: Savalek Date: Fri, 2 Feb 2018 12:24:33 +0300 Subject: [PATCH 183/492] [ZEPPELIN-3204] FIX: cursor in paragraph editor jumps ### What is this PR for? Sometimes when a user enters text in the paragraph field the lower part of the paragraph starts to jump. This PR fixes this. ### What type of PR is it? [Bug Fix] ### What is the Jira issue? [ZEPPELIN-3204](https://issues.apache.org/jira/browse/ZEPPELIN-3204) ### Screenshots ![gif](https://user-images.githubusercontent.com/30798933/35732168-812f4274-0829-11e8-9fd6-45c2665f9646.gif) ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: Savalek Closes #2762 from Savalek/ZEPPELIN-3131 and squashes the following commits: 0521e25 [Savalek] FIX: cursor in paragraph editor jumps (cherry picked from commit d4783040c4779b99c5559c908a827c61e4b10608) Signed-off-by: Jeff Zhang --- .../src/app/notebook/paragraph/paragraph.controller.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index fb99e636ceb..75a0fecac3b 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -1028,8 +1028,7 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca const autoAdjustEditorHeight = function (editor) { let height = editor.getSession().getScreenLength() * - editor.renderer.lineHeight + - editor.renderer.scrollBar.getWidth() + editor.renderer.lineHeight angular.element('#' + editor.container.id).height(height.toString() + 'px') editor.resize() From ec4f3e08a25eb05d5a7703555344ebb21d270490 Mon Sep 17 00:00:00 2001 From: Karthik Palaniappan Date: Sun, 28 Jan 2018 19:21:34 -0800 Subject: [PATCH 184/492] [ZEPPELIN-3182] Support saving notebooks to Google Cloud Storage Same as #2738 , but targeted toward `branch-0.8` Author: Karthik Palaniappan Closes #2751 from karth295/master and squashes the following commits: c4a45b7 [Karthik Palaniappan] [ZEPPELIN-3182] Support saving notebooks to Google Cloud Storage 8dc819e [Karthik Palaniappan] Unify logic to clear notebook runtime state on load from storage --- LICENSE | 5 +- conf/zeppelin-env.sh.template | 6 + conf/zeppelin-site.xml.template | 17 ++ docs/index.md | 1 + docs/interpreter/bigquery.md | 20 +- docs/setup/storage/storage.md | 92 +++++++ .../zeppelin/conf/ZeppelinConfiguration.java | 11 +- zeppelin-zengine/pom.xml | 103 +++++++- .../org/apache/zeppelin/notebook/Note.java | 13 + .../notebook/repo/AzureNotebookRepo.java | 28 +-- .../notebook/repo/GCSNotebookRepo.java | 234 +++++++++++++++++ .../notebook/repo/MongoNotebookRepo.java | 42 +--- .../notebook/repo/S3NotebookRepo.java | 17 +- .../notebook/repo/VFSNotebookRepo.java | 27 +- .../notebook/repo/GCSNotebookRepoTest.java | 235 ++++++++++++++++++ 15 files changed, 744 insertions(+), 107 deletions(-) create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GCSNotebookRepo.java create mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GCSNotebookRepoTest.java diff --git a/LICENSE b/LICENSE index 2252d6567c6..3b340531246 100644 --- a/LICENSE +++ b/LICENSE @@ -259,6 +259,7 @@ The text of each license is also included at licenses/LICENSE-[project]-[version (Apache 2.0) Gson extra (https://github.com/DanySK/gson-extras) (Apache 2.0) Nimbus JOSE+JWT (https://bitbucket.org/connect2id/nimbus-jose-jwt/wiki/Home) (Apache 2.0) jarchivelib (https://github.com/thrau/jarchivelib) + (Apache 2.0) Google Cloud Client Library for Java (https://github.com/GoogleCloudPlatform/google-cloud-java) ======================================================================== BSD 3-Clause licenses @@ -274,6 +275,8 @@ The following components are provided under the BSD 3-Clause license. See file (BSD 3 Clause) diff.js (https://github.com/kpdecker/jsdiff) + (BSD 3-Clause) Google Auth Library for Java (https://github.com/google/google-auth-library-java) + ======================================================================== BSD 2-Clause licenses ======================================================================== @@ -287,4 +290,4 @@ Jython Software License ======================================================================== The following components are provided under the Jython Software License. See file headers and project links for details. - (Jython Software License) jython-standalone - http://www.jython.org/ \ No newline at end of file + (Jython Software License) jython-standalone - http://www.jython.org/ diff --git a/conf/zeppelin-env.sh.template b/conf/zeppelin-env.sh.template index 7bc38d633e1..c7204bd2187 100644 --- a/conf/zeppelin-env.sh.template +++ b/conf/zeppelin-env.sh.template @@ -30,16 +30,22 @@ # export ZEPPELIN_NOTEBOOK_DIR # Where notebook saved # export ZEPPELIN_NOTEBOOK_HOMESCREEN # Id of notebook to be displayed in homescreen. ex) 2A94M5J1Z # export ZEPPELIN_NOTEBOOK_HOMESCREEN_HIDE # hide homescreen notebook from list when this value set to "true". default "false" + # export ZEPPELIN_NOTEBOOK_S3_BUCKET # Bucket where notebook saved # export ZEPPELIN_NOTEBOOK_S3_ENDPOINT # Endpoint of the bucket # export ZEPPELIN_NOTEBOOK_S3_USER # User in bucket where notebook saved. For example bucket/user/notebook/2A94M5J1Z/note.json # export ZEPPELIN_NOTEBOOK_S3_KMS_KEY_ID # AWS KMS key ID # export ZEPPELIN_NOTEBOOK_S3_KMS_KEY_REGION # AWS KMS key region # export ZEPPELIN_NOTEBOOK_S3_SSE # Server-side encryption enabled for notebooks + +# export ZEPPELIN_NOTEBOOK_GCS_STORAGE_DIR # GCS "directory" (prefix) under which notebooks are saved. E.g. gs://example-bucket/path/to/dir +# export GOOGLE_APPLICATION_CREDENTIALS # Provide a service account key file for GCS and BigQuery API calls (overrides application default credentials) + # export ZEPPELIN_NOTEBOOK_MONGO_URI # MongoDB connection URI used to connect to a MongoDB database server. Default "mongodb://localhost" # export ZEPPELIN_NOTEBOOK_MONGO_DATABASE # Database name to store notebook. Default "zeppelin" # export ZEPPELIN_NOTEBOOK_MONGO_COLLECTION # Collection name to store notebook. Default "notes" # export ZEPPELIN_NOTEBOOK_MONGO_AUTOIMPORT # If "true" import local notes under ZEPPELIN_NOTEBOOK_DIR on startup. Default "false" + # export ZEPPELIN_IDENT_STRING # A string representing this instance of zeppelin. $USER by default. # export ZEPPELIN_NICENESS # The scheduling priority for daemons. Defaults to 0. # export ZEPPELIN_INTERPRETER_LOCALREPO # Local repository for interpreter's additional dependency loading diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template index 9e9898bb4db..9774f0d7c5c 100755 --- a/conf/zeppelin-site.xml.template +++ b/conf/zeppelin-site.xml.template @@ -67,6 +67,23 @@ hide homescreen notebook from list when this value set to true + + diff --git a/docs/index.md b/docs/index.md index 587ae93ed58..3d42735d092 100644 --- a/docs/index.md +++ b/docs/index.md @@ -104,6 +104,7 @@ limitations under the License. * [Git Storage](./setup/storage/storage.html#notebook-storage-in-local-git-repository) * [S3 Storage](./setup/storage/storage.html#notebook-storage-in-s3) * [Azure Storage](./setup/storage/storage.html#notebook-storage-in-azure) + * [Google Cloud Storage](./setup/storage/storage.html#notebook-storage-in-gcs) * [ZeppelinHub Storage](./setup/storage/storage.html#notebook-storage-in-zeppelinhub) * [MongoDB Storage](./setup/storage/storage.html#notebook-storage-in-mongodb) * Operation diff --git a/docs/interpreter/bigquery.md b/docs/interpreter/bigquery.md index 7ebe2e2fda8..1b90f99357a 100644 --- a/docs/interpreter/bigquery.md +++ b/docs/interpreter/bigquery.md @@ -58,20 +58,12 @@ Zeppelin is built against BigQuery API version v2-rev265-1.21.0 - [API Javadocs] In a notebook, to enable the **BigQuery** interpreter, click the **Gear** icon and select **bigquery**. -### Setup service account credentials - -In order to run BigQuery interpreter outside of Google Cloud Engine you need to provide authentication credentials, -by [following this instructions](https://developers.google.com/identity/protocols/application-default-credentials): - - - Go to the [API Console Credentials page](https://console.developers.google.com/project/_/apis/credentials) - - From the project drop-down, select your project. - - On the `Credentials` page, select the `Create credentials` drop-down, then select `Service account key`. - - From the Service account drop-down, select an existing service account or create a new one. - - For `Key type`, select the `JSON` key option, then select `Create`. The file automatically downloads to your computer. - - Put the `*.json` file you just downloaded in a directory of your choosing. This directory must be private (you can't let anyone get access to this), but accessible to your Zeppelin instance. - - Set the environment variable `GOOGLE_APPLICATION_CREDENTIALS` to the path of the JSON file downloaded. - * either though GUI: in interpreter configuration page property names in CAPITAL_CASE set up env vars - * or though `zeppelin-env.sh`: just add it to the end of the file. +### Provide Application Default Credentials + +Within Google Cloud Platform (e.g. Google App Engine, Google Compute Engine), +built-in credentials are used by default. + +Outside of GCP, follow the Google API authentication instructions for [Zeppelin Google Cloud Storage](https://zeppelin.apache.org/docs/latest/storage/storage.html#notebook-storage-in-gcs) ## Using the BigQuery Interpreter diff --git a/docs/setup/storage/storage.md b/docs/setup/storage/storage.md index f34fc2cfc65..6f2ace43de5 100644 --- a/docs/setup/storage/storage.md +++ b/docs/setup/storage/storage.md @@ -33,6 +33,7 @@ There are few notebook storage systems available for a use out of the box: * all notes are saved in the notebook folder in hadoop compatible file system - `FileSystemNotebookRepo` * storage using Amazon S3 service - `S3NotebookRepo` * storage using Azure service - `AzureNotebookRepo` + * storage using Google Cloud Storage - `GCSNotebookRepo` * storage using MongoDB - `MongoNotebookRepo` * storage using GitHub - `GitHubNotebookRepo` @@ -263,6 +264,97 @@ Optionally, you can specify Azure folder structure name in the file **zeppelin-s ``` +
    +## Notebook Storage in Google Cloud Storage + +Using `GCSNotebookRepo` you can connect Zeppelin with Google Cloud Storage using [Application Default Credentials](https://cloud.google.com/docs/authentication/production). + +First, choose a GCS path under which to store notebooks. + +``` + + zeppelin.notebook.gcs.dir + + + A GCS path in the form gs://bucketname/path/to/dir. + Notes are stored at {zeppelin.notebook.gcs.dir}/{notebook-id}/note.json + + +``` + +Then, initialize the `GCSNotebookRepo` class in the file **zeppelin-site.xml** by commenting the next property: + +``` + + zeppelin.notebook.storage + org.apache.zeppelin.notebook.repo.GitNotebookRepo + versioned notebook persistence layer implementation + +``` + +and commenting out: + +``` + + zeppelin.notebook.storage + org.apache.zeppelin.notebook.repo.GCSNotebookRepo + notebook persistence layer implementation + +``` + +Or, if you want to simultaneously use your local git storage with GCS, use the following property instead: + + ``` + + zeppelin.notebook.storage + org.apache.zeppelin.notebook.repo.GitNotebookRepo,org.apache.zeppelin.notebook.repo.GCSNotebookRepo + notebook persistence layer implementation + +``` + +### Google Cloud API Authentication + +Note: On Google App Engine, Google Cloud Shell, and Google Compute Engine, these +steps are not necessary, as build-in credentials are used by default. + +For more information, see [Application Default Credentials](https://cloud.google.com/docs/authentication/production) + +#### Using gcloud auth application-default login + +See the [gcloud docs](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login) + +As the user running the zeppelin daemon, run: + +```bash +gcloud auth application-default login +``` + +You can also use `--scopes` to restrict access to specific Google APIs, such as +Cloud Storage and BigQuery. + +#### Using service account key files + +Alternatively, to use a [service account](https://cloud.google.com/compute/docs/access/service-accounts) +for authentication with GCS, you will need a JSON service account key file. + +1. Navigate to the [service accounts page](https://console.cloud.google.com/iam-admin/serviceaccounts/project) +2. Click `CREATE SERVICE ACCOUNT` +3. Select at least `Storage -> Storage Object Admin`. Note that this is + **different** than `Storage Admin`. +4. If you are also using the BigQuery Interpreter, add the appropriate + permissions (e.g. `Bigquery -> Bigquery Data Viewer and BigQuery User`) +5. Name your service account, and select "Furnish a new private key" to download + a `.json` file. Click "Create". +6. Move the downloaded file to a location of your choice (e.g. + `/path/to/my/key.json`), and give it appropriate permissions. Ensure at + least the user running the zeppelin daemon can read it. + +Then, point `GOOGLE_APPLICATION_CREDENTIALS` at your new key file in **zeppelin-env.sh**. For example: + +```bash +export GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/key.json +``` +
    ## Notebook Storage in ZeppelinHub diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java index 030d7813d1b..3a2b5a25b7e 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java @@ -360,15 +360,19 @@ public boolean isRecoveryEnabled() { "org.apache.zeppelin.interpreter.recovery.NullRecoveryStorage"); } - public String getUser() { + public String getGCSStorageDir() { + return getString(ConfVars.ZEPPELIN_NOTEBOOK_GCS_STORAGE_DIR); + } + + public String getS3User() { return getString(ConfVars.ZEPPELIN_NOTEBOOK_S3_USER); } - public String getBucketName() { + public String getS3BucketName() { return getString(ConfVars.ZEPPELIN_NOTEBOOK_S3_BUCKET); } - public String getEndpoint() { + public String getS3Endpoint() { return getString(ConfVars.ZEPPELIN_NOTEBOOK_S3_ENDPOINT); } @@ -697,6 +701,7 @@ public static enum ConfVars { ZEPPELIN_NOTEBOOK_HOMESCREEN("zeppelin.notebook.homescreen", null), // whether homescreen notebook will be hidden from notebook list or not ZEPPELIN_NOTEBOOK_HOMESCREEN_HIDE("zeppelin.notebook.homescreen.hide", false), + ZEPPELIN_NOTEBOOK_GCS_STORAGE_DIR("zeppelin.notebook.gcs.dir", ""), ZEPPELIN_NOTEBOOK_S3_BUCKET("zeppelin.notebook.s3.bucket", "zeppelin"), ZEPPELIN_NOTEBOOK_S3_ENDPOINT("zeppelin.notebook.s3.endpoint", "s3.amazonaws.com"), ZEPPELIN_NOTEBOOK_S3_USER("zeppelin.notebook.s3.user", "user"), diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index 9ba9ac13ad9..886664c175c 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -39,6 +39,7 @@ 2.7.3 3.4 2.0 + 1.14.0 1.10.62 2.1.4 1.5.2 @@ -51,6 +52,7 @@ 0.27 + 0.32.0-alpha
    @@ -114,10 +116,84 @@ httpasyncclient
    + + com.google.cloud + google-cloud-storage + ${gcs.storage.version} + + + com.google.code.findbugs + jsr305 + + + com.google.protobuf + protobuf-java + + + com.google.guava + guava + + + com.google.api + api-common + + + com.google.http-client + google-http-client-jackson2 + + + com.google.http-client + google-http-client + + + org.codehaus.jackson + jackson-core-asl + + + + + + com.google.api + api-common + 1.2.0 + + + com.google.guava + guava + + + com.google.code.findbugs + jsr305 + + + + + + com.google.http-client + google-http-client-jackson2 + 1.23.0 + + + com.fasterxml.jackson.core + jackson-core + + + com.google.code.findbugs + jsr305 + + + + com.amazonaws aws-java-sdk-s3 ${aws.sdk.s3.version} + + + joda-time + joda-time + + @@ -146,7 +222,7 @@ com.google.guava guava - 15.0 + 20.0 @@ -227,6 +303,23 @@ test + + com.google.cloud + google-cloud-nio + ${google.testing.nio.version} + test + + + com.google.code.findbugs + jsr305 + + + com.google.guava + guava + + + + com.google.truth truth @@ -611,6 +704,10 @@ com.google.protobuf protobuf-java + + com.google.protobuf + protobuf-java-util + com.google.guava guava @@ -623,6 +720,10 @@ io.grpc grpc-context + + com.google.api.grpc + proto-google-common-protos + diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java index 281c4dec034..0a8fb12bba5 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java @@ -963,6 +963,19 @@ public void postProcessParagraphs() { for (Paragraph p : paragraphs) { p.clearRuntimeInfos(); p.parseText(); + + if (p.getStatus() == Status.PENDING || p.getStatus() == Status.RUNNING) { + p.setStatus(Status.ABORT); + } + + List appStates = p.getAllApplicationStates(); + if (appStates != null) { + for (ApplicationState app : appStates) { + if (app.getStatus() != ApplicationState.Status.ERROR) { + app.setStatus(ApplicationState.Status.UNLOADED); + } + } + } } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/AzureNotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/AzureNotebookRepo.java index de337faf169..731a3e8763e 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/AzureNotebookRepo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/AzureNotebookRepo.java @@ -17,6 +17,13 @@ package org.apache.zeppelin.notebook.repo; +import com.microsoft.azure.storage.CloudStorageAccount; +import com.microsoft.azure.storage.StorageException; +import com.microsoft.azure.storage.file.CloudFile; +import com.microsoft.azure.storage.file.CloudFileClient; +import com.microsoft.azure.storage.file.CloudFileDirectory; +import com.microsoft.azure.storage.file.CloudFileShare; +import com.microsoft.azure.storage.file.ListFileItem; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -28,26 +35,15 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; - import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.NoteInfo; -import org.apache.zeppelin.notebook.Paragraph; -import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.user.AuthenticationInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.microsoft.azure.storage.CloudStorageAccount; -import com.microsoft.azure.storage.StorageException; -import com.microsoft.azure.storage.file.CloudFile; -import com.microsoft.azure.storage.file.CloudFileClient; -import com.microsoft.azure.storage.file.CloudFileDirectory; -import com.microsoft.azure.storage.file.CloudFileShare; -import com.microsoft.azure.storage.file.ListFileItem; - /** * Azure storage backend for notebooks */ @@ -128,15 +124,7 @@ private Note getNote(String noteId) throws IOException { String json = IOUtils.toString(ins, conf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_ENCODING)); ins.close(); - Note note = Note.fromJson(json); - - for (Paragraph p : note.getParagraphs()) { - if (p.getStatus() == Job.Status.PENDING || p.getStatus() == Job.Status.RUNNING) { - p.setStatus(Job.Status.ABORT); - } - } - - return note; + return Note.fromJson(json); } @Override diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GCSNotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GCSNotebookRepo.java new file mode 100644 index 00000000000..591c532e031 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GCSNotebookRepo.java @@ -0,0 +1,234 @@ +/* + * 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.notebook.repo; + +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.BlobId; +import com.google.cloud.storage.BlobInfo; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.Storage.BlobListOption; +import com.google.cloud.storage.StorageException; +import com.google.cloud.storage.StorageOptions; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.gson.JsonParseException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; +import org.apache.zeppelin.notebook.Note; +import org.apache.zeppelin.notebook.NoteInfo; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A NotebookRepo implementation for storing notebooks in Google Cloud Storage. + * + * Notes are stored in the GCS "directory" specified by zeppelin.notebook.gcs.dir. This path + * must be in the form gs://bucketName/path/to/Dir. The bucket must already exist. N.B: GCS is an + * object store, so this "directory" should not itself be an object. Instead, it represents the base + * path for the note.json files. + * + * Authentication is provided by google-auth-library-java. + * @see + * google-auth-library-java. + */ +public class GCSNotebookRepo implements NotebookRepo { + + private static final Logger LOG = LoggerFactory.getLogger(GCSNotebookRepo.class); + private String encoding; + private String bucketName; + private Optional basePath; + private Pattern noteNamePattern; + private Storage storage; + + public GCSNotebookRepo(ZeppelinConfiguration conf) throws IOException { + this(conf, StorageOptions.getDefaultInstance().getService()); + } + + // For tests to use an in-memory storage implementation + GCSNotebookRepo(ZeppelinConfiguration conf, Storage storage) throws IOException { + this.encoding = conf.getString(ConfVars.ZEPPELIN_ENCODING); + + String gcsStorageDir = conf.getGCSStorageDir(); + if (gcsStorageDir.isEmpty()) { + throw new IOException("GCS storage directory must be set using 'zeppelin.notebook.gcs.dir'"); + } + if (!gcsStorageDir.startsWith("gs://")) { + throw new IOException(String.format( + "GCS storage directory '%s' must start with 'gs://'.", gcsStorageDir)); + } + String storageDirWithoutScheme = gcsStorageDir.substring("gs://".length()); + + // pathComponents excludes empty string if trailing slash is present + List pathComponents = Arrays.asList(storageDirWithoutScheme.split("/")); + if (pathComponents.size() < 1) { + throw new IOException(String.format( + "GCS storage directory '%s' must be in the form gs://bucketname/path/to/dir", + gcsStorageDir)); + } + this.bucketName = pathComponents.get(0); + if (pathComponents.size() > 1) { + this.basePath = Optional.of(StringUtils.join( + pathComponents.subList(1, pathComponents.size()), "/")); + } else { + this.basePath = Optional.absent(); + } + + // Notes are stored at gs://bucketName/basePath//note.json + if (basePath.isPresent()) { + this.noteNamePattern = Pattern.compile( + "^" + Pattern.quote(basePath.get() + "/") + "([^/]+)/note\\.json$"); + } else { + this.noteNamePattern = Pattern.compile("^([^/]+)/note\\.json$"); + } + + this.storage = storage; + } + + private BlobId makeBlobId(String noteId) { + if (basePath.isPresent()) { + return BlobId.of(bucketName, basePath.get() + "/" + noteId + "/note.json"); + } else { + return BlobId.of(bucketName, noteId + "/note.json"); + } + } + + @Override + public List list(AuthenticationInfo subject) throws IOException { + try { + List infos = new ArrayList<>(); + Iterable blobsUnderDir; + if (basePath.isPresent()) { + blobsUnderDir = storage + .list(bucketName, BlobListOption.prefix(this.basePath.get() + "/")) + .iterateAll(); + } else { + blobsUnderDir = storage + .list(bucketName) + .iterateAll(); + } + for (Blob b : blobsUnderDir) { + Matcher matcher = noteNamePattern.matcher(b.getName()); + if (matcher.matches()) { + // Callers only use the id field, so do not fetch each note + // This matches the implementation in FileSystemNoteRepo#list + infos.add(new NoteInfo(matcher.group(1), "", null)); + } + } + return infos; + } catch (StorageException se) { + throw new IOException("Could not list GCS directory: " + se.getMessage(), se); + } + } + + @Override + public Note get(String noteId, AuthenticationInfo subject) throws IOException { + BlobId blobId = makeBlobId(noteId); + byte[] contents; + try { + contents = storage.readAllBytes(blobId); + } catch (StorageException se) { + throw new IOException("Could not read " + blobId.toString() + ": " + se.getMessage(), se); + } + + try { + return Note.fromJson(new String(contents, encoding)); + } catch (JsonParseException jpe) { + throw new IOException( + "Could note parse as json " + blobId.toString() + jpe.getMessage(), jpe); + } + } + + @Override + public void save(Note note, AuthenticationInfo subject) throws IOException { + BlobInfo info = BlobInfo.newBuilder(makeBlobId(note.getId())) + .setContentType("application/json") + .build(); + try { + storage.create(info, note.toJson().getBytes("UTF-8")); + } catch (StorageException se) { + throw new IOException("Could not write " + info.toString() + ": " + se.getMessage(), se); + } + } + + @Override + public void remove(String noteId, AuthenticationInfo subject) throws IOException { + Preconditions.checkArgument(!Strings.isNullOrEmpty(noteId)); + BlobId blobId = makeBlobId(noteId); + try { + boolean deleted = storage.delete(blobId); + if (!deleted) { + throw new IOException("Tried to remove nonexistent blob " + blobId.toString()); + } + } catch (StorageException se) { + throw new IOException("Could not remove " + blobId.toString() + ": " + se.getMessage(), se); + } + } + + @Override + public void close() { + //no-op + } + + @Override + public Revision checkpoint(String noteId, String checkpointMsg, AuthenticationInfo subject) + throws IOException { + LOG.warn("checkpoint is not implemented for GCSNotebookRepo"); + return null; + } + + @Override + public Note get(String noteId, String revId, AuthenticationInfo subject) throws IOException { + LOG.warn("get revId is not implemented for GCSNotebookRepo"); + return null; + } + + @Override + public List revisionHistory(String noteId, AuthenticationInfo subject) { + LOG.warn("revisionHistory is not implemented for GCSNotebookRepo"); + return Collections.emptyList(); + } + + @Override + public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subject) + throws IOException { + LOG.warn("setNoteRevision is not implemented for GCSNotebookRepo"); + return null; + } + + @Override + public List getSettings(AuthenticationInfo subject) { + LOG.warn("getSettings is not implemented for GCSNotebookRepo"); + return Collections.emptyList(); + } + + @Override + public void updateSettings(Map settings, AuthenticationInfo subject) { + LOG.warn("updateSettings is not implemented for GCSNotebookRepo"); + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/MongoNotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/MongoNotebookRepo.java index 273d75d4a87..376a98695ee 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/MongoNotebookRepo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/MongoNotebookRepo.java @@ -1,5 +1,9 @@ package org.apache.zeppelin.notebook.repo; +import static com.mongodb.client.model.Filters.eq; +import static com.mongodb.client.model.Filters.in; +import static com.mongodb.client.model.Filters.type; + import com.mongodb.MongoBulkWriteException; import com.mongodb.MongoClient; import com.mongodb.MongoClientURI; @@ -7,18 +11,18 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; -import static com.mongodb.client.model.Filters.eq; -import static com.mongodb.client.model.Filters.type; -import static com.mongodb.client.model.Filters.in; - import com.mongodb.client.model.InsertManyOptions; import com.mongodb.client.model.UpdateOptions; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.NoteInfo; -import org.apache.zeppelin.notebook.Paragraph; -import org.apache.zeppelin.notebook.ApplicationState; -import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.user.AuthenticationInfo; import org.bson.BsonType; import org.bson.Document; @@ -26,11 +30,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - /** * Backend for storing Notebook on MongoDB */ @@ -161,24 +160,7 @@ private Note documentToNote(Document doc) { // document to JSON String json = doc.toJson(); // JSON to note - Note note = Note.fromJson(json); - - for (Paragraph p : note.getParagraphs()) { - if (p.getStatus() == Job.Status.PENDING || p.getStatus() == Job.Status.RUNNING) { - p.setStatus(Job.Status.ABORT); - } - - List appStates = p.getAllApplicationStates(); - if (appStates != null) { - for (ApplicationState app : appStates) { - if (app.getStatus() != ApplicationState.Status.ERROR) { - app.setStatus(ApplicationState.Status.UNLOADED); - } - } - } - } - - return note; + return Note.fromJson(json); } /** diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java index 8828985e5e2..7d647024dd8 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java @@ -90,8 +90,8 @@ public class S3NotebookRepo implements NotebookRepo { public S3NotebookRepo(ZeppelinConfiguration conf) throws IOException { this.conf = conf; - bucketName = conf.getBucketName(); - user = conf.getUser(); + bucketName = conf.getS3BucketName(); + user = conf.getS3User(); useServerSideEncryption = conf.isS3ServerSideEncryption(); // always use the default provider chain @@ -123,7 +123,7 @@ else if (conf.getS3EncryptionMaterialsProviderClass() != null) { } // set S3 endpoint to use - s3client.setEndpoint(conf.getEndpoint()); + s3client.setEndpoint(conf.getS3Endpoint()); } /** @@ -205,19 +205,10 @@ private Note getNote(String key) throws IOException { throw new IOException("Unable to retrieve object from S3: " + ace, ace); } - Note note; try (InputStream ins = s3object.getObjectContent()) { String json = IOUtils.toString(ins, conf.getString(ConfVars.ZEPPELIN_ENCODING)); - note = Note.fromJson(json); + return Note.fromJson(json); } - - for (Paragraph p : note.getParagraphs()) { - if (p.getStatus() == Status.PENDING || p.getStatus() == Status.RUNNING) { - p.setStatus(Status.ABORT); - } - } - - return note; } private NoteInfo getNoteInfo(String key) throws IOException { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.java index 63395f9a771..481ea3dcd0f 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.java @@ -17,6 +17,7 @@ package org.apache.zeppelin.notebook.repo; +import com.google.common.collect.Lists; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -24,11 +25,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; -import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Map; - import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.vfs2.FileContent; @@ -40,17 +39,12 @@ import org.apache.commons.vfs2.VFS; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; -import org.apache.zeppelin.notebook.ApplicationState; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.NoteInfo; -import org.apache.zeppelin.notebook.Paragraph; -import org.apache.zeppelin.scheduler.Job.Status; import org.apache.zeppelin.user.AuthenticationInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.collect.Lists; - /** * */ @@ -167,24 +161,7 @@ private Note getNote(FileObject noteDir) throws IOException { String json = IOUtils.toString(ins, conf.getString(ConfVars.ZEPPELIN_ENCODING)); ins.close(); - Note note = Note.fromJson(json); - - for (Paragraph p : note.getParagraphs()) { - if (p.getStatus() == Status.PENDING || p.getStatus() == Status.RUNNING) { - p.setStatus(Status.ABORT); - } - - List appStates = p.getAllApplicationStates(); - if (appStates != null) { - for (ApplicationState app : appStates) { - if (app.getStatus() != ApplicationState.Status.ERROR) { - app.setStatus(ApplicationState.Status.UNLOADED); - } - } - } - } - - return note; + return Note.fromJson(json); } private NoteInfo getNoteInfo(FileObject noteDir) throws IOException { diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GCSNotebookRepoTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GCSNotebookRepoTest.java new file mode 100644 index 00000000000..c1fae67da15 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GCSNotebookRepoTest.java @@ -0,0 +1,235 @@ +/* + * 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.notebook.repo; + +import static com.google.common.truth.Truth.assertThat; +import static junit.framework.TestCase.fail; + +import com.google.cloud.storage.BlobId; +import com.google.cloud.storage.BlobInfo; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; +import org.apache.zeppelin.notebook.Note; +import org.apache.zeppelin.notebook.NoteInfo; +import org.apache.zeppelin.notebook.Paragraph; +import org.apache.zeppelin.scheduler.Job.Status; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class GCSNotebookRepoTest { + private static final AuthenticationInfo AUTH_INFO = AuthenticationInfo.ANONYMOUS; + + private GCSNotebookRepo notebookRepo; + private Storage storage; + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + { "bucketname", Optional.absent(), "gs://bucketname" }, + { "bucketname-with-slash", Optional.absent(), "gs://bucketname-with-slash/" }, + { "bucketname", Optional.of("path/to/dir"), "gs://bucketname/path/to/dir" }, + { "bucketname", Optional.of("trailing/slash"), "gs://bucketname/trailing/slash/" } + }); + } + + @Parameter(0) + public String bucketName; + + @Parameter(1) + public Optional basePath; + + @Parameter(2) + public String uriPath; + + private Note runningNote; + + @Before + public void setUp() throws Exception { + this.runningNote = makeRunningNote(); + + this.storage = LocalStorageHelper.getOptions().getService(); + + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_GCS_STORAGE_DIR.getVarName(), uriPath); + this.notebookRepo = new GCSNotebookRepo(new ZeppelinConfiguration(), storage); + } + + private static Note makeRunningNote() { + Note note = new Note(); + note.setConfig(ImmutableMap.of("key", "value")); + + Paragraph p = new Paragraph(note, null, null); + p.setText("text"); + p.setStatus(Status.RUNNING); + note.addParagraph(p); + return note; + } + + @Test + public void testList_nonexistent() throws Exception { + assertThat(notebookRepo.list(AUTH_INFO)).isEmpty(); + } + + @Test + public void testList() throws Exception { + createAt(runningNote, "note.json"); + createAt(runningNote, "/note.json"); + createAt(runningNote, "validid/note.json"); + createAt(runningNote, "validid-2/note.json"); + createAt(runningNote, "cannot-be-dir/note.json/foo"); + createAt(runningNote, "cannot/be/nested/note.json"); + + List infos = notebookRepo.list(AUTH_INFO); + List noteIds = new ArrayList<>(); + for (NoteInfo info : infos) { + noteIds.add(info.getId()); + } + // Only valid paths are gs://bucketname/path//note.json + assertThat(noteIds).containsExactlyElementsIn(ImmutableList.of("validid", "validid-2")); + } + + @Test + public void testGet_nonexistent() throws Exception { + try { + notebookRepo.get("id", AUTH_INFO); + fail(); + } catch (IOException e) {} + } + + @Test + public void testGet() throws Exception { + create(runningNote); + + // Status of saved running note is removed in get() + Note got = notebookRepo.get(runningNote.getId(), AUTH_INFO); + assertThat(got.getLastParagraph().getStatus()).isEqualTo(Status.ABORT); + + // But otherwise equal + got.getLastParagraph().setStatus(Status.RUNNING); + assertThat(got).isEqualTo(runningNote); + } + + @Test + public void testGet_malformed() throws Exception { + createMalformed("id"); + try { + notebookRepo.get("id", AUTH_INFO); + fail(); + } catch (IOException e) {} + } + + @Test + public void testSave_create() throws Exception { + notebookRepo.save(runningNote, AUTH_INFO); + // Output is saved + assertThat(storage.readAllBytes(makeBlobId(runningNote.getId()))) + .isEqualTo(runningNote.toJson().getBytes("UTF-8")); + } + + @Test + public void testSave_update() throws Exception { + notebookRepo.save(runningNote, AUTH_INFO); + // Change name of runningNote + runningNote.setName("new-name"); + notebookRepo.save(runningNote, AUTH_INFO); + assertThat(storage.readAllBytes(makeBlobId(runningNote.getId()))) + .isEqualTo(runningNote.toJson().getBytes("UTF-8")); + } + + @Test + public void testRemove_nonexistent() throws Exception { + try { + notebookRepo.remove("id", AUTH_INFO); + fail(); + } catch (IOException e) {} + } + + @Test + public void testRemove() throws Exception { + create(runningNote); + notebookRepo.remove(runningNote.getId(), AUTH_INFO); + assertThat(storage.get(makeBlobId(runningNote.getId()))).isNull(); + } + + private String makeName(String relativePath) { + if (basePath.isPresent()) { + return basePath.get() + "/" + relativePath; + } else { + return relativePath; + } + } + + private BlobId makeBlobId(String noteId) { + return BlobId.of(bucketName, makeName(noteId + "/note.json")); + } + + private void createAt(Note note, String relativePath) throws IOException { + BlobId id = BlobId.of(bucketName, makeName(relativePath)); + BlobInfo info = BlobInfo.newBuilder(id).setContentType("application/json").build(); + storage.create(info, note.toJson().getBytes("UTF-8")); + } + + private void create(Note note) throws IOException { + BlobInfo info = BlobInfo.newBuilder(makeBlobId(note.getId())) + .setContentType("application/json") + .build(); + storage.create(info, note.toJson().getBytes("UTF-8")); + } + + private void createMalformed(String noteId) throws IOException { + BlobInfo info = BlobInfo.newBuilder(makeBlobId(noteId)) + .setContentType("application/json") + .build(); + storage.create(info, "{ invalid-json }".getBytes("UTF-8")); + } + + /* These tests test path parsing for illegal paths, and do not use the parameterized vars */ + + @Test + public void testInitialization_pathNotSet() throws Exception { + try { + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_GCS_STORAGE_DIR.getVarName(), ""); + new GCSNotebookRepo(new ZeppelinConfiguration(), storage); + fail(); + } catch (IOException e) {} + } + + @Test + public void testInitialization_malformedPath() throws Exception { + try { + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_GCS_STORAGE_DIR.getVarName(), "foo"); + new GCSNotebookRepo(new ZeppelinConfiguration(), storage); + fail(); + } catch (IOException e) {} + } +} From b7676ad56b9b62abf59909bd55ca5220a7546763 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Mon, 12 Feb 2018 14:47:44 +0530 Subject: [PATCH 185/492] [ZEPPELIN-3198] UI should not show Version/GIT Control if the same if not supported Currently, UI shows an option for version/GIT Control even when it is not supported by the underlying implementing storage configuration. It is only after users try to save a commit and get an error "Couldn't checkpoint note revision: possibly storage doesn't support versioning. Please check the logs for more details.". So, if implementing storage configuration doesn't support git storage, UI should not show those options. [Improvement] * [ZEPPELIN-3198](https://issues.apache.org/jira/projects/ZEPPELIN/issues/ZEPPELIN-3198) On using "org.apache.zeppelin.notebook.repo.GitNotebookRepo" for `zeppelin.notebook.storage` user should see revision/version control option, for rest of the others e.g. "FileSystemNotebookRepo" user should not see that option * Does the licenses files need update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: Prabhjyot Singh Closes #2757 from prabhjyotsingh/ZEPPELIN-3198 and squashes the following commits: 1f854e80c [Prabhjyot Singh] check for NotebookRepoSync and fall back d2e19094d [Prabhjyot Singh] rename isDefaultRepoGit to isRevisionSupportedInDefaultRepo 70abc4420 [Prabhjyot Singh] revert NotebookRepoCommon e152ec194 [Prabhjyot Singh] rename NotebookGitRepo to NotebookRepoWithVersionControl 987edf05d [Prabhjyot Singh] fix test 580674d9c [Prabhjyot Singh] use {{isRepoGit(0)}} 11baf6f2d [Prabhjyot Singh] refactor into NotebookRepo and NotebookGitRepo c1d34df26 [Prabhjyot Singh] revert imports cd7fde105 [Prabhjyot Singh] ZEPPELIN-3198: add isRevisionSupported for NotebookRepo Change-Id: Ib464af447af49ee9e70da86e0e0f293ef38dd3a1 (cherry picked from commit 0605aae240f00c1c04667756599c0e61e4d8e48a) Signed-off-by: Prabhjyot Singh --- .../zeppelin/socket/NotebookServer.java | 2 +- .../src/app/notebook/notebook-actionBar.html | 2 +- .../org/apache/zeppelin/notebook/Note.java | 13 +++ .../apache/zeppelin/notebook/Notebook.java | 34 +++++-- .../notebook/repo/AzureNotebookRepo.java | 26 ----- .../notebook/repo/FileSystemNotebookRepo.java | 27 +----- .../notebook/repo/GitNotebookRepo.java | 3 +- .../notebook/repo/MongoNotebookRepo.java | 28 +----- .../zeppelin/notebook/repo/NotebookRepo.java | 62 ------------ .../notebook/repo/NotebookRepoSync.java | 54 +++++++---- .../repo/NotebookRepoWithVersionControl.java | 97 +++++++++++++++++++ .../notebook/repo/S3NotebookRepo.java | 26 ----- .../notebook/repo/VFSNotebookRepo.java | 28 +----- .../repo/zeppelinhub/ZeppelinHubRepo.java | 4 +- .../notebook/repo/GitNotebookRepoTest.java | 2 +- .../NotebookRepoSyncInitializationTest.java | 4 +- 16 files changed, 184 insertions(+), 228 deletions(-) create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoWithVersionControl.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 20d5ba9cc90..d1cf9e5c081 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 @@ -49,7 +49,7 @@ import org.apache.zeppelin.notebook.NotebookImportDeserializer; import org.apache.zeppelin.notebook.Paragraph; import org.apache.zeppelin.notebook.ParagraphJobListener; -import org.apache.zeppelin.notebook.repo.NotebookRepo.Revision; +import org.apache.zeppelin.notebook.repo.NotebookRepoWithVersionControl.Revision; import org.apache.zeppelin.notebook.socket.Message; import org.apache.zeppelin.notebook.socket.Message.OP; import org.apache.zeppelin.notebook.socket.WatcherMessage; diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index 9b50e819f79..f8ff830846c 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -100,7 +100,7 @@

    - +

    From 81136aead58ee70d10dc41b812eeca571bd09f0c Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Thu, 15 Feb 2018 14:19:00 +0800 Subject: [PATCH 190/492] ZEPPELIN-3234. z.show() compatibility with previous release ### What is this PR for? Enhance the ZeppelinContext in IPySparkInterpreter ### What type of PR is it? [Bug Fix | Improvement ] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3234 ### How should this be tested? * Unit test is added ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2807 from zjffdu/ZEPPELIN-3234 and squashes the following commits: 39637ee [Jeff Zhang] ZEPPELIN-3234. z.show() compatibility with previous release (cherry picked from commit 3c502cd948e9b877adea9c6589ab42d126cd4fbc) Signed-off-by: Jeff Zhang --- .../zeppelin/python/IPythonInterpreter.java | 16 +++++++++++----- .../zeppelin/spark/IPySparkInterpreter.java | 6 ++++++ .../main/resources/python/zeppelin_ipyspark.py | 14 ++++++++++++++ .../spark/IPySparkInterpreterTest.java | 18 ++++++++++++++++++ .../src/test/resources/log4j.properties | 3 +-- 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java b/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java index 81cfeb24d6c..8078670f8ac 100644 --- a/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java +++ b/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java @@ -30,6 +30,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; +import org.apache.zeppelin.interpreter.BaseZeppelinContext; import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterException; @@ -76,7 +77,7 @@ public class IPythonInterpreter extends Interpreter implements ExecuteResultHand private IPythonClient ipythonClient; private GatewayServer gatewayServer; - private PythonZeppelinContext zeppelinContext; + protected BaseZeppelinContext zeppelinContext; private String pythonExecutable; private long ipythonLaunchTimeout; private String additionalPythonPath; @@ -114,6 +115,12 @@ public void setAddBulitinPy4j(boolean add) { this.useBuiltinPy4j = add; } + public BaseZeppelinContext buildZeppelinContext() { + return new PythonZeppelinContext( + getInterpreterGroup().getInterpreterHookRegistry(), + Integer.parseInt(getProperty("zeppelin.python.maxResult", "1000"))); + } + @Override public void open() throws InterpreterException { try { @@ -130,9 +137,7 @@ public void open() throws InterpreterException { } ipythonLaunchTimeout = Long.parseLong( getProperty("zeppelin.ipython.launch.timeout", "30000")); - this.zeppelinContext = new PythonZeppelinContext( - getInterpreterGroup().getInterpreterHookRegistry(), - Integer.parseInt(getProperty("zeppelin.python.maxResult", "1000"))); + this.zeppelinContext = buildZeppelinContext(); int ipythonPort = RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces(); int jvmGatewayPort = RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces(); LOGGER.info("Launching IPython Kernel at port: " + ipythonPort); @@ -312,6 +317,7 @@ public void close() throws InterpreterException { public InterpreterResult interpret(String st, InterpreterContext context) { zeppelinContext.setGui(context.getGui()); zeppelinContext.setNoteGui(context.getNoteGui()); + zeppelinContext.setInterpreterContext(context); interpreterOutput.setInterpreterOutput(context.out); ExecuteResponse response = ipythonClient.stream_execute(ExecuteRequest.newBuilder().setCode(st).build(), @@ -361,7 +367,7 @@ public List completion(String buf, int cursor, return completions; } - public PythonZeppelinContext getZeppelinContext() { + public BaseZeppelinContext getZeppelinContext() { return zeppelinContext; } diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java index 37896f982a5..a75fda8c1d9 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java @@ -19,6 +19,7 @@ import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaSparkContext; +import org.apache.zeppelin.interpreter.BaseZeppelinContext; import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterException; @@ -92,6 +93,11 @@ private SparkInterpreter getSparkInterpreter() throws InterpreterException { return spark; } + @Override + public BaseZeppelinContext buildZeppelinContext() { + return sparkInterpreter.getZeppelinContext(); + } + @Override public void cancel(InterpreterContext context) throws InterpreterException { super.cancel(context); diff --git a/spark/interpreter/src/main/resources/python/zeppelin_ipyspark.py b/spark/interpreter/src/main/resources/python/zeppelin_ipyspark.py index 324f48155ec..5723f455336 100644 --- a/spark/interpreter/src/main/resources/python/zeppelin_ipyspark.py +++ b/spark/interpreter/src/main/resources/python/zeppelin_ipyspark.py @@ -51,3 +51,17 @@ sqlContext = sqlc = __zSqlc__ = __zSpark__._wrapped else: sqlContext = sqlc = __zSqlc__ = SQLContext(sparkContext=sc, sqlContext=intp.getSQLContext()) + +class IPySparkZeppelinContext(PyZeppelinContext): + + def __init__(self, z): + super(IPySparkZeppelinContext, self).__init__(z) + + def show(self, obj): + from pyspark.sql import DataFrame + if isinstance(obj, DataFrame): + print(self.z.showData(obj._jdf)) + else: + super(IPySparkZeppelinContext, self).show(obj) + +z = __zeppelin__ = IPySparkZeppelinContext(intp.getZeppelinContext()) diff --git a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java index 10d87a63e0a..5eaa42c4625 100644 --- a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java @@ -116,6 +116,15 @@ public void testBasics() throws InterruptedException, IOException, InterpreterEx "| 1| a|\n" + "| 2| b|\n" + "+---+---+\n\n", interpreterResultMessages.get(0).getData()); + + context = getInterpreterContext(); + result = iPySparkInterpreter.interpret("z.show(df)", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals( + "_1 _2\n" + + "1 a\n" + + "2 b\n", interpreterResultMessages.get(0).getData()); } else { result = iPySparkInterpreter.interpret("df = spark.createDataFrame([(1,'a'),(2,'b')])\ndf.show()", context); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); @@ -127,6 +136,15 @@ public void testBasics() throws InterruptedException, IOException, InterpreterEx "| 1| a|\n" + "| 2| b|\n" + "+---+---+\n\n", interpreterResultMessages.get(0).getData()); + + context = getInterpreterContext(); + result = iPySparkInterpreter.interpret("z.show(df)", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals( + "_1 _2\n" + + "1 a\n" + + "2 b\n", interpreterResultMessages.get(0).getData()); } // cancel diff --git a/spark/interpreter/src/test/resources/log4j.properties b/spark/interpreter/src/test/resources/log4j.properties index 6958d4c30fb..0dc7c89701f 100644 --- a/spark/interpreter/src/test/resources/log4j.properties +++ b/spark/interpreter/src/test/resources/log4j.properties @@ -46,7 +46,6 @@ log4j.logger.org.hibernate.type=ALL log4j.logger.org.apache.zeppelin.interpreter=DEBUG log4j.logger.org.apache.zeppelin.spark=DEBUG -log4j.logger.org.apache.zeppelin.python.IPythonInterpreter=DEBUG -log4j.logger.org.apache.zeppelin.python.IPythonClient=DEBUG +log4j.logger.org.apache.zeppelin.python=DEBUG log4j.logger.org.apache.spark.repl.Main=INFO From b1638fd45c34c0f54bbe52b337293c2ccc3ba2a1 Mon Sep 17 00:00:00 2001 From: Magyari Sandor Szilard Date: Wed, 31 Jan 2018 18:10:18 +0100 Subject: [PATCH 191/492] ZEPPELIN-3209. Preserve thread context classloader when running jobs in RemoteInterpreterServer ### What is this PR for? Spark jobs may change current thread context classloader sometimes. For example in case of issue ZEPPELIN-2475 using Spark 2.2 and Scala 2.11.8 when you run DepInterpreter that will start SparkILoop --> ILoop which changes the current thread context classloader, from LauncherAppClassloader to ScalaClassloaderURLClassloader. This result in classloading problems when SparkInterpreter is trying to build up Spark Context, in case SparkInterpreter is started by scheduler on same thread. In short when running subsequent paragraphs, users will get an ambiguous NullPointerException, which is hard to understand as it hides the root cause of the problem. As a safety measure to prevent such cases RemoteInterpreterServer should save & restore original thread context classloader. ### What type of PR is it? [Bug Fix] ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3209 ### How should this be tested? * Use Spark 2.2 and Scala 2.11.8 * create a notebook with two paragraphs * in first paragraph use %spark.dep interpreter and add load some dependencies * in second paragraph use %spark interpreter and run some spark code ### Questions: * Does the licenses files need update? * Is there breaking changes for older versions? * Does this needs documentation? Author: Magyari Sandor Szilard Closes #2771 from sancyx/master-ZEPPELIN-3209 and squashes the following commits: dd3a305da [Magyari Sandor Szilard] ZEPPELIN-3209. Preserve thread context classloader when running jobs in RemoteInterpreterServer (magyari_sandor) (cherry picked from commit 8df623b634cc1fa23b268e2bb27931ea391a1381) Signed-off-by: Lee moon soo --- .../zeppelin/interpreter/remote/RemoteInterpreterServer.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java index 37c20ad5253..71ae3dfe037 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java @@ -603,6 +603,7 @@ public void onPostExecute(String script) { @Override protected Object jobRun() throws Throwable { + ClassLoader currentThreadContextClassloader = Thread.currentThread().getContextClassLoader(); try { InterpreterContext.set(context); @@ -651,6 +652,7 @@ protected Object jobRun() throws Throwable { } return new InterpreterResult(result.code(), resultMessages); } finally { + Thread.currentThread().setContextClassLoader(currentThreadContextClassloader); InterpreterContext.remove(); } } From 800430e10790f6bd833e7907400f2640c88df30b Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 20 Feb 2018 16:58:47 +0800 Subject: [PATCH 192/492] ZEPPELIN-3239. unicode characters in an iPython paragraph makes Spark interpreter irresponsive ### What is this PR for? Fix the unicode issues in python2. ### What type of PR is it? [Bug Fix | Improvement ] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3239 ### How should this be tested? * Unit test is added ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2810 from zjffdu/ZEPPELIN-3239 and squashes the following commits: 70c8b4c [Jeff Zhang] ZEPPELIN-3239. unicode characters in an iPython paragraph makes Spark interpreter irrsponsive (cherry picked from commit a791fad5970905edd07bdb8afcc00497fb3540f5) Signed-off-by: Jeff Zhang --- .../resources/grpc/python/ipython_server.py | 3 +-- .../python/IPythonInterpreterTest.java | 21 ++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/python/src/main/resources/grpc/python/ipython_server.py b/python/src/main/resources/grpc/python/ipython_server.py index 98fa616c2d0..4b68efdf274 100644 --- a/python/src/main/resources/grpc/python/ipython_server.py +++ b/python/src/main/resources/grpc/python/ipython_server.py @@ -27,7 +27,6 @@ _ONE_DAY_IN_SECONDS = 60 * 60 * 24 - is_py2 = sys.version[0] == '2' if is_py2: import Queue as queue @@ -51,7 +50,7 @@ def start(self): def execute(self, request, context): print("execute code:\n") - print(request.code) + print(request.code.encode('utf-8')) sys.stdout.flush() stdout_queue = queue.Queue(maxsize = 10) stderr_queue = queue.Queue(maxsize = 10) diff --git a/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java b/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java index cb854d65704..ec594828f50 100644 --- a/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java @@ -81,18 +81,33 @@ public static void testInterpreter(final Interpreter interpreter) throws IOExcep InterpreterResult result = interpreter.interpret("from __future__ import print_function", getInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - result = interpreter.interpret("import sys\nprint(sys.version_info)", getInterpreterContext()); + + InterpreterContext context = getInterpreterContext(); + result = interpreter.interpret("import sys\nprint(sys.version[0])", context); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + Thread.sleep(100); + List interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + boolean isPython2 = interpreterResultMessages.get(0).getData().equals("2\n"); // single output without print - InterpreterContext context = getInterpreterContext(); + context = getInterpreterContext(); result = interpreter.interpret("'hello world'", context); Thread.sleep(100); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - List interpreterResultMessages = context.out.getInterpreterResultMessages(); + interpreterResultMessages = context.out.getInterpreterResultMessages(); assertEquals(1, interpreterResultMessages.size()); assertEquals("'hello world'", interpreterResultMessages.get(0).getData()); + // unicode + context = getInterpreterContext(); + result = interpreter.interpret("print(u'你好')", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertEquals("你好\n", interpreterResultMessages.get(0).getData()); + // only the last statement is printed context = getInterpreterContext(); result = interpreter.interpret("'hello world'\n'hello world2'", context); From bbb47c8d26869673981d890017cda261f9d8960d Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Thu, 15 Feb 2018 10:30:10 +0800 Subject: [PATCH 193/492] ZEPPELIN-3236. Make grpc framesize configurable ### What is this PR for? Add one new property `zeppelin.ipython.grpc.framesize` which is an advanced configuration. By default it is 32M which should be sufficient for most of scenarios. ### What type of PR is it? [ Improvement ] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3236 ### How should this be tested? * Unit test is added. ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2802 from zjffdu/ZEPPELIN-3236 and squashes the following commits: ffce774 [Jeff Zhang] ZEPPELIN-3236. Make grpc framesize configurable (cherry picked from commit de03a21ba62084a37fd4ef9bba8c00e33b6644cb) Signed-off-by: Jeff Zhang --- .../apache/zeppelin/python/IPythonClient.java | 8 ++++ .../zeppelin/python/IPythonInterpreter.java | 6 ++- .../main/resources/interpreter-setting.json | 6 +++ .../python/IPythonInterpreterTest.java | 40 +++++++++++++++++-- 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/python/src/main/java/org/apache/zeppelin/python/IPythonClient.java b/python/src/main/java/org/apache/zeppelin/python/IPythonClient.java index ac1020498bc..b3bc7fd7d2c 100644 --- a/python/src/main/java/org/apache/zeppelin/python/IPythonClient.java +++ b/python/src/main/java/org/apache/zeppelin/python/IPythonClient.java @@ -20,6 +20,7 @@ import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.stub.StreamObserver; +import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.zeppelin.interpreter.util.InterpreterOutputStream; import org.apache.zeppelin.python.proto.CancelRequest; import org.apache.zeppelin.python.proto.CancelResponse; @@ -131,11 +132,18 @@ public void onNext(ExecuteResponse executeResponse) { @Override public void onError(Throwable throwable) { try { + interpreterOutput.getInterpreterOutput().write(ExceptionUtils.getStackTrace(throwable)); interpreterOutput.getInterpreterOutput().flush(); } catch (IOException e) { LOGGER.error("Unexpected IOException", e); } LOGGER.error("Fail to call IPython grpc", throwable); + finalResponseBuilder.setStatus(ExecuteStatus.ERROR); + + completedFlag.set(true); + synchronized (completedFlag) { + completedFlag.notify(); + } } @Override diff --git a/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java b/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java index 8078670f8ac..10bf530b4ae 100644 --- a/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java +++ b/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java @@ -17,6 +17,7 @@ package org.apache.zeppelin.python; +import io.grpc.ManagedChannelBuilder; import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecutor; import org.apache.commons.exec.ExecuteException; @@ -142,7 +143,10 @@ public void open() throws InterpreterException { int jvmGatewayPort = RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces(); LOGGER.info("Launching IPython Kernel at port: " + ipythonPort); LOGGER.info("Launching JVM Gateway at port: " + jvmGatewayPort); - ipythonClient = new IPythonClient("127.0.0.1", ipythonPort); + int framesize = Integer.parseInt(getProperty("zeppelin.ipython.grpc.framesize", + 32 * 1024 * 1024 + "")); + ipythonClient = new IPythonClient(ManagedChannelBuilder.forAddress("127.0.0.1", ipythonPort) + .usePlaintext(true).maxInboundMessageSize(framesize)); launchIPythonKernel(ipythonPort); setupJVMGateway(jvmGatewayPort); } catch (Exception e) { diff --git a/python/src/main/resources/interpreter-setting.json b/python/src/main/resources/interpreter-setting.json index d6b35380385..3257e58abfb 100644 --- a/python/src/main/resources/interpreter-setting.json +++ b/python/src/main/resources/interpreter-setting.json @@ -40,6 +40,12 @@ "defaultValue": "30000", "description": "time out for ipython launch", "type": "number" + }, + "zeppelin.ipython.grpc.framesize": { + "propertyName": "zeppelin.ipython.grpc.framesize", + "defaultValue": "33554432", + "description": "grpc framesize, default is 32M", + "type": "number" } }, "editor": { diff --git a/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java b/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java index ec594828f50..dfc8c36b74a 100644 --- a/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java @@ -56,9 +56,7 @@ public class IPythonInterpreterTest { private static final Logger LOGGER = LoggerFactory.getLogger(IPythonInterpreterTest.class); private IPythonInterpreter interpreter; - @Before - public void setUp() throws InterpreterException { - Properties properties = new Properties(); + public void startInterpreter(Properties properties) throws InterpreterException { interpreter = new IPythonInterpreter(properties); InterpreterGroup mockInterpreterGroup = mock(InterpreterGroup.class); interpreter.setInterpreterGroup(mockInterpreterGroup); @@ -73,9 +71,45 @@ public void close() throws InterpreterException { @Test public void testIPython() throws IOException, InterruptedException, InterpreterException { + startInterpreter(new Properties()); testInterpreter(interpreter); } + @Test + public void testGrpcFrameSize() throws InterpreterException, IOException { + Properties properties = new Properties(); + properties.setProperty("zeppelin.ipython.grpc.framesize", "4"); + startInterpreter(properties); + + // to make this test can run under both python2 and python3 + InterpreterResult result = interpreter.interpret("from __future__ import print_function", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + InterpreterContext context = getInterpreterContext(); + result = interpreter.interpret("print(11111111111111111111111111111)", context); + assertEquals(InterpreterResult.Code.ERROR, result.code()); + List interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals(1, interpreterResultMessages.size()); + assertTrue(interpreterResultMessages.get(0).getData().contains("Frame size 32 exceeds maximum: 4")); + + // next call continue work + result = interpreter.interpret("print(1)", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + close(); + + // increase framesize to make it work + properties.setProperty("zeppelin.ipython.grpc.framesize", "40"); + startInterpreter(properties); + // to make this test can run under both python2 and python3 + result = interpreter.interpret("from __future__ import print_function", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + context = getInterpreterContext(); + result = interpreter.interpret("print(11111111111111111111111111111)", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + } + public static void testInterpreter(final Interpreter interpreter) throws IOException, InterruptedException, InterpreterException { // to make this test can run under both python2 and python3 InterpreterResult result = interpreter.interpret("from __future__ import print_function", getInterpreterContext()); From 5ee1f7981c99a1616948d839fb0bcdf6563add86 Mon Sep 17 00:00:00 2001 From: sravan Date: Mon, 22 Jan 2018 12:27:41 +0900 Subject: [PATCH 194/492] [ZEPPELIN-3177]Resize charts on paragaph resize ### What is this PR for? Resize charts on paragraph resize * Broadcast chart resize on para. resize with a timeout * Add warning on refresh missing ### What type of PR is it? [Bug Fix | Improvement] ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN/ZEPPELIN-3177 ### How should this be tested? Open a paragraph with charts and resize paragraph width(see the gif) ps- helium charts should be updated accordingly ### Screenshots (if appropriate) Before: ![zeppelin3](https://user-images.githubusercontent.com/11382805/35181438-ec771338-fe04-11e7-8803-a6b3aa15b149.gif) After: ![zeppelin3](https://user-images.githubusercontent.com/11382805/35181425-9623d962-fe04-11e7-8660-8dc82c54cd0e.gif) ### Questions: * Does the licenses files need update? N * Is there breaking changes for older versions? N * Does this needs documentation? N Author: sravan Closes #2735 from sravan-s/fix/resize-chart and squashes the following commits: 2f2deecff [sravan] Activate app after refresh 9bf989496 [sravan] Resize charts on paragraph resize (cherry picked from commit f6ef64f8470e30f2e166ee31d468174f0df0178a) Signed-off-by: Lee moon soo --- .../src/app/notebook/paragraph/paragraph.controller.js | 1 + .../src/app/notebook/paragraph/result/result.controller.js | 3 ++- zeppelin-web/src/app/visualization/visualization.js | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index 75a0fecac3b..07ebf896dd0 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -673,6 +673,7 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca $scope.changeColWidth = function (paragraph, width) { angular.element('.navbar-right.open').removeClass('open') paragraph.config.colWidth = width + $scope.$broadcast('paragraphResized', $scope.paragraph.id) commitParagraph(paragraph) } diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js index ec4eeda0230..5dfe3143971 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -634,6 +634,7 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio builtInViz.instance.setConfig(config) builtInViz.instance.render(transformed) builtInViz.instance.renderSetting(visualizationSettingTargetEl) + builtInViz.instance.activate() } } else { afterLoaded = function (loadedElem) { @@ -755,7 +756,7 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio if (paragraphId === paragraph.id) { let builtInViz = builtInVisualizations[$scope.graphMode] if (builtInViz && builtInViz.instance) { - builtInViz.instance.resize() + $timeout(_ => builtInViz.instance.resize(), 200) } } }) diff --git a/zeppelin-web/src/app/visualization/visualization.js b/zeppelin-web/src/app/visualization/visualization.js index 82704e3a00b..6b6e36aa387 100644 --- a/zeppelin-web/src/app/visualization/visualization.js +++ b/zeppelin-web/src/app/visualization/visualization.js @@ -48,6 +48,7 @@ export default class Visualization { */ refresh () { // override this + console.warn('A chart is missing refresh function, it might not work preperly') } /** From ed517a2001cabe60ed79cd4c45fc94da4854c436 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Fri, 23 Feb 2018 09:35:30 +0530 Subject: [PATCH 195/492] [ZEPPELIN-3245] checkstyle/eslintrc for zeppelin-web (JavaScript) Have added this PR to add a rule in eslinerc to have semicolons in javascript source [Improvement | Refactoring] * [ZEPPELIN-3245](https://issues.apache.org/jira/browse/ZEPPELIN-3245) ``` cd zeppelin-web npm install (or yarn install if you have yarn) npm run lint:once ``` Author: Prabhjyot Singh Closes #2804 from prabhjyotsingh/discuss/eslint_semi_rule and squashes the following commits: 4506f243e [Prabhjyot Singh] eslint rule for space bc43d68a6 [Prabhjyot Singh] merge `[ZEPPELIN-3177]Resize charts on paragaph resize` changes 2d57ba30b [Prabhjyot Singh] fix failing WEB_E2E="true" f23cb61d4 [Prabhjyot Singh] remove `"linebreak-style": 0,` and `"no-use-before-define": 0,` 39f37fb88 [Prabhjyot Singh] remove "standard" from eslint 6edac44f6 [Prabhjyot Singh] add `"semi": [2, "always"]` rule in eslinerc Change-Id: I91546ea973c2c9e7540da1586d6329fc93088eb0 (cherry picked from commit ea2c944742cea6e8e37e225d1acc67cdb195056e) Signed-off-by: Prabhjyot Singh --- .../apache/zeppelin/AbstractZeppelinIT.java | 2 +- zeppelin-web/.eslintrc | 24 +- zeppelin-web/src/app/app.controller.js | 52 +- zeppelin-web/src/app/app.controller.test.js | 44 +- zeppelin-web/src/app/app.js | 162 +- .../configuration/configuration.controller.js | 42 +- .../app/configuration/configuration.test.js | 92 +- .../app/credential/credential.controller.js | 236 +- .../src/app/credential/credential.test.js | 132 +- zeppelin-web/src/app/helium/helium-conf.js | 96 +- zeppelin-web/src/app/helium/helium-package.js | 32 +- zeppelin-web/src/app/helium/helium-type.js | 2 +- .../src/app/helium/helium.controller.js | 424 +-- zeppelin-web/src/app/helium/helium.service.js | 334 +-- zeppelin-web/src/app/helium/index.js | 4 +- zeppelin-web/src/app/home/home.controller.js | 188 +- .../interpreter/interpreter-item.directive.js | 20 +- .../app/interpreter/interpreter.controller.js | 838 +++--- .../src/app/interpreter/interpreter.filter.js | 12 +- .../widget/number-widget.directive.js | 20 +- zeppelin-web/src/app/jobmanager/job-status.js | 26 +- .../src/app/jobmanager/job/job.component.js | 118 +- .../app/jobmanager/job/job.component.test.js | 70 +- .../app/jobmanager/jobmanager.component.js | 177 +- .../jobmanager/jobmanager.component.test.js | 34 +- .../src/app/jobmanager/jobmanager.filter.js | 38 +- .../src/app/jobmanager/jobmanager.service.js | 46 +- .../app/jobmanager/jobmanager.service.test.js | 66 +- .../notebook-repository.controller.js | 78 +- .../dropdown-input.directive.js | 14 +- .../dynamic-forms/dynamic-forms.directive.js | 42 +- .../elastic-input/elastic-input.controller.js | 10 +- .../app/notebook/note-var-share.service.js | 36 +- .../src/app/notebook/notebook.controller.js | 1590 ++++++------ .../app/notebook/notebook.controller.test.js | 240 +- .../paragraph/clipboard.controller.js | 36 +- .../code-editor/code-editor.directive.js | 24 +- .../paragraph/paragraph.controller.js | 1824 ++++++------- .../paragraph/paragraph.controller.test.js | 68 +- .../notebook/paragraph/paragraph.status.js | 16 +- .../notebook/paragraph/resizable.directive.js | 66 +- .../paragraph/result/result.controller.js | 1118 ++++---- .../revisions-comparator.component.js | 162 +- .../save-as/browser-detect.service.js | 26 +- .../app/notebook/save-as/save-as.service.js | 60 +- .../src/app/search/result-list.controller.js | 134 +- zeppelin-web/src/app/search/search.service.js | 24 +- zeppelin-web/src/app/spell/index.js | 4 +- zeppelin-web/src/app/spell/spell-base.js | 14 +- zeppelin-web/src/app/spell/spell-result.js | 182 +- .../tabledata/advanced-transformation-util.js | 1010 ++++---- .../advanced-transformation-util.test.js | 2288 +++++++++-------- .../app/tabledata/advanced-transformation.js | 202 +- .../src/app/tabledata/columnselector.js | 54 +- zeppelin-web/src/app/tabledata/dataset.js | 6 +- .../src/app/tabledata/datasetfactory.js | 12 +- .../src/app/tabledata/datasetfactory.test.js | 46 +- zeppelin-web/src/app/tabledata/network.js | 22 +- zeppelin-web/src/app/tabledata/networkdata.js | 76 +- .../src/app/tabledata/networkdata.test.js | 72 +- zeppelin-web/src/app/tabledata/passthrough.js | 10 +- zeppelin-web/src/app/tabledata/pivot.js | 240 +- zeppelin-web/src/app/tabledata/tabledata.js | 64 +- .../src/app/tabledata/tabledata.test.js | 132 +- .../src/app/tabledata/transformation.js | 68 +- .../builtins/visualization-areachart.js | 144 +- .../builtins/visualization-barchart.js | 122 +- .../builtins/visualization-d3network.js | 256 +- .../builtins/visualization-linechart.js | 162 +- .../builtins/visualization-nvd3chart.js | 242 +- .../builtins/visualization-piechart.js | 66 +- .../builtins/visualization-scatterchart.js | 326 +-- .../builtins/visualization-table.js | 318 +-- .../builtins/visualization-util.js | 146 +- .../src/app/visualization/visualization.js | 108 +- .../array-ordering/array-ordering.service.js | 44 +- .../components/base-url/base-url.service.js | 40 +- .../src/components/login/login.controller.js | 88 +- .../expand-collapse.directive.js | 30 +- .../components/navbar/navbar.controller.js | 258 +- .../navbar/navbar.controller.test.js | 28 +- .../components/ng-enter/ng-enter.directive.js | 18 +- .../ng-enter/ng-enter.directive.test.js | 26 +- .../ng-escape/ng-escape.directive.js | 18 +- .../note-action/note-action.service.js | 144 +- .../note-create/note-create.controller.js | 120 +- .../note-create.controller.test.js | 54 +- .../note-create/visible.directive.js | 40 +- .../note-import/note-import.controller.js | 176 +- .../components/note-list/note-list.factory.js | 62 +- .../note-list/note-list.factory.test.js | 122 +- .../note-rename/note-rename.controller.js | 44 +- .../note-rename/note-rename.service.js | 12 +- .../websocket/websocket-event.factory.js | 204 +- .../websocket/websocket-message.service.js | 292 +-- zeppelin-web/src/index.js | 118 +- 96 files changed, 8777 insertions(+), 8452 deletions(-) diff --git a/zeppelin-integration/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java index b4ebfe9668e..e1992fb4d92 100644 --- a/zeppelin-integration/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java +++ b/zeppelin-integration/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java @@ -110,7 +110,7 @@ protected void createNewNote() { WebDriverWait block = new WebDriverWait(driver, MAX_BROWSER_TIMEOUT_SEC); block.until(ExpectedConditions.visibilityOfElementLocated(By.id("noteCreateModal"))); clickAndWait(By.id("createNoteButton")); - block.until(ExpectedConditions.invisibilityOfElementLocated(By.className("pull-right"))); + block.until(ExpectedConditions.invisibilityOfElementLocated(By.id("createNoteButton"))); } protected void deleteTestNotebook(final WebDriver driver) { diff --git a/zeppelin-web/.eslintrc b/zeppelin-web/.eslintrc index 6dca5c8982b..6207bb9de8c 100644 --- a/zeppelin-web/.eslintrc +++ b/zeppelin-web/.eslintrc @@ -1,5 +1,5 @@ { - "extends": ["eslint:recommended", "google", "standard"], + "extends": ["eslint:recommended", "google"], "env": { "browser": true, "jasmine": true, @@ -31,26 +31,11 @@ "process": false }, "rules": { - "array-bracket-spacing": 0, - "space-before-function-paren": 0, - "no-unneeded-ternary": 0, - "comma-dangle": 0, - "object-curly-spacing": 0, - "standard/object-curly-even-spacing": 0, - "arrow-parens": 0, - "require-jsdoc": 0, - "valid-jsdoc": 0, - "no-invalid-this": 0, - "no-console": 0, - "guard-for-in": 0, - "no-mixed-operators": 1, - "no-useless-escape": 1, "no-bitwise": 2, "camelcase": 2, "curly": 2, "eqeqeq": 2, "wrap-iife": [2, "any"], - "no-use-before-define": 0, "new-cap": 2, "no-caller": 2, "quotes": [2, "single"], @@ -59,6 +44,11 @@ "no-unused-vars": [2, { "vars": "local", "args": "none" }], "strict": [2, "global"], "max-len": [2, {"code": 120, "ignoreComments": true, "ignoreRegExpLiterals": true}], - "linebreak-style": 0 + "require-jsdoc": "off", + "no-console": ["off"], + "valid-jsdoc": "off", + "semi": [2, "always"], + "no-invalid-this": 1, + "indent": ["error", 2, { "SwitchCase": 1 }] } } diff --git a/zeppelin-web/src/app/app.controller.js b/zeppelin-web/src/app/app.controller.js index 6c64a33d180..904fbd797e4 100644 --- a/zeppelin-web/src/app/app.controller.js +++ b/zeppelin-web/src/app/app.controller.js @@ -12,48 +12,48 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').controller('MainCtrl', MainCtrl) +angular.module('zeppelinWebApp').controller('MainCtrl', MainCtrl); -function MainCtrl ($scope, $rootScope, $window, arrayOrderingSrv) { - 'ngInject' +function MainCtrl($scope, $rootScope, $window, arrayOrderingSrv) { + 'ngInject'; - $scope.looknfeel = 'default' + $scope.looknfeel = 'default'; - let init = function () { - $scope.asIframe = (($window.location.href.indexOf('asIframe') > -1) ? true : false) - } + let init = function() { + $scope.asIframe = (($window.location.href.indexOf('asIframe') > -1) ? true : false); + }; - init() + init(); - $rootScope.$on('setIframe', function (event, data) { + $rootScope.$on('setIframe', function(event, data) { if (!event.defaultPrevented) { - $scope.asIframe = data - event.preventDefault() + $scope.asIframe = data; + event.preventDefault(); } - }) + }); - $rootScope.$on('setLookAndFeel', function (event, data) { + $rootScope.$on('setLookAndFeel', function(event, data) { if (!event.defaultPrevented && data && data !== '' && data !== $scope.looknfeel) { - $scope.looknfeel = data - event.preventDefault() + $scope.looknfeel = data; + event.preventDefault(); } - }) + }); // Set The lookAndFeel to default on every page - $rootScope.$on('$routeChangeStart', function (event, next, current) { - $rootScope.$broadcast('setLookAndFeel', 'default') - }) + $rootScope.$on('$routeChangeStart', function(event, next, current) { + $rootScope.$broadcast('setLookAndFeel', 'default'); + }); - $rootScope.noteName = function (note) { + $rootScope.noteName = function(note) { if (!_.isEmpty(note)) { - return arrayOrderingSrv.getNoteName(note) + return arrayOrderingSrv.getNoteName(note); } - } + }; - BootstrapDialog.defaultOptions.onshown = function () { - angular.element('#' + this.id).find('.btn:last').focus() - } + BootstrapDialog.defaultOptions.onshown = function() { + angular.element('#' + this.id).find('.btn:last').focus(); + }; // Remove BootstrapDialog animation - BootstrapDialog.configDefaultOptions({animate: false}) + BootstrapDialog.configDefaultOptions({animate: false}); } diff --git a/zeppelin-web/src/app/app.controller.test.js b/zeppelin-web/src/app/app.controller.test.js index 67d50342946..b6c6261fe49 100644 --- a/zeppelin-web/src/app/app.controller.test.js +++ b/zeppelin-web/src/app/app.controller.test.js @@ -1,28 +1,28 @@ -describe('Controller: MainCtrl', function () { - beforeEach(angular.mock.module('zeppelinWebApp')) +describe('Controller: MainCtrl', function() { + beforeEach(angular.mock.module('zeppelinWebApp')); - let scope - let rootScope + let scope; + let rootScope; - beforeEach(inject(function ($controller, $rootScope) { - rootScope = $rootScope - scope = $rootScope.$new() + beforeEach(inject(function($controller, $rootScope) { + rootScope = $rootScope; + scope = $rootScope.$new(); $controller('MainCtrl', { - $scope: scope - }) - })) + $scope: scope, + }); + })); - it('should attach "asIframe" to the scope and the default value should be false', function () { - expect(scope.asIframe).toBeDefined() - expect(scope.asIframe).toEqual(false) - }) + it('should attach "asIframe" to the scope and the default value should be false', function() { + expect(scope.asIframe).toBeDefined(); + expect(scope.asIframe).toEqual(false); + }); - it('should set the default value of "looknfeel to "default"', function () { - expect(scope.looknfeel).toEqual('default') - }) + it('should set the default value of "looknfeel to "default"', function() { + expect(scope.looknfeel).toEqual('default'); + }); - it('should set "asIframe" flag to true when a controller broadcasts setIframe event', function () { - rootScope.$broadcast('setIframe', true) - expect(scope.asIframe).toEqual(true) - }) -}) + it('should set "asIframe" flag to true when a controller broadcasts setIframe event', function() { + rootScope.$broadcast('setIframe', true); + expect(scope.asIframe).toEqual(true); + }); +}); diff --git a/zeppelin-web/src/app/app.js b/zeppelin-web/src/app/app.js index ed89dd8abe9..64ceff00d79 100644 --- a/zeppelin-web/src/app/app.js +++ b/zeppelin-web/src/app/app.js @@ -15,14 +15,14 @@ * limitations under the License. */ -import 'headroom.js' -import 'headroom.js/dist/angular.headroom' +import 'headroom.js'; +import 'headroom.js/dist/angular.headroom'; -import 'scrollmonitor/scrollMonitor.js' -import 'angular-viewport-watch/angular-viewport-watch.js' +import 'scrollmonitor/scrollMonitor.js'; +import 'angular-viewport-watch/angular-viewport-watch.js'; -import 'angular-ui-grid/ui-grid.css' -import 'angular-ui-grid' +import 'angular-ui-grid/ui-grid.css'; +import 'angular-ui-grid'; const requiredModules = [ 'ngCookies', @@ -56,167 +56,169 @@ const requiredModules = [ 'ui.grid.moveColumns', 'ui.grid.pagination', 'ui.grid.saveState', -] +]; // headroom should not be used for CI, since we have to execute some integration tests. // otherwise, they will fail. -if (!process.env.BUILD_CI) { requiredModules.push('headroom') } +if (!process.env.BUILD_CI) { + requiredModules.push('headroom'); +} let zeppelinWebApp = angular.module('zeppelinWebApp', requiredModules) - .filter('breakFilter', function () { - return function (text) { + .filter('breakFilter', function() { + return function(text) { // eslint-disable-next-line no-extra-boolean-cast if (!!text) { - return text.replace(/\n/g, '
    ') + return text.replace(/\n/g, '
    '); } - } + }; }) - .config(function ($httpProvider, $routeProvider, ngToastProvider) { + .config(function($httpProvider, $routeProvider, ngToastProvider) { // withCredentials when running locally via grunt - $httpProvider.defaults.withCredentials = true + $httpProvider.defaults.withCredentials = true; let visBundleLoad = { - load: ['heliumService', function (heliumService) { - return heliumService.load - }] - } + load: ['heliumService', function(heliumService) { + return heliumService.load; + }], + }; $routeProvider .when('/', { - templateUrl: 'app/home/home.html' + templateUrl: 'app/home/home.html', }) .when('/notebook/:noteId', { templateUrl: 'app/notebook/notebook.html', controller: 'NotebookCtrl', - resolve: visBundleLoad + resolve: visBundleLoad, }) .when('/notebook/:noteId/paragraph?=:paragraphId', { templateUrl: 'app/notebook/notebook.html', controller: 'NotebookCtrl', - resolve: visBundleLoad + resolve: visBundleLoad, }) .when('/notebook/:noteId/paragraph/:paragraphId?', { templateUrl: 'app/notebook/notebook.html', controller: 'NotebookCtrl', - resolve: visBundleLoad + resolve: visBundleLoad, }) .when('/notebook/:noteId/revision/:revisionId', { templateUrl: 'app/notebook/notebook.html', controller: 'NotebookCtrl', - resolve: visBundleLoad + resolve: visBundleLoad, }) .when('/jobmanager', { templateUrl: 'app/jobmanager/jobmanager.html', - controller: 'JobManagerCtrl' + controller: 'JobManagerCtrl', }) .when('/interpreter', { templateUrl: 'app/interpreter/interpreter.html', - controller: 'InterpreterCtrl' + controller: 'InterpreterCtrl', }) .when('/notebookRepos', { templateUrl: 'app/notebook-repository/notebook-repository.html', controller: 'NotebookRepositoryCtrl', - controllerAs: 'noterepo' + controllerAs: 'noterepo', }) .when('/credential', { templateUrl: 'app/credential/credential.html', - controller: 'CredentialCtrl' + controller: 'CredentialCtrl', }) .when('/helium', { templateUrl: 'app/helium/helium.html', - controller: 'HeliumCtrl' + controller: 'HeliumCtrl', }) .when('/configuration', { templateUrl: 'app/configuration/configuration.html', - controller: 'ConfigurationCtrl' + controller: 'ConfigurationCtrl', }) .when('/search/:searchTerm', { templateUrl: 'app/search/result-list.html', - controller: 'SearchResultCtrl' + controller: 'SearchResultCtrl', }) .otherwise({ - redirectTo: '/' - }) + redirectTo: '/', + }); ngToastProvider.configure({ dismissButton: true, dismissOnClick: false, combineDuplications: true, - timeout: 6000 - }) + timeout: 6000, + }); }) // handel logout on API failure - .config(function ($httpProvider, $provide) { + .config(function($httpProvider, $provide) { if (process.env.PROD) { - $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' + $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; } - $provide.factory('httpInterceptor', function ($q, $rootScope) { + $provide.factory('httpInterceptor', function($q, $rootScope) { return { - 'responseError': function (rejection) { + 'responseError': function(rejection) { if (rejection.status === 405) { - let data = {} - data.info = '' - $rootScope.$broadcast('session_logout', data) + let data = {}; + data.info = ''; + $rootScope.$broadcast('session_logout', data); } - $rootScope.$broadcast('httpResponseError', rejection) - return $q.reject(rejection) - } - } - }) - $httpProvider.interceptors.push('httpInterceptor') + $rootScope.$broadcast('httpResponseError', rejection); + return $q.reject(rejection); + }, + }; + }); + $httpProvider.interceptors.push('httpInterceptor'); }) - .constant('TRASH_FOLDER_ID', '~Trash') + .constant('TRASH_FOLDER_ID', '~Trash'); -function auth () { - let $http = angular.injector(['ng']).get('$http') - let baseUrlSrv = angular.injector(['zeppelinWebApp']).get('baseUrlSrv') +function auth() { + let $http = angular.injector(['ng']).get('$http'); + let baseUrlSrv = angular.injector(['zeppelinWebApp']).get('baseUrlSrv'); // withCredentials when running locally via grunt - $http.defaults.withCredentials = true + $http.defaults.withCredentials = true; jQuery.ajaxSetup({ dataType: 'json', xhrFields: { - withCredentials: true + withCredentials: true, }, - crossDomain: true - }) - let config = (process.env.PROD) ? {headers: { 'X-Requested-With': 'XMLHttpRequest' }} : {} - return $http.get(baseUrlSrv.getRestApiBase() + '/security/ticket', config).then(function (response) { - zeppelinWebApp.run(function ($rootScope) { - let res = angular.fromJson(response.data).body + crossDomain: true, + }); + let config = (process.env.PROD) ? {headers: {'X-Requested-With': 'XMLHttpRequest'}} : {}; + return $http.get(baseUrlSrv.getRestApiBase() + '/security/ticket', config).then(function(response) { + zeppelinWebApp.run(function($rootScope) { + let res = angular.fromJson(response.data).body; if (res['redirectURL']) { - window.location.href = res['redirectURL'] + window.location.href + window.location.href = res['redirectURL'] + window.location.href; } else { - $rootScope.ticket = res - $rootScope.ticket.screenUsername = $rootScope.ticket.principal + $rootScope.ticket = res; + $rootScope.ticket.screenUsername = $rootScope.ticket.principal; if ($rootScope.ticket.principal.indexOf('#Pac4j') === 0) { - let re = ', name=(.*?),' - $rootScope.ticket.screenUsername = $rootScope.ticket.principal.match(re)[1] + let re = ', name=(.*?),'; + $rootScope.ticket.screenUsername = $rootScope.ticket.principal.match(re)[1]; } } - }) - }, function (errorResponse) { + }); + }, function(errorResponse) { // Handle error case - let redirect = errorResponse.headers('Location') + let redirect = errorResponse.headers('Location'); if (errorResponse.status === 401 && redirect !== undefined) { // Handle page redirect - window.location.href = redirect + window.location.href = redirect; } - }) + }); } -function bootstrapApplication () { - zeppelinWebApp.run(function ($rootScope, $location) { - $rootScope.$on('$routeChangeStart', function (event, next, current) { - $rootScope.pageTitle = 'Zeppelin' +function bootstrapApplication() { + zeppelinWebApp.run(function($rootScope, $location) { + $rootScope.$on('$routeChangeStart', function(event, next, current) { + $rootScope.pageTitle = 'Zeppelin'; if (!$rootScope.ticket && next.$$route && !next.$$route.publicAccess) { - $location.path('/') + $location.path('/'); } - }) - }) - angular.bootstrap(document, ['zeppelinWebApp']) + }); + }); + angular.bootstrap(document, ['zeppelinWebApp']); } -angular.element(document).ready(function () { - auth().then(bootstrapApplication) -}) +angular.element(document).ready(function() { + auth().then(bootstrapApplication); +}); diff --git a/zeppelin-web/src/app/configuration/configuration.controller.js b/zeppelin-web/src/app/configuration/configuration.controller.js index 0d845ded83f..0f5eba339a6 100644 --- a/zeppelin-web/src/app/configuration/configuration.controller.js +++ b/zeppelin-web/src/app/configuration/configuration.controller.js @@ -12,37 +12,37 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').controller('ConfigurationCtrl', ConfigurationCtrl) +angular.module('zeppelinWebApp').controller('ConfigurationCtrl', ConfigurationCtrl); -function ConfigurationCtrl ($scope, $http, baseUrlSrv, ngToast) { - 'ngInject' +function ConfigurationCtrl($scope, $http, baseUrlSrv, ngToast) { + 'ngInject'; - $scope.configrations = [] - ngToast.dismiss() + $scope.configrations = []; + ngToast.dismiss(); - let getConfigurations = function () { + let getConfigurations = function() { $http.get(baseUrlSrv.getRestApiBase() + '/configurations/all') - .success(function (data, status, headers, config) { - $scope.configurations = data.body + .success(function(data, status, headers, config) { + $scope.configurations = data.body; }) - .error(function (data, status, headers, config) { + .error(function(data, status, headers, config) { if (status === 401) { ngToast.danger({ content: 'You don\'t have permission on this page', verticalPosition: 'bottom', - timeout: '3000' - }) - setTimeout(function () { - window.location = baseUrlSrv.getBase() - }, 3000) + timeout: '3000', + }); + setTimeout(function() { + window.location = baseUrlSrv.getBase(); + }, 3000); } - console.log('Error %o %o', status, data.message) - }) - } + console.log('Error %o %o', status, data.message); + }); + }; - let init = function () { - getConfigurations() - } + let init = function() { + getConfigurations(); + }; - init() + init(); } diff --git a/zeppelin-web/src/app/configuration/configuration.test.js b/zeppelin-web/src/app/configuration/configuration.test.js index 8add1029f7b..4d98a08a533 100644 --- a/zeppelin-web/src/app/configuration/configuration.test.js +++ b/zeppelin-web/src/app/configuration/configuration.test.js @@ -1,69 +1,69 @@ -import template from './configuration.html' +import template from './configuration.html'; -describe('Controller: Configuration', function () { - beforeEach(angular.mock.module('zeppelinWebApp')) +describe('Controller: Configuration', function() { + beforeEach(angular.mock.module('zeppelinWebApp')); - let baseUrlSrvMock = { getRestApiBase: () => '' } + let baseUrlSrvMock = {getRestApiBase: () => ''}; - let ctrl // controller instance - let $scope - let $compile - let $controller // controller generator - let $httpBackend - let ngToast + let ctrl; // controller instance + let $scope; + let $compile; + let $controller; // controller generator + let $httpBackend; + let ngToast; beforeEach(inject((_$controller_, _$rootScope_, _$compile_, _$httpBackend_, _ngToast_) => { - $scope = _$rootScope_.$new() - $compile = _$compile_ - $controller = _$controller_ - $httpBackend = _$httpBackend_ - ngToast = _ngToast_ - })) + $scope = _$rootScope_.$new(); + $compile = _$compile_; + $controller = _$controller_; + $httpBackend = _$httpBackend_; + ngToast = _ngToast_; + })); - afterEach(function () { - $httpBackend.verifyNoOutstandingExpectation() - $httpBackend.verifyNoOutstandingRequest() - }) + afterEach(function() { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); it('should get configuration initially', () => { - const conf = { 'conf1': 'value1' } - ctrl = $controller('ConfigurationCtrl', { $scope: $scope, baseUrlSrv: baseUrlSrvMock, }) - expect(ctrl).toBeDefined() + const conf = {'conf1': 'value1'}; + ctrl = $controller('ConfigurationCtrl', {$scope: $scope, baseUrlSrv: baseUrlSrvMock}); + expect(ctrl).toBeDefined(); $httpBackend .when('GET', '/configurations/all') - .respond(200, { body: conf, }) - $httpBackend.expectGET('/configurations/all') - $httpBackend.flush() + .respond(200, {body: conf}); + $httpBackend.expectGET('/configurations/all'); + $httpBackend.flush(); - expect($scope.configurations).toEqual(conf) // scope is updated after $httpBackend.flush() - }) + expect($scope.configurations).toEqual(conf); // scope is updated after $httpBackend.flush() + }); it('should display ngToast when failed to get configuration properly', () => { - ctrl = $controller('ConfigurationCtrl', { $scope: $scope, baseUrlSrv: baseUrlSrvMock, }) - spyOn(ngToast, 'danger') + ctrl = $controller('ConfigurationCtrl', {$scope: $scope, baseUrlSrv: baseUrlSrvMock}); + spyOn(ngToast, 'danger'); - $httpBackend.when('GET', '/configurations/all').respond(401, {}) - $httpBackend.expectGET('/configurations/all') - $httpBackend.flush() + $httpBackend.when('GET', '/configurations/all').respond(401, {}); + $httpBackend.expectGET('/configurations/all'); + $httpBackend.flush(); - expect(ngToast.danger).toHaveBeenCalled() - }) + expect(ngToast.danger).toHaveBeenCalled(); + }); it('should render list of configurations as the sorted order', () => { $scope.configurations = { 'zeppelin.server.port': '8080', 'zeppelin.server.addr': '0.0.0.0', - } - const elem = $compile(template)($scope) - $scope.$digest() - const tbody = elem.find('tbody') - const tds = tbody.find('td') + }; + const elem = $compile(template)($scope); + $scope.$digest(); + const tbody = elem.find('tbody'); + const tds = tbody.find('td'); // should be sorted - expect(tds[0].innerText.trim()).toBe('zeppelin.server.addr') - expect(tds[1].innerText.trim()).toBe('0.0.0.0') - expect(tds[2].innerText.trim()).toBe('zeppelin.server.port') - expect(tds[3].innerText.trim()).toBe('8080') - }) -}) + expect(tds[0].innerText.trim()).toBe('zeppelin.server.addr'); + expect(tds[1].innerText.trim()).toBe('0.0.0.0'); + expect(tds[2].innerText.trim()).toBe('zeppelin.server.port'); + expect(tds[3].innerText.trim()).toBe('8080'); + }); +}); diff --git a/zeppelin-web/src/app/credential/credential.controller.js b/zeppelin-web/src/app/credential/credential.controller.js index 102876e32c4..cf6c3405bd9 100644 --- a/zeppelin-web/src/app/credential/credential.controller.js +++ b/zeppelin-web/src/app/credential/credential.controller.js @@ -12,194 +12,196 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').controller('CredentialCtrl', CredentialController) +angular.module('zeppelinWebApp').controller('CredentialCtrl', CredentialController); function CredentialController($scope, $http, baseUrlSrv, ngToast) { - 'ngInject' + 'ngInject'; - ngToast.dismiss() + ngToast.dismiss(); - $scope.credentialInfo = [] - $scope.showAddNewCredentialInfo = false - $scope.availableInterpreters = [] + $scope.credentialInfo = []; + $scope.showAddNewCredentialInfo = false; + $scope.availableInterpreters = []; - $scope.entity = '' - $scope.password = '' - $scope.username = '' + $scope.entity = ''; + $scope.password = ''; + $scope.username = ''; $scope.hasCredential = () => { - return Array.isArray($scope.credentialInfo) && $scope.credentialInfo.length - } + return Array.isArray($scope.credentialInfo) && $scope.credentialInfo.length; + }; - let getCredentialInfo = function () { + let getCredentialInfo = function() { $http.get(baseUrlSrv.getRestApiBase() + '/credential') - .success(function (data, status, headers, config) { - $scope.credentialInfo.length = 0 // keep the ref while cleaning - const returnedCredentials = data.body.userCredentials + .success(function(data, status, headers, config) { + $scope.credentialInfo.length = 0; // keep the ref while cleaning + const returnedCredentials = data.body.userCredentials; for (let key in returnedCredentials) { - const value = returnedCredentials[key] - $scope.credentialInfo.push({ - entity: key, - password: value.password, - username: value.username, - }) + if (returnedCredentials.hasOwnProperty(key)) { + const value = returnedCredentials[key]; + $scope.credentialInfo.push({ + entity: key, + password: value.password, + username: value.username, + }); + } } - console.log('Success %o %o', status, $scope.credentialInfo) + console.log('Success %o %o', status, $scope.credentialInfo); }) - .error(function (data, status, headers, config) { + .error(function(data, status, headers, config) { if (status === 401) { - showToast('You do not have permission on this page', 'danger') - setTimeout(function () { - window.location = baseUrlSrv.getBase() - }, 3000) + showToast('You do not have permission on this page', 'danger'); + setTimeout(function() { + window.location = baseUrlSrv.getBase(); + }, 3000); } - console.log('Error %o %o', status, data.message) - }) - } + console.log('Error %o %o', status, data.message); + }); + }; $scope.isValidCredential = function() { - return $scope.entity.trim() !== '' && $scope.username.trim() !== '' - } + return $scope.entity.trim() !== '' && $scope.username.trim() !== ''; + }; - $scope.addNewCredentialInfo = function () { + $scope.addNewCredentialInfo = function() { if (!$scope.isValidCredential()) { - showToast('Username \\ Entity can not be empty.', 'danger') - return + showToast('Username \\ Entity can not be empty.', 'danger'); + return; } let newCredential = { 'entity': $scope.entity, 'username': $scope.username, - 'password': $scope.password - } + 'password': $scope.password, + }; $http.put(baseUrlSrv.getRestApiBase() + '/credential', newCredential) - .success(function (data, status, headers, config) { - showToast('Successfully saved credentials.', 'success') - $scope.credentialInfo.push(newCredential) - resetCredentialInfo() - $scope.showAddNewCredentialInfo = false - console.log('Success %o %o', status, data.message) + .success(function(data, status, headers, config) { + showToast('Successfully saved credentials.', 'success'); + $scope.credentialInfo.push(newCredential); + resetCredentialInfo(); + $scope.showAddNewCredentialInfo = false; + console.log('Success %o %o', status, data.message); }) - .error(function (data, status, headers, config) { - showToast('Error saving credentials', 'danger') - console.log('Error %o %o', status, data.message) - }) - } + .error(function(data, status, headers, config) { + showToast('Error saving credentials', 'danger'); + console.log('Error %o %o', status, data.message); + }); + }; - let getAvailableInterpreters = function () { + let getAvailableInterpreters = function() { $http.get(baseUrlSrv.getRestApiBase() + '/interpreter/setting') - .success(function (data, status, headers, config) { + .success(function(data, status, headers, config) { for (let setting = 0; setting < data.body.length; setting++) { $scope.availableInterpreters.push( - data.body[setting].group + '.' + data.body[setting].name) + data.body[setting].group + '.' + data.body[setting].name); } angular.element('#entityname').autocomplete({ source: $scope.availableInterpreters, - select: function (event, selected) { - $scope.entity = selected.item.value - return false - } - }) + select: function(event, selected) { + $scope.entity = selected.item.value; + return false; + }, + }); }) - .error(function (data, status, headers, config) { - showToast(data.message, 'danger') - console.log('Error %o %o', status, data.message) - }) - } + .error(function(data, status, headers, config) { + showToast(data.message, 'danger'); + console.log('Error %o %o', status, data.message); + }); + }; - $scope.toggleAddNewCredentialInfo = function () { + $scope.toggleAddNewCredentialInfo = function() { if ($scope.showAddNewCredentialInfo) { - $scope.showAddNewCredentialInfo = false + $scope.showAddNewCredentialInfo = false; } else { - $scope.showAddNewCredentialInfo = true + $scope.showAddNewCredentialInfo = true; } - } + }; - $scope.cancelCredentialInfo = function () { - $scope.showAddNewCredentialInfo = false - resetCredentialInfo() - } + $scope.cancelCredentialInfo = function() { + $scope.showAddNewCredentialInfo = false; + resetCredentialInfo(); + }; - const resetCredentialInfo = function () { - $scope.entity = '' - $scope.username = '' - $scope.password = '' - } + const resetCredentialInfo = function() { + $scope.entity = ''; + $scope.username = ''; + $scope.password = ''; + }; - $scope.copyOriginCredentialsInfo = function () { - showToast('Since entity is a unique key, you can edit only username & password', 'info') - } + $scope.copyOriginCredentialsInfo = function() { + showToast('Since entity is a unique key, you can edit only username & password', 'info'); + }; - $scope.updateCredentialInfo = function (form, data, entity) { + $scope.updateCredentialInfo = function(form, data, entity) { if (!$scope.isValidCredential()) { - showToast('Username \\ Entity can not be empty.', 'danger') - return + showToast('Username \\ Entity can not be empty.', 'danger'); + return; } let credential = { entity: entity, username: data.username, - password: data.password - } + password: data.password, + }; $http.put(baseUrlSrv.getRestApiBase() + '/credential/', credential) - .success(function (data, status, headers, config) { - const index = $scope.credentialInfo.findIndex(elem => elem.entity === entity) - $scope.credentialInfo[index] = credential - return true - }) - .error(function (data, status, headers, config) { - showToast('We could not save the credential', 'danger') - console.log('Error %o %o', status, data.message) - form.$show() + .success(function(data, status, headers, config) { + const index = $scope.credentialInfo.findIndex((elem) => elem.entity === entity); + $scope.credentialInfo[index] = credential; + return true; }) - return false - } - - $scope.removeCredentialInfo = function (entity) { + .error(function(data, status, headers, config) { + showToast('We could not save the credential', 'danger'); + console.log('Error %o %o', status, data.message); + form.$show(); + }); + return false; + }; + + $scope.removeCredentialInfo = function(entity) { BootstrapDialog.confirm({ closable: false, closeByBackdrop: false, closeByKeyboard: false, title: '', message: 'Do you want to delete this credential information?', - callback: function (result) { + callback: function(result) { if (result) { $http.delete(baseUrlSrv.getRestApiBase() + '/credential/' + entity) - .success(function (data, status, headers, config) { - const index = $scope.credentialInfo.findIndex(elem => elem.entity === entity) - $scope.credentialInfo.splice(index, 1) - console.log('Success %o %o', status, data.message) - }) - .error(function (data, status, headers, config) { - showToast(data.message, 'danger') - console.log('Error %o %o', status, data.message) + .success(function(data, status, headers, config) { + const index = $scope.credentialInfo.findIndex((elem) => elem.entity === entity); + $scope.credentialInfo.splice(index, 1); + console.log('Success %o %o', status, data.message); }) + .error(function(data, status, headers, config) { + showToast(data.message, 'danger'); + console.log('Error %o %o', status, data.message); + }); } - } - }) - } + }, + }); + }; function showToast(message, type) { - const verticalPosition = 'bottom' - const timeout = '3000' + const verticalPosition = 'bottom'; + const timeout = '3000'; if (type === 'success') { - ngToast.success({ content: message, verticalPosition: verticalPosition, timeout: timeout, }) + ngToast.success({content: message, verticalPosition: verticalPosition, timeout: timeout}); } else if (type === 'info') { - ngToast.info({ content: message, verticalPosition: verticalPosition, timeout: timeout, }) + ngToast.info({content: message, verticalPosition: verticalPosition, timeout: timeout}); } else { - ngToast.danger({ content: message, verticalPosition: verticalPosition, timeout: timeout, }) + ngToast.danger({content: message, verticalPosition: verticalPosition, timeout: timeout}); } } - let init = function () { - getAvailableInterpreters() - getCredentialInfo() - } + let init = function() { + getAvailableInterpreters(); + getCredentialInfo(); + }; - init() + init(); } diff --git a/zeppelin-web/src/app/credential/credential.test.js b/zeppelin-web/src/app/credential/credential.test.js index d90567b65e4..2b3c17abb63 100644 --- a/zeppelin-web/src/app/credential/credential.test.js +++ b/zeppelin-web/src/app/credential/credential.test.js @@ -1,114 +1,114 @@ -describe('Controller: Credential', function () { - beforeEach(angular.mock.module('zeppelinWebApp')) +describe('Controller: Credential', function() { + beforeEach(angular.mock.module('zeppelinWebApp')); - let baseUrlSrvMock = { getRestApiBase: () => '' } + let baseUrlSrvMock = {getRestApiBase: () => ''}; - let $scope - let $controller // controller generator - let $httpBackend + let $scope; + let $controller; // controller generator + let $httpBackend; beforeEach(inject((_$controller_, _$rootScope_, _$compile_, _$httpBackend_, _ngToast_) => { - $scope = _$rootScope_.$new() - $controller = _$controller_ - $httpBackend = _$httpBackend_ - })) + $scope = _$rootScope_.$new(); + $controller = _$controller_; + $httpBackend = _$httpBackend_; + })); - const credentialResponse = { 'spark.testCredential': { username: 'user1', password: 'password1' }, } + const credentialResponse = {'spark.testCredential': {username: 'user1', password: 'password1'}}; const interpreterResponse = [ - { 'name': 'spark', 'group': 'spark', }, - { 'name': 'md', 'group': 'md', }, - ] // simplified + {'name': 'spark', 'group': 'spark'}, + {'name': 'md', 'group': 'md'}, + ]; // simplified function setupInitialization(credentialRes, interpreterRes) { // requests should follow the exact order $httpBackend .when('GET', '/interpreter/setting') - .respond(200, { body: interpreterRes, }) - $httpBackend.expectGET('/interpreter/setting') + .respond(200, {body: interpreterRes}); + $httpBackend.expectGET('/interpreter/setting'); $httpBackend .when('GET', '/credential') - .respond(200, { body: { userCredentials: credentialRes, } }) - $httpBackend.expectGET('/credential') + .respond(200, {body: {userCredentials: credentialRes}}); + $httpBackend.expectGET('/credential'); // should flush after calling this function } it('should get available interpreters and credentials initially', () => { - const ctrl = createController() - expect(ctrl).toBeDefined() + const ctrl = createController(); + expect(ctrl).toBeDefined(); - setupInitialization(credentialResponse, interpreterResponse) - $httpBackend.flush() + setupInitialization(credentialResponse, interpreterResponse); + $httpBackend.flush(); expect($scope.credentialInfo).toEqual( - [{ entity: 'spark.testCredential', username: 'user1', password: 'password1'}] - ) + [{entity: 'spark.testCredential', username: 'user1', password: 'password1'}] + ); expect($scope.availableInterpreters).toEqual( ['spark.spark', 'md.md'] - ) + ); - $httpBackend.verifyNoOutstandingExpectation() - $httpBackend.verifyNoOutstandingRequest() - }) + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); it('should toggle using toggleAddNewCredentialInfo', () => { - createController() + createController(); - expect($scope.showAddNewCredentialInfo).toBe(false) - $scope.toggleAddNewCredentialInfo() - expect($scope.showAddNewCredentialInfo).toBe(true) - $scope.toggleAddNewCredentialInfo() - expect($scope.showAddNewCredentialInfo).toBe(false) - }) + expect($scope.showAddNewCredentialInfo).toBe(false); + $scope.toggleAddNewCredentialInfo(); + expect($scope.showAddNewCredentialInfo).toBe(true); + $scope.toggleAddNewCredentialInfo(); + expect($scope.showAddNewCredentialInfo).toBe(false); + }); it('should check empty credentials using isInvalidCredential', () => { - createController() + createController(); - $scope.entity = '' - $scope.username = '' - expect($scope.isValidCredential()).toBe(false) + $scope.entity = ''; + $scope.username = ''; + expect($scope.isValidCredential()).toBe(false); - $scope.entity = 'spark1' - $scope.username = '' - expect($scope.isValidCredential()).toBe(false) + $scope.entity = 'spark1'; + $scope.username = ''; + expect($scope.isValidCredential()).toBe(false); - $scope.entity = '' - $scope.username = 'user1' - expect($scope.isValidCredential()).toBe(false) + $scope.entity = ''; + $scope.username = 'user1'; + expect($scope.isValidCredential()).toBe(false); - $scope.entity = 'spark' - $scope.username = 'user1' - expect($scope.isValidCredential()).toBe(true) - }) + $scope.entity = 'spark'; + $scope.username = 'user1'; + expect($scope.isValidCredential()).toBe(true); + }); it('should be able to add credential via addNewCredentialInfo', () => { - const ctrl = createController() - expect(ctrl).toBeDefined() - setupInitialization(credentialResponse, interpreterResponse) + const ctrl = createController(); + expect(ctrl).toBeDefined(); + setupInitialization(credentialResponse, interpreterResponse); // when - const newCredential = { entity: 'spark.sql', username: 'user2', password: 'password2'} + const newCredential = {entity: 'spark.sql', username: 'user2', password: 'password2'}; $httpBackend .when('PUT', '/credential', newCredential) - .respond(200, { }) - $httpBackend.expectPUT('/credential', newCredential) + .respond(200, { }); + $httpBackend.expectPUT('/credential', newCredential); - $scope.entity = newCredential.entity - $scope.username = newCredential.username - $scope.password = newCredential.password - $scope.addNewCredentialInfo() + $scope.entity = newCredential.entity; + $scope.username = newCredential.username; + $scope.password = newCredential.password; + $scope.addNewCredentialInfo(); - $httpBackend.flush() + $httpBackend.flush(); - expect($scope.credentialInfo[1]).toEqual(newCredential) + expect($scope.credentialInfo[1]).toEqual(newCredential); - $httpBackend.verifyNoOutstandingExpectation() - $httpBackend.verifyNoOutstandingRequest() - }) + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); function createController() { - return $controller('CredentialCtrl', { $scope: $scope, baseUrlSrv: baseUrlSrvMock, }) + return $controller('CredentialCtrl', {$scope: $scope, baseUrlSrv: baseUrlSrvMock}); } -}) +}); diff --git a/zeppelin-web/src/app/helium/helium-conf.js b/zeppelin-web/src/app/helium/helium-conf.js index 10ca18adf84..05a58cf4f67 100644 --- a/zeppelin-web/src/app/helium/helium-conf.js +++ b/zeppelin-web/src/app/helium/helium-conf.js @@ -16,84 +16,92 @@ export const HeliumConfFieldType = { NUMBER: 'number', JSON: 'json', STRING: 'string', -} +}; /** * @param persisted including `type`, `description`, `defaultValue` for each conf key * @param spec including `value` for each conf key */ -export function mergePersistedConfWithSpec (persisted, spec) { - const confs = [] +export function mergePersistedConfWithSpec(persisted, spec) { + const confs = []; for (let name in spec) { - const specField = spec[name] - const persistedValue = persisted[name] - - const value = (persistedValue) ? persistedValue : specField.defaultValue - const merged = { - name: name, - type: specField.type, - description: specField.description, - value: value, - defaultValue: specField.defaultValue, + if (spec.hasOwnProperty(name)) { + const specField = spec[name]; + const persistedValue = persisted[name]; + + const value = (persistedValue) ? persistedValue : specField.defaultValue; + const merged = { + name: name, + type: specField.type, + description: specField.description, + value: value, + defaultValue: specField.defaultValue, + }; + + confs.push(merged); } - - confs.push(merged) } - return confs + return confs; } -export function createAllPackageConfigs (defaultPackages, persistedConfs) { - let packageConfs = {} +export function createAllPackageConfigs(defaultPackages, persistedConfs) { + let packageConfs = {}; for (let name in defaultPackages) { - const pkgSearchResult = defaultPackages[name] - - const spec = pkgSearchResult.pkg.config - if (!spec) { continue } - - const artifact = pkgSearchResult.pkg.artifact - if (!artifact) { continue } - - let persistedConf = {} - if (persistedConfs[artifact]) { - persistedConf = persistedConfs[artifact] + if (defaultPackages.hasOwnProperty(name)) { + const pkgSearchResult = defaultPackages[name]; + + const spec = pkgSearchResult.pkg.config; + if (!spec) { + continue; + } + + const artifact = pkgSearchResult.pkg.artifact; + if (!artifact) { + continue; + } + + let persistedConf = {}; + if (persistedConfs[artifact]) { + persistedConf = persistedConfs[artifact]; + } + + const confs = mergePersistedConfWithSpec(persistedConf, spec); + packageConfs[name] = confs; } - - const confs = mergePersistedConfWithSpec(persistedConf, spec) - packageConfs[name] = confs } - return packageConfs + return packageConfs; } -export function parseConfigValue (type, stringified) { - let value = stringified +export function parseConfigValue(type, stringified) { + let value = stringified; try { if (HeliumConfFieldType.NUMBER === type) { - value = parseFloat(stringified) + value = parseFloat(stringified); } else if (HeliumConfFieldType.JSON === type) { - value = JSON.parse(stringified) + value = JSON.parse(stringified); } } catch (error) { // return just the stringified one - console.error(`Failed to parse conf type ${type}, value ${value}`) + console.error(`Failed to parse conf type ${type}, value ${value}`); } - return value + return value; } /** * persist key-value only * since other info (e.g type, desc) can be provided by default config */ -export function createPersistableConfig (currentConfs) { +export function createPersistableConfig(currentConfs) { const filtered = currentConfs.reduce((acc, c) => { - acc[c.name] = parseConfigValue(c.type, c.value) - return acc - }, {}) + acc[c.name] = parseConfigValue(c.type, c.value); + return acc; + }, {}); - return filtered + return filtered; } diff --git a/zeppelin-web/src/app/helium/helium-package.js b/zeppelin-web/src/app/helium/helium-package.js index 88d191a7a8e..2fe9bf58964 100644 --- a/zeppelin-web/src/app/helium/helium-package.js +++ b/zeppelin-web/src/app/helium/helium-package.js @@ -12,20 +12,22 @@ * limitations under the License. */ -export function createDefaultPackage (pkgSearchResult, sce) { +export function createDefaultPackage(pkgSearchResult, sce) { for (let pkgIdx in pkgSearchResult) { - const pkg = pkgSearchResult[pkgIdx] - pkg.pkg.icon = sce.trustAsHtml(pkg.pkg.icon) - if (pkg.enabled) { - pkgSearchResult.splice(pkgIdx, 1) - return pkg + if (pkgSearchResult.hasOwnProperty(pkgIdx)) { + const pkg = pkgSearchResult[pkgIdx]; + pkg.pkg.icon = sce.trustAsHtml(pkg.pkg.icon); + if (pkg.enabled) { + pkgSearchResult.splice(pkgIdx, 1); + return pkg; + } } } // show first available version if package is not enabled - const result = pkgSearchResult[0] - pkgSearchResult.splice(0, 1) - return result + const result = pkgSearchResult[0]; + pkgSearchResult.splice(0, 1); + return result; } /** @@ -35,13 +37,15 @@ export function createDefaultPackage (pkgSearchResult, sce) { * @param sce angular `$sce` object * @returns {Object} including {name, pkgInfo} */ -export function createDefaultPackages (pkgSearchResults, sce) { - const defaultPackages = {} +export function createDefaultPackages(pkgSearchResults, sce) { + const defaultPackages = {}; // show enabled version if any version of package is enabled for (let name in pkgSearchResults) { - const pkgSearchResult = pkgSearchResults[name] - defaultPackages[name] = createDefaultPackage(pkgSearchResult, sce) + if (pkgSearchResults.hasOwnProperty(name)) { + const pkgSearchResult = pkgSearchResults[name]; + defaultPackages[name] = createDefaultPackage(pkgSearchResult, sce); + } } - return defaultPackages + return defaultPackages; } diff --git a/zeppelin-web/src/app/helium/helium-type.js b/zeppelin-web/src/app/helium/helium-type.js index 27b34fa6960..0b37a418837 100644 --- a/zeppelin-web/src/app/helium/helium-type.js +++ b/zeppelin-web/src/app/helium/helium-type.js @@ -17,4 +17,4 @@ export const HeliumType = { SPELL: 'SPELL', INTERPRETER: 'INTERPRETER', APPLICATION: 'APPLICATION', -} +}; diff --git a/zeppelin-web/src/app/helium/helium.controller.js b/zeppelin-web/src/app/helium/helium.controller.js index a397aceea34..4728e0896ff 100644 --- a/zeppelin-web/src/app/helium/helium.controller.js +++ b/zeppelin-web/src/app/helium/helium.controller.js @@ -12,92 +12,94 @@ * limitations under the License. */ -import { HeliumType, } from './helium-type' +import {HeliumType} from './helium-type'; -export default function HeliumCtrl ($scope, $rootScope, $sce, +export default function HeliumCtrl($scope, $rootScope, $sce, baseUrlSrv, ngToast, heliumService) { - 'ngInject' - - $scope.pkgSearchResults = {} - $scope.defaultPackages = {} - $scope.showVersions = {} - $scope.bundleOrder = [] - $scope.bundleOrderChanged = false - $scope.vizTypePkg = {} - $scope.spellTypePkg = {} - $scope.intpTypePkg = {} - $scope.appTypePkg = {} - $scope.numberOfEachPackageByType = {} - $scope.allPackageTypes = [HeliumType][0] - $scope.pkgListByType = 'VISUALIZATION' - $scope.defaultPackageConfigs = {} // { pkgName, [{name, type, desc, value, defaultValue}] } - $scope.intpDefaultIcon = $sce.trustAsHtml('') - - function init () { + 'ngInject'; + + $scope.pkgSearchResults = {}; + $scope.defaultPackages = {}; + $scope.showVersions = {}; + $scope.bundleOrder = []; + $scope.bundleOrderChanged = false; + $scope.vizTypePkg = {}; + $scope.spellTypePkg = {}; + $scope.intpTypePkg = {}; + $scope.appTypePkg = {}; + $scope.numberOfEachPackageByType = {}; + $scope.allPackageTypes = [HeliumType][0]; + $scope.pkgListByType = 'VISUALIZATION'; + $scope.defaultPackageConfigs = {}; // { pkgName, [{name, type, desc, value, defaultValue}] } + $scope.intpDefaultIcon = $sce.trustAsHtml(''); + + function init() { // get all package info and set config heliumService.getAllPackageInfoAndDefaultPackages() - .then(({ pkgSearchResults, defaultPackages }) => { + .then(({pkgSearchResults, defaultPackages}) => { // pagination - $scope.itemsPerPage = 10 - $scope.currentPage = 1 - $scope.maxSize = 5 + $scope.itemsPerPage = 10; + $scope.currentPage = 1; + $scope.maxSize = 5; - $scope.pkgSearchResults = pkgSearchResults - $scope.defaultPackages = defaultPackages - classifyPkgType($scope.defaultPackages) + $scope.pkgSearchResults = pkgSearchResults; + $scope.defaultPackages = defaultPackages; + classifyPkgType($scope.defaultPackages); - return heliumService.getAllPackageConfigs() + return heliumService.getAllPackageConfigs(); }) - .then(defaultPackageConfigs => { - $scope.defaultPackageConfigs = defaultPackageConfigs - return heliumService.getVisualizationPackageOrder() - }) - .then(visPackageOrder => { - setVisPackageOrder(visPackageOrder) + .then((defaultPackageConfigs) => { + $scope.defaultPackageConfigs = defaultPackageConfigs; + return heliumService.getVisualizationPackageOrder(); }) + .then((visPackageOrder) => { + setVisPackageOrder(visPackageOrder); + }); } const setVisPackageOrder = function(visPackageOrder) { - $scope.bundleOrder = visPackageOrder - $scope.bundleOrderChanged = false - } + $scope.bundleOrder = visPackageOrder; + $scope.bundleOrderChanged = false; + }; - let orderPackageByPubDate = function (a, b) { + let orderPackageByPubDate = function(a, b) { if (!a.pkg.published) { // Because local registry pkgs don't have 'published' field, put current time instead to show them first - a.pkg.published = new Date().getTime() + a.pkg.published = new Date().getTime(); } - return new Date(a.pkg.published).getTime() - new Date(b.pkg.published).getTime() - } + return new Date(a.pkg.published).getTime() - new Date(b.pkg.published).getTime(); + }; - const classifyPkgType = function (packageInfo) { - let allTypesOfPkg = {} - let vizTypePkg = [] - let spellTypePkg = [] - let intpTypePkg = [] - let appTypePkg = [] + const classifyPkgType = function(packageInfo) { + let allTypesOfPkg = {}; + let vizTypePkg = []; + let spellTypePkg = []; + let intpTypePkg = []; + let appTypePkg = []; - let packageInfoArr = Object.keys(packageInfo).map(key => packageInfo[key]) - packageInfoArr = packageInfoArr.sort(orderPackageByPubDate).reverse() + let packageInfoArr = Object.keys(packageInfo).map((key) => packageInfo[key]); + packageInfoArr = packageInfoArr.sort(orderPackageByPubDate).reverse(); for (let name in packageInfoArr) { - let pkgs = packageInfoArr[name] - let pkgType = pkgs.pkg.type - - switch (pkgType) { - case HeliumType.VISUALIZATION: - vizTypePkg.push(pkgs) - break - case HeliumType.SPELL: - spellTypePkg.push(pkgs) - break - case HeliumType.INTERPRETER: - intpTypePkg.push(pkgs) - break - case HeliumType.APPLICATION: - appTypePkg.push(pkgs) - break + if (packageInfoArr.hasOwnProperty(name)) { + let pkgs = packageInfoArr[name]; + let pkgType = pkgs.pkg.type; + + switch (pkgType) { + case HeliumType.VISUALIZATION: + vizTypePkg.push(pkgs); + break; + case HeliumType.SPELL: + spellTypePkg.push(pkgs); + break; + case HeliumType.INTERPRETER: + intpTypePkg.push(pkgs); + break; + case HeliumType.APPLICATION: + appTypePkg.push(pkgs); + break; + } } } @@ -105,95 +107,99 @@ export default function HeliumCtrl ($scope, $rootScope, $sce, vizTypePkg, spellTypePkg, intpTypePkg, - appTypePkg - ] + appTypePkg, + ]; for (let idx in _.keys(HeliumType)) { - allTypesOfPkg[_.keys(HeliumType)[idx]] = pkgsArr[idx] + if (_.keys(HeliumType).hasOwnProperty(idx)) { + allTypesOfPkg[_.keys(HeliumType)[idx]] = pkgsArr[idx]; + } } - $scope.allTypesOfPkg = allTypesOfPkg - } + $scope.allTypesOfPkg = allTypesOfPkg; + }; $scope.bundleOrderListeners = { - accept: function (sourceItemHandleScope, destSortableScope) { return true }, - itemMoved: function (event) {}, - orderChanged: function (event) { - $scope.bundleOrderChanged = true - } - } - - $scope.saveBundleOrder = function () { + accept: function(sourceItemHandleScope, destSortableScope) { + return true; + }, + itemMoved: function(event) {}, + orderChanged: function(event) { + $scope.bundleOrderChanged = true; + }, + }; + + $scope.saveBundleOrder = function() { const confirm = BootstrapDialog.confirm({ closable: false, closeByBackdrop: false, closeByKeyboard: false, title: '', message: 'Save changes?', - callback: function (result) { + callback: function(result) { if (result) { - confirm.$modalFooter.find('button').addClass('disabled') + confirm.$modalFooter.find('button').addClass('disabled'); confirm.$modalFooter.find('button:contains("OK")') - .html(' Enabling') + .html(' Enabling'); heliumService.setVisualizationPackageOrder($scope.bundleOrder) - .success(function (data, status) { - setVisPackageOrder($scope.bundleOrder) - confirm.close() + .success(function(data, status) { + setVisPackageOrder($scope.bundleOrder); + confirm.close(); }) - .error(function (data, status) { - confirm.close() - console.log('Failed to save order') + .error(function(data, status) { + confirm.close(); + console.log('Failed to save order'); BootstrapDialog.show({ title: 'Error on saving order ', - message: data.message - }) - }) - return false + message: data.message, + }); + }); + return false; } - } - }) - } + }, + }); + }; - let getLicense = function (name, artifact) { - let filteredPkgSearchResults = _.filter($scope.defaultPackages[name], function (p) { - return p.artifact === artifact - }) + let getLicense = function(name, artifact) { + let filteredPkgSearchResults = _.filter($scope.defaultPackages[name], function(p) { + return p.artifact === artifact; + }); - let license + let license; if (filteredPkgSearchResults.length === 0) { - filteredPkgSearchResults = _.filter($scope.pkgSearchResults[name], function (p) { - return p.pkg.artifact === artifact - }) + filteredPkgSearchResults = _.filter($scope.pkgSearchResults[name], function(p) { + return p.pkg.artifact === artifact; + }); if (filteredPkgSearchResults.length > 0) { - license = filteredPkgSearchResults[0].pkg.license + license = filteredPkgSearchResults[0].pkg.license; } } else { - license = filteredPkgSearchResults[0].license + license = filteredPkgSearchResults[0].license; } if (!license) { - license = 'Unknown' + license = 'Unknown'; } - return license - } + return license; + }; - const getHeliumTypeText = function (type) { + const getHeliumTypeText = function(type) { if (type === HeliumType.VISUALIZATION) { - return `${type}` // eslint-disable-line max-len + return `${type}`; // eslint-disable-line max-len } else if (type === HeliumType.SPELL) { - return `${type}` // eslint-disable-line max-len + return `${type}`; // eslint-disable-line max-len } else { - return type + return type; } - } + }; - $scope.enable = function (name, artifact, type, groupId, description) { - let license = getLicense(name, artifact) - let mavenArtifactInfoToHTML = groupId + ':' + artifact.split('@')[0] + ':' + artifact.split('@')[1] - let zeppelinVersion = $rootScope.zeppelinVersion - let url = 'https://zeppelin.apache.org/docs/' + zeppelinVersion + '/manual/interpreterinstallation.html' + $scope.enable = function(name, artifact, type, groupId, description) { + let license = getLicense(name, artifact); + let mavenArtifactInfoToHTML = groupId + ':' + artifact.split('@')[0] + ':' + artifact.split('@')[1]; + let zeppelinVersion = $rootScope.zeppelinVersion; + let url = 'https://zeppelin.apache.org/docs/' + zeppelinVersion + '/manual/interpreterinstallation.html'; - let confirm = '' + let confirm = ''; if (type === HeliumType.INTERPRETER) { confirm = BootstrapDialog.show({ title: '', @@ -206,8 +212,8 @@ export default function HeliumCtrl ($scope, $rootScope, $sce, mavenArtifactInfoToHTML + ' ' + '

    After restart Zeppelin, create interpreter setting and bind it with your note. ' + 'For more detailed information, see Interpreter Installation.

    ' - }) + url + '>Interpreter Installation.

    ', + }); } else { confirm = BootstrapDialog.confirm({ closable: false, @@ -226,138 +232,138 @@ export default function HeliumCtrl ($scope, $rootScope, $sce, '
    ' + '
    License
    ' + `
    ${license}
    `, - callback: function (result) { + callback: function(result) { if (result) { - confirm.$modalFooter.find('button').addClass('disabled') + confirm.$modalFooter.find('button').addClass('disabled'); confirm.$modalFooter.find('button:contains("OK")') - .html(' Enabling') - heliumService.enable(name, artifact, type).success(function (data, status) { - init() - confirm.close() - }).error(function (data, status) { - confirm.close() - console.log('Failed to enable package %o %o. %o', name, artifact, data) + .html(' Enabling'); + heliumService.enable(name, artifact, type).success(function(data, status) { + init(); + confirm.close(); + }).error(function(data, status) { + confirm.close(); + console.log('Failed to enable package %o %o. %o', name, artifact, data); BootstrapDialog.show({ title: 'Error on enabling ' + name, - message: data.message - }) - }) - return false + message: data.message, + }); + }); + return false; } - } - }) + }, + }); } - } + }; - $scope.disable = function (name, artifact) { + $scope.disable = function(name, artifact) { const confirm = BootstrapDialog.confirm({ closable: false, closeByBackdrop: false, closeByKeyboard: false, title: '
    Do you want to disable Helium Package?
    ', message: artifact, - callback: function (result) { + callback: function(result) { if (result) { - confirm.$modalFooter.find('button').addClass('disabled') + confirm.$modalFooter.find('button').addClass('disabled'); confirm.$modalFooter.find('button:contains("OK")') - .html(' Disabling') + .html(' Disabling'); heliumService.disable(name) - .success(function (data, status) { - init() - confirm.close() + .success(function(data, status) { + init(); + confirm.close(); }) - .error(function (data, status) { - confirm.close() - console.log('Failed to disable package %o. %o', name, data) + .error(function(data, status) { + confirm.close(); + console.log('Failed to disable package %o. %o', name, data); BootstrapDialog.show({ title: 'Error on disabling ' + name, - message: data.message - }) - }) - return false + message: data.message, + }); + }); + return false; } - } - }) - } + }, + }); + }; - $scope.toggleVersions = function (pkgName) { + $scope.toggleVersions = function(pkgName) { if ($scope.showVersions[pkgName]) { - $scope.showVersions[pkgName] = false + $scope.showVersions[pkgName] = false; } else { - $scope.showVersions[pkgName] = true + $scope.showVersions[pkgName] = true; } - } + }; - $scope.isLocalPackage = function (pkgSearchResult) { - const pkg = pkgSearchResult.pkg - return pkg.artifact && !pkg.artifact.includes('@') - } + $scope.isLocalPackage = function(pkgSearchResult) { + const pkg = pkgSearchResult.pkg; + return pkg.artifact && !pkg.artifact.includes('@'); + }; - $scope.hasNpmLink = function (pkgSearchResult) { - const pkg = pkgSearchResult.pkg + $scope.hasNpmLink = function(pkgSearchResult) { + const pkg = pkgSearchResult.pkg; return (pkg.type === HeliumType.SPELL || pkg.type === HeliumType.VISUALIZATION) && - !$scope.isLocalPackage(pkgSearchResult) - } + !$scope.isLocalPackage(pkgSearchResult); + }; - $scope.hasMavenLink = function (pkgSearchResult) { - const pkg = pkgSearchResult.pkg + $scope.hasMavenLink = function(pkgSearchResult) { + const pkg = pkgSearchResult.pkg; return (pkg.type === HeliumType.APPLICATION || pkg.type === HeliumType.INTERPRETER) && - !$scope.isLocalPackage(pkgSearchResult) - } - - $scope.getPackageSize = function (pkgSearchResult, targetPkgType) { - let result = [] - _.map(pkgSearchResult, function (pkg) { - result.push(_.find(pkg, {type: targetPkgType})) - }) - return _.compact(result).length - } - - $scope.configExists = function (pkgSearchResult) { + !$scope.isLocalPackage(pkgSearchResult); + }; + + $scope.getPackageSize = function(pkgSearchResult, targetPkgType) { + let result = []; + _.map(pkgSearchResult, function(pkg) { + result.push(_.find(pkg, {type: targetPkgType})); + }); + return _.compact(result).length; + }; + + $scope.configExists = function(pkgSearchResult) { // helium package config is persisted per version - return pkgSearchResult.pkg.config && pkgSearchResult.pkg.artifact - } + return pkgSearchResult.pkg.config && pkgSearchResult.pkg.artifact; + }; - $scope.configOpened = function (pkgSearchResult) { - return pkgSearchResult.configOpened && !pkgSearchResult.configFetching - } + $scope.configOpened = function(pkgSearchResult) { + return pkgSearchResult.configOpened && !pkgSearchResult.configFetching; + }; - $scope.getConfigButtonClass = function (pkgSearchResult) { + $scope.getConfigButtonClass = function(pkgSearchResult) { return (pkgSearchResult.configOpened && pkgSearchResult.configFetching) - ? 'disabled' : '' - } + ? 'disabled' : ''; + }; - $scope.toggleConfigButton = function (pkgSearchResult) { + $scope.toggleConfigButton = function(pkgSearchResult) { if (pkgSearchResult.configOpened) { - pkgSearchResult.configOpened = false - return + pkgSearchResult.configOpened = false; + return; } - const pkg = pkgSearchResult.pkg - const pkgName = pkg.name - pkgSearchResult.configFetching = true - pkgSearchResult.configOpened = true + const pkg = pkgSearchResult.pkg; + const pkgName = pkg.name; + pkgSearchResult.configFetching = true; + pkgSearchResult.configOpened = true; heliumService.getSinglePackageConfigs(pkg) - .then(confs => { - $scope.defaultPackageConfigs[pkgName] = confs - pkgSearchResult.configFetching = false - }) - } + .then((confs) => { + $scope.defaultPackageConfigs[pkgName] = confs; + pkgSearchResult.configFetching = false; + }); + }; - $scope.saveConfig = function (pkgSearchResult) { - const pkgName = pkgSearchResult.pkg.name - const currentConf = $scope.defaultPackageConfigs[pkgName] + $scope.saveConfig = function(pkgSearchResult) { + const pkgName = pkgSearchResult.pkg.name; + const currentConf = $scope.defaultPackageConfigs[pkgName]; heliumService.saveConfig(pkgSearchResult.pkg, currentConf, () => { // close after config is saved - pkgSearchResult.configOpened = false - }) - } + pkgSearchResult.configOpened = false; + }); + }; - $scope.getDescriptionText = function (pkgSearchResult) { - return $sce.trustAsHtml(pkgSearchResult.pkg.description) - } + $scope.getDescriptionText = function(pkgSearchResult) { + return $sce.trustAsHtml(pkgSearchResult.pkg.description); + }; - init() + init(); } diff --git a/zeppelin-web/src/app/helium/helium.service.js b/zeppelin-web/src/app/helium/helium.service.js index d2054b320f9..7501fae827f 100644 --- a/zeppelin-web/src/app/helium/helium.service.js +++ b/zeppelin-web/src/app/helium/helium.service.js @@ -12,290 +12,294 @@ * limitations under the License. */ -import { HeliumType, } from './helium-type' +import {HeliumType} from './helium-type'; import { createAllPackageConfigs, createPersistableConfig, mergePersistedConfWithSpec, -} from './helium-conf' +} from './helium-conf'; import { createDefaultPackages, -} from './helium-package' +} from './helium-package'; -angular.module('zeppelinWebApp').service('heliumService', HeliumService) +angular.module('zeppelinWebApp').service('heliumService', HeliumService); export default function HeliumService($http, $sce, baseUrlSrv) { - 'ngInject' + 'ngInject'; - let visualizationBundles = [] - let visualizationPackageOrder = [] + let visualizationBundles = []; + let visualizationPackageOrder = []; // name `heliumBundles` should be same as `HeliumBundleFactory.HELIUM_BUNDLES_VAR` - let heliumBundles = [] + let heliumBundles = []; // map for `{ magic: interpreter }` - let spellPerMagic = {} + let spellPerMagic = {}; // map for `{ magic: package-name }` - let pkgNamePerMagic = {} + let pkgNamePerMagic = {}; /** * @param magic {string} e.g `%flowchart` * @returns {SpellBase} undefined if magic is not registered */ - this.getSpellByMagic = function (magic) { - return spellPerMagic[magic] - } + this.getSpellByMagic = function(magic) { + return spellPerMagic[magic]; + }; - this.executeSpell = function (magic, textWithoutMagic) { + this.executeSpell = function(magic, textWithoutMagic) { const promisedConf = this.getSinglePackageConfigUsingMagic(magic) - .then(confs => createPersistableConfig(confs)) + .then((confs) => createPersistableConfig(confs)); - return promisedConf.then(conf => { - const spell = this.getSpellByMagic(magic) - const spellResult = spell.interpret(textWithoutMagic, conf) + return promisedConf.then((conf) => { + const spell = this.getSpellByMagic(magic); + const spellResult = spell.interpret(textWithoutMagic, conf); const parsed = spellResult.getAllParsedDataWithTypes( - spellPerMagic, magic, textWithoutMagic) + spellPerMagic, magic, textWithoutMagic); - return parsed - }) - } + return parsed; + }); + }; - this.executeSpellAsDisplaySystem = function (magic, textWithoutMagic) { + this.executeSpellAsDisplaySystem = function(magic, textWithoutMagic) { const promisedConf = this.getSinglePackageConfigUsingMagic(magic) - .then(confs => createPersistableConfig(confs)) + .then((confs) => createPersistableConfig(confs)); - return promisedConf.then(conf => { - const spell = this.getSpellByMagic(magic) - const spellResult = spell.interpret(textWithoutMagic.trim(), conf) - const parsed = spellResult.getAllParsedDataWithTypes(spellPerMagic) + return promisedConf.then((conf) => { + const spell = this.getSpellByMagic(magic); + const spellResult = spell.interpret(textWithoutMagic.trim(), conf); + const parsed = spellResult.getAllParsedDataWithTypes(spellPerMagic); - return parsed - }) - } + return parsed; + }); + }; - this.getVisualizationCachedPackages = function () { - return visualizationBundles - } + this.getVisualizationCachedPackages = function() { + return visualizationBundles; + }; - this.getVisualizationCachedPackageOrder = function () { - return visualizationPackageOrder - } + this.getVisualizationCachedPackageOrder = function() { + return visualizationPackageOrder; + }; /** * @returns {Promise} which returns bundleOrder and cache it in `visualizationPackageOrder` */ - this.getVisualizationPackageOrder = function () { + this.getVisualizationPackageOrder = function() { return $http.get(baseUrlSrv.getRestApiBase() + '/helium/order/visualization') - .then(function (response, status) { - const order = response.data.body - visualizationPackageOrder = order - return order - }) - .catch(function (error) { - console.error('Can not get bundle order', error) + .then(function(response, status) { + const order = response.data.body; + visualizationPackageOrder = order; + return order; }) - } + .catch(function(error) { + console.error('Can not get bundle order', error); + }); + }; - this.setVisualizationPackageOrder = function (list) { - return $http.post(baseUrlSrv.getRestApiBase() + '/helium/order/visualization', list) - } + this.setVisualizationPackageOrder = function(list) { + return $http.post(baseUrlSrv.getRestApiBase() + '/helium/order/visualization', list); + }; - this.enable = function (name, artifact) { - return $http.post(baseUrlSrv.getRestApiBase() + '/helium/enable/' + name, artifact) - } + this.enable = function(name, artifact) { + return $http.post(baseUrlSrv.getRestApiBase() + '/helium/enable/' + name, artifact); + }; - this.disable = function (name) { - return $http.post(baseUrlSrv.getRestApiBase() + '/helium/disable/' + name) - } + this.disable = function(name) { + return $http.post(baseUrlSrv.getRestApiBase() + '/helium/disable/' + name); + }; - this.saveConfig = function (pkg, defaultPackageConfig, closeConfigPanelCallback) { + this.saveConfig = function(pkg, defaultPackageConfig, closeConfigPanelCallback) { // in case of local package, it will include `/` - const pkgArtifact = encodeURIComponent(pkg.artifact) - const pkgName = pkg.name - const filtered = createPersistableConfig(defaultPackageConfig) + const pkgArtifact = encodeURIComponent(pkg.artifact); + const pkgName = pkg.name; + const filtered = createPersistableConfig(defaultPackageConfig); if (!pkgName || !pkgArtifact || !filtered) { console.error( - `Can't save config for helium package '${pkgArtifact}'`, filtered) - return + `Can't save config for helium package '${pkgArtifact}'`, filtered); + return; } - const url = `${baseUrlSrv.getRestApiBase()}/helium/config/${pkgName}/${pkgArtifact}` + const url = `${baseUrlSrv.getRestApiBase()}/helium/config/${pkgName}/${pkgArtifact}`; return $http.post(url, filtered) .then(() => { - if (closeConfigPanelCallback) { closeConfigPanelCallback() } + if (closeConfigPanelCallback) { + closeConfigPanelCallback(); + } }).catch((error) => { - console.error(`Failed to save config for ${pkgArtifact}`, error) - }) - } + console.error(`Failed to save config for ${pkgArtifact}`, error); + }); + }; /** * @returns {Promise} which including {name, Array} */ - this.getAllPackageInfo = function () { + this.getAllPackageInfo = function() { return $http.get(`${baseUrlSrv.getRestApiBase()}/helium/package`) - .then(function (response, status) { - return response.data.body - }) - .catch(function (error) { - console.error('Failed to get all package infos', error) + .then(function(response, status) { + return response.data.body; }) - } + .catch(function(error) { + console.error('Failed to get all package infos', error); + }); + }; - this.getAllEnabledPackages = function () { + this.getAllEnabledPackages = function() { return $http.get(`${baseUrlSrv.getRestApiBase()}/helium/enabledPackage`) - .then(function (response, status) { - return response.data.body + .then(function(response, status) { + return response.data.body; }) - .catch(function (error) { - console.error('Failed to get all enabled package infos', error) - }) - } + .catch(function(error) { + console.error('Failed to get all enabled package infos', error); + }); + }; - this.getSingleBundle = function (pkgName) { - let url = `${baseUrlSrv.getRestApiBase()}/helium/bundle/load/${pkgName}` + this.getSingleBundle = function(pkgName) { + let url = `${baseUrlSrv.getRestApiBase()}/helium/bundle/load/${pkgName}`; if (process.env.HELIUM_BUNDLE_DEV) { - url = url + '?refresh=true' + url = url + '?refresh=true'; } return $http.get(url) - .then(function (response, status) { - const bundle = response.data + .then(function(response, status) { + const bundle = response.data; if (bundle.substring(0, 'ERROR:'.length) === 'ERROR:') { - console.error(`Failed to get bundle: ${pkgName}`, bundle) - return '' // empty bundle will be filtered later + console.error(`Failed to get bundle: ${pkgName}`, bundle); + return ''; // empty bundle will be filtered later } - return bundle - }) - .catch(function (error) { - console.error(`Failed to get single bundle: ${pkgName}`, error) + return bundle; }) - } + .catch(function(error) { + console.error(`Failed to get single bundle: ${pkgName}`, error); + }); + }; - this.getDefaultPackages = function () { + this.getDefaultPackages = function() { return this.getAllPackageInfo() - .then(pkgSearchResults => { - return createDefaultPackages(pkgSearchResults, $sce) - }) - } + .then((pkgSearchResults) => { + return createDefaultPackages(pkgSearchResults, $sce); + }); + }; - this.getAllPackageInfoAndDefaultPackages = function () { + this.getAllPackageInfoAndDefaultPackages = function() { return this.getAllPackageInfo() - .then(pkgSearchResults => { + .then((pkgSearchResults) => { return { pkgSearchResults: pkgSearchResults, defaultPackages: createDefaultPackages(pkgSearchResults, $sce), - } - }) - } + }; + }); + }; /** * get all package configs. * @return { Promise<{name, Array}> } */ - this.getAllPackageConfigs = function () { - const promisedDefaultPackages = this.getDefaultPackages() + this.getAllPackageConfigs = function() { + const promisedDefaultPackages = this.getDefaultPackages(); const promisedPersistedConfs = $http.get(`${baseUrlSrv.getRestApiBase()}/helium/config`) - .then(function (response, status) { - return response.data.body - }) + .then(function(response, status) { + return response.data.body; + }); return Promise.all([promisedDefaultPackages, promisedPersistedConfs]) - .then(values => { - const defaultPackages = values[0] - const persistedConfs = values[1] + .then((values) => { + const defaultPackages = values[0]; + const persistedConfs = values[1]; - return createAllPackageConfigs(defaultPackages, persistedConfs) - }) - .catch(function (error) { - console.error('Failed to get all package configs', error) + return createAllPackageConfigs(defaultPackages, persistedConfs); }) - } + .catch(function(error) { + console.error('Failed to get all package configs', error); + }); + }; /** * get the package config which is persisted in server. * @return { Promise> } */ - this.getSinglePackageConfigs = function (pkg) { - const pkgName = pkg.name + this.getSinglePackageConfigs = function(pkg) { + const pkgName = pkg.name; // in case of local package, it will include `/` - const pkgArtifact = encodeURIComponent(pkg.artifact) + const pkgArtifact = encodeURIComponent(pkg.artifact); if (!pkgName || !pkgArtifact) { - console.error('Failed to fetch config for\n', pkg) - return Promise.resolve([]) + console.error('Failed to fetch config for\n', pkg); + return Promise.resolve([]); } - const confUrl = `${baseUrlSrv.getRestApiBase()}/helium/config/${pkgName}/${pkgArtifact}` + const confUrl = `${baseUrlSrv.getRestApiBase()}/helium/config/${pkgName}/${pkgArtifact}`; const promisedConf = $http.get(confUrl) - .then(function (response, status) { - return response.data.body - }) + .then(function(response, status) { + return response.data.body; + }); return promisedConf.then(({confSpec, confPersisted}) => { - const merged = mergePersistedConfWithSpec(confPersisted, confSpec) - return merged - }) - } + const merged = mergePersistedConfWithSpec(confPersisted, confSpec); + return merged; + }); + }; - this.getSinglePackageConfigUsingMagic = function (magic) { - const pkgName = pkgNamePerMagic[magic] + this.getSinglePackageConfigUsingMagic = function(magic) { + const pkgName = pkgNamePerMagic[magic]; - const confUrl = `${baseUrlSrv.getRestApiBase()}/helium/spell/config/${pkgName}` + const confUrl = `${baseUrlSrv.getRestApiBase()}/helium/spell/config/${pkgName}`; const promisedConf = $http.get(confUrl) - .then(function (response, status) { - return response.data.body - }) + .then(function(response, status) { + return response.data.body; + }); return promisedConf.then(({confSpec, confPersisted}) => { - const merged = mergePersistedConfWithSpec(confPersisted, confSpec) - return merged - }) - } + const merged = mergePersistedConfWithSpec(confPersisted, confSpec); + return merged; + }); + }; const p = this.getAllEnabledPackages() - .then(enabledPackageSearchResults => { - const promises = enabledPackageSearchResults.map(packageSearchResult => { - const pkgName = packageSearchResult.pkg.name - return this.getSingleBundle(pkgName) - }) + .then((enabledPackageSearchResults) => { + const promises = enabledPackageSearchResults.map((packageSearchResult) => { + const pkgName = packageSearchResult.pkg.name; + return this.getSingleBundle(pkgName); + }); - return Promise.all(promises) + return Promise.all(promises); }) - .then(bundles => { + .then((bundles) => { return bundles.reduce((acc, b) => { // filter out empty bundle - if (b === '') { return acc } - acc.push(b) - return acc - }, []) - }) + if (b === '') { + return acc; + } + acc.push(b); + return acc; + }, []); + }); // load should be promise - this.load = p.then(availableBundles => { + this.load = p.then((availableBundles) => { // evaluate bundles - availableBundles.map(b => { + availableBundles.map((b) => { // eslint-disable-next-line no-eval - eval(b) - }) + eval(b); + }); // extract bundles by type - heliumBundles.map(b => { + heliumBundles.map((b) => { if (b.type === HeliumType.SPELL) { - const spell = new b.class() // eslint-disable-line new-cap - const pkgName = b.id - spellPerMagic[spell.getMagic()] = spell - pkgNamePerMagic[spell.getMagic()] = pkgName + const spell = new b.class(); // eslint-disable-line new-cap + const pkgName = b.id; + spellPerMagic[spell.getMagic()] = spell; + pkgNamePerMagic[spell.getMagic()] = pkgName; } else if (b.type === HeliumType.VISUALIZATION) { - visualizationBundles.push(b) + visualizationBundles.push(b); } - }) - }) + }); + }); this.init = function() { - this.getVisualizationPackageOrder() - } + this.getVisualizationPackageOrder(); + }; // init - this.init() + this.init(); } diff --git a/zeppelin-web/src/app/helium/index.js b/zeppelin-web/src/app/helium/index.js index 2b27d6036ce..754c9499f4f 100644 --- a/zeppelin-web/src/app/helium/index.js +++ b/zeppelin-web/src/app/helium/index.js @@ -12,7 +12,7 @@ * limitations under the License. */ -import HeliumController from './helium.controller' +import HeliumController from './helium.controller'; angular.module('zeppelinWebApp') - .controller('HeliumCtrl', HeliumController) + .controller('HeliumCtrl', HeliumController); diff --git a/zeppelin-web/src/app/home/home.controller.js b/zeppelin-web/src/app/home/home.controller.js index d2823dd6f6f..7ae5e44d70c 100644 --- a/zeppelin-web/src/app/home/home.controller.js +++ b/zeppelin-web/src/app/home/home.controller.js @@ -12,145 +12,145 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').controller('HomeCtrl', HomeCtrl) +angular.module('zeppelinWebApp').controller('HomeCtrl', HomeCtrl); -function HomeCtrl ($scope, noteListFactory, websocketMsgSrv, $rootScope, arrayOrderingSrv, +function HomeCtrl($scope, noteListFactory, websocketMsgSrv, $rootScope, arrayOrderingSrv, ngToast, noteActionService, TRASH_FOLDER_ID) { - 'ngInject' - - ngToast.dismiss() - let vm = this - vm.notes = noteListFactory - vm.websocketMsgSrv = websocketMsgSrv - vm.arrayOrderingSrv = arrayOrderingSrv - vm.noteActionService = noteActionService - vm.numberOfNotesDisplayed = window.innerHeight / 20 - - vm.notebookHome = false - vm.noteCustomHome = true + 'ngInject'; + + ngToast.dismiss(); + let vm = this; + vm.notes = noteListFactory; + vm.websocketMsgSrv = websocketMsgSrv; + vm.arrayOrderingSrv = arrayOrderingSrv; + vm.noteActionService = noteActionService; + vm.numberOfNotesDisplayed = window.innerHeight / 20; + + vm.notebookHome = false; + vm.noteCustomHome = true; if ($rootScope.ticket !== undefined) { - vm.staticHome = false + vm.staticHome = false; } else { - vm.staticHome = true + vm.staticHome = true; } - $scope.isReloading = false - $scope.TRASH_FOLDER_ID = TRASH_FOLDER_ID - $scope.query = {q: ''} + $scope.isReloading = false; + $scope.TRASH_FOLDER_ID = TRASH_FOLDER_ID; + $scope.query = {q: ''}; - $scope.initHome = function () { - websocketMsgSrv.getHomeNote() - vm.noteCustomHome = false - } + $scope.initHome = function() { + websocketMsgSrv.getHomeNote(); + vm.noteCustomHome = false; + }; - $scope.reloadNoteList = function () { - websocketMsgSrv.reloadAllNotesFromRepo() - $scope.isReloadingNotes = true - } + $scope.reloadNoteList = function() { + websocketMsgSrv.reloadAllNotesFromRepo(); + $scope.isReloadingNotes = true; + }; - $scope.toggleFolderNode = function (node) { - node.hidden = !node.hidden - } + $scope.toggleFolderNode = function(node) { + node.hidden = !node.hidden; + }; - angular.element('#loginModal').on('hidden.bs.modal', function (e) { - $rootScope.$broadcast('initLoginValues') - }) + angular.element('#loginModal').on('hidden.bs.modal', function(e) { + $rootScope.$broadcast('initLoginValues'); + }); /* ** $scope.$on functions below */ - $scope.$on('setNoteMenu', function (event, notes) { - $scope.isReloadingNotes = false - }) + $scope.$on('setNoteMenu', function(event, notes) { + $scope.isReloadingNotes = false; + }); - $scope.$on('setNoteContent', function (event, note) { + $scope.$on('setNoteContent', function(event, note) { if (vm.noteCustomHome) { - return + return; } if (note) { - vm.note = note + vm.note = note; // initialize look And Feel - $rootScope.$broadcast('setLookAndFeel', 'home') + $rootScope.$broadcast('setLookAndFeel', 'home'); // make it read only - vm.viewOnly = true + vm.viewOnly = true; - vm.notebookHome = true - vm.staticHome = false + vm.notebookHome = true; + vm.staticHome = false; } else { - vm.staticHome = true - vm.notebookHome = false + vm.staticHome = true; + vm.notebookHome = false; } - }) + }); - $scope.loadMoreNotes = function () { - vm.numberOfNotesDisplayed += 10 - } + $scope.loadMoreNotes = function() { + vm.numberOfNotesDisplayed += 10; + }; - $scope.renameNote = function (nodeId, nodePath) { - vm.noteActionService.renameNote(nodeId, nodePath) - } + $scope.renameNote = function(nodeId, nodePath) { + vm.noteActionService.renameNote(nodeId, nodePath); + }; - $scope.moveNoteToTrash = function (noteId) { - vm.noteActionService.moveNoteToTrash(noteId, false) - } + $scope.moveNoteToTrash = function(noteId) { + vm.noteActionService.moveNoteToTrash(noteId, false); + }; - $scope.moveFolderToTrash = function (folderId) { - vm.noteActionService.moveFolderToTrash(folderId) - } + $scope.moveFolderToTrash = function(folderId) { + vm.noteActionService.moveFolderToTrash(folderId); + }; - $scope.restoreNote = function (noteId) { - websocketMsgSrv.restoreNote(noteId) - } + $scope.restoreNote = function(noteId) { + websocketMsgSrv.restoreNote(noteId); + }; - $scope.restoreFolder = function (folderId) { - websocketMsgSrv.restoreFolder(folderId) - } + $scope.restoreFolder = function(folderId) { + websocketMsgSrv.restoreFolder(folderId); + }; - $scope.restoreAll = function () { - vm.noteActionService.restoreAll() - } + $scope.restoreAll = function() { + vm.noteActionService.restoreAll(); + }; - $scope.renameFolder = function (node) { - vm.noteActionService.renameFolder(node.id) - } + $scope.renameFolder = function(node) { + vm.noteActionService.renameFolder(node.id); + }; - $scope.removeNote = function (noteId) { - vm.noteActionService.removeNote(noteId, false) - } + $scope.removeNote = function(noteId) { + vm.noteActionService.removeNote(noteId, false); + }; - $scope.removeFolder = function (folderId) { - vm.noteActionService.removeFolder(folderId) - } + $scope.removeFolder = function(folderId) { + vm.noteActionService.removeFolder(folderId); + }; - $scope.emptyTrash = function () { - vm.noteActionService.emptyTrash() - } + $scope.emptyTrash = function() { + vm.noteActionService.emptyTrash(); + }; - $scope.clearAllParagraphOutput = function (noteId) { - vm.noteActionService.clearAllParagraphOutput(noteId) - } + $scope.clearAllParagraphOutput = function(noteId) { + vm.noteActionService.clearAllParagraphOutput(noteId); + }; - $scope.isFilterNote = function (note) { + $scope.isFilterNote = function(note) { if (!$scope.query.q) { - return true + return true; } - let noteName = note.name + let noteName = note.name; if (noteName.toLowerCase().indexOf($scope.query.q.toLowerCase()) > -1) { - return true + return true; } - return false - } + return false; + }; - $scope.getNoteName = function (note) { - return arrayOrderingSrv.getNoteName(note) - } + $scope.getNoteName = function(note) { + return arrayOrderingSrv.getNoteName(note); + }; - $scope.noteComparator = function (note1, note2) { - return arrayOrderingSrv.noteComparator(note1, note2) - } + $scope.noteComparator = function(note1, note2) { + return arrayOrderingSrv.noteComparator(note1, note2); + }; } diff --git a/zeppelin-web/src/app/interpreter/interpreter-item.directive.js b/zeppelin-web/src/app/interpreter/interpreter-item.directive.js index 4bde44d16c1..cfb109a12fc 100644 --- a/zeppelin-web/src/app/interpreter/interpreter-item.directive.js +++ b/zeppelin-web/src/app/interpreter/interpreter-item.directive.js @@ -12,20 +12,20 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').directive('interpreterItem', InterpreterItemDirective) +angular.module('zeppelinWebApp').directive('interpreterItem', InterpreterItemDirective); -function InterpreterItemDirective ($timeout) { - 'ngInject' +function InterpreterItemDirective($timeout) { + 'ngInject'; return { restrict: 'A', - link: function (scope, element, attr) { + link: function(scope, element, attr) { if (scope.$last === true) { - $timeout(function () { - let id = 'ngRenderFinished' - scope.$emit(id) - }) + $timeout(function() { + let id = 'ngRenderFinished'; + scope.$emit(id); + }); } - } - } + }, + }; } diff --git a/zeppelin-web/src/app/interpreter/interpreter.controller.js b/zeppelin-web/src/app/interpreter/interpreter.controller.js index ef8840240e7..060c6b6132b 100644 --- a/zeppelin-web/src/app/interpreter/interpreter.controller.js +++ b/zeppelin-web/src/app/interpreter/interpreter.controller.js @@ -12,548 +12,552 @@ * limitations under the License. */ -import { ParagraphStatus, } from '../notebook/paragraph/paragraph.status' +import {ParagraphStatus} from '../notebook/paragraph/paragraph.status'; -angular.module('zeppelinWebApp').controller('InterpreterCtrl', InterpreterCtrl) +angular.module('zeppelinWebApp').controller('InterpreterCtrl', InterpreterCtrl); function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeout, $route) { - 'ngInject' - - let interpreterSettingsTmp = [] - $scope.interpreterSettings = [] - $scope.availableInterpreters = {} - $scope.showAddNewSetting = false - $scope.showRepositoryInfo = false - $scope.searchInterpreter = '' - $scope._ = _ - $scope.interpreterPropertyTypes = [] - ngToast.dismiss() - - $scope.openPermissions = function () { - $scope.showInterpreterAuth = true - } - - $scope.closePermissions = function () { - $scope.showInterpreterAuth = false - } - - let getSelectJson = function () { + 'ngInject'; + + let interpreterSettingsTmp = []; + $scope.interpreterSettings = []; + $scope.availableInterpreters = {}; + $scope.showAddNewSetting = false; + $scope.showRepositoryInfo = false; + $scope.searchInterpreter = ''; + $scope._ = _; + $scope.interpreterPropertyTypes = []; + ngToast.dismiss(); + + $scope.openPermissions = function() { + $scope.showInterpreterAuth = true; + }; + + $scope.closePermissions = function() { + $scope.showInterpreterAuth = false; + }; + + let getSelectJson = function() { let selectJson = { tags: true, minimumInputLength: 3, multiple: true, tokenSeparators: [',', ' '], ajax: { - url: function (params) { + url: function(params) { if (!params.term) { - return false + return false; } - return baseUrlSrv.getRestApiBase() + '/security/userlist/' + params.term + return baseUrlSrv.getRestApiBase() + '/security/userlist/' + params.term; }, delay: 250, - processResults: function (data, params) { - let results = [] + processResults: function(data, params) { + let results = []; if (data.body.users.length !== 0) { - let users = [] + let users = []; for (let len = 0; len < data.body.users.length; len++) { users.push({ 'id': data.body.users[len], - 'text': data.body.users[len] - }) + 'text': data.body.users[len], + }); } results.push({ 'text': 'Users :', - 'children': users - }) + 'children': users, + }); } if (data.body.roles.length !== 0) { - let roles = [] + let roles = []; for (let len = 0; len < data.body.roles.length; len++) { roles.push({ 'id': data.body.roles[len], - 'text': data.body.roles[len] - }) + 'text': data.body.roles[len], + }); } results.push({ 'text': 'Roles :', - 'children': roles - }) + 'children': roles, + }); } return { results: results, pagination: { - more: false - } - } + more: false, + }, + }; }, - cache: false - } - } - return selectJson - } - - $scope.togglePermissions = function (intpName) { - angular.element('#' + intpName + 'Owners').select2(getSelectJson()) + cache: false, + }, + }; + return selectJson; + }; + + $scope.togglePermissions = function(intpName) { + angular.element('#' + intpName + 'Owners').select2(getSelectJson()); if ($scope.showInterpreterAuth) { - $scope.closePermissions() + $scope.closePermissions(); } else { - $scope.openPermissions() + $scope.openPermissions(); } - } + }; - $scope.$on('ngRenderFinished', function (event, data) { + $scope.$on('ngRenderFinished', function(event, data) { for (let setting = 0; setting < $scope.interpreterSettings.length; setting++) { - angular.element('#' + $scope.interpreterSettings[setting].name + 'Owners').select2(getSelectJson()) + angular.element('#' + $scope.interpreterSettings[setting].name + 'Owners').select2(getSelectJson()); } - }) + }); - let getInterpreterSettings = function () { + let getInterpreterSettings = function() { $http.get(baseUrlSrv.getRestApiBase() + '/interpreter/setting') - .then(function (res) { - $scope.interpreterSettings = res.data.body - checkDownloadingDependencies() - }).catch(function (res) { + .then(function(res) { + $scope.interpreterSettings = res.data.body; + checkDownloadingDependencies(); + }).catch(function(res) { if (res.status === 401) { ngToast.danger({ content: 'You don\'t have permission on this page', verticalPosition: 'bottom', - timeout: '3000' - }) - setTimeout(function () { - window.location = baseUrlSrv.getBase() - }, 3000) + timeout: '3000', + }); + setTimeout(function() { + window.location = baseUrlSrv.getBase(); + }, 3000); } - console.log('Error %o %o', res.status, res.data ? res.data.message : '') - }) - } + console.log('Error %o %o', res.status, res.data ? res.data.message : ''); + }); + }; - const checkDownloadingDependencies = function () { - let isDownloading = false + const checkDownloadingDependencies = function() { + let isDownloading = false; for (let index = 0; index < $scope.interpreterSettings.length; index++) { - let setting = $scope.interpreterSettings[index] + let setting = $scope.interpreterSettings[index]; if (setting.status === 'DOWNLOADING_DEPENDENCIES') { - isDownloading = true + isDownloading = true; } if (setting.status === ParagraphStatus.ERROR || setting.errorReason) { ngToast.danger({content: 'Error setting properties for interpreter \'' + setting.group + '.' + setting.name + '\': ' + setting.errorReason, verticalPosition: 'top', - dismissOnTimeout: false - }) + dismissOnTimeout: false, + }); } } if (isDownloading) { - $timeout(function () { + $timeout(function() { if ($route.current.$$route.originalPath === '/interpreter') { - getInterpreterSettings() + getInterpreterSettings(); } - }, 2000) + }, 2000); } - } + }; - let getAvailableInterpreters = function () { - $http.get(baseUrlSrv.getRestApiBase() + '/interpreter').then(function (res) { - $scope.availableInterpreters = res.data.body - }).catch(function (res) { - console.log('Error %o %o', res.status, res.data ? res.data.message : '') - }) - } + let getAvailableInterpreters = function() { + $http.get(baseUrlSrv.getRestApiBase() + '/interpreter').then(function(res) { + $scope.availableInterpreters = res.data.body; + }).catch(function(res) { + console.log('Error %o %o', res.status, res.data ? res.data.message : ''); + }); + }; - let getAvailableInterpreterPropertyWidgets = function () { + let getAvailableInterpreterPropertyWidgets = function() { $http.get(baseUrlSrv.getRestApiBase() + '/interpreter/property/types') - .then(function (res) { - $scope.interpreterPropertyTypes = res.data.body - }).catch(function (res) { - console.log('Error %o %o', res.status, res.data ? res.data.message : '') - }) - } + .then(function(res) { + $scope.interpreterPropertyTypes = res.data.body; + }).catch(function(res) { + console.log('Error %o %o', res.status, res.data ? res.data.message : ''); + }); + }; let emptyNewProperty = function(object) { - angular.extend(object, {propertyValue: '', propertyKey: '', propertyType: $scope.interpreterPropertyTypes[0]}) - } + angular.extend(object, {propertyValue: '', propertyKey: '', propertyType: $scope.interpreterPropertyTypes[0]}); + }; - let emptyNewDependency = function (object) { - angular.extend(object, {depArtifact: '', depExclude: ''}) - } + let emptyNewDependency = function(object) { + angular.extend(object, {depArtifact: '', depExclude: ''}); + }; - let removeTMPSettings = function (index) { - interpreterSettingsTmp.splice(index, 1) - } + let removeTMPSettings = function(index) { + interpreterSettingsTmp.splice(index, 1); + }; - $scope.copyOriginInterpreterSettingProperties = function (settingId) { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - interpreterSettingsTmp[index] = angular.copy($scope.interpreterSettings[index]) - } + $scope.copyOriginInterpreterSettingProperties = function(settingId) { + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); + interpreterSettingsTmp[index] = angular.copy($scope.interpreterSettings[index]); + }; - $scope.setPerNoteOption = function (settingId, sessionOption) { - let option + $scope.setPerNoteOption = function(settingId, sessionOption) { + let option; if (settingId === undefined) { - option = $scope.newInterpreterSetting.option + option = $scope.newInterpreterSetting.option; } else { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - let setting = $scope.interpreterSettings[index] - option = setting.option + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); + let setting = $scope.interpreterSettings[index]; + option = setting.option; } if (sessionOption === 'isolated') { - option.perNote = sessionOption - option.session = false - option.process = true + option.perNote = sessionOption; + option.session = false; + option.process = true; } else if (sessionOption === 'scoped') { - option.perNote = sessionOption - option.session = true - option.process = false + option.perNote = sessionOption; + option.session = true; + option.process = false; } else { - option.perNote = 'shared' - option.session = false - option.process = false + option.perNote = 'shared'; + option.session = false; + option.process = false; } - } + }; - $scope.defaultValueByType = function (setting) { + $scope.defaultValueByType = function(setting) { if (setting.propertyType === 'checkbox') { - setting.propertyValue = false - return + setting.propertyValue = false; + return; } - setting.propertyValue = '' - } + setting.propertyValue = ''; + }; - $scope.setPerUserOption = function (settingId, sessionOption) { - let option + $scope.setPerUserOption = function(settingId, sessionOption) { + let option; if (settingId === undefined) { - option = $scope.newInterpreterSetting.option + option = $scope.newInterpreterSetting.option; } else { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - let setting = $scope.interpreterSettings[index] - option = setting.option + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); + let setting = $scope.interpreterSettings[index]; + option = setting.option; } if (sessionOption === 'isolated') { - option.perUser = sessionOption - option.session = false - option.process = true + option.perUser = sessionOption; + option.session = false; + option.process = true; } else if (sessionOption === 'scoped') { - option.perUser = sessionOption - option.session = true - option.process = false + option.perUser = sessionOption; + option.session = true; + option.process = false; } else { - option.perUser = 'shared' - option.session = false - option.process = false + option.perUser = 'shared'; + option.session = false; + option.process = false; } - } + }; - $scope.getPerNoteOption = function (settingId) { - let option + $scope.getPerNoteOption = function(settingId) { + let option; if (settingId === undefined) { - option = $scope.newInterpreterSetting.option + option = $scope.newInterpreterSetting.option; } else { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - let setting = $scope.interpreterSettings[index] - option = setting.option + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); + let setting = $scope.interpreterSettings[index]; + option = setting.option; } if (option.perNote === 'scoped') { - return 'scoped' + return 'scoped'; } else if (option.perNote === 'isolated') { - return 'isolated' + return 'isolated'; } else { - return 'shared' + return 'shared'; } - } + }; - $scope.getPerUserOption = function (settingId) { - let option + $scope.getPerUserOption = function(settingId) { + let option; if (settingId === undefined) { - option = $scope.newInterpreterSetting.option + option = $scope.newInterpreterSetting.option; } else { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - let setting = $scope.interpreterSettings[index] - option = setting.option + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); + let setting = $scope.interpreterSettings[index]; + option = setting.option; } if (option.perUser === 'scoped') { - return 'scoped' + return 'scoped'; } else if (option.perUser === 'isolated') { - return 'isolated' + return 'isolated'; } else { - return 'shared' + return 'shared'; } - } + }; - $scope.getInterpreterRunningOption = function (settingId) { - let sharedModeName = 'shared' + $scope.getInterpreterRunningOption = function(settingId) { + let sharedModeName = 'shared'; - let globallyModeName = 'Globally' - let perNoteModeName = 'Per Note' - let perUserModeName = 'Per User' + let globallyModeName = 'Globally'; + let perNoteModeName = 'Per Note'; + let perUserModeName = 'Per User'; - let option + let option; if (settingId === undefined) { - option = $scope.newInterpreterSetting.option + option = $scope.newInterpreterSetting.option; } else { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - let setting = $scope.interpreterSettings[index] - option = setting.option + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); + let setting = $scope.interpreterSettings[index]; + option = setting.option; } - let perNote = option.perNote - let perUser = option.perUser + let perNote = option.perNote; + let perUser = option.perUser; // Globally == shared_perNote + shared_perUser if (perNote === sharedModeName && perUser === sharedModeName) { - return globallyModeName + return globallyModeName; } if ($rootScope.ticket.ticket === 'anonymous' && $rootScope.ticket.roles === '[]') { if (perNote !== undefined && typeof perNote === 'string' && perNote !== '') { - return perNoteModeName + return perNoteModeName; } } else if ($rootScope.ticket.ticket !== 'anonymous') { if (perNote !== undefined && typeof perNote === 'string' && perNote !== '') { if (perUser !== undefined && typeof perUser === 'string' && perUser !== '') { - return perUserModeName + return perUserModeName; } - return perNoteModeName + return perNoteModeName; } } - option.perNote = sharedModeName - option.perUser = sharedModeName - return globallyModeName - } + option.perNote = sharedModeName; + option.perUser = sharedModeName; + return globallyModeName; + }; - $scope.setInterpreterRunningOption = function (settingId, isPerNoteMode, isPerUserMode) { - let option + $scope.setInterpreterRunningOption = function(settingId, isPerNoteMode, isPerUserMode) { + let option; if (settingId === undefined) { - option = $scope.newInterpreterSetting.option + option = $scope.newInterpreterSetting.option; } else { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - let setting = $scope.interpreterSettings[index] - option = setting.option + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); + let setting = $scope.interpreterSettings[index]; + option = setting.option; } - option.perNote = isPerNoteMode - option.perUser = isPerUserMode - } + option.perNote = isPerNoteMode; + option.perUser = isPerUserMode; + }; - $scope.updateInterpreterSetting = function (form, settingId) { + $scope.updateInterpreterSetting = function(form, settingId) { const thisConfirm = BootstrapDialog.confirm({ closable: false, closeByBackdrop: false, closeByKeyboard: false, title: '', message: 'Do you want to update this interpreter and restart with new settings?', - callback: function (result) { + callback: function(result) { if (result) { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - let setting = $scope.interpreterSettings[index] + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); + let setting = $scope.interpreterSettings[index]; if (setting.propertyKey !== '' || setting.propertyKey) { - $scope.addNewInterpreterProperty(settingId) + $scope.addNewInterpreterProperty(settingId); } if (setting.depArtifact !== '' || setting.depArtifact) { - $scope.addNewInterpreterDependency(settingId) + $scope.addNewInterpreterDependency(settingId); } // add missing field of option if (!setting.option) { - setting.option = {} + setting.option = {}; } if (setting.option.isExistingProcess === undefined) { - setting.option.isExistingProcess = false + setting.option.isExistingProcess = false; } if (setting.option.setPermission === undefined) { - setting.option.setPermission = false + setting.option.setPermission = false; } if (setting.option.isUserImpersonate === undefined) { - setting.option.isUserImpersonate = false + setting.option.isUserImpersonate = false; } if (!($scope.getInterpreterRunningOption(settingId) === 'Per User' && $scope.getPerUserOption(settingId) === 'isolated')) { - setting.option.isUserImpersonate = false + setting.option.isUserImpersonate = false; } if (setting.option.remote === undefined) { // remote always true for now - setting.option.remote = true + setting.option.remote = true; } - setting.option.owners = angular.element('#' + setting.name + 'Owners').val() + setting.option.owners = angular.element('#' + setting.name + 'Owners').val(); let request = { option: angular.copy(setting.option), properties: angular.copy(setting.properties), - dependencies: angular.copy(setting.dependencies) - } + dependencies: angular.copy(setting.dependencies), + }; - thisConfirm.$modalFooter.find('button').addClass('disabled') + thisConfirm.$modalFooter.find('button').addClass('disabled'); thisConfirm.$modalFooter.find('button:contains("OK")') - .html(' Saving Setting') + .html(' Saving Setting'); $http.put(baseUrlSrv.getRestApiBase() + '/interpreter/setting/' + settingId, request) - .then(function (res) { - $scope.interpreterSettings[index] = res.data.body - removeTMPSettings(index) - checkDownloadingDependencies() - thisConfirm.close() - }) - .catch(function (res) { - const message = res.data ? res.data.message : 'Could not connect to server.' - console.log('Error %o %o', res.status, message) - ngToast.danger({content: message, verticalPosition: 'bottom'}) - form.$show() - thisConfirm.close() + .then(function(res) { + $scope.interpreterSettings[index] = res.data.body; + removeTMPSettings(index); + checkDownloadingDependencies(); + thisConfirm.close(); }) - return false + .catch(function(res) { + const message = res.data ? res.data.message : 'Could not connect to server.'; + console.log('Error %o %o', res.status, message); + ngToast.danger({content: message, verticalPosition: 'bottom'}); + form.$show(); + thisConfirm.close(); + }); + return false; } else { - form.$show() + form.$show(); } - } - }) - } + }, + }); + }; - $scope.resetInterpreterSetting = function (settingId) { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) + $scope.resetInterpreterSetting = function(settingId) { + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); // Set the old settings back - $scope.interpreterSettings[index] = angular.copy(interpreterSettingsTmp[index]) - removeTMPSettings(index) - } + $scope.interpreterSettings[index] = angular.copy(interpreterSettingsTmp[index]); + removeTMPSettings(index); + }; - $scope.removeInterpreterSetting = function (settingId) { + $scope.removeInterpreterSetting = function(settingId) { BootstrapDialog.confirm({ closable: true, title: '', message: 'Do you want to delete this interpreter setting?', - callback: function (result) { + callback: function(result) { if (result) { $http.delete(baseUrlSrv.getRestApiBase() + '/interpreter/setting/' + settingId) - .then(function (res) { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - $scope.interpreterSettings.splice(index, 1) - }).catch(function (res) { - console.log('Error %o %o', res.status, res.data ? res.data.message : '') - }) + .then(function(res) { + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); + $scope.interpreterSettings.splice(index, 1); + }).catch(function(res) { + console.log('Error %o %o', res.status, res.data ? res.data.message : ''); + }); } - } - }) - } + }, + }); + }; - $scope.newInterpreterGroupChange = function () { + $scope.newInterpreterGroupChange = function() { let el = _.pluck(_.filter($scope.availableInterpreters, {'name': $scope.newInterpreterSetting.group}), - 'properties') - let properties = {} + 'properties'); + let properties = {}; for (let i = 0; i < el.length; i++) { - let intpInfo = el[i] + let intpInfo = el[i]; for (let key in intpInfo) { - properties[key] = { - value: intpInfo[key].defaultValue, - description: intpInfo[key].description, - type: intpInfo[key].type + if (intpInfo.hasOwnProperty(key)) { + properties[key] = { + value: intpInfo[key].defaultValue, + description: intpInfo[key].description, + type: intpInfo[key].type, + }; } } } - $scope.newInterpreterSetting.properties = properties - } + $scope.newInterpreterSetting.properties = properties; + }; - $scope.restartInterpreterSetting = function (settingId) { + $scope.restartInterpreterSetting = function(settingId) { BootstrapDialog.confirm({ closable: true, title: '', message: 'Do you want to restart this interpreter?', - callback: function (result) { + callback: function(result) { if (result) { $http.put(baseUrlSrv.getRestApiBase() + '/interpreter/setting/restart/' + settingId) - .then(function (res) { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - $scope.interpreterSettings[index] = res.data.body - ngToast.info('Interpreter stopped. Will be lazily started on next run.') - }).catch(function (res) { - let errorMsg = (res.data !== null) ? res.data.message : 'Could not connect to server.' - console.log('Error %o %o', res.status, errorMsg) - ngToast.danger(errorMsg) - }) + .then(function(res) { + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); + $scope.interpreterSettings[index] = res.data.body; + ngToast.info('Interpreter stopped. Will be lazily started on next run.'); + }).catch(function(res) { + let errorMsg = (res.data !== null) ? res.data.message : 'Could not connect to server.'; + console.log('Error %o %o', res.status, errorMsg); + ngToast.danger(errorMsg); + }); } - } - }) - } + }, + }); + }; - $scope.addNewInterpreterSetting = function () { + $scope.addNewInterpreterSetting = function() { // user input validation on interpreter creation if (!$scope.newInterpreterSetting.name || !$scope.newInterpreterSetting.name.trim() || !$scope.newInterpreterSetting.group) { BootstrapDialog.alert({ closable: true, title: 'Add interpreter', - message: 'Please fill in interpreter name and choose a group' - }) - return + message: 'Please fill in interpreter name and choose a group', + }); + return; } if ($scope.newInterpreterSetting.name.indexOf('.') >= 0) { BootstrapDialog.alert({ closable: true, title: 'Add interpreter', - message: '\'.\' is invalid for interpreter name' - }) - return + message: '\'.\' is invalid for interpreter name', + }); + return; } if (_.findIndex($scope.interpreterSettings, {'name': $scope.newInterpreterSetting.name}) >= 0) { BootstrapDialog.alert({ closable: true, title: 'Add interpreter', - message: 'Name ' + $scope.newInterpreterSetting.name + ' already exists' - }) - return + message: 'Name ' + $scope.newInterpreterSetting.name + ' already exists', + }); + return; } - let newSetting = $scope.newInterpreterSetting + let newSetting = $scope.newInterpreterSetting; if (newSetting.propertyKey !== '' || newSetting.propertyKey) { - $scope.addNewInterpreterProperty() + $scope.addNewInterpreterProperty(); } if (newSetting.depArtifact !== '' || newSetting.depArtifact) { - $scope.addNewInterpreterDependency() + $scope.addNewInterpreterDependency(); } if (newSetting.option.setPermission === undefined) { - newSetting.option.setPermission = false + newSetting.option.setPermission = false; } - newSetting.option.owners = angular.element('#newInterpreterOwners').val() + newSetting.option.owners = angular.element('#newInterpreterOwners').val(); - let request = angular.copy($scope.newInterpreterSetting) + let request = angular.copy($scope.newInterpreterSetting); // Change properties to proper request format - let newProperties = {} + let newProperties = {}; for (let p in newSetting.properties) { - newProperties[p] = { - value: newSetting.properties[p].value, - type: newSetting.properties[p].type, - name: p + if (newSetting.properties.hasOwnProperty(p)) { + newProperties[p] = { + value: newSetting.properties[p].value, + type: newSetting.properties[p].type, + name: p, + }; } } - request.properties = newProperties + request.properties = newProperties; $http.post(baseUrlSrv.getRestApiBase() + '/interpreter/setting', request) - .then(function (res) { - $scope.resetNewInterpreterSetting() - getInterpreterSettings() - $scope.showAddNewSetting = false - checkDownloadingDependencies() - }).catch(function (res) { - const errorMsg = res.data ? res.data.message : 'Could not connect to server.' - console.log('Error %o %o', res.status, errorMsg) - ngToast.danger({content: errorMsg, verticalPosition: 'bottom'}) - }) - } - - $scope.cancelInterpreterSetting = function () { - $scope.showAddNewSetting = false - $scope.resetNewInterpreterSetting() - } - - $scope.resetNewInterpreterSetting = function () { + .then(function(res) { + $scope.resetNewInterpreterSetting(); + getInterpreterSettings(); + $scope.showAddNewSetting = false; + checkDownloadingDependencies(); + }).catch(function(res) { + const errorMsg = res.data ? res.data.message : 'Could not connect to server.'; + console.log('Error %o %o', res.status, errorMsg); + ngToast.danger({content: errorMsg, verticalPosition: 'bottom'}); + }); + }; + + $scope.cancelInterpreterSetting = function() { + $scope.showAddNewSetting = false; + $scope.resetNewInterpreterSetting(); + }; + + $scope.resetNewInterpreterSetting = function() { $scope.newInterpreterSetting = { name: undefined, group: undefined, @@ -564,94 +568,94 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou isExistingProcess: false, setPermission: false, session: false, - process: false + process: false, - } - } - emptyNewProperty($scope.newInterpreterSetting) - } + }, + }; + emptyNewProperty($scope.newInterpreterSetting); + }; - $scope.removeInterpreterProperty = function (key, settingId) { + $scope.removeInterpreterProperty = function(key, settingId) { if (settingId === undefined) { - delete $scope.newInterpreterSetting.properties[key] + delete $scope.newInterpreterSetting.properties[key]; } else { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - delete $scope.interpreterSettings[index].properties[key] + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); + delete $scope.interpreterSettings[index].properties[key]; } - } + }; - $scope.removeInterpreterDependency = function (artifact, settingId) { + $scope.removeInterpreterDependency = function(artifact, settingId) { if (settingId === undefined) { $scope.newInterpreterSetting.dependencies = _.reject($scope.newInterpreterSetting.dependencies, - function (el) { - return el.groupArtifactVersion === artifact - }) + function(el) { + return el.groupArtifactVersion === artifact; + }); } else { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); $scope.interpreterSettings[index].dependencies = _.reject($scope.interpreterSettings[index].dependencies, - function (el) { - return el.groupArtifactVersion === artifact - }) + function(el) { + return el.groupArtifactVersion === artifact; + }); } - } + }; - $scope.addNewInterpreterProperty = function (settingId) { + $scope.addNewInterpreterProperty = function(settingId) { if (settingId === undefined) { // Add new property from create form if (!$scope.newInterpreterSetting.propertyKey || $scope.newInterpreterSetting.propertyKey === '') { - return + return; } $scope.newInterpreterSetting.properties[$scope.newInterpreterSetting.propertyKey] = { value: $scope.newInterpreterSetting.propertyValue, - type: $scope.newInterpreterSetting.propertyType - } - emptyNewProperty($scope.newInterpreterSetting) + type: $scope.newInterpreterSetting.propertyType, + }; + emptyNewProperty($scope.newInterpreterSetting); } else { // Add new property from edit form - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - let setting = $scope.interpreterSettings[index] + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); + let setting = $scope.interpreterSettings[index]; if (!setting.propertyKey || setting.propertyKey === '') { - return + return; } setting.properties[setting.propertyKey] = - {value: setting.propertyValue, type: setting.propertyType} + {value: setting.propertyValue, type: setting.propertyType}; - emptyNewProperty(setting) + emptyNewProperty(setting); } - } + }; - $scope.addNewInterpreterDependency = function (settingId) { + $scope.addNewInterpreterDependency = function(settingId) { if (settingId === undefined) { // Add new dependency from create form if (!$scope.newInterpreterSetting.depArtifact || $scope.newInterpreterSetting.depArtifact === '') { - return + return; } // overwrite if artifact already exists - let newSetting = $scope.newInterpreterSetting + let newSetting = $scope.newInterpreterSetting; for (let d in newSetting.dependencies) { if (newSetting.dependencies[d].groupArtifactVersion === newSetting.depArtifact) { newSetting.dependencies[d] = { 'groupArtifactVersion': newSetting.depArtifact, - 'exclusions': newSetting.depExclude - } - newSetting.dependencies.splice(d, 1) + 'exclusions': newSetting.depExclude, + }; + newSetting.dependencies.splice(d, 1); } } newSetting.dependencies.push({ 'groupArtifactVersion': newSetting.depArtifact, - 'exclusions': (newSetting.depExclude === '') ? [] : newSetting.depExclude - }) - emptyNewDependency(newSetting) + 'exclusions': (newSetting.depExclude === '') ? [] : newSetting.depExclude, + }); + emptyNewDependency(newSetting); } else { // Add new dependency from edit form - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - let setting = $scope.interpreterSettings[index] + let index = _.findIndex($scope.interpreterSettings, {'id': settingId}); + let setting = $scope.interpreterSettings[index]; if (!setting.depArtifact || setting.depArtifact === '') { - return + return; } // overwrite if artifact already exists @@ -659,21 +663,21 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou if (setting.dependencies[dep].groupArtifactVersion === setting.depArtifact) { setting.dependencies[dep] = { 'groupArtifactVersion': setting.depArtifact, - 'exclusions': setting.depExclude - } - setting.dependencies.splice(dep, 1) + 'exclusions': setting.depExclude, + }; + setting.dependencies.splice(dep, 1); } } setting.dependencies.push({ 'groupArtifactVersion': setting.depArtifact, - 'exclusions': (setting.depExclude === '') ? [] : setting.depExclude - }) - emptyNewDependency(setting) + 'exclusions': (setting.depExclude === '') ? [] : setting.depExclude, + }); + emptyNewDependency(setting); } - } + }; - $scope.resetNewRepositorySetting = function () { + $scope.resetNewRepositorySetting = function() { $scope.newRepoSetting = { id: '', url: '', @@ -684,102 +688,102 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou proxyHost: '', proxyPort: null, proxyLogin: '', - proxyPassword: '' - } - } + proxyPassword: '', + }; + }; - let getRepositories = function () { + let getRepositories = function() { $http.get(baseUrlSrv.getRestApiBase() + '/interpreter/repository') - .success(function (data, status, headers, config) { - $scope.repositories = data.body - }).error(function (data, status, headers, config) { - console.log('Error %o %o', status, data.message) - }) - } + .success(function(data, status, headers, config) { + $scope.repositories = data.body; + }).error(function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); + }); + }; - $scope.addNewRepository = function () { - let request = angular.copy($scope.newRepoSetting) + $scope.addNewRepository = function() { + let request = angular.copy($scope.newRepoSetting); $http.post(baseUrlSrv.getRestApiBase() + '/interpreter/repository', request) - .then(function (res) { - getRepositories() - $scope.resetNewRepositorySetting() - angular.element('#repoModal').modal('hide') - }).catch(function (res) { - console.log('Error %o %o', res.headers, res.config) - }) - } - - $scope.removeRepository = function (repoId) { + .then(function(res) { + getRepositories(); + $scope.resetNewRepositorySetting(); + angular.element('#repoModal').modal('hide'); + }).catch(function(res) { + console.log('Error %o %o', res.headers, res.config); + }); + }; + + $scope.removeRepository = function(repoId) { BootstrapDialog.confirm({ closable: true, title: '', message: 'Do you want to delete this repository?', - callback: function (result) { + callback: function(result) { if (result) { $http.delete(baseUrlSrv.getRestApiBase() + '/interpreter/repository/' + repoId) - .then(function (res) { - let index = _.findIndex($scope.repositories, {'id': repoId}) - $scope.repositories.splice(index, 1) - }).catch(function (res) { - console.log('Error %o %o', res.status, res.data ? res.data.message : '') - }) + .then(function(res) { + let index = _.findIndex($scope.repositories, {'id': repoId}); + $scope.repositories.splice(index, 1); + }).catch(function(res) { + console.log('Error %o %o', res.status, res.data ? res.data.message : ''); + }); } - } - }) - } + }, + }); + }; - $scope.isDefaultRepository = function (repoId) { + $scope.isDefaultRepository = function(repoId) { if (repoId === 'central' || repoId === 'local') { - return true + return true; } else { - return false + return false; } - } + }; - $scope.showErrorMessage = function (setting) { + $scope.showErrorMessage = function(setting) { BootstrapDialog.show({ title: 'Error downloading dependencies', - message: setting.errorReason - }) - } + message: setting.errorReason, + }); + }; let init = function() { - getAvailableInterpreterPropertyWidgets() + getAvailableInterpreterPropertyWidgets(); - $scope.resetNewInterpreterSetting() - $scope.resetNewRepositorySetting() + $scope.resetNewInterpreterSetting(); + $scope.resetNewRepositorySetting(); - getInterpreterSettings() - getAvailableInterpreters() - getRepositories() - } + getInterpreterSettings(); + getAvailableInterpreters(); + getRepositories(); + }; - $scope.showSparkUI = function (settingId) { + $scope.showSparkUI = function(settingId) { $http.get(baseUrlSrv.getRestApiBase() + '/interpreter/metadata/' + settingId) - .then(function (res) { + .then(function(res) { if (res.data.body === undefined) { BootstrapDialog.alert({ - message: 'No spark application running' - }) - return + message: 'No spark application running', + }); + return; } if (res.data.body.url) { - window.open(res.data.body.url, '_blank') + window.open(res.data.body.url, '_blank'); } else { BootstrapDialog.alert({ - message: res.data.body.message - }) + message: res.data.body.message, + }); } - }).catch(function (res) { - console.log('Error %o %o', res.status, res.data ? res.data.message : '') - }) - } + }).catch(function(res) { + console.log('Error %o %o', res.status, res.data ? res.data.message : ''); + }); + }; $scope.getInterpreterBindingModeDocsLink = function() { - const currentVersion = $rootScope.zeppelinVersion - return `https://zeppelin.apache.org/docs/${currentVersion}/usage/interpreter/interpreter_binding_mode.html` - } + const currentVersion = $rootScope.zeppelinVersion; + return `https://zeppelin.apache.org/docs/${currentVersion}/usage/interpreter/interpreter_binding_mode.html`; + }; - init() + init(); } diff --git a/zeppelin-web/src/app/interpreter/interpreter.filter.js b/zeppelin-web/src/app/interpreter/interpreter.filter.js index 3f42572015a..7b5ace0298a 100644 --- a/zeppelin-web/src/app/interpreter/interpreter.filter.js +++ b/zeppelin-web/src/app/interpreter/interpreter.filter.js @@ -12,11 +12,11 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').filter('sortByKey', sortByKey) +angular.module('zeppelinWebApp').filter('sortByKey', sortByKey); -function sortByKey () { - return function (properties) { - let sortedKeys = properties ? Object.keys(properties) : [] - return sortedKeys.sort() - } +function sortByKey() { + return function(properties) { + let sortedKeys = properties ? Object.keys(properties) : []; + return sortedKeys.sort(); + }; } diff --git a/zeppelin-web/src/app/interpreter/widget/number-widget.directive.js b/zeppelin-web/src/app/interpreter/widget/number-widget.directive.js index 2046b94d924..6ea129ad903 100644 --- a/zeppelin-web/src/app/interpreter/widget/number-widget.directive.js +++ b/zeppelin-web/src/app/interpreter/widget/number-widget.directive.js @@ -12,20 +12,20 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').directive('numberWidget', InterpreterNumberDirective) +angular.module('zeppelinWebApp').directive('numberWidget', InterpreterNumberDirective); function InterpreterNumberDirective() { return { require: 'ngModel', - link: function (scope, element, attrs, modelCtrl) { - modelCtrl.$parsers.push(function (inputValue) { - let transformedInput = inputValue ? inputValue.replace(/[^\d.-]/g, '') : null + link: function(scope, element, attrs, modelCtrl) { + modelCtrl.$parsers.push(function(inputValue) { + let transformedInput = inputValue ? inputValue.replace(/[^\d.-]/g, '') : null; if (transformedInput !== inputValue) { - modelCtrl.$setViewValue(transformedInput) - modelCtrl.$render() + modelCtrl.$setViewValue(transformedInput); + modelCtrl.$render(); } - return transformedInput - }) - } - } + return transformedInput; + }); + }, + }; } diff --git a/zeppelin-web/src/app/jobmanager/job-status.js b/zeppelin-web/src/app/jobmanager/job-status.js index eda41b1a38f..d918299f724 100644 --- a/zeppelin-web/src/app/jobmanager/job-status.js +++ b/zeppelin-web/src/app/jobmanager/job-status.js @@ -19,36 +19,36 @@ export const JobStatus = { ERROR: 'ERROR', PENDING: 'PENDING', RUNNING: 'RUNNING', -} +}; export function getJobIconByStatus(jobStatus) { if (jobStatus === JobStatus.READY) { - return 'fa fa-circle-o' + return 'fa fa-circle-o'; } else if (jobStatus === JobStatus.FINISHED) { - return 'fa fa-circle' + return 'fa fa-circle'; } else if (jobStatus === JobStatus.ABORT) { - return 'fa fa-circle' + return 'fa fa-circle'; } else if (jobStatus === JobStatus.ERROR) { - return 'fa fa-circle' + return 'fa fa-circle'; } else if (jobStatus === JobStatus.PENDING) { - return 'fa fa-circle' + return 'fa fa-circle'; } else if (jobStatus === JobStatus.RUNNING) { - return 'fa fa-spinner' + return 'fa fa-spinner'; } } export function getJobColorByStatus(jobStatus) { if (jobStatus === JobStatus.READY) { - return 'green' + return 'green'; } else if (jobStatus === JobStatus.FINISHED) { - return 'green' + return 'green'; } else if (jobStatus === JobStatus.ABORT) { - return 'orange' + return 'orange'; } else if (jobStatus === JobStatus.ERROR) { - return 'red' + return 'red'; } else if (jobStatus === JobStatus.PENDING) { - return 'gray' + return 'gray'; } else if (jobStatus === JobStatus.RUNNING) { - return 'blue' + return 'blue'; } } diff --git a/zeppelin-web/src/app/jobmanager/job/job.component.js b/zeppelin-web/src/app/jobmanager/job/job.component.js index c4d4f514307..e6f102f2387 100644 --- a/zeppelin-web/src/app/jobmanager/job/job.component.js +++ b/zeppelin-web/src/app/jobmanager/job/job.component.js @@ -12,35 +12,35 @@ * limitations under the License. */ -import moment from 'moment' +import moment from 'moment'; -import { ParagraphStatus, } from '../../notebook/paragraph/paragraph.status' -import { getJobColorByStatus, getJobIconByStatus } from '../job-status' +import {ParagraphStatus} from '../../notebook/paragraph/paragraph.status'; +import {getJobColorByStatus, getJobIconByStatus} from '../job-status'; -import jobTemplate from './job.html' -import './job.css' +import jobTemplate from './job.html'; +import './job.css'; class JobController { constructor($http, JobManagerService) { - 'ngInject' - this.$http = $http - this.JobManagerService = JobManagerService + 'ngInject'; + this.$http = $http; + this.JobManagerService = JobManagerService; } isRunning() { - return this.note.isRunningJob + return this.note.isRunningJob; } getParagraphs() { - return this.note.paragraphs + return this.note.paragraphs; } getNoteId() { - return this.note.noteId + return this.note.noteId; } getNoteName() { - return this.note.noteName + return this.note.noteName; } runJob() { @@ -48,19 +48,21 @@ class JobController { closable: true, title: 'Job Dialog', message: 'Run all paragraphs?', - callback: clickOk => { - if (!clickOk) { return } + callback: (clickOk) => { + if (!clickOk) { + return; + } - const noteId = this.getNoteId() + const noteId = this.getNoteId(); // if the request is handled successfully, the job page will get updated using websocket this.JobManagerService.sendRunJobRequest(noteId) - .catch(response => { + .catch((response) => { let message = (response.data && response.data.message) - ? response.data.message : 'SERVER ERROR' - this.showErrorDialog('Execution Failure', message) - }) - } - }) + ? response.data.message : 'SERVER ERROR'; + this.showErrorDialog('Execution Failure', message); + }); + }, + }); } stopJob() { @@ -68,81 +70,85 @@ class JobController { closable: true, title: 'Job Dialog', message: 'Stop all paragraphs?', - callback: clickOk => { - if (!clickOk) { return } + callback: (clickOk) => { + if (!clickOk) { + return; + } - const noteId = this.getNoteId() + const noteId = this.getNoteId(); // if the request is handled successfully, the job page will get updated using websocket this.JobManagerService.sendStopJobRequest(noteId) - .catch(response => { + .catch((response) => { let message = (response.data && response.data.message) - ? response.data.message : 'SERVER ERROR' - this.showErrorDialog('Stop Failure', message) - }) - } - }) + ? response.data.message : 'SERVER ERROR'; + this.showErrorDialog('Stop Failure', message); + }); + }, + }); } showErrorDialog(title, errorMessage) { - if (!errorMessage) { errorMessage = 'SERVER ERROR' } + if (!errorMessage) { + errorMessage = 'SERVER ERROR'; + } BootstrapDialog.alert({ closable: true, title: title, - message: errorMessage - }) + message: errorMessage, + }); } lastExecuteTime() { - const timestamp = this.note.unixTimeLastRun - return moment.unix(timestamp / 1000).fromNow() + const timestamp = this.note.unixTimeLastRun; + return moment.unix(timestamp / 1000).fromNow(); } getInterpreterName() { return typeof this.note.interpreter === 'undefined' - ? 'interpreter is not set' : this.note.interpreter + ? 'interpreter is not set' : this.note.interpreter; } getInterpreterNameStyle() { return typeof this.note.interpreter === 'undefined' - ? { color: 'gray' } : { color: 'black' } + ? {color: 'gray'} : {color: 'black'}; } getJobTypeIcon() { - const noteType = this.note.noteType + const noteType = this.note.noteType; if (noteType === 'normal') { - return 'icon-doc' + return 'icon-doc'; } else if (noteType === 'cron') { - return 'icon-clock' + return 'icon-clock'; } else { - return 'icon-question' + return 'icon-question'; } } getJobColorByStatus(status) { - return getJobColorByStatus(status) + return getJobColorByStatus(status); } getJobIconByStatus(status) { - return getJobIconByStatus(status) + return getJobIconByStatus(status); } getProgress() { - const paragraphs = this.getParagraphs() - let paragraphStatuses = paragraphs.map(p => p.status) - let runningOrFinishedParagraphs = paragraphStatuses.filter(status => { - return status === ParagraphStatus.RUNNING || status === ParagraphStatus.FINISHED - }) + const paragraphs = this.getParagraphs(); + let paragraphStatuses = paragraphs.map((p) => p.status); + let runningOrFinishedParagraphs = paragraphStatuses.filter((status) => { + return status === ParagraphStatus.RUNNING || status === ParagraphStatus.FINISHED; + }); - let totalCount = paragraphStatuses.length - let runningCount = runningOrFinishedParagraphs.length - let result = Math.ceil(runningCount / totalCount * 100) - result = isNaN(result) ? 0 : result + let totalCount = paragraphStatuses.length; + let runningCount = runningOrFinishedParagraphs.length; + let result = Math.ceil(runningCount / totalCount * 100); + result = isNaN(result) ? 0 : result; - return `${result}%` + return `${result}%`; } showPercentProgressBar() { - return this.getProgress() > 0 && this.getProgress() < 100 + return this.getProgress() > 0 && this.getProgress() < 100; } } @@ -152,9 +158,9 @@ export const JobComponent = { }, template: jobTemplate, controller: JobController, -} +}; export const JobModule = angular .module('zeppelinWebApp') .component('job', JobComponent) - .name + .name; diff --git a/zeppelin-web/src/app/jobmanager/job/job.component.test.js b/zeppelin-web/src/app/jobmanager/job/job.component.test.js index 6ca285cc89d..5b6bec1a6d4 100644 --- a/zeppelin-web/src/app/jobmanager/job/job.component.test.js +++ b/zeppelin-web/src/app/jobmanager/job/job.component.test.js @@ -1,55 +1,55 @@ -import { ParagraphStatus } from '../../notebook/paragraph/paragraph.status' +import {ParagraphStatus} from '../../notebook/paragraph/paragraph.status'; describe('JobComponent', () => { - let $componentController + let $componentController; - beforeEach(angular.mock.module('zeppelinWebApp')) + beforeEach(angular.mock.module('zeppelinWebApp')); beforeEach(angular.mock.inject((_$componentController_) => { - $componentController = _$componentController_ - })) + $componentController = _$componentController_; + })); it('should get progress when there is a finished paragraph', () => { const paragraphs = [ - { status: ParagraphStatus.FINISHED }, - ] - const mockNote = createMockNote(paragraphs) - const bindings = { note: mockNote, } + {status: ParagraphStatus.FINISHED}, + ]; + const mockNote = createMockNote(paragraphs); + const bindings = {note: mockNote}; - const ctrl = $componentController('job', null, bindings) - expect(ctrl).toBeDefined() + const ctrl = $componentController('job', null, bindings); + expect(ctrl).toBeDefined(); - const progress1 = ctrl.getProgress() - expect(progress1).toBe('100%') - }) + const progress1 = ctrl.getProgress(); + expect(progress1).toBe('100%'); + }); it('should get progress when there is pending and finished paragraphs', () => { const paragraphs = [ - { status: ParagraphStatus.PENDING }, - { status: ParagraphStatus.FINISHED}, - ] - const mockNote = createMockNote(paragraphs) - const bindings = { note: mockNote, } + {status: ParagraphStatus.PENDING}, + {status: ParagraphStatus.FINISHED}, + ]; + const mockNote = createMockNote(paragraphs); + const bindings = {note: mockNote}; - const ctrl = $componentController('job', null, bindings) + const ctrl = $componentController('job', null, bindings); - const progress1 = ctrl.getProgress() - expect(progress1).toBe('50%') - }) + const progress1 = ctrl.getProgress(); + expect(progress1).toBe('50%'); + }); it('should get proper job type icons', () => { - const paragraphs = [ { status: ParagraphStatus.PENDING }, ] - const mockNote = createMockNote(paragraphs) - const bindings = { note: mockNote, } + const paragraphs = [{status: ParagraphStatus.PENDING}]; + const mockNote = createMockNote(paragraphs); + const bindings = {note: mockNote}; - const ctrl = $componentController('job', null, bindings) + const ctrl = $componentController('job', null, bindings); - let icon = ctrl.getJobTypeIcon() - expect(icon).toBe('icon-doc') + let icon = ctrl.getJobTypeIcon(); + expect(icon).toBe('icon-doc'); - mockNote.noteType = 'cron' - icon = ctrl.getJobTypeIcon() - expect(icon).toBe('icon-clock') - }) + mockNote.noteType = 'cron'; + icon = ctrl.getJobTypeIcon(); + expect(icon).toBe('icon-clock'); + }); function createMockNote(paragraphs) { return { @@ -58,6 +58,6 @@ describe('JobComponent', () => { noteId: 'NT01', noteName: 'TestNote01', noteType: 'normal', - } + }; } -}) +}); diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.component.js b/zeppelin-web/src/app/jobmanager/jobmanager.component.js index 364cc45a0cc..c883a11effc 100644 --- a/zeppelin-web/src/app/jobmanager/jobmanager.component.js +++ b/zeppelin-web/src/app/jobmanager/jobmanager.component.js @@ -12,129 +12,132 @@ * limitations under the License. */ -import './job/job.component' -import { JobManagerFilter } from './jobmanager.filter' -import { JobManagerService} from './jobmanager.service' +import './job/job.component'; +import {JobManagerFilter} from './jobmanager.filter'; +import {JobManagerService} from './jobmanager.service'; -import { getJobIconByStatus, getJobColorByStatus } from './job-status' +import {getJobIconByStatus, getJobColorByStatus} from './job-status'; angular.module('zeppelinWebApp') .controller('JobManagerCtrl', JobManagerController) .filter('JobManager', JobManagerFilter) - .service('JobManagerService', JobManagerService) + .service('JobManagerService', JobManagerService); const JobDateSorter = { RECENTLY_UPDATED: 'Recently Update', OLDEST_UPDATED: 'Oldest Updated', -} +}; function JobManagerController($scope, ngToast, JobManagerFilter, JobManagerService) { - 'ngInject' + 'ngInject'; - $scope.isFilterLoaded = false - $scope.jobs = [] + $scope.isFilterLoaded = false; + $scope.jobs = []; $scope.sorter = { - availableDateSorter: Object.keys(JobDateSorter).map(key => { return JobDateSorter[key] }), + availableDateSorter: Object.keys(JobDateSorter).map((key) => { + return JobDateSorter[key]; + }), currentDateSorter: JobDateSorter.RECENTLY_UPDATED, - } - $scope.filteredJobs = $scope.jobs + }; + $scope.filteredJobs = $scope.jobs; $scope.filterConfig = { isRunningAlwaysTop: true, noteNameFilterValue: '', interpreterFilterValue: '*', isSortByAsc: true, - } + }; $scope.pagination = { currentPage: 1, itemsPerPage: 10, maxPageCount: 5, - } + }; - ngToast.dismiss() - init() + ngToast.dismiss(); + init(); /** functions */ $scope.setJobDateSorter = function(dateSorter) { - $scope.sorter.currentDateSorter = dateSorter - } + $scope.sorter.currentDateSorter = dateSorter; + }; $scope.getJobsInCurrentPage = function(jobs) { - const cp = $scope.pagination.currentPage - const itp = $scope.pagination.itemsPerPage - return jobs.slice((cp - 1) * itp, (cp * itp)) - } + const cp = $scope.pagination.currentPage; + const itp = $scope.pagination.itemsPerPage; + return jobs.slice((cp - 1) * itp, (cp * itp)); + }; - let asyncNotebookJobFilter = function (jobs, filterConfig) { + let asyncNotebookJobFilter = function(jobs, filterConfig) { return new Promise((resolve, reject) => { - $scope.filteredJobs = JobManagerFilter(jobs, filterConfig) - resolve($scope.filteredJobs) - }) - } + // eslint-disable-next-line new-cap + $scope.filteredJobs = JobManagerFilter(jobs, filterConfig); + resolve($scope.filteredJobs); + }); + }; $scope.$watch('sorter.currentDateSorter', function() { $scope.filterConfig.isSortByAsc = - $scope.sorter.currentDateSorter === JobDateSorter.OLDEST_UPDATED - asyncNotebookJobFilter($scope.jobs, $scope.filterConfig) - }) + $scope.sorter.currentDateSorter === JobDateSorter.OLDEST_UPDATED; + asyncNotebookJobFilter($scope.jobs, $scope.filterConfig); + }); - $scope.getJobIconByStatus = getJobIconByStatus - $scope.getJobColorByStatus = getJobColorByStatus + $scope.getJobIconByStatus = getJobIconByStatus; + $scope.getJobColorByStatus = getJobColorByStatus; - $scope.filterJobs = function (jobs, filterConfig) { + $scope.filterJobs = function(jobs, filterConfig) { asyncNotebookJobFilter(jobs, filterConfig) .then(() => { - $scope.isFilterLoaded = true - }) - .catch(error => { - console.error('Failed to search jobs from server', error) + $scope.isFilterLoaded = true; }) - } + .catch((error) => { + console.error('Failed to search jobs from server', error); + }); + }; - $scope.filterValueToName = function (filterValue, maxStringLength) { + $scope.filterValueToName = function(filterValue, maxStringLength) { if (typeof $scope.defaultInterpreters === 'undefined') { - return + return; } - let index = $scope.defaultInterpreters.findIndex(intp => intp.value === filterValue) + let index = $scope.defaultInterpreters.findIndex((intp) => intp.value === filterValue); if (typeof $scope.defaultInterpreters[index].name !== 'undefined') { if (typeof maxStringLength !== 'undefined' && maxStringLength > $scope.defaultInterpreters[index].name) { - return $scope.defaultInterpreters[index].name.substr(0, maxStringLength - 3) + '...' + return $scope.defaultInterpreters[index].name.substr(0, maxStringLength - 3) + '...'; } - return $scope.defaultInterpreters[index].name + return $scope.defaultInterpreters[index].name; } else { - return 'NONE' + return 'NONE'; } - } + }; - $scope.setFilterValue = function (filterValue) { - $scope.filterConfig.interpreterFilterValue = filterValue - $scope.filterJobs($scope.jobs, $scope.filterConfig) - } + $scope.setFilterValue = function(filterValue) { + $scope.filterConfig.interpreterFilterValue = filterValue; + $scope.filterJobs($scope.jobs, $scope.filterConfig); + }; $scope.setJobs = function(jobs) { - $scope.jobs = jobs + $scope.jobs = jobs; let interpreters = $scope.jobs - .filter(j => typeof j.interpreter !== 'undefined') - .map(j => j.interpreter) - interpreters = [...new Set(interpreters)] // remove duplicated interpreters + .filter((j) => typeof j.interpreter !== 'undefined') + .map((j) => j.interpreter); + interpreters = [...new Set(interpreters)]; // remove duplicated interpreters - $scope.defaultInterpreters = [ { name: 'ALL', value: '*' } ] + $scope.defaultInterpreters = [{name: 'ALL', value: '*'}]; for (let i = 0; i < interpreters.length; i++) { - $scope.defaultInterpreters.push({ name: interpreters[i], value: interpreters[i] }) + $scope.defaultInterpreters.push({name: interpreters[i], value: interpreters[i]}); } - } + }; function init() { - JobManagerService.getJobs() - JobManagerService.subscribeSetJobs($scope, setJobsCallback) - JobManagerService.subscribeUpdateJobs($scope, updateJobsCallback) + JobManagerService.getJobs(); + JobManagerService.subscribeSetJobs($scope, setJobsCallback); + JobManagerService.subscribeUpdateJobs($scope, updateJobsCallback); - $scope.$on('$destroy', function () { - JobManagerService.disconnect() - }) + $scope.$on('$destroy', function() { + JobManagerService.disconnect(); + }); } /* @@ -142,45 +145,45 @@ function JobManagerController($scope, ngToast, JobManagerFilter, JobManagerServi */ function setJobsCallback(event, response) { - const jobs = response.jobs - $scope.setJobs(jobs) - $scope.filterJobs($scope.jobs, $scope.filterConfig) + const jobs = response.jobs; + $scope.setJobs(jobs); + $scope.filterJobs($scope.jobs, $scope.filterConfig); } function updateJobsCallback(event, response) { - let jobs = $scope.jobs + let jobs = $scope.jobs; let jobByNoteId = jobs.reduce((acc, j) => { - const noteId = j.noteId - acc[noteId] = j - return acc - }, {}) + const noteId = j.noteId; + acc[noteId] = j; + return acc; + }, {}); - let updatedJobs = response.jobs - updatedJobs.map(updatedJob => { + let updatedJobs = response.jobs; + updatedJobs.map((updatedJob) => { if (typeof jobByNoteId[updatedJob.noteId] === 'undefined') { - let newItem = angular.copy(updatedJob) - jobs.push(newItem) - jobByNoteId[updatedJob.noteId] = newItem + let newItem = angular.copy(updatedJob); + jobs.push(newItem); + jobByNoteId[updatedJob.noteId] = newItem; } else { - let job = jobByNoteId[updatedJob.noteId] + let job = jobByNoteId[updatedJob.noteId]; if (updatedJob.isRemoved === true) { - delete jobByNoteId[updatedJob.noteId] - let removeIndex = jobs.findIndex(j => j.noteId === updatedJob.noteId) + delete jobByNoteId[updatedJob.noteId]; + let removeIndex = jobs.findIndex((j) => j.noteId === updatedJob.noteId); if (removeIndex) { - jobs.splice(removeIndex, 1) + jobs.splice(removeIndex, 1); } } else { // update the job - job.isRunningJob = updatedJob.isRunningJob - job.noteName = updatedJob.noteName - job.noteType = updatedJob.noteType - job.interpreter = updatedJob.interpreter - job.unixTimeLastRun = updatedJob.unixTimeLastRun - job.paragraphs = updatedJob.paragraphs + job.isRunningJob = updatedJob.isRunningJob; + job.noteName = updatedJob.noteName; + job.noteType = updatedJob.noteType; + job.interpreter = updatedJob.interpreter; + job.unixTimeLastRun = updatedJob.unixTimeLastRun; + job.paragraphs = updatedJob.paragraphs; } } - }) - $scope.filterJobs(jobs, $scope.filterConfig) + }); + $scope.filterJobs(jobs, $scope.filterConfig); } } diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.component.test.js b/zeppelin-web/src/app/jobmanager/jobmanager.component.test.js index a4b858b95e2..760414244ce 100644 --- a/zeppelin-web/src/app/jobmanager/jobmanager.component.test.js +++ b/zeppelin-web/src/app/jobmanager/jobmanager.component.test.js @@ -1,26 +1,26 @@ describe('JobManagerComponent', () => { - let $scope - let $controller + let $scope; + let $controller; - beforeEach(angular.mock.module('zeppelinWebApp')) + beforeEach(angular.mock.module('zeppelinWebApp')); beforeEach(angular.mock.inject((_$rootScope_, _$controller_) => { - $scope = _$rootScope_.$new() - $controller = _$controller_ - })) + $scope = _$rootScope_.$new(); + $controller = _$controller_; + })); it('should set jobs using `setJobs`', () => { - let ctrl = $controller('JobManagerCtrl', { $scope: $scope, }) - expect(ctrl).toBeDefined() + let ctrl = $controller('JobManagerCtrl', {$scope: $scope}); + expect(ctrl).toBeDefined(); const mockJobs = [ - { noteId: 'TN01', interpreter: 'spark', }, - { noteId: 'TN02', interpreter: 'spark', }, - ] + {noteId: 'TN01', interpreter: 'spark'}, + {noteId: 'TN02', interpreter: 'spark'}, + ]; - $scope.setJobs(mockJobs) + $scope.setJobs(mockJobs); expect($scope.defaultInterpreters).toEqual([ - { name: 'ALL', value: '*', }, - { name: 'spark', value: 'spark', }, - ]) - }) -}) + {name: 'ALL', value: '*'}, + {name: 'spark', value: 'spark'}, + ]); + }); +}); diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.filter.js b/zeppelin-web/src/app/jobmanager/jobmanager.filter.js index d6c8d69c744..c4abb1ca440 100644 --- a/zeppelin-web/src/app/jobmanager/jobmanager.filter.js +++ b/zeppelin-web/src/app/jobmanager/jobmanager.filter.js @@ -13,44 +13,44 @@ */ export function JobManagerFilter() { - function filterContext (jobs, filterConfig) { - let interpreter = filterConfig.interpreterFilterValue - let noteName = filterConfig.noteNameFilterValue - let isSortByAsc = filterConfig.isSortByAsc - let filteredJobs = jobs + function filterContext(jobs, filterConfig) { + let interpreter = filterConfig.interpreterFilterValue; + let noteName = filterConfig.noteNameFilterValue; + let isSortByAsc = filterConfig.isSortByAsc; + let filteredJobs = jobs; if (typeof interpreter === 'undefined') { filteredJobs = filteredJobs.filter((jobItem) => { - return typeof jobItem.interpreter === 'undefined' - }) + return typeof jobItem.interpreter === 'undefined'; + }); } else if (interpreter !== '*') { - filteredJobs = filteredJobs.filter(j => j.interpreter === interpreter) + filteredJobs = filteredJobs.filter((j) => j.interpreter === interpreter); } // filter by note name if (noteName !== '') { filteredJobs = filteredJobs.filter((jobItem) => { - let lowerFilterValue = noteName.toLocaleLowerCase() - let lowerNotebookName = jobItem.noteName.toLocaleLowerCase() - return lowerNotebookName.match(new RegExp('.*' + lowerFilterValue + '.*')) - }) + let lowerFilterValue = noteName.toLocaleLowerCase(); + let lowerNotebookName = jobItem.noteName.toLocaleLowerCase(); + return lowerNotebookName.match(new RegExp('.*' + lowerFilterValue + '.*')); + }); } // sort by name filteredJobs = filteredJobs.sort((jobItem) => { - return jobItem.noteName.toLowerCase() - }) + return jobItem.noteName.toLowerCase(); + }); // sort by timestamp filteredJobs = filteredJobs.sort((x, y) => { if (isSortByAsc) { - return x.unixTimeLastRun - y.unixTimeLastRun + return x.unixTimeLastRun - y.unixTimeLastRun; } else { - return y.unixTimeLastRun - x.unixTimeLastRun + return y.unixTimeLastRun - x.unixTimeLastRun; } - }) + }); - return filteredJobs + return filteredJobs; } - return filterContext + return filterContext; } diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.service.js b/zeppelin-web/src/app/jobmanager/jobmanager.service.js index 603950fee0e..472ac6d2f39 100644 --- a/zeppelin-web/src/app/jobmanager/jobmanager.service.js +++ b/zeppelin-web/src/app/jobmanager/jobmanager.service.js @@ -14,51 +14,51 @@ export class JobManagerService { constructor($http, $rootScope, baseUrlSrv, websocketMsgSrv) { - 'ngInject' + 'ngInject'; - this.$http = $http - this.$rootScope = $rootScope - this.BaseUrlService = baseUrlSrv - this.WebsocketMessageService = websocketMsgSrv + this.$http = $http; + this.$rootScope = $rootScope; + this.BaseUrlService = baseUrlSrv; + this.WebsocketMessageService = websocketMsgSrv; } sendStopJobRequest(noteId) { - const apiURL = this.BaseUrlService.getRestApiBase() + `/notebook/job/${noteId}` - return this.$http({ method: 'DELETE', url: apiURL, }) + const apiURL = this.BaseUrlService.getRestApiBase() + `/notebook/job/${noteId}`; + return this.$http({method: 'DELETE', url: apiURL}); } sendRunJobRequest(noteId) { - const apiURL = this.BaseUrlService.getRestApiBase() + `/notebook/job/${noteId}` - return this.$http({ method: 'POST', url: apiURL, }) + const apiURL = this.BaseUrlService.getRestApiBase() + `/notebook/job/${noteId}`; + return this.$http({method: 'POST', url: apiURL}); } getJobs() { - this.WebsocketMessageService.getJobs() + this.WebsocketMessageService.getJobs(); } disconnect() { - this.WebsocketMessageService.disconnectJobEvent() + this.WebsocketMessageService.disconnectJobEvent(); } subscribeSetJobs(controllerScope, receiveCallback) { - const event = 'jobmanager:set-jobs' - console.log(`(Event) Subscribed: ${event}`) - const unsubscribeHandler = this.$rootScope.$on(event, receiveCallback) + const event = 'jobmanager:set-jobs'; + console.log(`(Event) Subscribed: ${event}`); + const unsubscribeHandler = this.$rootScope.$on(event, receiveCallback); controllerScope.$on('$destroy', () => { - console.log(`(Event) Unsubscribed: ${event}`) - unsubscribeHandler() - }) + console.log(`(Event) Unsubscribed: ${event}`); + unsubscribeHandler(); + }); } subscribeUpdateJobs(controllerScope, receiveCallback) { - const event = 'jobmanager:update-jobs' - console.log(`(Event) Subscribed: ${event}`) - const unsubscribeHandler = this.$rootScope.$on(event, receiveCallback) + const event = 'jobmanager:update-jobs'; + console.log(`(Event) Subscribed: ${event}`); + const unsubscribeHandler = this.$rootScope.$on(event, receiveCallback); controllerScope.$on('$destroy', () => { - console.log(`(Event) Unsubscribed: ${event}`) - unsubscribeHandler() - }) + console.log(`(Event) Unsubscribed: ${event}`); + unsubscribeHandler(); + }); } } diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.service.test.js b/zeppelin-web/src/app/jobmanager/jobmanager.service.test.js index fbb082929c7..be7196d8b7b 100644 --- a/zeppelin-web/src/app/jobmanager/jobmanager.service.test.js +++ b/zeppelin-web/src/app/jobmanager/jobmanager.service.test.js @@ -1,56 +1,56 @@ -import { ParagraphStatus } from '../notebook/paragraph/paragraph.status' -import { JobManagerService } from './jobmanager.service' +import {ParagraphStatus} from '../notebook/paragraph/paragraph.status'; +import {JobManagerService} from './jobmanager.service'; describe('JobManagerService', () => { - const baseUrlSrvMock = { getRestApiBase: () => '' } - let service - let $httpBackend + const baseUrlSrvMock = {getRestApiBase: () => ''}; + let service; + let $httpBackend; - beforeEach(angular.mock.module('zeppelinWebApp')) + beforeEach(angular.mock.module('zeppelinWebApp')); beforeEach(angular.mock.inject((_$rootScope_, _$httpBackend_, _$http_, _websocketMsgSrv_) => { - $httpBackend = _$httpBackend_ - service = new JobManagerService(_$http_, _$rootScope_, baseUrlSrvMock, _websocketMsgSrv_) - })) + $httpBackend = _$httpBackend_; + service = new JobManagerService(_$http_, _$rootScope_, baseUrlSrvMock, _websocketMsgSrv_); + })); it('should sent valid request to run a job', () => { - const paragraphs = [ { status: ParagraphStatus.PENDING }, ] - const mockNote = createMockNote(paragraphs) + const paragraphs = [{status: ParagraphStatus.PENDING}]; + const mockNote = createMockNote(paragraphs); - const noteId = mockNote.noteId - service.sendRunJobRequest(noteId) + const noteId = mockNote.noteId; + service.sendRunJobRequest(noteId); - const url = `/notebook/job/${noteId}` + const url = `/notebook/job/${noteId}`; $httpBackend .when('POST', url) - .respond(200, { /** return nothing */ }) - $httpBackend.expectPOST(url) - $httpBackend.flush() + .respond(200, { /** return nothing */ }); + $httpBackend.expectPOST(url); + $httpBackend.flush(); - checkUnknownHttpRequests() - }) + checkUnknownHttpRequests(); + }); it('should sent valid request to stop a job', () => { - const paragraphs = [ { status: ParagraphStatus.PENDING }, ] - const mockNote = createMockNote(paragraphs) + const paragraphs = [{status: ParagraphStatus.PENDING}]; + const mockNote = createMockNote(paragraphs); - const noteId = mockNote.noteId - service.sendStopJobRequest(noteId) + const noteId = mockNote.noteId; + service.sendStopJobRequest(noteId); - const url = `/notebook/job/${noteId}` + const url = `/notebook/job/${noteId}`; $httpBackend .when('DELETE', url) - .respond(200, { /** return nothing */ }) - $httpBackend.expectDELETE(url) - $httpBackend.flush() + .respond(200, { /** return nothing */ }); + $httpBackend.expectDELETE(url); + $httpBackend.flush(); - checkUnknownHttpRequests() - }) + checkUnknownHttpRequests(); + }); function checkUnknownHttpRequests() { - $httpBackend.verifyNoOutstandingExpectation() - $httpBackend.verifyNoOutstandingRequest() + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); } function createMockNote(paragraphs) { @@ -60,6 +60,6 @@ describe('JobManagerService', () => { noteId: 'NT01', noteName: 'TestNote01', noteType: 'normal', - } + }; } -}) +}); diff --git a/zeppelin-web/src/app/notebook-repository/notebook-repository.controller.js b/zeppelin-web/src/app/notebook-repository/notebook-repository.controller.js index 0f62bc0c2a3..d6d13b32c79 100644 --- a/zeppelin-web/src/app/notebook-repository/notebook-repository.controller.js +++ b/zeppelin-web/src/app/notebook-repository/notebook-repository.controller.js @@ -12,76 +12,76 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').controller('NotebookRepositoryCtrl', NotebookRepositoryCtrl) +angular.module('zeppelinWebApp').controller('NotebookRepositoryCtrl', NotebookRepositoryCtrl); function NotebookRepositoryCtrl($http, baseUrlSrv, ngToast) { - 'ngInject' + 'ngInject'; - let vm = this - vm.notebookRepos = [] - vm.showDropdownSelected = showDropdownSelected - vm.saveNotebookRepo = saveNotebookRepo + let vm = this; + vm.notebookRepos = []; + vm.showDropdownSelected = showDropdownSelected; + vm.saveNotebookRepo = saveNotebookRepo; - _init() + _init(); // Public functions - function saveNotebookRepo (valueform, repo, data) { - console.log('data %o', data) + function saveNotebookRepo(valueform, repo, data) { + console.log('data %o', data); $http.put(baseUrlSrv.getRestApiBase() + '/notebook-repositories', { 'name': repo.className, - 'settings': data - }).success(function (data) { - let index = _.findIndex(vm.notebookRepos, {'className': repo.className}) + 'settings': data, + }).success(function(data) { + let index = _.findIndex(vm.notebookRepos, {'className': repo.className}); if (index >= 0) { - vm.notebookRepos[index] = data.body - console.log('repos %o, data %o', vm.notebookRepos, data.body) + vm.notebookRepos[index] = data.body; + console.log('repos %o, data %o', vm.notebookRepos, data.body); } - valueform.$show() - }).error(function () { + valueform.$show(); + }).error(function() { ngToast.danger({ content: 'We couldn\'t save that NotebookRepo\'s settings', verticalPosition: 'bottom', - timeout: '3000' - }) - valueform.$show() - }) + timeout: '3000', + }); + valueform.$show(); + }); - return 'manual' + return 'manual'; } - function showDropdownSelected (setting) { - let index = _.findIndex(setting.value, {'value': setting.selected}) + function showDropdownSelected(setting) { + let index = _.findIndex(setting.value, {'value': setting.selected}); if (index < 0) { - return 'No value' + return 'No value'; } else { - return setting.value[index].name + return setting.value[index].name; } } // Private functions - function _getInterpreterSettings () { + function _getInterpreterSettings() { $http.get(baseUrlSrv.getRestApiBase() + '/notebook-repositories') - .success(function (data, status, headers, config) { - vm.notebookRepos = data.body - console.log('ya notebookRepos %o', vm.notebookRepos) - }).error(function (data, status, headers, config) { + .success(function(data, status, headers, config) { + vm.notebookRepos = data.body; + console.log('ya notebookRepos %o', vm.notebookRepos); + }).error(function(data, status, headers, config) { if (status === 401) { ngToast.danger({ content: 'You don\'t have permission on this page', verticalPosition: 'bottom', - timeout: '3000' - }) - setTimeout(function () { - window.location = baseUrlSrv.getBase() - }, 3000) + timeout: '3000', + }); + setTimeout(function() { + window.location = baseUrlSrv.getBase(); + }, 3000); } - console.log('Error %o %o', status, data.message) - }) + console.log('Error %o %o', status, data.message); + }); } - function _init () { - _getInterpreterSettings() + function _init() { + _getInterpreterSettings(); } } diff --git a/zeppelin-web/src/app/notebook/dropdown-input/dropdown-input.directive.js b/zeppelin-web/src/app/notebook/dropdown-input/dropdown-input.directive.js index a64204af063..0fe43f7eaa3 100644 --- a/zeppelin-web/src/app/notebook/dropdown-input/dropdown-input.directive.js +++ b/zeppelin-web/src/app/notebook/dropdown-input/dropdown-input.directive.js @@ -12,15 +12,15 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').directive('dropdownInput', dropdownInputDirective) +angular.module('zeppelinWebApp').directive('dropdownInput', dropdownInputDirective); function dropdownInputDirective() { return { restrict: 'A', - link: function (scope, element) { - element.bind('click', function (event) { - event.stopPropagation() - }) - } - } + link: function(scope, element) { + element.bind('click', function(event) { + event.stopPropagation(); + }); + }, + }; } diff --git a/zeppelin-web/src/app/notebook/dynamic-forms/dynamic-forms.directive.js b/zeppelin-web/src/app/notebook/dynamic-forms/dynamic-forms.directive.js index 40a70eb94e0..1b1043e8b75 100644 --- a/zeppelin-web/src/app/notebook/dynamic-forms/dynamic-forms.directive.js +++ b/zeppelin-web/src/app/notebook/dynamic-forms/dynamic-forms.directive.js @@ -12,9 +12,9 @@ * limitations under the License. */ -import './dynamic-forms.css' +import './dynamic-forms.css'; -angular.module('zeppelinWebApp').directive('dynamicForms', DynamicFormDirective) +angular.module('zeppelinWebApp').directive('dynamicForms', DynamicFormDirective); function DynamicFormDirective($templateRequest, $compile) { return { @@ -27,36 +27,36 @@ function DynamicFormDirective($templateRequest, $compile) { forms: '=forms', params: '=params', action: '=action', - removeaction: '=removeaction' + removeaction: '=removeaction', }, - link: function (scope, element, attrs, controller) { - scope.loadForm = this.loadForm - scope.toggleCheckbox = this.toggleCheckbox - $templateRequest('app/notebook/dynamic-forms/dynamic-forms.directive.html').then(function (formsHtml) { - let forms = angular.element(formsHtml) - element.append(forms) - $compile(forms)(scope) - }) + link: function(scope, element, attrs, controller) { + scope.loadForm = this.loadForm; + scope.toggleCheckbox = this.toggleCheckbox; + $templateRequest('app/notebook/dynamic-forms/dynamic-forms.directive.html').then(function(formsHtml) { + let forms = angular.element(formsHtml); + element.append(forms); + $compile(forms)(scope); + }); }, - loadForm: function (formulaire, params) { - let value = formulaire.defaultValue + loadForm: function(formulaire, params) { + let value = formulaire.defaultValue; if (params[formulaire.name]) { - value = params[formulaire.name] + value = params[formulaire.name]; } - params[formulaire.name] = value + params[formulaire.name] = value; }, - toggleCheckbox: function (formulaire, option, params) { - let idx = params[formulaire.name].indexOf(option.value) + toggleCheckbox: function(formulaire, option, params) { + let idx = params[formulaire.name].indexOf(option.value); if (idx > -1) { - params[formulaire.name].splice(idx, 1) + params[formulaire.name].splice(idx, 1); } else { - params[formulaire.name].push(option.value) + params[formulaire.name].push(option.value); } - } + }, - } + }; } diff --git a/zeppelin-web/src/app/notebook/elastic-input/elastic-input.controller.js b/zeppelin-web/src/app/notebook/elastic-input/elastic-input.controller.js index 507b2f61493..c11f95ba124 100644 --- a/zeppelin-web/src/app/notebook/elastic-input/elastic-input.controller.js +++ b/zeppelin-web/src/app/notebook/elastic-input/elastic-input.controller.js @@ -12,10 +12,10 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').controller('ElasticInputCtrl', ElasticInputCtrl) +angular.module('zeppelinWebApp').controller('ElasticInputCtrl', ElasticInputCtrl); -function ElasticInputCtrl () { - let vm = this - vm.showEditor = false - vm.value = '' +function ElasticInputCtrl() { + let vm = this; + vm.showEditor = false; + vm.value = ''; } diff --git a/zeppelin-web/src/app/notebook/note-var-share.service.js b/zeppelin-web/src/app/notebook/note-var-share.service.js index e79f389cc3e..a5975ce49a5 100644 --- a/zeppelin-web/src/app/notebook/note-var-share.service.js +++ b/zeppelin-web/src/app/notebook/note-var-share.service.js @@ -12,28 +12,28 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').service('noteVarShareService', NoteVarShareService) +angular.module('zeppelinWebApp').service('noteVarShareService', NoteVarShareService); -function NoteVarShareService () { - 'ngInject' +function NoteVarShareService() { + 'ngInject'; - let store = {} + let store = {}; - this.clear = function () { - store = {} - } + this.clear = function() { + store = {}; + }; - this.put = function (key, value) { - store[key] = value - } + this.put = function(key, value) { + store[key] = value; + }; - this.get = function (key) { - return store[key] - } + this.get = function(key) { + return store[key]; + }; - this.del = function (key) { - let v = store[key] - delete store[key] - return v - } + this.del = function(key) { + let v = store[key]; + delete store[key]; + return v; + }; } diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index b02b74ee8d5..05ab9fb7992 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -12,30 +12,30 @@ * limitations under the License. */ -import moment from 'moment' +import moment from 'moment'; -import { isParagraphRunning, } from './paragraph/paragraph.status' +import {isParagraphRunning} from './paragraph/paragraph.status'; -angular.module('zeppelinWebApp').controller('NotebookCtrl', NotebookCtrl) +angular.module('zeppelinWebApp').controller('NotebookCtrl', NotebookCtrl); -function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, +function NotebookCtrl($scope, $route, $routeParams, $location, $rootScope, $http, websocketMsgSrv, baseUrlSrv, $timeout, saveAsService, ngToast, noteActionService, noteVarShareService, TRASH_FOLDER_ID, heliumService) { - 'ngInject' - - ngToast.dismiss() - - $scope.note = null - $scope.actionOnFormSelectionChange = true - $scope.hideForms = false - $scope.disableForms = false - $scope.editorToggled = false - $scope.tableToggled = false - $scope.viewOnly = false - $scope.showSetting = false - $scope.showRevisionsComparator = false - $scope.looknfeelOption = ['default', 'simple', 'report'] + 'ngInject'; + + ngToast.dismiss(); + + $scope.note = null; + $scope.actionOnFormSelectionChange = true; + $scope.hideForms = false; + $scope.disableForms = false; + $scope.editorToggled = false; + $scope.tableToggled = false; + $scope.viewOnly = false; + $scope.showSetting = false; + $scope.showRevisionsComparator = false; + $scope.looknfeelOption = ['default', 'simple', 'report']; $scope.cronOption = [ {name: 'None', value: undefined}, {name: '1m', value: '0 0/1 * * * ?'}, @@ -44,28 +44,28 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, {name: '3h', value: '0 0 0/3 * * ?'}, {name: '6h', value: '0 0 0/6 * * ?'}, {name: '12h', value: '0 0 0/12 * * ?'}, - {name: '1d', value: '0 0 0 * * ?'} - ] + {name: '1d', value: '0 0 0 * * ?'}, + ]; $scope.formatRevisionDate = function(date) { - return moment.unix(date).format('MMMM Do YYYY, h:mm a') - } + return moment.unix(date).format('MMMM Do YYYY, h:mm a'); + }; - $scope.interpreterSettings = [] - $scope.interpreterBindings = [] - $scope.isNoteDirty = null - $scope.saveTimer = null - $scope.paragraphWarningDialog = {} + $scope.interpreterSettings = []; + $scope.interpreterBindings = []; + $scope.isNoteDirty = null; + $scope.saveTimer = null; + $scope.paragraphWarningDialog = {}; - let connectedOnce = false - let isRevisionPath = function (path) { - let pattern = new RegExp('^.*\/notebook\/[a-zA-Z0-9_]*\/revision\/[a-zA-Z0-9_]*') - return pattern.test(path) - } + let connectedOnce = false; + let isRevisionPath = function(path) { + let pattern = new RegExp('^.*\/notebook\/[a-zA-Z0-9_]*\/revision\/[a-zA-Z0-9_]*'); + return pattern.test(path); + }; - $scope.noteRevisions = [] - $scope.currentRevision = 'Head' - $scope.revisionView = isRevisionPath($location.path()) + $scope.noteRevisions = []; + $scope.currentRevision = 'Head'; + $scope.revisionView = isRevisionPath($location.path()); $scope.search = { searchText: '', @@ -78,48 +78,48 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, currentOccurrence: 0, searchBoxOpened: false, searchBoxWidth: 350, - left: '0px' - } - let currentSearchParagraph = 0 + left: '0px', + }; + let currentSearchParagraph = 0; - $scope.$watch('note', function (value) { - let title + $scope.$watch('note', function(value) { + let title; if (value) { - title = value.name.substr(value.name.lastIndexOf('/') + 1, value.name.length) - title += ' - Zeppelin' + title = value.name.substr(value.name.lastIndexOf('/') + 1, value.name.length); + title += ' - Zeppelin'; } else { - title = 'Zeppelin' + title = 'Zeppelin'; } - $rootScope.pageTitle = title - }, true) + $rootScope.pageTitle = title; + }, true); - $scope.$on('setConnectedStatus', function (event, param) { + $scope.$on('setConnectedStatus', function(event, param) { if (connectedOnce && param) { - initNotebook() + initNotebook(); } - connectedOnce = true - }) + connectedOnce = true; + }); - $scope.getCronOptionNameFromValue = function (value) { + $scope.getCronOptionNameFromValue = function(value) { if (!value) { - return '' + return ''; } for (let o in $scope.cronOption) { if ($scope.cronOption[o].value === value) { - return $scope.cronOption[o].name + return $scope.cronOption[o].name; } } - return value - } + return value; + }; - $scope.blockAnonUsers = function () { - let zeppelinVersion = $rootScope.zeppelinVersion - let url = 'https://zeppelin.apache.org/docs/' + zeppelinVersion + '/security/notebook_authorization.html' + $scope.blockAnonUsers = function() { + let zeppelinVersion = $rootScope.zeppelinVersion; + let url = 'https://zeppelin.apache.org/docs/' + zeppelinVersion + '/security/notebook_authorization.html'; let content = 'Only authenticated user can set the permission.' + '' + '' + - '' + ''; BootstrapDialog.show({ closable: false, closeByBackdrop: false, @@ -128,866 +128,878 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, message: content, buttons: [{ label: 'Close', - action: function (dialog) { - dialog.close() - } - }] - }) - } + action: function(dialog) { + dialog.close(); + }, + }], + }); + }; /** Init the new controller */ - const initNotebook = function () { - noteVarShareService.clear() + const initNotebook = function() { + noteVarShareService.clear(); if ($routeParams.revisionId) { - websocketMsgSrv.getNoteByRevision($routeParams.noteId, $routeParams.revisionId) + websocketMsgSrv.getNoteByRevision($routeParams.noteId, $routeParams.revisionId); } else { - websocketMsgSrv.getNote($routeParams.noteId) + websocketMsgSrv.getNote($routeParams.noteId); } - websocketMsgSrv.listRevisionHistory($routeParams.noteId) - let currentRoute = $route.current + websocketMsgSrv.listRevisionHistory($routeParams.noteId); + let currentRoute = $route.current; if (currentRoute) { setTimeout( - function () { - let routeParams = currentRoute.params - let $id = angular.element('#' + routeParams.paragraph + '_container') + function() { + let routeParams = currentRoute.params; + let $id = angular.element('#' + routeParams.paragraph + '_container'); if ($id.length > 0) { // adjust for navbar - let top = $id.offset().top - 103 - angular.element('html, body').scrollTo({top: top, left: 0}) + let top = $id.offset().top - 103; + angular.element('html, body').scrollTo({top: top, left: 0}); } }, 1000 - ) + ); } - } + }; - initNotebook() + initNotebook(); - $scope.focusParagraphOnClick = function (clickEvent) { + $scope.focusParagraphOnClick = function(clickEvent) { if (!$scope.note) { - return + return; } for (let i = 0; i < $scope.note.paragraphs.length; i++) { - let paragraphId = $scope.note.paragraphs[i].id + let paragraphId = $scope.note.paragraphs[i].id; if (jQuery.contains(angular.element('#' + paragraphId + '_container')[0], clickEvent.target)) { - $scope.$broadcast('focusParagraph', paragraphId, 0, null, true) - break + $scope.$broadcast('focusParagraph', paragraphId, 0, null, true); + break; } } - } + }; // register mouseevent handler for focus paragraph - document.addEventListener('click', $scope.focusParagraphOnClick) + document.addEventListener('click', $scope.focusParagraphOnClick); - let keyboardShortcut = function (keyEvent) { + let keyboardShortcut = function(keyEvent) { // handle keyevent if (!$scope.viewOnly && !$scope.revisionView) { - $scope.$broadcast('keyEvent', keyEvent) + $scope.$broadcast('keyEvent', keyEvent); } - } + }; - $scope.keydownEvent = function (keyEvent) { + $scope.keydownEvent = function(keyEvent) { if ((keyEvent.ctrlKey || keyEvent.metaKey) && String.fromCharCode(keyEvent.which).toLowerCase() === 's') { - keyEvent.preventDefault() + keyEvent.preventDefault(); } - keyboardShortcut(keyEvent) - } + keyboardShortcut(keyEvent); + }; // register mouseevent handler for focus paragraph - document.addEventListener('keydown', $scope.keydownEvent) + document.addEventListener('keydown', $scope.keydownEvent); - $scope.paragraphOnDoubleClick = function (paragraphId) { - $scope.$broadcast('doubleClickParagraph', paragraphId) - } + $scope.paragraphOnDoubleClick = function(paragraphId) { + $scope.$broadcast('doubleClickParagraph', paragraphId); + }; // Move the note to trash and go back to the main page - $scope.moveNoteToTrash = function (noteId) { - noteActionService.moveNoteToTrash(noteId, true) - } + $scope.moveNoteToTrash = function(noteId) { + noteActionService.moveNoteToTrash(noteId, true); + }; // Remove the note permanently if it's in the trash - $scope.removeNote = function (noteId) { - noteActionService.removeNote(noteId, true) - } + $scope.removeNote = function(noteId) { + noteActionService.removeNote(noteId, true); + }; - $scope.isTrash = function (note) { - return note ? note.name.split('/')[0] === TRASH_FOLDER_ID : false - } + $scope.isTrash = function(note) { + return note ? note.name.split('/')[0] === TRASH_FOLDER_ID : false; + }; // Export notebook - $scope.exportNote = function () { - let jsonContent = JSON.stringify($scope.note) - saveAsService.saveAs(jsonContent, $scope.note.name, 'json') - } + $scope.exportNote = function() { + let jsonContent = JSON.stringify($scope.note); + saveAsService.saveAs(jsonContent, $scope.note.name, 'json'); + }; // Clone note - $scope.cloneNote = function (noteId) { + $scope.cloneNote = function(noteId) { BootstrapDialog.confirm({ closable: true, title: '', message: 'Do you want to clone this note?', - callback: function (result) { + callback: function(result) { if (result) { - websocketMsgSrv.cloneNote(noteId) - $location.path('/') + websocketMsgSrv.cloneNote(noteId); + $location.path('/'); } - } - }) - } + }, + }); + }; // checkpoint/commit notebook - $scope.checkpointNote = function (commitMessage) { + $scope.checkpointNote = function(commitMessage) { BootstrapDialog.confirm({ closable: true, title: '', message: 'Commit note to current repository?', - callback: function (result) { + callback: function(result) { if (result) { - websocketMsgSrv.checkpointNote($routeParams.noteId, commitMessage) + websocketMsgSrv.checkpointNote($routeParams.noteId, commitMessage); } - } - }) - document.getElementById('note.checkpoint.message').value = '' - } + }, + }); + document.getElementById('note.checkpoint.message').value = ''; + }; // set notebook head to given revision - $scope.setNoteRevision = function () { + $scope.setNoteRevision = function() { BootstrapDialog.confirm({ closable: true, title: '', message: 'Set notebook head to current revision?', - callback: function (result) { + callback: function(result) { if (result) { - websocketMsgSrv.setNoteRevision($routeParams.noteId, $routeParams.revisionId) + websocketMsgSrv.setNoteRevision($routeParams.noteId, $routeParams.revisionId); } - } - }) - } + }, + }); + }; $scope.preVisibleRevisionsComparator = function() { - $scope.mergeNoteRevisionsForCompare = null - $scope.firstNoteRevisionForCompare = null - $scope.secondNoteRevisionForCompare = null - $scope.currentFirstRevisionForCompare = 'Choose...' - $scope.currentSecondRevisionForCompare = 'Choose...' - $scope.$apply() - } - - $scope.$on('listRevisionHistory', function (event, data) { - console.debug('received list of revisions %o', data) - $scope.noteRevisions = data.revisionList + $scope.mergeNoteRevisionsForCompare = null; + $scope.firstNoteRevisionForCompare = null; + $scope.secondNoteRevisionForCompare = null; + $scope.currentFirstRevisionForCompare = 'Choose...'; + $scope.currentSecondRevisionForCompare = 'Choose...'; + $scope.$apply(); + }; + + $scope.$on('listRevisionHistory', function(event, data) { + console.debug('received list of revisions %o', data); + $scope.noteRevisions = data.revisionList; if ($scope.noteRevisions.length === 0 || $scope.noteRevisions[0].id !== 'Head') { $scope.noteRevisions.splice(0, 0, { id: 'Head', - message: 'Head' - }) + message: 'Head', + }); } if ($routeParams.revisionId) { - let index = _.findIndex($scope.noteRevisions, {'id': $routeParams.revisionId}) + let index = _.findIndex($scope.noteRevisions, {'id': $routeParams.revisionId}); if (index > -1) { - $scope.currentRevision = $scope.noteRevisions[index].message + $scope.currentRevision = $scope.noteRevisions[index].message; } } - }) + }); - $scope.$on('noteRevision', function (event, data) { - console.log('received note revision %o', data) + $scope.$on('noteRevision', function(event, data) { + console.log('received note revision %o', data); if (data.note) { - $scope.note = data.note - initializeLookAndFeel() + $scope.note = data.note; + initializeLookAndFeel(); } else { - $location.path('/') + $location.path('/'); } - }) + }); - $scope.$on('setNoteRevisionResult', function (event, data) { - console.log('received set note revision result %o', data) + $scope.$on('setNoteRevisionResult', function(event, data) { + console.log('received set note revision result %o', data); if (data.status) { - $location.path('/notebook/' + $routeParams.noteId) + $location.path('/notebook/' + $routeParams.noteId); } - }) + }); - $scope.visitRevision = function (revision) { + $scope.visitRevision = function(revision) { if (revision.id) { if (revision.id === 'Head') { - $location.path('/notebook/' + $routeParams.noteId) + $location.path('/notebook/' + $routeParams.noteId); } else { - $location.path('/notebook/' + $routeParams.noteId + '/revision/' + revision.id) + $location.path('/notebook/' + $routeParams.noteId + '/revision/' + revision.id); } } else { ngToast.danger({content: 'There is a problem with this Revision', verticalPosition: 'top', - dismissOnTimeout: false - }) + dismissOnTimeout: false, + }); } - } + }; - $scope.runAllParagraphs = function (noteId) { + $scope.runAllParagraphs = function(noteId) { BootstrapDialog.confirm({ closable: true, title: '', message: 'Run all paragraphs?', - callback: function (result) { + callback: function(result) { if (result) { - const paragraphs = $scope.note.paragraphs.map(p => { + const paragraphs = $scope.note.paragraphs.map((p) => { return { id: p.id, title: p.title, paragraph: p.text, config: p.config, - params: p.settings.params - } - }) - websocketMsgSrv.runAllParagraphs(noteId, paragraphs) + params: p.settings.params, + }; + }); + websocketMsgSrv.runAllParagraphs(noteId, paragraphs); } - } - }) - } + }, + }); + }; - $scope.saveNote = function () { + $scope.saveNote = function() { if ($scope.note && $scope.note.paragraphs) { - _.forEach($scope.note.paragraphs, function (par) { + _.forEach($scope.note.paragraphs, function(par) { angular .element('#' + par.id + '_paragraphColumn_main') .scope() - .saveParagraph(par) - }) - $scope.isNoteDirty = null + .saveParagraph(par); + }); + $scope.isNoteDirty = null; } - } + }; - $scope.clearAllParagraphOutput = function (noteId) { - noteActionService.clearAllParagraphOutput(noteId) - } + $scope.clearAllParagraphOutput = function(noteId) { + noteActionService.clearAllParagraphOutput(noteId); + }; - $scope.toggleAllEditor = function () { + $scope.toggleAllEditor = function() { if ($scope.editorToggled) { - $scope.$broadcast('openEditor') + $scope.$broadcast('openEditor'); } else { - $scope.$broadcast('closeEditor') + $scope.$broadcast('closeEditor'); } - $scope.editorToggled = !$scope.editorToggled - } + $scope.editorToggled = !$scope.editorToggled; + }; - $scope.showAllEditor = function () { - $scope.$broadcast('openEditor') - } + $scope.showAllEditor = function() { + $scope.$broadcast('openEditor'); + }; - $scope.hideAllEditor = function () { - $scope.$broadcast('closeEditor') - } + $scope.hideAllEditor = function() { + $scope.$broadcast('closeEditor'); + }; - $scope.toggleAllTable = function () { + $scope.toggleAllTable = function() { if ($scope.tableToggled) { - $scope.$broadcast('openTable') + $scope.$broadcast('openTable'); } else { - $scope.$broadcast('closeTable') + $scope.$broadcast('closeTable'); } - $scope.tableToggled = !$scope.tableToggled - } + $scope.tableToggled = !$scope.tableToggled; + }; - $scope.showAllTable = function () { - $scope.$broadcast('openTable') - } + $scope.showAllTable = function() { + $scope.$broadcast('openTable'); + }; - $scope.hideAllTable = function () { - $scope.$broadcast('closeTable') - } + $scope.hideAllTable = function() { + $scope.$broadcast('closeTable'); + }; /** * @returns {boolean} true if one more paragraphs are running. otherwise return false. */ - $scope.isNoteRunning = function () { - if (!$scope.note) { return false } + $scope.isNoteRunning = function() { + if (!$scope.note) { + return false; + } for (let i = 0; i < $scope.note.paragraphs.length; i++) { if (isParagraphRunning($scope.note.paragraphs[i])) { - return true + return true; } } - return false - } + return false; + }; - $scope.killSaveTimer = function () { + $scope.killSaveTimer = function() { if ($scope.saveTimer) { - $timeout.cancel($scope.saveTimer) - $scope.saveTimer = null + $timeout.cancel($scope.saveTimer); + $scope.saveTimer = null; } - } + }; - $scope.startSaveTimer = function () { - $scope.killSaveTimer() - $scope.isNoteDirty = true + $scope.startSaveTimer = function() { + $scope.killSaveTimer(); + $scope.isNoteDirty = true; // console.log('startSaveTimer called ' + $scope.note.id); - $scope.saveTimer = $timeout(function () { - $scope.saveNote() - }, 10000) - } + $scope.saveTimer = $timeout(function() { + $scope.saveNote(); + }, 10000); + }; - $scope.setLookAndFeel = function (looknfeel) { - $scope.note.config.looknfeel = looknfeel + $scope.setLookAndFeel = function(looknfeel) { + $scope.note.config.looknfeel = looknfeel; if ($scope.revisionView === true) { - $rootScope.$broadcast('setLookAndFeel', $scope.note.config.looknfeel) + $rootScope.$broadcast('setLookAndFeel', $scope.note.config.looknfeel); } else { - $scope.setConfig() + $scope.setConfig(); } - } + }; /** Set cron expression for this note **/ - $scope.setCronScheduler = function (cronExpr) { + $scope.setCronScheduler = function(cronExpr) { if (cronExpr) { if (!$scope.note.config.cronExecutingUser) { - $scope.note.config.cronExecutingUser = $rootScope.ticket.principal + $scope.note.config.cronExecutingUser = $rootScope.ticket.principal; } } else { - $scope.note.config.cronExecutingUser = '' + $scope.note.config.cronExecutingUser = ''; } - $scope.note.config.cron = cronExpr - $scope.setConfig() - } + $scope.note.config.cron = cronExpr; + $scope.setConfig(); + }; /** Set the username of the user to be used to execute all notes in notebook **/ - $scope.setCronExecutingUser = function (cronExecutingUser) { - $scope.note.config.cronExecutingUser = cronExecutingUser - $scope.setConfig() - } + $scope.setCronExecutingUser = function(cronExecutingUser) { + $scope.note.config.cronExecutingUser = cronExecutingUser; + $scope.setConfig(); + }; /** Set release resource for this note **/ - $scope.setReleaseResource = function (value) { - $scope.note.config.releaseresource = value - $scope.setConfig() - } + $scope.setReleaseResource = function(value) { + $scope.note.config.releaseresource = value; + $scope.setConfig(); + }; /** Update note config **/ - $scope.setConfig = function (config) { + $scope.setConfig = function(config) { if (config) { - $scope.note.config = config + $scope.note.config = config; } - websocketMsgSrv.updateNote($scope.note.id, $scope.note.name, $scope.note.config) - } + websocketMsgSrv.updateNote($scope.note.id, $scope.note.name, $scope.note.config); + }; /** Update the note name */ - $scope.updateNoteName = function (newName) { - const trimmedNewName = newName.trim() + $scope.updateNoteName = function(newName) { + const trimmedNewName = newName.trim(); if (trimmedNewName.length > 0 && $scope.note.name !== trimmedNewName) { - $scope.note.name = trimmedNewName - websocketMsgSrv.renameNote($scope.note.id, $scope.note.name) + $scope.note.name = trimmedNewName; + websocketMsgSrv.renameNote($scope.note.id, $scope.note.name); } - } + }; - const initializeLookAndFeel = function () { + const initializeLookAndFeel = function() { if (!$scope.note.config.looknfeel) { - $scope.note.config.looknfeel = 'default' + $scope.note.config.looknfeel = 'default'; } else { - $scope.viewOnly = $scope.note.config.looknfeel === 'report' ? true : false + $scope.viewOnly = $scope.note.config.looknfeel === 'report' ? true : false; } if ($scope.note.paragraphs && $scope.note.paragraphs[0]) { - $scope.note.paragraphs[0].focus = true - } - $rootScope.$broadcast('setLookAndFeel', $scope.note.config.looknfeel) - } - - let cleanParagraphExcept = function (paragraphId, note) { - let noteCopy = {} - noteCopy.id = note.id - noteCopy.name = note.name - noteCopy.config = note.config - noteCopy.info = note.info - noteCopy.paragraphs = [] + $scope.note.paragraphs[0].focus = true; + } + $rootScope.$broadcast('setLookAndFeel', $scope.note.config.looknfeel); + }; + + let cleanParagraphExcept = function(paragraphId, note) { + let noteCopy = {}; + noteCopy.id = note.id; + noteCopy.name = note.name; + noteCopy.config = note.config; + noteCopy.info = note.info; + noteCopy.paragraphs = []; for (let i = 0; i < note.paragraphs.length; i++) { if (note.paragraphs[i].id === paragraphId) { - noteCopy.paragraphs[0] = note.paragraphs[i] + noteCopy.paragraphs[0] = note.paragraphs[i]; if (!noteCopy.paragraphs[0].config) { - noteCopy.paragraphs[0].config = {} + noteCopy.paragraphs[0].config = {}; } - noteCopy.paragraphs[0].config.editorHide = true - noteCopy.paragraphs[0].config.tableHide = false - break + noteCopy.paragraphs[0].config.editorHide = true; + noteCopy.paragraphs[0].config.tableHide = false; + break; } } - return noteCopy - } + return noteCopy; + }; - let addPara = function (paragraph, index) { - $scope.note.paragraphs.splice(index, 0, paragraph) - $scope.note.paragraphs.map(para => { + let addPara = function(paragraph, index) { + $scope.note.paragraphs.splice(index, 0, paragraph); + $scope.note.paragraphs.map((para) => { if (para.id === paragraph.id) { - para.focus = true + para.focus = true; // we need `$timeout` since angular DOM might not be initialized - $timeout(() => { $scope.$broadcast('focusParagraph', para.id, 0, null, false) }) + $timeout(() => { + $scope.$broadcast('focusParagraph', para.id, 0, null, false); + }); } - }) - } + }); + }; - let removePara = function (paragraphId) { - let removeIdx - _.each($scope.note.paragraphs, function (para, idx) { + let removePara = function(paragraphId) { + let removeIdx; + _.each($scope.note.paragraphs, function(para, idx) { if (para.id === paragraphId) { - removeIdx = idx + removeIdx = idx; } - }) - return $scope.note.paragraphs.splice(removeIdx, 1) - } + }); + return $scope.note.paragraphs.splice(removeIdx, 1); + }; - $scope.$on('addParagraph', function (event, paragraph, index) { + $scope.$on('addParagraph', function(event, paragraph, index) { if ($scope.paragraphUrl || $scope.revisionView === true) { - return + return; } - addPara(paragraph, index) - }) + addPara(paragraph, index); + }); - $scope.$on('removeParagraph', function (event, paragraphId) { + $scope.$on('removeParagraph', function(event, paragraphId) { if ($scope.paragraphUrl || $scope.revisionView === true) { - return + return; } - removePara(paragraphId) - }) + removePara(paragraphId); + }); - $scope.$on('moveParagraph', function (event, paragraphId, newIdx) { + $scope.$on('moveParagraph', function(event, paragraphId, newIdx) { if ($scope.revisionView === true) { - return + return; } - let removedPara = removePara(paragraphId) + let removedPara = removePara(paragraphId); if (removedPara && removedPara.length === 1) { - addPara(removedPara[0], newIdx) + addPara(removedPara[0], newIdx); } - }) + }); - $scope.$on('updateNote', function (event, name, config, info) { + $scope.$on('updateNote', function(event, name, config, info) { /** update Note name */ if (name !== $scope.note.name) { - console.log('change note name to : %o', $scope.note.name) - $scope.note.name = name + console.log('change note name to : %o', $scope.note.name); + $scope.note.name = name; } - $scope.note.config = config - $scope.note.info = info - initializeLookAndFeel() - }) + $scope.note.config = config; + $scope.note.info = info; + initializeLookAndFeel(); + }); - let getInterpreterBindings = function () { - websocketMsgSrv.getInterpreterBindings($scope.note.id) - } + let getInterpreterBindings = function() { + websocketMsgSrv.getInterpreterBindings($scope.note.id); + }; - $scope.$on('interpreterBindings', function (event, data) { - $scope.interpreterBindings = data.interpreterBindings - $scope.interpreterBindingsOrig = angular.copy($scope.interpreterBindings) // to check dirty + $scope.$on('interpreterBindings', function(event, data) { + $scope.interpreterBindings = data.interpreterBindings; + $scope.interpreterBindingsOrig = angular.copy($scope.interpreterBindings); // to check dirty - let selected = false - let key - let setting + let selected = false; + let key; + let setting; for (key in $scope.interpreterBindings) { - setting = $scope.interpreterBindings[key] - if (setting.selected) { - selected = true - break + if($scope.interpreterBindings.hasOwnProperty(key)) { + setting = $scope.interpreterBindings[key]; + if (setting.selected) { + selected = true; + break; + } } } if (!selected) { // make default selection - let selectedIntp = {} + let selectedIntp = {}; for (key in $scope.interpreterBindings) { - setting = $scope.interpreterBindings[key] - if (!selectedIntp[setting.name]) { - setting.selected = true - selectedIntp[setting.name] = true + if ($scope.interpreterBindings.hasOwnProperty(key)) { + setting = $scope.interpreterBindings[key]; + if (!selectedIntp[setting.name]) { + setting.selected = true; + selectedIntp[setting.name] = true; + } } } - $scope.showSetting = true + $scope.showSetting = true; } - }) + }); $scope.interpreterSelectionListeners = { - accept: function (sourceItemHandleScope, destSortableScope) { return true }, - itemMoved: function (event) {}, - orderChanged: function (event) {} - } + accept: function(sourceItemHandleScope, destSortableScope) { + return true; + }, + itemMoved: function(event) {}, + orderChanged: function(event) {}, + }; $scope.closeAdditionalBoards = function() { - $scope.closeSetting() - $scope.closePermissions() - $scope.closeRevisionsComparator() - } + $scope.closeSetting(); + $scope.closePermissions(); + $scope.closeRevisionsComparator(); + }; - $scope.openSetting = function () { - $scope.showSetting = true - getInterpreterBindings() - } + $scope.openSetting = function() { + $scope.showSetting = true; + getInterpreterBindings(); + }; - $scope.closeSetting = function () { + $scope.closeSetting = function() { if (isSettingDirty()) { BootstrapDialog.confirm({ closable: true, title: '', message: 'Interpreter setting changes will be discarded.', - callback: function (result) { + callback: function(result) { if (result) { - $scope.$apply(function () { - $scope.showSetting = false - }) + $scope.$apply(function() { + $scope.showSetting = false; + }); } - } - }) + }, + }); } else { - $scope.showSetting = false + $scope.showSetting = false; } - } + }; - $scope.saveSetting = function () { - let selectedSettingIds = [] + $scope.saveSetting = function() { + let selectedSettingIds = []; for (let no in $scope.interpreterBindings) { - let setting = $scope.interpreterBindings[no] - if (setting.selected) { - selectedSettingIds.push(setting.id) + if ($scope.interpreterBindings.hasOwnProperty(no)) { + let setting = $scope.interpreterBindings[no]; + if (setting.selected) { + selectedSettingIds.push(setting.id); + } } } - websocketMsgSrv.saveInterpreterBindings($scope.note.id, selectedSettingIds) - console.log('Interpreter bindings %o saved', selectedSettingIds) + websocketMsgSrv.saveInterpreterBindings($scope.note.id, selectedSettingIds); + console.log('Interpreter bindings %o saved', selectedSettingIds); - _.forEach($scope.note.paragraphs, function (n, key) { - let regExp = /^\s*%/g + _.forEach($scope.note.paragraphs, function(n, key) { + let regExp = /^\s*%/g; if (n.text && !regExp.exec(n.text)) { - $scope.$broadcast('saveInterpreterBindings', n.id) + $scope.$broadcast('saveInterpreterBindings', n.id); } - }) + }); - $scope.showSetting = false - } + $scope.showSetting = false; + }; - $scope.toggleSetting = function () { + $scope.toggleSetting = function() { if ($scope.showSetting) { - $scope.closeSetting() + $scope.closeSetting(); } else { - $scope.closeAdditionalBoards() - $scope.openSetting() - angular.element('html, body').animate({ scrollTop: 0 }, 'slow') + $scope.closeAdditionalBoards(); + $scope.openSetting(); + angular.element('html, body').animate({scrollTop: 0}, 'slow'); } - } + }; - $scope.openRevisionsComparator = function () { - $scope.showRevisionsComparator = true - } + $scope.openRevisionsComparator = function() { + $scope.showRevisionsComparator = true; + }; - $scope.closeRevisionsComparator = function () { - $scope.showRevisionsComparator = false - } + $scope.closeRevisionsComparator = function() { + $scope.showRevisionsComparator = false; + }; - $scope.toggleRevisionsComparator = function () { + $scope.toggleRevisionsComparator = function() { if ($scope.showRevisionsComparator) { - $scope.closeRevisionsComparator() + $scope.closeRevisionsComparator(); } else { - $scope.closeAdditionalBoards() - $scope.openRevisionsComparator() - angular.element('html, body').animate({ scrollTop: 0 }, 'slow') + $scope.closeAdditionalBoards(); + $scope.openRevisionsComparator(); + angular.element('html, body').animate({scrollTop: 0}, 'slow'); } - } + }; - let getPermissions = function (callback) { + let getPermissions = function(callback) { $http.get(baseUrlSrv.getRestApiBase() + '/notebook/' + $scope.note.id + '/permissions') - .success(function (data, status, headers, config) { - $scope.permissions = data.body - $scope.permissionsOrig = angular.copy($scope.permissions) // to check dirty + .success(function(data, status, headers, config) { + $scope.permissions = data.body; + $scope.permissionsOrig = angular.copy($scope.permissions); // to check dirty let selectJson = { tokenSeparators: [',', ' '], ajax: { - url: function (params) { + url: function(params) { if (!params.term) { - return false + return false; } - return baseUrlSrv.getRestApiBase() + '/security/userlist/' + params.term + return baseUrlSrv.getRestApiBase() + '/security/userlist/' + params.term; }, delay: 250, - processResults: function (data, params) { - let results = [] + processResults: function(data, params) { + let results = []; if (data.body.users.length !== 0) { - let users = [] + let users = []; for (let len = 0; len < data.body.users.length; len++) { users.push({ 'id': data.body.users[len], - 'text': data.body.users[len] - }) + 'text': data.body.users[len], + }); } results.push({ 'text': 'Users :', - 'children': users - }) + 'children': users, + }); } if (data.body.roles.length !== 0) { - let roles = [] + let roles = []; for (let len = 0; len < data.body.roles.length; len++) { roles.push({ 'id': data.body.roles[len], - 'text': data.body.roles[len] - }) + 'text': data.body.roles[len], + }); } results.push({ 'text': 'Roles :', - 'children': roles - }) + 'children': roles, + }); } return { results: results, pagination: { - more: false - } - } + more: false, + }, + }; }, - cache: false + cache: false, }, width: ' ', tags: true, - minimumInputLength: 3 - } - - $scope.setIamOwner() - angular.element('#selectOwners').select2(selectJson) - angular.element('#selectReaders').select2(selectJson) - angular.element('#selectRunners').select2(selectJson) - angular.element('#selectWriters').select2(selectJson) + minimumInputLength: 3, + }; + + $scope.setIamOwner(); + angular.element('#selectOwners').select2(selectJson); + angular.element('#selectReaders').select2(selectJson); + angular.element('#selectRunners').select2(selectJson); + angular.element('#selectWriters').select2(selectJson); if (callback) { - callback() + callback(); } }) - .error(function (data, status, headers, config) { + .error(function(data, status, headers, config) { if (status !== 0) { - console.log('Error %o %o', status, data.message) + console.log('Error %o %o', status, data.message); } - }) - } + }); + }; - $scope.openPermissions = function () { - $scope.showPermissions = true - getPermissions() - } + $scope.openPermissions = function() { + $scope.showPermissions = true; + getPermissions(); + }; - $scope.closePermissions = function () { + $scope.closePermissions = function() { if (isPermissionsDirty()) { BootstrapDialog.confirm({ closable: true, title: '', message: 'Changes will be discarded.', - callback: function (result) { + callback: function(result) { if (result) { - $scope.$apply(function () { - $scope.showPermissions = false - }) + $scope.$apply(function() { + $scope.showPermissions = false; + }); } - } - }) + }, + }); } else { - $scope.showPermissions = false + $scope.showPermissions = false; } - } + }; - function convertPermissionsToArray () { - $scope.permissions.owners = angular.element('#selectOwners').val() - $scope.permissions.readers = angular.element('#selectReaders').val() - $scope.permissions.runners = angular.element('#selectRunners').val() - $scope.permissions.writers = angular.element('#selectWriters').val() - angular.element('.permissionsForm select').find('option:not([is-select2="false"])').remove() + function convertPermissionsToArray() { + $scope.permissions.owners = angular.element('#selectOwners').val(); + $scope.permissions.readers = angular.element('#selectReaders').val(); + $scope.permissions.runners = angular.element('#selectRunners').val(); + $scope.permissions.writers = angular.element('#selectWriters').val(); + angular.element('.permissionsForm select').find('option:not([is-select2="false"])').remove(); } $scope.hasMatches = function() { - return $scope.search.occurrencesCount > 0 - } + return $scope.search.occurrencesCount > 0; + }; const markAllOccurrences = function() { - $scope.search.occurrencesCount = 0 - $scope.search.occurrencesHidden = false - currentSearchParagraph = 0 - $scope.$broadcast('markAllOccurrences', $scope.search.searchText) - $scope.search.currentOccurrence = $scope.search.occurrencesCount > 0 ? 1 : 0 - } + $scope.search.occurrencesCount = 0; + $scope.search.occurrencesHidden = false; + currentSearchParagraph = 0; + $scope.$broadcast('markAllOccurrences', $scope.search.searchText); + $scope.search.currentOccurrence = $scope.search.occurrencesCount > 0 ? 1 : 0; + }; $scope.markAllOccurrencesAndHighlightFirst = function() { - $scope.search.needHighlightFirst = true - markAllOccurrences() - } + $scope.search.needHighlightFirst = true; + markAllOccurrences(); + }; const increaseCurrentOccurence = function() { - ++$scope.search.currentOccurrence + ++$scope.search.currentOccurrence; if ($scope.search.currentOccurrence > $scope.search.occurrencesCount) { - $scope.search.currentOccurrence = 1 + $scope.search.currentOccurrence = 1; } - } + }; const decreaseCurrentOccurence = function() { - --$scope.search.currentOccurrence + --$scope.search.currentOccurrence; if ($scope.search.currentOccurrence === 0) { - $scope.search.currentOccurrence = $scope.search.occurrencesCount + $scope.search.currentOccurrence = $scope.search.occurrencesCount; } - } + }; const sendNextOccurrenceMessage = function() { if ($scope.search.occurrencesCount === 0) { - markAllOccurrences() + markAllOccurrences(); if ($scope.search.occurrencesCount === 0) { - return + return; } } if ($scope.search.occurrencesHidden) { - markAllOccurrences() + markAllOccurrences(); } - $scope.$broadcast('nextOccurrence', $scope.note.paragraphs[currentSearchParagraph].id) - } + $scope.$broadcast('nextOccurrence', $scope.note.paragraphs[currentSearchParagraph].id); + }; const sendPrevOccurrenceMessage = function() { if ($scope.search.occurrencesCount === 0) { - markAllOccurrences() + markAllOccurrences(); if ($scope.search.occurrencesCount === 0) { - return + return; } } if ($scope.search.occurrencesHidden) { - markAllOccurrences() - currentSearchParagraph = $scope.note.paragraphs.length - 1 + markAllOccurrences(); + currentSearchParagraph = $scope.note.paragraphs.length - 1; } - $scope.$broadcast('prevOccurrence', $scope.note.paragraphs[currentSearchParagraph].id) - } + $scope.$broadcast('prevOccurrence', $scope.note.paragraphs[currentSearchParagraph].id); + }; const increaseCurrentSearchParagraph = function() { - ++currentSearchParagraph + ++currentSearchParagraph; if (currentSearchParagraph >= $scope.note.paragraphs.length) { - currentSearchParagraph = 0 + currentSearchParagraph = 0; } - } + }; - const decreaseCurrentSearchParagraph = function () { - --currentSearchParagraph + const decreaseCurrentSearchParagraph = function() { + --currentSearchParagraph; if (currentSearchParagraph === -1) { - currentSearchParagraph = $scope.note.paragraphs.length - 1 + currentSearchParagraph = $scope.note.paragraphs.length - 1; } - } + }; $scope.$on('occurrencesExists', function(event, count) { - $scope.search.occurrencesCount += count + $scope.search.occurrencesCount += count; if ($scope.search.needHighlightFirst) { - sendNextOccurrenceMessage() - $scope.search.needHighlightFirst = false + sendNextOccurrenceMessage(); + $scope.search.needHighlightFirst = false; } - }) + }); $scope.nextOccurrence = function() { - sendNextOccurrenceMessage() - increaseCurrentOccurence() - } + sendNextOccurrenceMessage(); + increaseCurrentOccurence(); + }; $scope.$on('noNextOccurrence', function(event) { - increaseCurrentSearchParagraph() - sendNextOccurrenceMessage() - }) + increaseCurrentSearchParagraph(); + sendNextOccurrenceMessage(); + }); $scope.prevOccurrence = function() { - sendPrevOccurrenceMessage() - decreaseCurrentOccurence() - } + sendPrevOccurrenceMessage(); + decreaseCurrentOccurence(); + }; $scope.$on('noPrevOccurrence', function(event) { - decreaseCurrentSearchParagraph() - sendPrevOccurrenceMessage() - }) + decreaseCurrentSearchParagraph(); + sendPrevOccurrenceMessage(); + }); $scope.$on('editorClicked', function() { - $scope.search.occurrencesHidden = true - $scope.$broadcast('unmarkAll') - }) + $scope.search.occurrencesHidden = true; + $scope.$broadcast('unmarkAll'); + }); $scope.replace = function() { if ($scope.search.occurrencesCount === 0) { - $scope.markAllOccurrencesAndHighlightFirst() + $scope.markAllOccurrencesAndHighlightFirst(); if ($scope.search.occurrencesCount === 0) { - return + return; } } if ($scope.search.occurrencesHidden) { - $scope.markAllOccurrencesAndHighlightFirst() - return + $scope.markAllOccurrencesAndHighlightFirst(); + return; } - $scope.$broadcast('replaceCurrent', $scope.search.searchText, $scope.search.replaceText) + $scope.$broadcast('replaceCurrent', $scope.search.searchText, $scope.search.replaceText); if ($scope.search.needToSendNextOccurrenceAfterReplace) { - sendNextOccurrenceMessage() - $scope.search.needToSendNextOccurrenceAfterReplace = false + sendNextOccurrenceMessage(); + $scope.search.needToSendNextOccurrenceAfterReplace = false; } - } + }; $scope.$on('occurrencesCountChanged', function(event, cnt) { - $scope.search.occurrencesCount += cnt + $scope.search.occurrencesCount += cnt; if ($scope.search.occurrencesCount === 0) { - $scope.search.currentOccurrence = 0 + $scope.search.currentOccurrence = 0; } else { - $scope.search.currentOccurrence += cnt + 1 + $scope.search.currentOccurrence += cnt + 1; if ($scope.search.currentOccurrence > $scope.search.occurrencesCount) { - $scope.search.currentOccurrence = 1 + $scope.search.currentOccurrence = 1; } } - }) + }); $scope.replaceAll = function() { if ($scope.search.occurrencesCount === 0) { - return + return; } if ($scope.search.occurrencesHidden) { - $scope.markAllOccurrencesAndHighlightFirst() + $scope.markAllOccurrencesAndHighlightFirst(); } - $scope.$broadcast('replaceAll', $scope.search.searchText, $scope.search.replaceText) - $scope.markAllOccurrencesAndHighlightFirst() - } + $scope.$broadcast('replaceAll', $scope.search.searchText, $scope.search.replaceText); + $scope.markAllOccurrencesAndHighlightFirst(); + }; $scope.$on('noNextOccurrenceAfterReplace', function() { - $scope.search.occurrencesCount = 0 - $scope.search.needHighlightFirst = false - $scope.search.needToSendNextOccurrenceAfterReplace = false - $scope.$broadcast('checkOccurrences') - increaseCurrentSearchParagraph() + $scope.search.occurrencesCount = 0; + $scope.search.needHighlightFirst = false; + $scope.search.needToSendNextOccurrenceAfterReplace = false; + $scope.$broadcast('checkOccurrences'); + increaseCurrentSearchParagraph(); if ($scope.search.occurrencesCount > 0) { - $scope.search.needToSendNextOccurrenceAfterReplace = true + $scope.search.needToSendNextOccurrenceAfterReplace = true; } - }) + }); $scope.onPressOnFindInput = function(event) { if (event.keyCode === 13) { - $scope.nextOccurrence() + $scope.nextOccurrence(); } - } + }; let makeSearchBoxVisible = function() { if ($scope.search.searchBoxOpened) { - $scope.search.searchBoxOpened = false - console.log('make 0') - $scope.search.left = '0px' + $scope.search.searchBoxOpened = false; + console.log('make 0'); + $scope.search.left = '0px'; } else { - $scope.search.searchBoxOpened = true - let searchGroupRect = angular.element('#searchGroup')[0].getBoundingClientRect() - console.log('make visible') - let dropdownRight = searchGroupRect.left + $scope.search.searchBoxWidth - console.log(dropdownRight + ' ' + window.innerWidth) + $scope.search.searchBoxOpened = true; + let searchGroupRect = angular.element('#searchGroup')[0].getBoundingClientRect(); + console.log('make visible'); + let dropdownRight = searchGroupRect.left + $scope.search.searchBoxWidth; + console.log(dropdownRight + ' ' + window.innerWidth); if (dropdownRight + 5 > window.innerWidth) { - $scope.search.left = window.innerWidth - dropdownRight - 15 + 'px' + $scope.search.left = window.innerWidth - dropdownRight - 15 + 'px'; } } - } + }; $scope.searchClicked = function() { - makeSearchBoxVisible() - } + makeSearchBoxVisible(); + }; $scope.$on('toggleSearchBox', function() { - let elem = angular.element('#searchGroup') + let elem = angular.element('#searchGroup'); if ($scope.search.searchBoxOpened) { - elem.removeClass('open') + elem.removeClass('open'); } else { - elem.addClass('open') + elem.addClass('open'); } - $timeout(makeSearchBoxVisible()) - }) + $timeout(makeSearchBoxVisible()); + }); $scope.restartInterpreter = function(interpreter) { const thisConfirm = BootstrapDialog.confirm({ @@ -999,37 +1011,37 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, callback: function(result) { if (result) { let payload = { - 'noteId': $scope.note.id - } + 'noteId': $scope.note.id, + }; - thisConfirm.$modalFooter.find('button').addClass('disabled') + thisConfirm.$modalFooter.find('button').addClass('disabled'); thisConfirm.$modalFooter.find('button:contains("OK")') - .html(' Saving Setting') + .html(' Saving Setting'); $http.put(baseUrlSrv.getRestApiBase() + '/interpreter/setting/restart/' + interpreter.id, payload) .success(function(data, status, headers, config) { - let index = _.findIndex($scope.interpreterSettings, {'id': interpreter.id}) - $scope.interpreterSettings[index] = data.body - thisConfirm.close() - }).error(function (data, status, headers, config) { - thisConfirm.close() - console.log('Error %o %o', status, data.message) + let index = _.findIndex($scope.interpreterSettings, {'id': interpreter.id}); + $scope.interpreterSettings[index] = data.body; + thisConfirm.close(); + }).error(function(data, status, headers, config) { + thisConfirm.close(); + console.log('Error %o %o', status, data.message); BootstrapDialog.show({ title: 'Error restart interpreter.', - message: data.message - }) - }) - return false + message: data.message, + }); + }); + return false; } - } - }) - } + }, + }); + }; - $scope.savePermissions = function () { + $scope.savePermissions = function() { if ($scope.isAnonymous || $rootScope.ticket.principal.trim().length === 0) { - $scope.blockAnonUsers() + $scope.blockAnonUsers(); } - convertPermissionsToArray() + convertPermissionsToArray(); if ($scope.isOwnerEmpty()) { BootstrapDialog.show({ closable: false, @@ -1040,43 +1052,43 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, { label: 'Set', action: function(dialog) { - dialog.close() - $scope.permissions.owners = [$rootScope.ticket.principal] - $scope.setPermissions() - } + dialog.close(); + $scope.permissions.owners = [$rootScope.ticket.principal]; + $scope.setPermissions(); + }, }, { label: 'Cancel', action: function(dialog) { - dialog.close() - $scope.openPermissions() - } - } - ] - }) + dialog.close(); + $scope.openPermissions(); + }, + }, + ], + }); } else { - $scope.setPermissions() + $scope.setPermissions(); } - } + }; $scope.setPermissions = function() { $http.put(baseUrlSrv.getRestApiBase() + '/notebook/' + $scope.note.id + '/permissions', $scope.permissions, {withCredentials: true}) - .success(function (data, status, headers, config) { - getPermissions(function () { - console.log('Note permissions %o saved', $scope.permissions) + .success(function(data, status, headers, config) { + getPermissions(function() { + console.log('Note permissions %o saved', $scope.permissions); BootstrapDialog.alert({ closable: true, title: 'Permissions Saved Successfully', message: 'Owners : ' + $scope.permissions.owners + '\n\n' + 'Readers : ' + $scope.permissions.readers + '\n\n' + 'Runners : ' + $scope.permissions.runners + - '\n\n' + 'Writers : ' + $scope.permissions.writers - }) - $scope.showPermissions = false - }) + '\n\n' + 'Writers : ' + $scope.permissions.writers, + }); + $scope.showPermissions = false; + }); }) - .error(function (data, status, headers, config) { - console.log('Error %o %o', status, data.message) + .error(function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); BootstrapDialog.show({ closable: false, closeByBackdrop: false, @@ -1086,362 +1098,366 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, buttons: [ { label: 'Login', - action: function (dialog) { - dialog.close() + action: function(dialog) { + dialog.close(); angular.element('#loginModal').modal({ - show: 'true' - }) - } + show: 'true', + }); + }, }, { label: 'Cancel', - action: function (dialog) { - dialog.close() - $location.path('/') - } - } - ] - }) - }) - } - - $scope.togglePermissions = function () { - let principal = $rootScope.ticket.principal - $scope.isAnonymous = principal === 'anonymous' ? true : false + action: function(dialog) { + dialog.close(); + $location.path('/'); + }, + }, + ], + }); + }); + }; + + $scope.togglePermissions = function() { + let principal = $rootScope.ticket.principal; + $scope.isAnonymous = principal === 'anonymous' ? true : false; if (!!principal && $scope.isAnonymous) { - $scope.blockAnonUsers() + $scope.blockAnonUsers(); } else { if ($scope.showPermissions) { - $scope.closePermissions() - angular.element('#selectOwners').select2({}) - angular.element('#selectReaders').select2({}) - angular.element('#selectRunners').select2({}) - angular.element('#selectWriters').select2({}) + $scope.closePermissions(); + angular.element('#selectOwners').select2({}); + angular.element('#selectReaders').select2({}); + angular.element('#selectRunners').select2({}); + angular.element('#selectWriters').select2({}); } else { - $scope.closeAdditionalBoards() - $scope.openPermissions() + $scope.closeAdditionalBoards(); + $scope.openPermissions(); } } - } + }; - $scope.setIamOwner = function () { + $scope.setIamOwner = function() { if ($scope.permissions.owners.length > 0 && _.indexOf($scope.permissions.owners, $rootScope.ticket.principal) < 0) { - $scope.isOwner = false - return false + $scope.isOwner = false; + return false; } - $scope.isOwner = true - return true - } + $scope.isOwner = true; + return true; + }; - $scope.toggleNotePersonalizedMode = function () { - let personalizedMode = $scope.note.config.personalizedMode + $scope.toggleNotePersonalizedMode = function() { + let personalizedMode = $scope.note.config.personalizedMode; if ($scope.isOwner) { BootstrapDialog.confirm({ closable: true, title: 'Setting the result display', - message: function (dialog) { - let modeText = $scope.note.config.personalizedMode === 'true' ? 'collaborate' : 'personalize' - return 'Do you want to ' + modeText + ' your analysis?' + message: function(dialog) { + let modeText = $scope.note.config.personalizedMode === 'true' ? 'collaborate' : 'personalize'; + return 'Do you want to ' + modeText + ' your analysis?'; }, - callback: function (result) { + callback: function(result) { if (result) { if ($scope.note.config.personalizedMode === undefined) { - $scope.note.config.personalizedMode = 'false' + $scope.note.config.personalizedMode = 'false'; } - $scope.note.config.personalizedMode = personalizedMode === 'true' ? 'false' : 'true' - websocketMsgSrv.updatePersonalizedMode($scope.note.id, $scope.note.config.personalizedMode) + $scope.note.config.personalizedMode = personalizedMode === 'true' ? 'false' : 'true'; + websocketMsgSrv.updatePersonalizedMode($scope.note.id, $scope.note.config.personalizedMode); } - } - }) + }, + }); } - } + }; - const isSettingDirty = function () { + const isSettingDirty = function() { if (angular.equals($scope.interpreterBindings, $scope.interpreterBindingsOrig)) { - return false + return false; } else { - return true + return true; } - } + }; - const isPermissionsDirty = function () { + const isPermissionsDirty = function() { if (angular.equals($scope.permissions, $scope.permissionsOrig)) { - return false + return false; } else { - return true + return true; } - } + }; - angular.element(document).click(function () { - angular.element('.ace_autocomplete').hide() - }) + angular.element(document).click(function() { + angular.element('.ace_autocomplete').hide(); + }); $scope.isOwnerEmpty = function() { if ($scope.permissions.owners.length > 0) { for (let i = 0; i < $scope.permissions.owners.length; i++) { if ($scope.permissions.owners[i].trim().length > 0) { - return false + return false; } else if (i === $scope.permissions.owners.length - 1) { - return true + return true; } } } else { - return true + return true; } - } + }; /* ** $scope.$on functions below */ - $scope.$on('runAllAbove', function (event, paragraph, isNeedConfirm) { - let allParagraphs = $scope.note.paragraphs - let toRunParagraphs = [] + $scope.$on('runAllAbove', function(event, paragraph, isNeedConfirm) { + let allParagraphs = $scope.note.paragraphs; + let toRunParagraphs = []; for (let i = 0; allParagraphs[i] !== paragraph; i++) { - if (i === allParagraphs.length - 1) { return } // if paragraph not in array of all paragraphs - toRunParagraphs.push(allParagraphs[i]) + if (i === allParagraphs.length - 1) { + return; + } // if paragraph not in array of all paragraphs + toRunParagraphs.push(allParagraphs[i]); } - const paragraphs = toRunParagraphs.map(p => { + const paragraphs = toRunParagraphs.map((p) => { return { id: p.id, title: p.title, paragraph: p.text, config: p.config, - params: p.settings.params - } - }) + params: p.settings.params, + }; + }); if (!isNeedConfirm) { - websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs) + websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs); } else { BootstrapDialog.confirm({ closable: true, title: '', message: 'Run all above?', - callback: function (result) { + callback: function(result) { if (result) { - websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs) + websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs); } - } - }) + }, + }); } - $scope.saveCursorPosition(paragraph) - }) + $scope.saveCursorPosition(paragraph); + }); - $scope.$on('runAllBelowAndCurrent', function (event, paragraph, isNeedConfirm) { - let allParagraphs = $scope.note.paragraphs - let toRunParagraphs = [] + $scope.$on('runAllBelowAndCurrent', function(event, paragraph, isNeedConfirm) { + let allParagraphs = $scope.note.paragraphs; + let toRunParagraphs = []; for (let i = allParagraphs.length - 1; allParagraphs[i] !== paragraph; i--) { - if (i < 0) { return } // if paragraph not in array of all paragraphs - toRunParagraphs.push(allParagraphs[i]) + if (i < 0) { + return; + } // if paragraph not in array of all paragraphs + toRunParagraphs.push(allParagraphs[i]); } - toRunParagraphs.push(paragraph) - toRunParagraphs.reverse() + toRunParagraphs.push(paragraph); + toRunParagraphs.reverse(); - const paragraphs = toRunParagraphs.map(p => { + const paragraphs = toRunParagraphs.map((p) => { return { id: p.id, title: p.title, paragraph: p.text, config: p.config, - params: p.settings.params - } - }) + params: p.settings.params, + }; + }); if (!isNeedConfirm) { - websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs) + websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs); } else { BootstrapDialog.confirm({ closable: true, title: '', message: 'Run current and all below?', - callback: function (result) { + callback: function(result) { if (result) { - websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs) + websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs); } - } - }) + }, + }); } - $scope.saveCursorPosition(paragraph) - }) + $scope.saveCursorPosition(paragraph); + }); - $scope.saveCursorPosition = function (paragraph) { + $scope.saveCursorPosition = function(paragraph) { let angParagEditor = angular .element('#' + paragraph.id + '_paragraphColumn_main') - .scope().editor - let col = angParagEditor.selection.lead.column - let row = angParagEditor.selection.lead.row - $scope.$broadcast('focusParagraph', paragraph.id, row + 1, col) - } + .scope().editor; + let col = angParagEditor.selection.lead.column; + let row = angParagEditor.selection.lead.row; + $scope.$broadcast('focusParagraph', paragraph.id, row + 1, col); + }; - $scope.$on('setConnectedStatus', function (event, param) { + $scope.$on('setConnectedStatus', function(event, param) { if (connectedOnce && param) { - initNotebook() + initNotebook(); } - connectedOnce = true - }) + connectedOnce = true; + }); - $scope.$on('moveParagraphUp', function (event, paragraph) { - let newIndex = -1 + $scope.$on('moveParagraphUp', function(event, paragraph) { + let newIndex = -1; for (let i = 0; i < $scope.note.paragraphs.length; i++) { if ($scope.note.paragraphs[i].id === paragraph.id) { - newIndex = i - 1 - break + newIndex = i - 1; + break; } } if (newIndex < 0 || newIndex >= $scope.note.paragraphs.length) { - return + return; } // save dirtyText of moving paragraphs. - let prevParagraph = $scope.note.paragraphs[newIndex] + let prevParagraph = $scope.note.paragraphs[newIndex]; angular .element('#' + paragraph.id + '_paragraphColumn_main') .scope() - .saveParagraph(paragraph) + .saveParagraph(paragraph); angular .element('#' + prevParagraph.id + '_paragraphColumn_main') .scope() - .saveParagraph(prevParagraph) - websocketMsgSrv.moveParagraph(paragraph.id, newIndex) - }) + .saveParagraph(prevParagraph); + websocketMsgSrv.moveParagraph(paragraph.id, newIndex); + }); - $scope.$on('moveParagraphDown', function (event, paragraph) { - let newIndex = -1 + $scope.$on('moveParagraphDown', function(event, paragraph) { + let newIndex = -1; for (let i = 0; i < $scope.note.paragraphs.length; i++) { if ($scope.note.paragraphs[i].id === paragraph.id) { - newIndex = i + 1 - break + newIndex = i + 1; + break; } } if (newIndex < 0 || newIndex >= $scope.note.paragraphs.length) { - return + return; } // save dirtyText of moving paragraphs. - let nextParagraph = $scope.note.paragraphs[newIndex] + let nextParagraph = $scope.note.paragraphs[newIndex]; angular .element('#' + paragraph.id + '_paragraphColumn_main') .scope() - .saveParagraph(paragraph) + .saveParagraph(paragraph); angular .element('#' + nextParagraph.id + '_paragraphColumn_main') .scope() - .saveParagraph(nextParagraph) - websocketMsgSrv.moveParagraph(paragraph.id, newIndex) - }) + .saveParagraph(nextParagraph); + websocketMsgSrv.moveParagraph(paragraph.id, newIndex); + }); - $scope.$on('moveFocusToPreviousParagraph', function (event, currentParagraphId) { - let focus = false + $scope.$on('moveFocusToPreviousParagraph', function(event, currentParagraphId) { + let focus = false; for (let i = $scope.note.paragraphs.length - 1; i >= 0; i--) { if (focus === false) { if ($scope.note.paragraphs[i].id === currentParagraphId) { - focus = true - continue + focus = true; + continue; } } else { - $scope.$broadcast('focusParagraph', $scope.note.paragraphs[i].id, -1) - break + $scope.$broadcast('focusParagraph', $scope.note.paragraphs[i].id, -1); + break; } } - }) + }); - $scope.$on('moveFocusToNextParagraph', function (event, currentParagraphId) { - let focus = false + $scope.$on('moveFocusToNextParagraph', function(event, currentParagraphId) { + let focus = false; for (let i = 0; i < $scope.note.paragraphs.length; i++) { if (focus === false) { if ($scope.note.paragraphs[i].id === currentParagraphId) { - focus = true - continue + focus = true; + continue; } } else { - $scope.$broadcast('focusParagraph', $scope.note.paragraphs[i].id, 0) - break + $scope.$broadcast('focusParagraph', $scope.note.paragraphs[i].id, 0); + break; } } - }) + }); - $scope.$on('insertParagraph', function (event, paragraphId, position) { + $scope.$on('insertParagraph', function(event, paragraphId, position) { if ($scope.revisionView === true) { - return + return; } - let newIndex = -1 + let newIndex = -1; for (let i = 0; i < $scope.note.paragraphs.length; i++) { if ($scope.note.paragraphs[i].id === paragraphId) { // determine position of where to add new paragraph; default is below if (position === 'above') { - newIndex = i + newIndex = i; } else { - newIndex = i + 1 + newIndex = i + 1; } - break + break; } } if (newIndex < 0 || newIndex > $scope.note.paragraphs.length) { - return + return; } - websocketMsgSrv.insertParagraph(newIndex) - }) + websocketMsgSrv.insertParagraph(newIndex); + }); - $scope.$on('setNoteContent', function (event, note) { + $scope.$on('setNoteContent', function(event, note) { if (note === undefined) { - $location.path('/') + $location.path('/'); } - $scope.note = note + $scope.note = note; - $scope.paragraphUrl = $routeParams.paragraphId - $scope.asIframe = $routeParams.asIframe + $scope.paragraphUrl = $routeParams.paragraphId; + $scope.asIframe = $routeParams.asIframe; if ($scope.paragraphUrl) { - $scope.note = cleanParagraphExcept($scope.paragraphUrl, $scope.note) - $scope.$broadcast('$unBindKeyEvent', $scope.$unBindKeyEvent) - $rootScope.$broadcast('setIframe', $scope.asIframe) - initializeLookAndFeel() - return + $scope.note = cleanParagraphExcept($scope.paragraphUrl, $scope.note); + $scope.$broadcast('$unBindKeyEvent', $scope.$unBindKeyEvent); + $rootScope.$broadcast('setIframe', $scope.asIframe); + initializeLookAndFeel(); + return; } - initializeLookAndFeel() + initializeLookAndFeel(); // open interpreter binding setting when there're none selected - getInterpreterBindings() - getPermissions() - let isPersonalized = $scope.note.config.personalizedMode - isPersonalized = isPersonalized === undefined ? 'false' : isPersonalized - $scope.note.config.personalizedMode = isPersonalized - }) - - $scope.$on('$routeChangeStart', function (event, next, current) { + getInterpreterBindings(); + getPermissions(); + let isPersonalized = $scope.note.config.personalizedMode; + isPersonalized = isPersonalized === undefined ? 'false' : isPersonalized; + $scope.note.config.personalizedMode = isPersonalized; + }); + + $scope.$on('$routeChangeStart', function(event, next, current) { if (!$scope.note || !$scope.note.paragraphs) { - return + return; } if ($scope.note && $scope.note.paragraphs) { - $scope.note.paragraphs.map(par => { + $scope.note.paragraphs.map((par) => { if ($scope.allowLeave === true) { - return + return; } let thisScope = angular.element( - '#' + par.id + '_paragraphColumn_main').scope() + '#' + par.id + '_paragraphColumn_main').scope(); if (thisScope.dirtyText === undefined || thisScope.originalText === undefined || thisScope.dirtyText === thisScope.originalText) { - return true + return true; } else { - event.preventDefault() - $scope.showParagraphWarning(next) + event.preventDefault(); + $scope.showParagraphWarning(next); } - }) + }); } - }) + }); - $scope.showParagraphWarning = function (next) { + $scope.showParagraphWarning = function(next) { if ($scope.paragraphWarningDialog.opened !== true) { $scope.paragraphWarningDialog = BootstrapDialog.show({ closable: false, @@ -1451,62 +1467,62 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, message: 'Changes that you have made will not be saved.', buttons: [{ label: 'Stay', - action: function (dialog) { - dialog.close() - } + action: function(dialog) { + dialog.close(); + }, }, { label: 'Leave', - action: function (dialog) { - dialog.close() - let locationToRedirect = next['$$route']['originalPath'] - Object.keys(next.pathParams).map(key => { + action: function(dialog) { + dialog.close(); + let locationToRedirect = next['$$route']['originalPath']; + Object.keys(next.pathParams).map((key) => { locationToRedirect = locationToRedirect.replace(':' + key, - next.pathParams[key]) - }) - $scope.allowLeave = true - $location.path(locationToRedirect) - } - }] - }) + next.pathParams[key]); + }); + $scope.allowLeave = true; + $location.path(locationToRedirect); + }, + }], + }); } - } + }; - $scope.$on('saveNoteForms', function (event, data) { - $scope.note.noteForms = data.formsData.forms - $scope.note.noteParams = data.formsData.params - }) + $scope.$on('saveNoteForms', function(event, data) { + $scope.note.noteForms = data.formsData.forms; + $scope.note.noteParams = data.formsData.params; + }); $scope.isShowNoteForms = function() { if ($scope.note && !angular.equals({}, $scope.note.noteForms)) { - return true + return true; } - return false - } + return false; + }; - $scope.saveNoteForms = function () { - websocketMsgSrv.saveNoteForms($scope.note) - } + $scope.saveNoteForms = function() { + websocketMsgSrv.saveNoteForms($scope.note); + }; - $scope.removeNoteForms = function (formName) { - websocketMsgSrv.removeNoteForms($scope.note, formName) - } + $scope.removeNoteForms = function(formName) { + websocketMsgSrv.removeNoteForms($scope.note, formName); + }; - $scope.$on('$destroy', function () { - angular.element(window).off('beforeunload') - $scope.killSaveTimer() - $scope.saveNote() + $scope.$on('$destroy', function() { + angular.element(window).off('beforeunload'); + $scope.killSaveTimer(); + $scope.saveNote(); - document.removeEventListener('click', $scope.focusParagraphOnClick) - document.removeEventListener('keydown', $scope.keyboardShortcut) - }) + document.removeEventListener('click', $scope.focusParagraphOnClick); + document.removeEventListener('keydown', $scope.keyboardShortcut); + }); - $scope.$on('$unBindKeyEvent', function () { - document.removeEventListener('click', $scope.focusParagraphOnClick) - document.removeEventListener('keydown', $scope.keyboardShortcut) - }) + $scope.$on('$unBindKeyEvent', function() { + document.removeEventListener('click', $scope.focusParagraphOnClick); + document.removeEventListener('keydown', $scope.keyboardShortcut); + }); - angular.element(window).bind('resize', function () { - const actionbarHeight = document.getElementById('actionbar').lastElementChild.clientHeight - angular.element(document.getElementById('content')).css('padding-top', actionbarHeight - 20) - }) + angular.element(window).bind('resize', function() { + const actionbarHeight = document.getElementById('actionbar').lastElementChild.clientHeight; + angular.element(document.getElementById('content')).css('padding-top', actionbarHeight - 20); + }); } diff --git a/zeppelin-web/src/app/notebook/notebook.controller.test.js b/zeppelin-web/src/app/notebook/notebook.controller.test.js index f393d2c09c2..be9f9568e46 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.test.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.test.js @@ -1,139 +1,139 @@ -describe('Controller: NotebookCtrl', function () { - beforeEach(angular.mock.module('zeppelinWebApp')) +describe('Controller: NotebookCtrl', function() { + beforeEach(angular.mock.module('zeppelinWebApp')); - let scope + let scope; let websocketMsgSrvMock = { - getNote: function () {}, - listRevisionHistory: function () {}, - getInterpreterBindings: function () {}, - updateNote: function () {}, - renameNote: function () {} - } + getNote: function() {}, + listRevisionHistory: function() {}, + getInterpreterBindings: function() {}, + updateNote: function() {}, + renameNote: function() {}, + }; let baseUrlSrvMock = { - getRestApiBase: function () { - return 'http://localhost:8080' - } - } + getRestApiBase: function() { + return 'http://localhost:8080'; + }, + }; let noteMock = { id: 1, name: 'my note', config: {}, - } + }; - beforeEach(inject(function ($controller, $rootScope) { - scope = $rootScope.$new() + beforeEach(inject(function($controller, $rootScope) { + scope = $rootScope.$new(); $controller('NotebookCtrl', { $scope: scope, websocketMsgSrv: websocketMsgSrvMock, - baseUrlSrv: baseUrlSrvMock - }) - })) + baseUrlSrv: baseUrlSrvMock, + }); + })); - beforeEach(function () { - scope.note = noteMock - }) + beforeEach(function() { + scope.note = noteMock; + }); let functions = ['getCronOptionNameFromValue', 'removeNote', 'runAllParagraphs', 'saveNote', 'toggleAllEditor', 'showAllEditor', 'hideAllEditor', 'toggleAllTable', 'hideAllTable', 'showAllTable', 'isNoteRunning', 'killSaveTimer', 'startSaveTimer', 'setLookAndFeel', 'setCronScheduler', 'setConfig', 'updateNoteName', - 'openSetting', 'closeSetting', 'saveSetting', 'toggleSetting'] - - functions.forEach(function (fn) { - it('check for scope functions to be defined : ' + fn, function () { - expect(scope[fn]).toBeDefined() - }) - }) - - it('should set default value of "editorToggled" to false', function () { - expect(scope.editorToggled).toEqual(false) - }) - - it('should set "showSetting" to true when openSetting() is called', function () { - scope.openSetting() - expect(scope.showSetting).toEqual(true) - }) - - it('should set "showSetting" to false when closeSetting() is called', function () { - scope.closeSetting() - expect(scope.showSetting).toEqual(false) - }) - - it('should return the correct value for getCronOptionNameFromValue()', function () { - let none = scope.getCronOptionNameFromValue() - let oneMin = scope.getCronOptionNameFromValue('0 0/1 * * * ?') - let fiveMin = scope.getCronOptionNameFromValue('0 0/5 * * * ?') - let oneHour = scope.getCronOptionNameFromValue('0 0 0/1 * * ?') - let threeHours = scope.getCronOptionNameFromValue('0 0 0/3 * * ?') - let sixHours = scope.getCronOptionNameFromValue('0 0 0/6 * * ?') - let twelveHours = scope.getCronOptionNameFromValue('0 0 0/12 * * ?') - let oneDay = scope.getCronOptionNameFromValue('0 0 0 * * ?') - - expect(none).toEqual('') - expect(oneMin).toEqual('1m') - expect(fiveMin).toEqual('5m') - expect(oneHour).toEqual('1h') - expect(threeHours).toEqual('3h') - expect(sixHours).toEqual('6h') - expect(twelveHours).toEqual('12h') - expect(oneDay).toEqual('1d') - }) - - it('should have "isNoteDirty" as null by default', function () { - expect(scope.isNoteDirty).toEqual(null) - }) - - it('should first call killSaveTimer() when calling startSaveTimer()', function () { - expect(scope.saveTimer).toEqual(null) - spyOn(scope, 'killSaveTimer') - scope.startSaveTimer() - expect(scope.killSaveTimer).toHaveBeenCalled() - }) - - it('should set "saveTimer" when saveTimer() and killSaveTimer() are called', function () { - expect(scope.saveTimer).toEqual(null) - scope.startSaveTimer() - expect(scope.saveTimer).toBeTruthy() - scope.killSaveTimer() - expect(scope.saveTimer).toEqual(null) - }) - - it('should NOT update note name when updateNoteName() is called with an invalid name', function () { - spyOn(websocketMsgSrvMock, 'renameNote') - scope.updateNoteName('') - expect(scope.note.name).toEqual(noteMock.name) - expect(websocketMsgSrvMock.renameNote).not.toHaveBeenCalled() - scope.updateNoteName(' ') - expect(scope.note.name).toEqual(noteMock.name) - expect(websocketMsgSrvMock.renameNote).not.toHaveBeenCalled() - scope.updateNoteName(scope.note.name) - expect(scope.note.name).toEqual(noteMock.name) - expect(websocketMsgSrvMock.renameNote).not.toHaveBeenCalled() - }) - - it('should update note name when updateNoteName() is called with a valid name', function () { - spyOn(websocketMsgSrvMock, 'renameNote') - let newName = 'Your Note' - scope.updateNoteName(newName) - expect(scope.note.name).toEqual(newName) - expect(websocketMsgSrvMock.renameNote).toHaveBeenCalled() - }) - - it('should reload note info once per one "setNoteMenu" event', function () { - spyOn(websocketMsgSrvMock, 'getNote') - spyOn(websocketMsgSrvMock, 'listRevisionHistory') - - scope.$broadcast('setNoteMenu') - expect(websocketMsgSrvMock.getNote.calls.count()).toEqual(0) - expect(websocketMsgSrvMock.listRevisionHistory.calls.count()).toEqual(0) - - websocketMsgSrvMock.getNote.calls.reset() - websocketMsgSrvMock.listRevisionHistory.calls.reset() - - scope.$broadcast('setNoteMenu') - expect(websocketMsgSrvMock.getNote.calls.count()).toEqual(0) - expect(websocketMsgSrvMock.listRevisionHistory.calls.count()).toEqual(0) - }) -}) + 'openSetting', 'closeSetting', 'saveSetting', 'toggleSetting']; + + functions.forEach(function(fn) { + it('check for scope functions to be defined : ' + fn, function() { + expect(scope[fn]).toBeDefined(); + }); + }); + + it('should set default value of "editorToggled" to false', function() { + expect(scope.editorToggled).toEqual(false); + }); + + it('should set "showSetting" to true when openSetting() is called', function() { + scope.openSetting(); + expect(scope.showSetting).toEqual(true); + }); + + it('should set "showSetting" to false when closeSetting() is called', function() { + scope.closeSetting(); + expect(scope.showSetting).toEqual(false); + }); + + it('should return the correct value for getCronOptionNameFromValue()', function() { + let none = scope.getCronOptionNameFromValue(); + let oneMin = scope.getCronOptionNameFromValue('0 0/1 * * * ?'); + let fiveMin = scope.getCronOptionNameFromValue('0 0/5 * * * ?'); + let oneHour = scope.getCronOptionNameFromValue('0 0 0/1 * * ?'); + let threeHours = scope.getCronOptionNameFromValue('0 0 0/3 * * ?'); + let sixHours = scope.getCronOptionNameFromValue('0 0 0/6 * * ?'); + let twelveHours = scope.getCronOptionNameFromValue('0 0 0/12 * * ?'); + let oneDay = scope.getCronOptionNameFromValue('0 0 0 * * ?'); + + expect(none).toEqual(''); + expect(oneMin).toEqual('1m'); + expect(fiveMin).toEqual('5m'); + expect(oneHour).toEqual('1h'); + expect(threeHours).toEqual('3h'); + expect(sixHours).toEqual('6h'); + expect(twelveHours).toEqual('12h'); + expect(oneDay).toEqual('1d'); + }); + + it('should have "isNoteDirty" as null by default', function() { + expect(scope.isNoteDirty).toEqual(null); + }); + + it('should first call killSaveTimer() when calling startSaveTimer()', function() { + expect(scope.saveTimer).toEqual(null); + spyOn(scope, 'killSaveTimer'); + scope.startSaveTimer(); + expect(scope.killSaveTimer).toHaveBeenCalled(); + }); + + it('should set "saveTimer" when saveTimer() and killSaveTimer() are called', function() { + expect(scope.saveTimer).toEqual(null); + scope.startSaveTimer(); + expect(scope.saveTimer).toBeTruthy(); + scope.killSaveTimer(); + expect(scope.saveTimer).toEqual(null); + }); + + it('should NOT update note name when updateNoteName() is called with an invalid name', function() { + spyOn(websocketMsgSrvMock, 'renameNote'); + scope.updateNoteName(''); + expect(scope.note.name).toEqual(noteMock.name); + expect(websocketMsgSrvMock.renameNote).not.toHaveBeenCalled(); + scope.updateNoteName(' '); + expect(scope.note.name).toEqual(noteMock.name); + expect(websocketMsgSrvMock.renameNote).not.toHaveBeenCalled(); + scope.updateNoteName(scope.note.name); + expect(scope.note.name).toEqual(noteMock.name); + expect(websocketMsgSrvMock.renameNote).not.toHaveBeenCalled(); + }); + + it('should update note name when updateNoteName() is called with a valid name', function() { + spyOn(websocketMsgSrvMock, 'renameNote'); + let newName = 'Your Note'; + scope.updateNoteName(newName); + expect(scope.note.name).toEqual(newName); + expect(websocketMsgSrvMock.renameNote).toHaveBeenCalled(); + }); + + it('should reload note info once per one "setNoteMenu" event', function() { + spyOn(websocketMsgSrvMock, 'getNote'); + spyOn(websocketMsgSrvMock, 'listRevisionHistory'); + + scope.$broadcast('setNoteMenu'); + expect(websocketMsgSrvMock.getNote.calls.count()).toEqual(0); + expect(websocketMsgSrvMock.listRevisionHistory.calls.count()).toEqual(0); + + websocketMsgSrvMock.getNote.calls.reset(); + websocketMsgSrvMock.listRevisionHistory.calls.reset(); + + scope.$broadcast('setNoteMenu'); + expect(websocketMsgSrvMock.getNote.calls.count()).toEqual(0); + expect(websocketMsgSrvMock.listRevisionHistory.calls.count()).toEqual(0); + }); +}); diff --git a/zeppelin-web/src/app/notebook/paragraph/clipboard.controller.js b/zeppelin-web/src/app/notebook/paragraph/clipboard.controller.js index 0eb78e390c0..ea75b2751db 100644 --- a/zeppelin-web/src/app/notebook/paragraph/clipboard.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/clipboard.controller.js @@ -11,24 +11,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -angular.module('zeppelinWebApp').controller('clipboardCtrl', ClipboardController) +angular.module('zeppelinWebApp').controller('clipboardCtrl', ClipboardController); -function ClipboardController ($scope) { - 'ngInject' +function ClipboardController($scope) { + 'ngInject'; - $scope.complete = function (e) { - $scope.copied = true - $scope.tooltip = 'Copied!' - setTimeout(function () { - $scope.tooltip = 'Copy to clipboard' - }, 400) - } - $scope.$watch('input', function () { - $scope.copied = false - $scope.tooltip = 'Copy to clipboard' - }) - $scope.clipError = function (e) { - console.log('Error: ' + e.name + ' - ' + e.message) - $scope.tooltip = 'Not supported browser' - } + $scope.complete = function(e) { + $scope.copied = true; + $scope.tooltip = 'Copied!'; + setTimeout(function() { + $scope.tooltip = 'Copy to clipboard'; + }, 400); + }; + $scope.$watch('input', function() { + $scope.copied = false; + $scope.tooltip = 'Copy to clipboard'; + }); + $scope.clipError = function(e) { + console.log('Error: ' + e.name + ' - ' + e.message); + $scope.tooltip = 'Not supported browser'; + }; } diff --git a/zeppelin-web/src/app/notebook/paragraph/code-editor/code-editor.directive.js b/zeppelin-web/src/app/notebook/paragraph/code-editor/code-editor.directive.js index 944f05d2782..db4a98fc498 100644 --- a/zeppelin-web/src/app/notebook/paragraph/code-editor/code-editor.directive.js +++ b/zeppelin-web/src/app/notebook/paragraph/code-editor/code-editor.directive.js @@ -12,7 +12,7 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').directive('codeEditor', CodeEditorDirective) +angular.module('zeppelinWebApp').directive('codeEditor', CodeEditorDirective); function CodeEditorDirective($templateRequest, $compile) { return { @@ -23,16 +23,16 @@ function CodeEditorDirective($templateRequest, $compile) { dirtyText: '=dirtyText', originalText: '=originalText', onLoad: '=onLoad', - revisionView: '=revisionView' + revisionView: '=revisionView', }, - link: function (scope, element, attrs, controller) { - $templateRequest('app/notebook/paragraph/code-editor/code-editor.directive.html').then(function (editorHtml) { - let editor = angular.element(editorHtml) - editor.attr('id', scope.paragraphId + '_editor') - element.append(editor) - $compile(editor)(scope) - console.debug('codeEditor directive revision view is ' + scope.revisionView) - }) - } - } + link: function(scope, element, attrs, controller) { + $templateRequest('app/notebook/paragraph/code-editor/code-editor.directive.html').then(function(editorHtml) { + let editor = angular.element(editorHtml); + editor.attr('id', scope.paragraphId + '_editor'); + element.append(editor); + $compile(editor)(scope); + console.debug('codeEditor directive revision view is ' + scope.revisionView); + }); + }, + }; } diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index 07ebf896dd0..971257cf502 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -12,35 +12,35 @@ * limitations under the License. */ -import {SpellResult} from '../../spell' -import {isParagraphRunning, ParagraphStatus} from './paragraph.status' +import {SpellResult} from '../../spell'; +import {isParagraphRunning, ParagraphStatus} from './paragraph.status'; -import moment from 'moment' +import moment from 'moment'; -require('moment-duration-format') +require('moment-duration-format'); const ParagraphExecutor = { SPELL: 'SPELL', INTERPRETER: 'INTERPRETER', NONE: '', /** meaning `DONE` */ -} +}; -angular.module('zeppelinWebApp').controller('ParagraphCtrl', ParagraphCtrl) +angular.module('zeppelinWebApp').controller('ParagraphCtrl', ParagraphCtrl); -function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $location, +function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $location, $timeout, $compile, $http, $q, websocketMsgSrv, baseUrlSrv, ngToast, noteVarShareService, heliumService) { - 'ngInject' + 'ngInject'; - let ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_' - $rootScope.keys = Object.keys - $scope.parentNote = null - $scope.paragraph = {} - $scope.paragraph.results = {} - $scope.paragraph.results.msg = [] - $scope.originalText = '' - $scope.editor = null + let ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_'; + $rootScope.keys = Object.keys; + $scope.parentNote = null; + $scope.paragraph = {}; + $scope.paragraph.results = {}; + $scope.paragraph.results.msg = []; + $scope.originalText = ''; + $scope.editor = null; // transactional info for spell execution $scope.spellTransaction = { @@ -49,161 +49,161 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca propagated: false, resultsMsg: [], paragraphText: '', - } + }; - let searchRanges = [] + let searchRanges = []; const getCurrentRangeDefault = function() { - return {id: -1, markerId: -1} - } - let currentRange = getCurrentRangeDefault() + return {id: -1, markerId: -1}; + }; + let currentRange = getCurrentRangeDefault(); - let editorSetting = {} + let editorSetting = {}; // flag that is used to set editor setting on paste percent sign - let pastePercentSign = false + let pastePercentSign = false; // flag that is used to set editor setting on save interpreter bindings - let setInterpreterBindings = false - let paragraphScope = $rootScope.$new(true, $rootScope) + let setInterpreterBindings = false; + let paragraphScope = $rootScope.$new(true, $rootScope); // to keep backward compatibility - $scope.compiledScope = paragraphScope + $scope.compiledScope = paragraphScope; paragraphScope.z = { // z.runParagraph('20150213-231621_168813393') - runParagraph: function (paragraphId) { + runParagraph: function(paragraphId) { if (paragraphId) { - let filtered = $scope.parentNote.paragraphs.filter(function (x) { - return x.id === paragraphId - }) + let filtered = $scope.parentNote.paragraphs.filter(function(x) { + return x.id === paragraphId; + }); if (filtered.length === 1) { - let paragraph = filtered[0] + let paragraph = filtered[0]; websocketMsgSrv.runParagraph(paragraph.id, paragraph.title, paragraph.text, - paragraph.config, paragraph.settings.params) + paragraph.config, paragraph.settings.params); } else { ngToast.danger({ content: 'Cannot find a paragraph with id \'' + paragraphId + '\'', verticalPosition: 'top', - dismissOnTimeout: false - }) + dismissOnTimeout: false, + }); } } else { ngToast.danger({ content: 'Please provide a \'paragraphId\' when calling z.runParagraph(paragraphId)', verticalPosition: 'top', - dismissOnTimeout: false - }) + dismissOnTimeout: false, + }); } }, // Example: z.angularBind('my_var', 'Test Value', '20150213-231621_168813393') - angularBind: function (varName, value, paragraphId) { + angularBind: function(varName, value, paragraphId) { // Only push to server if there paragraphId is defined if (paragraphId) { - websocketMsgSrv.clientBindAngularObject($routeParams.noteId, varName, value, paragraphId) + websocketMsgSrv.clientBindAngularObject($routeParams.noteId, varName, value, paragraphId); } else { ngToast.danger({ content: 'Please provide a \'paragraphId\' when calling ' + 'z.angularBind(varName, value, \'PUT_HERE_PARAGRAPH_ID\')', verticalPosition: 'top', - dismissOnTimeout: false - }) + dismissOnTimeout: false, + }); } }, // Example: z.angularUnBind('my_var', '20150213-231621_168813393') - angularUnbind: function (varName, paragraphId) { + angularUnbind: function(varName, paragraphId) { // Only push to server if paragraphId is defined if (paragraphId) { - websocketMsgSrv.clientUnbindAngularObject($routeParams.noteId, varName, paragraphId) + websocketMsgSrv.clientUnbindAngularObject($routeParams.noteId, varName, paragraphId); } else { ngToast.danger({ content: 'Please provide a \'paragraphId\' when calling ' + 'z.angularUnbind(varName, \'PUT_HERE_PARAGRAPH_ID\')', verticalPosition: 'top', - dismissOnTimeout: false}) + dismissOnTimeout: false}); } - } - } + }, + }; - let angularObjectRegistry = {} + let angularObjectRegistry = {}; // Controller init - $scope.init = function (newParagraph, note) { - $scope.paragraph = newParagraph - $scope.parentNote = note - $scope.originalText = angular.copy(newParagraph.text) - $scope.chart = {} - $scope.baseMapOption = ['Streets', 'Satellite', 'Hybrid', 'Topo', 'Gray', 'Oceans', 'Terrain'] - $scope.colWidthOption = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - $scope.fontSizeOption = [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] - $scope.paragraphFocused = false + $scope.init = function(newParagraph, note) { + $scope.paragraph = newParagraph; + $scope.parentNote = note; + $scope.originalText = angular.copy(newParagraph.text); + $scope.chart = {}; + $scope.baseMapOption = ['Streets', 'Satellite', 'Hybrid', 'Topo', 'Gray', 'Oceans', 'Terrain']; + $scope.colWidthOption = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + $scope.fontSizeOption = [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + $scope.paragraphFocused = false; if (newParagraph.focus) { - $scope.paragraphFocused = true + $scope.paragraphFocused = true; } if (!$scope.paragraph.config) { - $scope.paragraph.config = {} + $scope.paragraph.config = {}; } - noteVarShareService.put($scope.paragraph.id + '_paragraphScope', paragraphScope) + noteVarShareService.put($scope.paragraph.id + '_paragraphScope', paragraphScope); - initializeDefault($scope.paragraph.config) - } + initializeDefault($scope.paragraph.config); + }; - const initializeDefault = function (config) { - let forms = $scope.paragraph.settings.forms + const initializeDefault = function(config) { + let forms = $scope.paragraph.settings.forms; if (!config.colWidth) { - config.colWidth = 12 + config.colWidth = 12; } if (!config.fontSize) { - config.fontSize = 9 + config.fontSize = 9; } if (config.enabled === undefined) { - config.enabled = true + config.enabled = true; } for (let idx in forms) { if (forms[idx]) { if (forms[idx].options) { if (config.runOnSelectionChange === undefined) { - config.runOnSelectionChange = true + config.runOnSelectionChange = true; } } } } if (!config.results) { - config.results = {} + config.results = {}; } if (!config.editorSetting) { - config.editorSetting = {} + config.editorSetting = {}; } else if (config.editorSetting.editOnDblClick) { - editorSetting.isOutputHidden = config.editorSetting.editOnDblClick + editorSetting.isOutputHidden = config.editorSetting.editOnDblClick; } - } + }; const isTabCompletion = function() { - const completionKey = $scope.paragraph.config.editorSetting.completionKey - return completionKey === 'TAB' - } + const completionKey = $scope.paragraph.config.editorSetting.completionKey; + return completionKey === 'TAB'; + }; - $scope.$on('updateParagraphOutput', function (event, data) { + $scope.$on('updateParagraphOutput', function(event, data) { if ($scope.paragraph.id === data.paragraphId) { if (!$scope.paragraph.results) { - $scope.paragraph.results = {} + $scope.paragraph.results = {}; } if (!$scope.paragraph.results.msg) { - $scope.paragraph.results.msg = [] + $scope.paragraph.results.msg = []; } - let update = ($scope.paragraph.results.msg[data.index]) ? true : false + let update = ($scope.paragraph.results.msg[data.index]) ? true : false; $scope.paragraph.results.msg[data.index] = { data: data.data, - type: data.type - } + type: data.type, + }; if (update) { $rootScope.$broadcast( @@ -211,62 +211,62 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca $scope.paragraph.results.msg[data.index], $scope.paragraph.config.results[data.index], $scope.paragraph, - data.index) + data.index); } } - }) + }); - $scope.getIframeDimensions = function () { + $scope.getIframeDimensions = function() { if ($scope.asIframe) { - let paragraphid = '#' + $routeParams.paragraphId + '_container' - let height = angular.element(paragraphid).height() - return height + let paragraphid = '#' + $routeParams.paragraphId + '_container'; + let height = angular.element(paragraphid).height(); + return height; } - return 0 - } + return 0; + }; - $scope.$watch($scope.getIframeDimensions, function (newValue, oldValue) { + $scope.$watch($scope.getIframeDimensions, function(newValue, oldValue) { if ($scope.asIframe && newValue) { - let message = {} - message.height = newValue - message.url = $location.$$absUrl - $window.parent.postMessage(angular.toJson(message), '*') + let message = {}; + message.height = newValue; + message.url = $location.$$absUrl; + $window.parent.postMessage(angular.toJson(message), '*'); } - }) + }); - $scope.getEditor = function () { - return $scope.editor - } + $scope.getEditor = function() { + return $scope.editor; + }; - $scope.$watch($scope.getEditor, function (newValue, oldValue) { + $scope.$watch($scope.getEditor, function(newValue, oldValue) { if (!$scope.editor) { - return + return; } if (newValue === null || newValue === undefined) { - console.log('editor isnt loaded yet, returning') - return + console.log('editor isnt loaded yet, returning'); + return; } if ($scope.revisionView === true) { - $scope.editor.setReadOnly(true) + $scope.editor.setReadOnly(true); } else { - $scope.editor.setReadOnly(false) + $scope.editor.setReadOnly(false); } - }) + }); - let isEmpty = function (object) { - return !object - } + let isEmpty = function(object) { + return !object; + }; - $scope.isRunning = function (paragraph) { - return isParagraphRunning(paragraph) - } + $scope.isRunning = function(paragraph) { + return isParagraphRunning(paragraph); + }; - $scope.cancelParagraph = function (paragraph) { - console.log('Cancel %o', paragraph.id) - websocketMsgSrv.cancelParagraphRun(paragraph.id) - } + $scope.cancelParagraph = function(paragraph) { + console.log('Cancel %o', paragraph.id); + websocketMsgSrv.cancelParagraphRun(paragraph.id); + }; - $scope.propagateSpellResult = function (paragraphId, paragraphTitle, + $scope.propagateSpellResult = function(paragraphId, paragraphTitle, paragraphText, paragraphResults, paragraphStatus, paragraphErrorMessage, paragraphConfig, paragraphSettingsParam, @@ -277,18 +277,18 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca paragraphStatus, paragraphErrorMessage, paragraphConfig, paragraphSettingsParam, paragraphDateStarted, paragraphDateFinished - ) - } + ); + }; - $scope.handleSpellError = function (paragraphText, error, + $scope.handleSpellError = function(paragraphText, error, digestRequired, propagated) { - const errorMessage = error.stack - $scope.paragraph.status = ParagraphStatus.ERROR - $scope.paragraph.errorMessage = errorMessage - console.error('Failed to execute interpret() in spell\n', error) + const errorMessage = error.stack; + $scope.paragraph.status = ParagraphStatus.ERROR; + $scope.paragraph.errorMessage = errorMessage; + console.error('Failed to execute interpret() in spell\n', error); if (!propagated) { - $scope.paragraph.dateFinished = $scope.getFormattedParagraphTime() + $scope.paragraph.dateFinished = $scope.getFormattedParagraphTime(); } if (!propagated) { @@ -296,547 +296,551 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca $scope.paragraph.id, $scope.paragraph.title, paragraphText, [], $scope.paragraph.status, errorMessage, $scope.paragraph.config, $scope.paragraph.settings.params, - $scope.paragraph.dateStarted, $scope.paragraph.dateFinished) + $scope.paragraph.dateStarted, $scope.paragraph.dateFinished); } - } + }; - $scope.prepareSpellTransaction = function (resultsMsg, propagated, paragraphText) { - $scope.spellTransaction.totalResultCount = resultsMsg.length - $scope.spellTransaction.renderedResultCount = 0 - $scope.spellTransaction.propagated = propagated - $scope.spellTransaction.resultsMsg = resultsMsg - $scope.spellTransaction.paragraphText = paragraphText - } + $scope.prepareSpellTransaction = function(resultsMsg, propagated, paragraphText) { + $scope.spellTransaction.totalResultCount = resultsMsg.length; + $scope.spellTransaction.renderedResultCount = 0; + $scope.spellTransaction.propagated = propagated; + $scope.spellTransaction.resultsMsg = resultsMsg; + $scope.spellTransaction.paragraphText = paragraphText; + }; /** * - update spell transaction count and * - check transaction is finished based on the result count * @returns {boolean} */ - $scope.increaseSpellTransactionResultCount = function () { - $scope.spellTransaction.renderedResultCount += 1 + $scope.increaseSpellTransactionResultCount = function() { + $scope.spellTransaction.renderedResultCount += 1; - const total = $scope.spellTransaction.totalResultCount - const current = $scope.spellTransaction.renderedResultCount - return total === current - } + const total = $scope.spellTransaction.totalResultCount; + const current = $scope.spellTransaction.renderedResultCount; + return total === current; + }; - $scope.cleanupSpellTransaction = function () { - const status = ParagraphStatus.FINISHED - $scope.paragraph.executor = ParagraphExecutor.NONE - $scope.paragraph.status = status - $scope.paragraph.results.code = status + $scope.cleanupSpellTransaction = function() { + const status = ParagraphStatus.FINISHED; + $scope.paragraph.executor = ParagraphExecutor.NONE; + $scope.paragraph.status = status; + $scope.paragraph.results.code = status; - const propagated = $scope.spellTransaction.propagated - const resultsMsg = $scope.spellTransaction.resultsMsg - const paragraphText = $scope.spellTransaction.paragraphText + const propagated = $scope.spellTransaction.propagated; + const resultsMsg = $scope.spellTransaction.resultsMsg; + const paragraphText = $scope.spellTransaction.paragraphText; if (!propagated) { - $scope.paragraph.dateFinished = $scope.getFormattedParagraphTime() + $scope.paragraph.dateFinished = $scope.getFormattedParagraphTime(); } if (!propagated) { - const propagable = SpellResult.createPropagable(resultsMsg) + const propagable = SpellResult.createPropagable(resultsMsg); $scope.propagateSpellResult( $scope.paragraph.id, $scope.paragraph.title, paragraphText, propagable, status, '', $scope.paragraph.config, $scope.paragraph.settings.params, - $scope.paragraph.dateStarted, $scope.paragraph.dateFinished) + $scope.paragraph.dateStarted, $scope.paragraph.dateFinished); } - } + }; - $scope.runParagraphUsingSpell = function (paragraphText, + $scope.runParagraphUsingSpell = function(paragraphText, magic, digestRequired, propagated) { - $scope.paragraph.status = 'RUNNING' - $scope.paragraph.executor = ParagraphExecutor.SPELL - $scope.paragraph.results = {} - $scope.paragraph.errorMessage = '' - if (digestRequired) { $scope.$digest() } + $scope.paragraph.status = 'RUNNING'; + $scope.paragraph.executor = ParagraphExecutor.SPELL; + $scope.paragraph.results = {}; + $scope.paragraph.errorMessage = ''; + if (digestRequired) { + $scope.$digest(); + } try { // remove magic from paragraphText - const splited = paragraphText.split(magic) + const splited = paragraphText.split(magic); // remove leading spaces - const textWithoutMagic = splited[1].replace(/^\s+/g, '') + const textWithoutMagic = splited[1].replace(/^\s+/g, ''); if (!propagated) { - $scope.paragraph.dateStarted = $scope.getFormattedParagraphTime() + $scope.paragraph.dateStarted = $scope.getFormattedParagraphTime(); } // handle actual result message in promise heliumService.executeSpell(magic, textWithoutMagic) - .then(resultsMsg => { - $scope.prepareSpellTransaction(resultsMsg, propagated, paragraphText) + .then((resultsMsg) => { + $scope.prepareSpellTransaction(resultsMsg, propagated, paragraphText); - $scope.paragraph.results.msg = resultsMsg - $scope.paragraph.config.tableHide = false + $scope.paragraph.results.msg = resultsMsg; + $scope.paragraph.config.tableHide = false; }) - .catch(error => { + .catch((error) => { $scope.handleSpellError(paragraphText, error, - digestRequired, propagated) - }) + digestRequired, propagated); + }); } catch (error) { $scope.handleSpellError(paragraphText, error, - digestRequired, propagated) + digestRequired, propagated); } - } + }; - $scope.runParagraphUsingBackendInterpreter = function (paragraphText) { + $scope.runParagraphUsingBackendInterpreter = function(paragraphText) { websocketMsgSrv.runParagraph($scope.paragraph.id, $scope.paragraph.title, - paragraphText, $scope.paragraph.config, $scope.paragraph.settings.params) - } + paragraphText, $scope.paragraph.config, $scope.paragraph.settings.params); + }; - $scope.bindBeforeUnload = function () { - angular.element(window).off('beforeunload') + $scope.bindBeforeUnload = function() { + angular.element(window).off('beforeunload'); - let confirmOnPageExit = function (e) { + let confirmOnPageExit = function(e) { // If we haven't been passed the event get the window.event - e = e || window.event - let message = 'Do you want to reload this site?' + e = e || window.event; + let message = 'Do you want to reload this site?'; // For IE6-8 and Firefox prior to version 4 if (e) { - e.returnValue = message + e.returnValue = message; } // For Chrome, Safari, IE8+ and Opera 12+ - return message - } - angular.element(window).on('beforeunload', confirmOnPageExit) - } + return message; + }; + angular.element(window).on('beforeunload', confirmOnPageExit); + }; - $scope.unBindBeforeUnload = function () { - angular.element(window).off('beforeunload') - } + $scope.unBindBeforeUnload = function() { + angular.element(window).off('beforeunload'); + }; - $scope.saveParagraph = function (paragraph) { - const dirtyText = paragraph.text + $scope.saveParagraph = function(paragraph) { + const dirtyText = paragraph.text; if (dirtyText === undefined || dirtyText === $scope.originalText) { - return + return; } - $scope.bindBeforeUnload() + $scope.bindBeforeUnload(); - commitParagraph(paragraph).then(function () { - $scope.originalText = dirtyText - $scope.dirtyText = undefined - $scope.unBindBeforeUnload() - }) - } + commitParagraph(paragraph).then(function() { + $scope.originalText = dirtyText; + $scope.dirtyText = undefined; + $scope.unBindBeforeUnload(); + }); + }; - $scope.toggleEnableDisable = function (paragraph) { - paragraph.config.enabled = !paragraph.config.enabled - commitParagraph(paragraph) - } + $scope.toggleEnableDisable = function(paragraph) { + paragraph.config.enabled = !paragraph.config.enabled; + commitParagraph(paragraph); + }; /** * @param paragraphText to be parsed * @param digestRequired true if calling `$digest` is required * @param propagated true if update request is sent from other client */ - $scope.runParagraph = function (paragraphText, digestRequired, propagated) { + $scope.runParagraph = function(paragraphText, digestRequired, propagated) { if (!paragraphText || $scope.isRunning($scope.paragraph)) { - return + return; } - const magic = SpellResult.extractMagic(paragraphText) + const magic = SpellResult.extractMagic(paragraphText); if (heliumService.getSpellByMagic(magic)) { - $scope.runParagraphUsingSpell(paragraphText, magic, digestRequired, propagated) + $scope.runParagraphUsingSpell(paragraphText, magic, digestRequired, propagated); } else { - $scope.runParagraphUsingBackendInterpreter(paragraphText) + $scope.runParagraphUsingBackendInterpreter(paragraphText); } - $scope.originalText = angular.copy(paragraphText) - $scope.dirtyText = undefined + $scope.originalText = angular.copy(paragraphText); + $scope.dirtyText = undefined; if ($scope.paragraph.config.editorSetting.editOnDblClick) { - closeEditorAndOpenTable($scope.paragraph) + closeEditorAndOpenTable($scope.paragraph); } else if (editorSetting.isOutputHidden && !$scope.paragraph.config.editorSetting.editOnDblClick) { // %md/%angular repl make output to be hidden by default after running // so should open output if repl changed from %md/%angular to another - openEditorAndOpenTable($scope.paragraph) + openEditorAndOpenTable($scope.paragraph); } - editorSetting.isOutputHidden = $scope.paragraph.config.editorSetting.editOnDblClick - } + editorSetting.isOutputHidden = $scope.paragraph.config.editorSetting.editOnDblClick; + }; - $scope.runParagraphFromShortcut = function (paragraphText) { + $scope.runParagraphFromShortcut = function(paragraphText) { // passing `digestRequired` as true to update view immediately // without this, results cannot be rendered in view more than once - $scope.runParagraph(paragraphText, true, false) - } + $scope.runParagraph(paragraphText, true, false); + }; - $scope.runParagraphFromButton = function () { + $scope.runParagraphFromButton = function() { // we come here from the view, so we don't need to call `$digest()` - $scope.runParagraph($scope.getEditorValue(), false, false) - } + $scope.runParagraph($scope.getEditorValue(), false, false); + }; $scope.runAllToThis = function(paragraph) { - $scope.$emit('runAllAbove', paragraph, true) - } + $scope.$emit('runAllAbove', paragraph, true); + }; $scope.runAllFromThis = function(paragraph) { - $scope.$emit('runAllBelowAndCurrent', paragraph, true) - } + $scope.$emit('runAllBelowAndCurrent', paragraph, true); + }; - $scope.runAllToOrFromThis = function (paragraph) { + $scope.runAllToOrFromThis = function(paragraph) { BootstrapDialog.show({ message: 'Run paragraphs:', title: '', buttons: [{ label: 'Close', action: function(dialog) { - dialog.close() - } + dialog.close(); + }, }, { label: 'Run all above', cssClass: 'btn-primary', action: function(dialog) { - $scope.$emit('runAllAbove', paragraph, false) - dialog.close() - } + $scope.$emit('runAllAbove', paragraph, false); + dialog.close(); + }, }, { label: 'Run current and all below', cssClass: 'btn-primary', action: function(dialog) { - $scope.$emit('runAllBelowAndCurrent', paragraph, false) - dialog.close() - } - }] - }) - } + $scope.$emit('runAllBelowAndCurrent', paragraph, false); + dialog.close(); + }, + }], + }); + }; - $scope.turnOnAutoRun = function (paragraph) { - paragraph.config.runOnSelectionChange = !paragraph.config.runOnSelectionChange - commitParagraph(paragraph) - } + $scope.turnOnAutoRun = function(paragraph) { + paragraph.config.runOnSelectionChange = !paragraph.config.runOnSelectionChange; + commitParagraph(paragraph); + }; - $scope.moveUp = function (paragraph) { - $scope.$emit('moveParagraphUp', paragraph) - } + $scope.moveUp = function(paragraph) { + $scope.$emit('moveParagraphUp', paragraph); + }; - $scope.moveDown = function (paragraph) { - $scope.$emit('moveParagraphDown', paragraph) - } + $scope.moveDown = function(paragraph) { + $scope.$emit('moveParagraphDown', paragraph); + }; - $scope.insertNew = function (position) { - $scope.$emit('insertParagraph', $scope.paragraph.id, position) - } + $scope.insertNew = function(position) { + $scope.$emit('insertParagraph', $scope.paragraph.id, position); + }; - $scope.copyPara = function (position) { - let editorValue = $scope.getEditorValue() + $scope.copyPara = function(position) { + let editorValue = $scope.getEditorValue(); if (editorValue) { - $scope.copyParagraph(editorValue, position) + $scope.copyParagraph(editorValue, position); } - } + }; - $scope.copyParagraph = function (data, position) { - let newIndex = -1 + $scope.copyParagraph = function(data, position) { + let newIndex = -1; for (let i = 0; i < $scope.note.paragraphs.length; i++) { if ($scope.note.paragraphs[i].id === $scope.paragraph.id) { // determine position of where to add new paragraph; default is below if (position === 'above') { - newIndex = i + newIndex = i; } else { - newIndex = i + 1 + newIndex = i + 1; } - break + break; } } if (newIndex < 0 || newIndex > $scope.note.paragraphs.length) { - return + return; } - let config = angular.copy($scope.paragraph.config) - config.editorHide = false + let config = angular.copy($scope.paragraph.config); + config.editorHide = false; websocketMsgSrv.copyParagraph(newIndex, $scope.paragraph.title, data, - config, $scope.paragraph.settings.params) - } + config, $scope.paragraph.settings.params); + }; - $scope.removeParagraph = function (paragraph) { + $scope.removeParagraph = function(paragraph) { if ($scope.note.paragraphs.length === 1) { BootstrapDialog.alert({ closable: true, - message: 'All the paragraphs can\'t be deleted.' - }) + message: 'All the paragraphs can\'t be deleted.', + }); } else { BootstrapDialog.confirm({ closable: true, title: '', message: 'Do you want to delete this paragraph?', - callback: function (result) { + callback: function(result) { if (result) { - console.log('Remove paragraph') - websocketMsgSrv.removeParagraph(paragraph.id) - $scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id) + console.log('Remove paragraph'); + websocketMsgSrv.removeParagraph(paragraph.id); + $scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id); } - } - }) + }, + }); } - } + }; - $scope.clearParagraphOutput = function (paragraph) { - websocketMsgSrv.clearParagraphOutput(paragraph.id) - } + $scope.clearParagraphOutput = function(paragraph) { + websocketMsgSrv.clearParagraphOutput(paragraph.id); + }; - $scope.toggleEditor = function (paragraph) { + $scope.toggleEditor = function(paragraph) { if (paragraph.config.editorHide) { - $scope.openEditor(paragraph) + $scope.openEditor(paragraph); } else { - $scope.closeEditor(paragraph) - } - } - - $scope.closeEditor = function (paragraph) { - console.log('close the note') - paragraph.config.editorHide = true - commitParagraph(paragraph) - } - - $scope.openEditor = function (paragraph) { - console.log('open the note') - paragraph.config.editorHide = false - commitParagraph(paragraph) - } - - $scope.closeTable = function (paragraph) { - console.log('close the output') - paragraph.config.tableHide = true - commitParagraph(paragraph) - } - - $scope.openTable = function (paragraph) { - console.log('open the output') - paragraph.config.tableHide = false - commitParagraph(paragraph) - } - - let openEditorAndCloseTable = function (paragraph) { - manageEditorAndTableState(paragraph, false, true) - } - - const closeEditorAndOpenTable = function (paragraph) { - manageEditorAndTableState(paragraph, true, false) - } - - const openEditorAndOpenTable = function (paragraph) { - manageEditorAndTableState(paragraph, false, false) - } - - const manageEditorAndTableState = function (paragraph, hideEditor, hideTable) { - paragraph.config.editorHide = hideEditor - paragraph.config.tableHide = hideTable - commitParagraph(paragraph) - } - - $scope.showTitle = function (paragraph) { - paragraph.config.title = true - commitParagraph(paragraph) - } - - $scope.hideTitle = function (paragraph) { - paragraph.config.title = false - commitParagraph(paragraph) - } - - $scope.setTitle = function (paragraph) { - commitParagraph(paragraph) - } - - $scope.showLineNumbers = function (paragraph) { + $scope.closeEditor(paragraph); + } + }; + + $scope.closeEditor = function(paragraph) { + console.log('close the note'); + paragraph.config.editorHide = true; + commitParagraph(paragraph); + }; + + $scope.openEditor = function(paragraph) { + console.log('open the note'); + paragraph.config.editorHide = false; + commitParagraph(paragraph); + }; + + $scope.closeTable = function(paragraph) { + console.log('close the output'); + paragraph.config.tableHide = true; + commitParagraph(paragraph); + }; + + $scope.openTable = function(paragraph) { + console.log('open the output'); + paragraph.config.tableHide = false; + commitParagraph(paragraph); + }; + + let openEditorAndCloseTable = function(paragraph) { + manageEditorAndTableState(paragraph, false, true); + }; + + const closeEditorAndOpenTable = function(paragraph) { + manageEditorAndTableState(paragraph, true, false); + }; + + const openEditorAndOpenTable = function(paragraph) { + manageEditorAndTableState(paragraph, false, false); + }; + + const manageEditorAndTableState = function(paragraph, hideEditor, hideTable) { + paragraph.config.editorHide = hideEditor; + paragraph.config.tableHide = hideTable; + commitParagraph(paragraph); + }; + + $scope.showTitle = function(paragraph) { + paragraph.config.title = true; + commitParagraph(paragraph); + }; + + $scope.hideTitle = function(paragraph) { + paragraph.config.title = false; + commitParagraph(paragraph); + }; + + $scope.setTitle = function(paragraph) { + commitParagraph(paragraph); + }; + + $scope.showLineNumbers = function(paragraph) { if ($scope.editor) { - paragraph.config.lineNumbers = true - $scope.editor.renderer.setShowGutter(true) - commitParagraph(paragraph) + paragraph.config.lineNumbers = true; + $scope.editor.renderer.setShowGutter(true); + commitParagraph(paragraph); } - } + }; - $scope.hideLineNumbers = function (paragraph) { + $scope.hideLineNumbers = function(paragraph) { if ($scope.editor) { - paragraph.config.lineNumbers = false - $scope.editor.renderer.setShowGutter(false) - commitParagraph(paragraph) + paragraph.config.lineNumbers = false; + $scope.editor.renderer.setShowGutter(false); + commitParagraph(paragraph); } - } + }; - $scope.columnWidthClass = function (n) { + $scope.columnWidthClass = function(n) { if ($scope.asIframe) { - return 'col-md-12' + return 'col-md-12'; } else { - return 'paragraph-col col-md-' + n + return 'paragraph-col col-md-' + n; } - } + }; - $scope.changeColWidth = function (paragraph, width) { - angular.element('.navbar-right.open').removeClass('open') - paragraph.config.colWidth = width - $scope.$broadcast('paragraphResized', $scope.paragraph.id) - commitParagraph(paragraph) - } + $scope.changeColWidth = function(paragraph, width) { + angular.element('.navbar-right.open').removeClass('open'); + paragraph.config.colWidth = width; + $scope.$broadcast('paragraphResized', $scope.paragraph.id); + commitParagraph(paragraph); + }; - $scope.changeFontSize = function (paragraph, fontSize) { - angular.element('.navbar-right.open').removeClass('open') + $scope.changeFontSize = function(paragraph, fontSize) { + angular.element('.navbar-right.open').removeClass('open'); if ($scope.editor) { $scope.editor.setOptions({ - fontSize: fontSize + 'pt' - }) - autoAdjustEditorHeight($scope.editor) - paragraph.config.fontSize = fontSize - commitParagraph(paragraph) - } - } - - $scope.toggleOutput = function (paragraph) { - paragraph.config.tableHide = !paragraph.config.tableHide - commitParagraph(paragraph) - } - - $scope.aceChanged = function (_, editor) { - let session = editor.getSession() - let dirtyText = session.getValue() - $scope.dirtyText = dirtyText + fontSize: fontSize + 'pt', + }); + autoAdjustEditorHeight($scope.editor); + paragraph.config.fontSize = fontSize; + commitParagraph(paragraph); + } + }; + + $scope.toggleOutput = function(paragraph) { + paragraph.config.tableHide = !paragraph.config.tableHide; + commitParagraph(paragraph); + }; + + $scope.aceChanged = function(_, editor) { + let session = editor.getSession(); + let dirtyText = session.getValue(); + $scope.dirtyText = dirtyText; if ($scope.dirtyText !== $scope.originalText) { - $scope.startSaveTimer() + $scope.startSaveTimer(); } - setParagraphMode(session, dirtyText, editor.getCursorPosition()) - } + setParagraphMode(session, dirtyText, editor.getCursorPosition()); + }; - $scope.aceLoaded = function (_editor) { - let langTools = ace.require('ace/ext/language_tools') - let Range = ace.require('ace/range').Range + $scope.aceLoaded = function(_editor) { + let langTools = ace.require('ace/ext/language_tools'); + let Range = ace.require('ace/range').Range; - _editor.$blockScrolling = Infinity - $scope.editor = _editor - $scope.editor.on('input', $scope.aceChanged) + _editor.$blockScrolling = Infinity; + $scope.editor = _editor; + $scope.editor.on('input', $scope.aceChanged); if (_editor.container.id !== '{{paragraph.id}}_editor') { - $scope.editor.renderer.setShowGutter($scope.paragraph.config.lineNumbers) - $scope.editor.setShowFoldWidgets(false) - $scope.editor.setHighlightActiveLine(false) - $scope.editor.getSession().setUseWrapMode(true) - $scope.editor.setTheme('ace/theme/chrome') - $scope.editor.setReadOnly($scope.isRunning($scope.paragraph)) - $scope.editor.setHighlightActiveLine($scope.paragraphFocused) + $scope.editor.renderer.setShowGutter($scope.paragraph.config.lineNumbers); + $scope.editor.setShowFoldWidgets(false); + $scope.editor.setHighlightActiveLine(false); + $scope.editor.getSession().setUseWrapMode(true); + $scope.editor.setTheme('ace/theme/chrome'); + $scope.editor.setReadOnly($scope.isRunning($scope.paragraph)); + $scope.editor.setHighlightActiveLine($scope.paragraphFocused); if ($scope.paragraphFocused) { - let prefix = '%' + getInterpreterName($scope.paragraph.text) - let paragraphText = $scope.paragraph.text ? $scope.paragraph.text.trim() : '' + let prefix = '%' + getInterpreterName($scope.paragraph.text); + let paragraphText = $scope.paragraph.text ? $scope.paragraph.text.trim() : ''; - $scope.editor.focus() - $scope.goToEnd($scope.editor) + $scope.editor.focus(); + $scope.goToEnd($scope.editor); if (prefix === paragraphText) { - $timeout(function () { - $scope.editor.gotoLine(2, 0) - }, 0) + $timeout(function() { + $scope.editor.gotoLine(2, 0); + }, 0); } } - autoAdjustEditorHeight(_editor) - angular.element(window).resize(function () { - autoAdjustEditorHeight(_editor) - }) + autoAdjustEditorHeight(_editor); + angular.element(window).resize(function() { + autoAdjustEditorHeight(_editor); + }); if (navigator.appVersion.indexOf('Mac') !== -1) { - $scope.editor.setKeyboardHandler('ace/keyboard/emacs') - $rootScope.isMac = true + $scope.editor.setKeyboardHandler('ace/keyboard/emacs'); + $rootScope.isMac = true; } else if (navigator.appVersion.indexOf('Win') !== -1 || navigator.appVersion.indexOf('X11') !== -1 || navigator.appVersion.indexOf('Linux') !== -1) { - $rootScope.isMac = false + $rootScope.isMac = false; // not applying emacs key binding while the binding override Ctrl-v. default behavior of paste text on windows. } let remoteCompleter = { getCompletions: function(editor, session, pos, prefix, callback) { - let langTools = ace.require('ace/ext/language_tools') - let defaultKeywords = new Set() + let langTools = ace.require('ace/ext/language_tools'); + let defaultKeywords = new Set(); // eslint-disable-next-line handle-callback-err let getDefaultKeywords = function(err, completions) { if (completions !== undefined) { completions.forEach(function(c) { - defaultKeywords.add(c.value) - }) + defaultKeywords.add(c.value); + }); } - } + }; if (langTools.keyWordCompleter !== undefined) { - langTools.keyWordCompleter.getCompletions(editor, session, pos, prefix, getDefaultKeywords) + langTools.keyWordCompleter.getCompletions(editor, session, pos, prefix, getDefaultKeywords); } if (!editor.isFocused()) { - return + return; } - pos = session.getTextRange(new Range(0, 0, pos.row, pos.column)).length - let buf = session.getValue() + pos = session.getTextRange(new Range(0, 0, pos.row, pos.column)).length; + let buf = session.getValue(); - websocketMsgSrv.completion($scope.paragraph.id, buf, pos) + websocketMsgSrv.completion($scope.paragraph.id, buf, pos); $scope.$on('completionList', function(event, data) { let computeCaption = function(value, meta) { - let metaLength = meta !== undefined ? meta.length : 0 - let length = 42 - let whitespaceLength = 3 - let ellipses = '...' - let maxLengthCaption = length - metaLength - whitespaceLength - ellipses.length + let metaLength = meta !== undefined ? meta.length : 0; + let length = 42; + let whitespaceLength = 3; + let ellipses = '...'; + let maxLengthCaption = length - metaLength - whitespaceLength - ellipses.length; if (value !== undefined && value.length > maxLengthCaption) { - return value.substr(0, maxLengthCaption) + ellipses + return value.substr(0, maxLengthCaption) + ellipses; } - return value - } + return value; + }; if (data.completions) { - let completions = [] + let completions = []; for (let c in data.completions) { - let v = data.completions[c] - if (v.meta !== undefined && v.meta === 'keyword' && defaultKeywords.has(v.value.trim())) { - continue + if (data.completions.hasOwnProperty(c)) { + let v = data.completions[c]; + if (v.meta !== undefined && v.meta === 'keyword' && defaultKeywords.has(v.value.trim())) { + continue; + } + completions.push({ + name: v.name, + value: v.value, + meta: v.meta, + caption: computeCaption(v.value, v.meta), + score: 300, + }); } - completions.push({ - name: v.name, - value: v.value, - meta: v.meta, - caption: computeCaption(v.value, v.meta), - score: 300 - }) } - callback(null, completions) + callback(null, completions); } - }) - } - } + }); + }, + }; langTools.setCompleters([remoteCompleter, langTools.keyWordCompleter, langTools.snippetCompleter, - langTools.textCompleter]) + langTools.textCompleter]); $scope.editor.setOptions({ fontSize: $scope.paragraph.config.fontSize + 'pt', enableBasicAutocompletion: true, enableSnippets: false, - enableLiveAutocompletion: false - }) + enableLiveAutocompletion: false, + }); - $scope.editor.on('focus', function () { - handleFocus(true) - }) + $scope.editor.on('focus', function() { + handleFocus(true); + }); - $scope.editor.on('blur', function () { - handleFocus(false) - $scope.saveParagraph($scope.paragraph) - }) + $scope.editor.on('blur', function() { + handleFocus(false); + $scope.saveParagraph($scope.paragraph); + }); - $scope.editor.on('paste', function (e) { + $scope.editor.on('paste', function(e) { if (e.text.indexOf('%') === 0) { - pastePercentSign = true + pastePercentSign = true; } - }) + }); - $scope.editor.getSession().on('change', function (e, editSession) { - autoAdjustEditorHeight(_editor) - }) + $scope.editor.getSession().on('change', function(e, editSession) { + autoAdjustEditorHeight(_editor); + }); - setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue()) + setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue()); // autocomplete on '.' /* @@ -851,29 +855,29 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca */ // remove binding - $scope.editor.commands.removeCommand('showSettingsMenu') - $scope.editor.commands.removeCommand('find') - $scope.editor.commands.removeCommand('replace') + $scope.editor.commands.removeCommand('showSettingsMenu'); + $scope.editor.commands.removeCommand('find'); + $scope.editor.commands.removeCommand('replace'); - let isOption = $rootScope.isMac ? 'option' : 'alt' + let isOption = $rootScope.isMac ? 'option' : 'alt'; - $scope.editor.commands.bindKey('ctrl-' + isOption + '-n.', null) - $scope.editor.commands.bindKey('ctrl-' + isOption + '-l', null) - $scope.editor.commands.bindKey('ctrl-' + isOption + '-w', null) - $scope.editor.commands.bindKey('ctrl-' + isOption + '-a', null) - $scope.editor.commands.bindKey('ctrl-' + isOption + '-k', null) - $scope.editor.commands.bindKey('ctrl-' + isOption + '-e', null) - $scope.editor.commands.bindKey('ctrl-' + isOption + '-t', null) - $scope.editor.commands.bindKey('ctrl-space', null) + $scope.editor.commands.bindKey('ctrl-' + isOption + '-n.', null); + $scope.editor.commands.bindKey('ctrl-' + isOption + '-l', null); + $scope.editor.commands.bindKey('ctrl-' + isOption + '-w', null); + $scope.editor.commands.bindKey('ctrl-' + isOption + '-a', null); + $scope.editor.commands.bindKey('ctrl-' + isOption + '-k', null); + $scope.editor.commands.bindKey('ctrl-' + isOption + '-e', null); + $scope.editor.commands.bindKey('ctrl-' + isOption + '-t', null); + $scope.editor.commands.bindKey('ctrl-space', null); if ($rootScope.isMac) { - $scope.editor.commands.bindKey('command-l', null) + $scope.editor.commands.bindKey('command-l', null); } else { - $scope.editor.commands.bindKey('ctrl-l', null) + $scope.editor.commands.bindKey('ctrl-l', null); } // autocomplete on 'ctrl+.' - $scope.editor.commands.bindKey('ctrl-.', 'startAutocomplete') + $scope.editor.commands.bindKey('ctrl-.', 'startAutocomplete'); // Show autocomplete on tab $scope.editor.commands.addCommand({ @@ -881,113 +885,123 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca bindKey: { win: 'tab', mac: 'tab', - sender: 'editor|cli' + sender: 'editor|cli', }, exec: function(env, args, request) { - let iCursor = $scope.editor.getCursorPosition() - let currentLine = $scope.editor.session.getLine(iCursor.row) + let iCursor = $scope.editor.getCursorPosition(); + let currentLine = $scope.editor.session.getLine(iCursor.row); let isAllTabs = currentLine.substring(0, iCursor.column - 1).split('').every(function(char) { - return (char === '\t' || char === ' ') - }) + return (char === '\t' || char === ' '); + }); // If user has pressed tab on first line char or if isTabCompletion() is false, keep existing behavior // If user has pressed tab anywhere in between and editor mode is not %md, show autocomplete if (!isAllTabs && iCursor.column && isTabCompletion()) { - $scope.editor.execCommand('startAutocomplete') + $scope.editor.execCommand('startAutocomplete'); } else { - ace.config.loadModule('ace/ext/language_tools', function () { - $scope.editor.insertSnippet('\t') - }) + ace.config.loadModule('ace/ext/language_tools', function() { + $scope.editor.insertSnippet('\t'); + }); } - } - }) + }, + }); - let keyBindingEditorFocusAction = function (scrollValue) { - let numRows = $scope.editor.getSession().getLength() - let currentRow = $scope.editor.getCursorPosition().row + let keyBindingEditorFocusAction = function(scrollValue) { + let numRows = $scope.editor.getSession().getLength(); + let currentRow = $scope.editor.getCursorPosition().row; if (currentRow === 0 && scrollValue <= 0) { // move focus to previous paragraph - $scope.$emit('moveFocusToPreviousParagraph', $scope.paragraph.id) + $scope.$emit('moveFocusToPreviousParagraph', $scope.paragraph.id); } else if (currentRow === numRows - 1 && scrollValue >= 0) { - $scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id) + $scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id); } else { - $scope.scrollToCursor($scope.paragraph.id, scrollValue) + $scope.scrollToCursor($scope.paragraph.id, scrollValue); } - } + }; // handle cursor moves - $scope.editor.keyBinding.origOnCommandKey = $scope.editor.keyBinding.onCommandKey - $scope.editor.keyBinding.onCommandKey = function (e, hashId, keyCode) { + $scope.editor.keyBinding.origOnCommandKey = $scope.editor.keyBinding.onCommandKey; + $scope.editor.keyBinding.onCommandKey = function(e, hashId, keyCode) { if ($scope.editor.completer && $scope.editor.completer.activated) { // if autocompleter is active } else { // fix ace editor focus issue in chrome (textarea element goes to top: -1000px after focused by cursor move) if (parseInt(angular.element('#' + $scope.paragraph.id + '_editor > textarea') .css('top').replace('px', '')) < 0) { - let position = $scope.editor.getCursorPosition() - let cursorPos = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true) - angular.element('#' + $scope.paragraph.id + '_editor > textarea').css('top', cursorPos.top) + let position = $scope.editor.getCursorPosition(); + let cursorPos = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true); + angular.element('#' + $scope.paragraph.id + '_editor > textarea').css('top', cursorPos.top); } - let ROW_UP = -1 - let ROW_DOWN = 1 + let ROW_UP = -1; + let ROW_DOWN = 1; switch (keyCode) { case 38: - if (!e.shiftKey) { keyBindingEditorFocusAction(ROW_UP) } - break + if (!e.shiftKey) { + keyBindingEditorFocusAction(ROW_UP); + } + break; case 80: - if (e.ctrlKey && !e.altKey) { keyBindingEditorFocusAction(ROW_UP) } - break + if (e.ctrlKey && !e.altKey) { + keyBindingEditorFocusAction(ROW_UP); + } + break; case 40: - if (!e.shiftKey) { keyBindingEditorFocusAction(ROW_DOWN) } - break + if (!e.shiftKey) { + keyBindingEditorFocusAction(ROW_DOWN); + } + break; case 78: - if (e.ctrlKey && !e.altKey) { keyBindingEditorFocusAction(ROW_DOWN) } - break + if (e.ctrlKey && !e.altKey) { + keyBindingEditorFocusAction(ROW_DOWN); + } + break; } } - this.origOnCommandKey(e, hashId, keyCode) - } + this.origOnCommandKey(e, hashId, keyCode); + }; } - } + }; - const handleFocus = function (focused, isDigestPass) { - $scope.paragraphFocused = focused + const handleFocus = function(focused, isDigestPass) { + $scope.paragraphFocused = focused; - if ($scope.editor) { $scope.editor.setHighlightActiveLine(focused) } + if ($scope.editor) { + $scope.editor.setHighlightActiveLine(focused); + } if (isDigestPass === false || isDigestPass === undefined) { // Protect against error in case digest is already running - $timeout(function () { + $timeout(function() { // Apply changes since they come from 3rd party library - $scope.$digest() - }) + $scope.$digest(); + }); } - } + }; - let getEditorSetting = function (paragraph, interpreterName) { - let deferred = $q.defer() + let getEditorSetting = function(paragraph, interpreterName) { + let deferred = $q.defer(); if (!$scope.revisionView) { - websocketMsgSrv.getEditorSetting(paragraph.id, interpreterName) + websocketMsgSrv.getEditorSetting(paragraph.id, interpreterName); $timeout( - $scope.$on('editorSetting', function (event, data) { + $scope.$on('editorSetting', function(event, data) { if (paragraph.id === data.paragraphId) { - deferred.resolve(data) + deferred.resolve(data); } } - ), 1000) + ), 1000); } - return deferred.promise - } + return deferred.promise; + }; - let setEditorLanguage = function (session, language) { - let mode = 'ace/mode/' - mode += language - $scope.paragraph.config.editorMode = mode - session.setMode(mode) - } + let setEditorLanguage = function(session, language) { + let mode = 'ace/mode/'; + mode += language; + $scope.paragraph.config.editorMode = mode; + session.setMode(mode); + }; - const setParagraphMode = function (session, paragraphText, pos) { + const setParagraphMode = function(session, paragraphText, pos) { // Evaluate the mode only if the the position is undefined // or the first 30 characters of the paragraph have been modified // or cursor position is at beginning of second line.(in case user hit enter after typing %magic) @@ -996,319 +1010,321 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca // If paragraph loading, use config value if exists if ((typeof pos === 'undefined') && $scope.paragraph.config.editorMode && !setInterpreterBindings) { - session.setMode($scope.paragraph.config.editorMode) + session.setMode($scope.paragraph.config.editorMode); } else { - let magic = getInterpreterName(paragraphText) + let magic = getInterpreterName(paragraphText); if (editorSetting.magic !== magic) { - editorSetting.magic = magic + editorSetting.magic = magic; getEditorSetting($scope.paragraph, magic) - .then(function (setting) { - setEditorLanguage(session, setting.editor.language) - _.merge($scope.paragraph.config.editorSetting, setting.editor) - }) + .then(function(setting) { + setEditorLanguage(session, setting.editor.language); + _.merge($scope.paragraph.config.editorSetting, setting.editor); + }); } } } - pastePercentSign = false - setInterpreterBindings = false - } + pastePercentSign = false; + setInterpreterBindings = false; + }; const getInterpreterName = function(paragraphText) { - let intpNameRegexp = /^\s*%(.+?)(\s|\()/g - let match = intpNameRegexp.exec(paragraphText) + let intpNameRegexp = /^\s*%(.+?)(\s|\()/g; + let match = intpNameRegexp.exec(paragraphText); if (match) { - return match[1].trim() + return match[1].trim(); // get default interpreter name if paragraph text doesn't start with '%' // TODO(mina): dig into the cause what makes interpreterBindings to have no element } else if ($scope.$parent.interpreterBindings && $scope.$parent.interpreterBindings.length !== 0) { - return $scope.$parent.interpreterBindings[0].name + return $scope.$parent.interpreterBindings[0].name; } - return '' - } + return ''; + }; - const autoAdjustEditorHeight = function (editor) { + const autoAdjustEditorHeight = function(editor) { let height = editor.getSession().getScreenLength() * - editor.renderer.lineHeight + editor.renderer.lineHeight; - angular.element('#' + editor.container.id).height(height.toString() + 'px') - editor.resize() - } + angular.element('#' + editor.container.id).height(height.toString() + 'px'); + editor.resize(); + }; - $rootScope.$on('scrollToCursor', function (event) { + $rootScope.$on('scrollToCursor', function(event) { // scroll on 'scrollToCursor' event only when cursor is in the last paragraph - let paragraphs = angular.element('div[id$="_paragraphColumn_main"]') + let paragraphs = angular.element('div[id$="_paragraphColumn_main"]'); if (paragraphs[paragraphs.length - 1].id.indexOf($scope.paragraph.id) === 0) { - $scope.scrollToCursor($scope.paragraph.id, 0) + $scope.scrollToCursor($scope.paragraph.id, 0); } - }) + }); /** scrollToCursor if it is necessary * when cursor touches scrollTriggerEdgeMargin from the top (or bottom) of the screen, it autoscroll to place cursor around 1/3 of screen height from the top (or bottom) * paragraphId : paragraph that has active cursor * lastCursorMove : 1(down), 0, -1(up) last cursor move event **/ - $scope.scrollToCursor = function (paragraphId, lastCursorMove) { + $scope.scrollToCursor = function(paragraphId, lastCursorMove) { if (!$scope.editor || !$scope.editor.isFocused()) { // only make sense when editor is focused - return + return; } - let lineHeight = $scope.editor.renderer.lineHeight - let headerHeight = 103 // menubar, notebook titlebar - let scrollTriggerEdgeMargin = 50 + let lineHeight = $scope.editor.renderer.lineHeight; + let headerHeight = 103; // menubar, notebook titlebar + let scrollTriggerEdgeMargin = 50; - let documentHeight = angular.element(document).height() - let windowHeight = angular.element(window).height() // actual viewport height + let documentHeight = angular.element(document).height(); + let windowHeight = angular.element(window).height(); // actual viewport height - let scrollPosition = angular.element(document).scrollTop() - let editorPosition = angular.element('#' + paragraphId + '_editor').offset() - let position = $scope.editor.getCursorPosition() - let lastCursorPosition = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true) + let scrollPosition = angular.element(document).scrollTop(); + let editorPosition = angular.element('#' + paragraphId + '_editor').offset(); + let position = $scope.editor.getCursorPosition(); + let lastCursorPosition = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true); - let calculatedCursorPosition = editorPosition.top + lastCursorPosition.top + lineHeight * lastCursorMove + let calculatedCursorPosition = editorPosition.top + lastCursorPosition.top + lineHeight * lastCursorMove; - let scrollTargetPos + let scrollTargetPos; if (calculatedCursorPosition < scrollPosition + headerHeight + scrollTriggerEdgeMargin) { - scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight - headerHeight) / 3) + scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight - headerHeight) / 3); if (scrollTargetPos < 0) { - scrollTargetPos = 0 + scrollTargetPos = 0; } } else if (calculatedCursorPosition > scrollPosition + scrollTriggerEdgeMargin + windowHeight - headerHeight) { - scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight - headerHeight) * 2 / 3) + scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight - headerHeight) * 2 / 3); if (scrollTargetPos > documentHeight) { - scrollTargetPos = documentHeight + scrollTargetPos = documentHeight; } } // cancel previous scroll animation - let bodyEl = angular.element('body') - bodyEl.stop() - bodyEl.finish() + let bodyEl = angular.element('body'); + bodyEl.stop(); + bodyEl.finish(); // scroll to scrollTargetPos - bodyEl.scrollTo(scrollTargetPos, {axis: 'y', interrupt: true, duration: 100}) - } + bodyEl.scrollTo(scrollTargetPos, {axis: 'y', interrupt: true, duration: 100}); + }; - $scope.getEditorValue = function () { - return !$scope.editor ? $scope.paragraph.text : $scope.editor.getValue() - } + $scope.getEditorValue = function() { + return !$scope.editor ? $scope.paragraph.text : $scope.editor.getValue(); + }; - $scope.getProgress = function () { - return $scope.currentProgress || 0 - } + $scope.getProgress = function() { + return $scope.currentProgress || 0; + }; $scope.getFormattedParagraphTime = () => { - return moment().toISOString() - } + return moment().toISOString(); + }; - $scope.getExecutionTime = function (pdata) { - const end = pdata.dateFinished - const start = pdata.dateStarted - let timeMs = Date.parse(end) - Date.parse(start) + $scope.getExecutionTime = function(pdata) { + const end = pdata.dateFinished; + const start = pdata.dateStarted; + let timeMs = Date.parse(end) - Date.parse(start); if (isNaN(timeMs) || timeMs < 0) { if ($scope.isResultOutdated(pdata)) { - return 'outdated' + return 'outdated'; } - return '' + return ''; } - const durationFormat = moment.duration((timeMs / 1000), 'seconds').format('h [hrs] m [min] s [sec]') - const endFormat = moment(pdata.dateFinished).format('MMMM DD YYYY, h:mm:ss A') + const durationFormat = moment.duration((timeMs / 1000), 'seconds').format('h [hrs] m [min] s [sec]'); + const endFormat = moment(pdata.dateFinished).format('MMMM DD YYYY, h:mm:ss A'); - let user = (pdata.user === undefined || pdata.user === null) ? 'anonymous' : pdata.user - let desc = `Took ${durationFormat}. Last updated by ${user} at ${endFormat}.` + let user = (pdata.user === undefined || pdata.user === null) ? 'anonymous' : pdata.user; + let desc = `Took ${durationFormat}. Last updated by ${user} at ${endFormat}.`; - if ($scope.isResultOutdated(pdata)) { desc += ' (outdated)' } + if ($scope.isResultOutdated(pdata)) { + desc += ' (outdated)'; + } - return desc - } + return desc; + }; - $scope.getElapsedTime = function (paragraph) { - return 'Started ' + moment(paragraph.dateStarted).fromNow() + '.' - } + $scope.getElapsedTime = function(paragraph) { + return 'Started ' + moment(paragraph.dateStarted).fromNow() + '.'; + }; - $scope.isResultOutdated = function (pdata) { + $scope.isResultOutdated = function(pdata) { if (pdata.dateUpdated !== undefined && Date.parse(pdata.dateUpdated) > Date.parse(pdata.dateStarted)) { - return true + return true; } - return false - } + return false; + }; - $scope.goToEnd = function (editor) { - editor.navigateFileEnd() - } + $scope.goToEnd = function(editor) { + editor.navigateFileEnd(); + }; - $scope.parseTableCell = function (cell) { + $scope.parseTableCell = function(cell) { if (!isNaN(cell)) { if (cell.length === 0 || Number(cell) > Number.MAX_SAFE_INTEGER || Number(cell) < Number.MIN_SAFE_INTEGER) { - return cell + return cell; } else { - return Number(cell) + return Number(cell); } } - let d = moment(cell) + let d = moment(cell); if (d.isValid()) { - return d + return d; } - return cell - } + return cell; + }; - const commitParagraph = function (paragraph) { + const commitParagraph = function(paragraph) { const { id, title, text, config, settings: {params}, - } = paragraph + } = paragraph; return websocketMsgSrv.commitParagraph(id, title, text, config, params, - $route.current.pathParams.noteId) - } + $route.current.pathParams.noteId); + }; /** Utility function */ - $scope.goToSingleParagraph = function () { - let noteId = $route.current.pathParams.noteId + $scope.goToSingleParagraph = function() { + let noteId = $route.current.pathParams.noteId; let redirectToUrl = location.protocol + '//' + location.host + location.pathname + '#/notebook/' + noteId + - '/paragraph/' + $scope.paragraph.id + '?asIframe' - $window.open(redirectToUrl) - } + '/paragraph/' + $scope.paragraph.id + '?asIframe'; + $window.open(redirectToUrl); + }; - $scope.showScrollDownIcon = function (id) { - let doc = angular.element('#p' + id + '_text') + $scope.showScrollDownIcon = function(id) { + let doc = angular.element('#p' + id + '_text'); if (doc[0]) { - return doc[0].scrollHeight > doc.innerHeight() + return doc[0].scrollHeight > doc.innerHeight(); } - return false - } + return false; + }; - $scope.scrollParagraphDown = function (id) { - let doc = angular.element('#p' + id + '_text') - doc.animate({scrollTop: doc[0].scrollHeight}, 500) - $scope.keepScrollDown = true - } + $scope.scrollParagraphDown = function(id) { + let doc = angular.element('#p' + id + '_text'); + doc.animate({scrollTop: doc[0].scrollHeight}, 500); + $scope.keepScrollDown = true; + }; - $scope.showScrollUpIcon = function (id) { + $scope.showScrollUpIcon = function(id) { if (angular.element('#p' + id + '_text')[0]) { - return angular.element('#p' + id + '_text')[0].scrollTop !== 0 + return angular.element('#p' + id + '_text')[0].scrollTop !== 0; } - return false - } + return false; + }; - $scope.scrollParagraphUp = function (id) { - let doc = angular.element('#p' + id + '_text') - doc.animate({scrollTop: 0}, 500) - $scope.keepScrollDown = false - } + $scope.scrollParagraphUp = function(id) { + let doc = angular.element('#p' + id + '_text'); + doc.animate({scrollTop: 0}, 500); + $scope.keepScrollDown = false; + }; - $scope.$on('angularObjectUpdate', function (event, data) { - let noteId = $route.current.pathParams.noteId + $scope.$on('angularObjectUpdate', function(event, data) { + let noteId = $route.current.pathParams.noteId; if (!data.noteId || data.noteId === noteId) { - let scope - let registry + let scope; + let registry; if (!data.paragraphId || data.paragraphId === $scope.paragraph.id) { - scope = paragraphScope - registry = angularObjectRegistry + scope = paragraphScope; + registry = angularObjectRegistry; } else { - return + return; } - let varName = data.angularObject.name + let varName = data.angularObject.name; if (angular.equals(data.angularObject.object, scope[varName])) { // return when update has no change - return + return; } if (!registry[varName]) { registry[varName] = { interpreterGroupId: data.interpreterGroupId, noteId: data.noteId, - paragraphId: data.paragraphId - } + paragraphId: data.paragraphId, + }; } else { - registry[varName].noteId = registry[varName].noteId || data.noteId - registry[varName].paragraphId = registry[varName].paragraphId || data.paragraphId + registry[varName].noteId = registry[varName].noteId || data.noteId; + registry[varName].paragraphId = registry[varName].paragraphId || data.paragraphId; } - registry[varName].skipEmit = true + registry[varName].skipEmit = true; if (!registry[varName].clearWatcher) { - registry[varName].clearWatcher = scope.$watch(varName, function (newValue, oldValue) { - console.log('angular object (paragraph) updated %o %o', varName, registry[varName]) + 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 + registry[varName].skipEmit = false; + return; } websocketMsgSrv.updateAngularObject( registry[varName].noteId, registry[varName].paragraphId, varName, newValue, - registry[varName].interpreterGroupId) - }) + registry[varName].interpreterGroupId); + }); } - console.log('angular object (paragraph) created %o', varName) - scope[varName] = data.angularObject.object + console.log('angular object (paragraph) created %o', varName); + scope[varName] = data.angularObject.object; // create proxy for AngularFunction if (varName.indexOf(ANGULAR_FUNCTION_OBJECT_NAME_PREFIX) === 0) { - let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length) - scope[funcName] = function () { + let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length); + scope[funcName] = function() { // eslint-disable-next-line prefer-rest-params - scope[varName] = arguments + scope[varName] = arguments; // eslint-disable-next-line prefer-rest-params - console.log('angular function (paragraph) invoked %o', arguments) - } + console.log('angular function (paragraph) invoked %o', arguments); + }; - console.log('angular function (paragraph) created %o', scope[funcName]) + console.log('angular function (paragraph) created %o', scope[funcName]); } } - }) + }); - $scope.$on('updateParaInfos', function (event, data) { + $scope.$on('updateParaInfos', function(event, data) { if (data.id === $scope.paragraph.id) { - $scope.paragraph.runtimeInfos = data.infos + $scope.paragraph.runtimeInfos = data.infos; } - }) + }); - $scope.$on('angularObjectRemove', function (event, data) { - let noteId = $route.current.pathParams.noteId + $scope.$on('angularObjectRemove', function(event, data) { + let noteId = $route.current.pathParams.noteId; if (!data.noteId || data.noteId === noteId) { - let scope - let registry + let scope; + let registry; if (!data.paragraphId || data.paragraphId === $scope.paragraph.id) { - scope = paragraphScope - registry = angularObjectRegistry + scope = paragraphScope; + registry = angularObjectRegistry; } else { - return + return; } - let varName = data.name + let varName = data.name; // clear watcher if (registry[varName]) { - registry[varName].clearWatcher() - registry[varName] = undefined + registry[varName].clearWatcher(); + registry[varName] = undefined; } // remove scope variable - scope[varName] = undefined + scope[varName] = undefined; // remove proxy for AngularFunction if (varName.indexOf(ANGULAR_FUNCTION_OBJECT_NAME_PREFIX) === 0) { - let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length) - scope[funcName] = undefined + let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length); + scope[funcName] = undefined; } } - }) + }); /** * @returns {boolean} true if updated is needed */ - function isUpdateRequired (oldPara, newPara) { + function isUpdateRequired(oldPara, newPara) { return (newPara.id === oldPara.id && (newPara.dateCreated !== oldPara.dateCreated || newPara.text !== oldPara.text || @@ -1322,453 +1338,457 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca newPara.errorMessage !== oldPara.errorMessage || !angular.equals(newPara.settings, oldPara.settings) || !angular.equals(newPara.config, oldPara.config) || - !angular.equals(newPara.runtimeInfos, oldPara.runtimeInfos))) + !angular.equals(newPara.runtimeInfos, oldPara.runtimeInfos))); } - $scope.updateAllScopeTexts = function (oldPara, newPara) { + $scope.updateAllScopeTexts = function(oldPara, newPara) { if (oldPara.text !== newPara.text) { if ($scope.dirtyText) { // check if editor has local update if ($scope.dirtyText === newPara.text) { // when local update is the same from remote, clear local update - $scope.paragraph.text = newPara.text - $scope.dirtyText = undefined - $scope.originalText = angular.copy(newPara.text) + $scope.paragraph.text = newPara.text; + $scope.dirtyText = undefined; + $scope.originalText = angular.copy(newPara.text); } else { // if there're local update, keep it. - $scope.paragraph.text = newPara.text + $scope.paragraph.text = newPara.text; } } else { - $scope.paragraph.text = newPara.text - $scope.originalText = angular.copy(newPara.text) + $scope.paragraph.text = newPara.text; + $scope.originalText = angular.copy(newPara.text); } } - } + }; - $scope.updateParagraphObjectWhenUpdated = function (newPara) { + $scope.updateParagraphObjectWhenUpdated = function(newPara) { // resize col width if ($scope.paragraph.config.colWidth !== newPara.config.colWidth) { - $scope.$broadcast('paragraphResized', $scope.paragraph.id) + $scope.$broadcast('paragraphResized', $scope.paragraph.id); } if ($scope.paragraph.config.fontSize !== newPara.config.fontSize) { - $rootScope.$broadcast('fontSizeChanged', newPara.config.fontSize) + $rootScope.$broadcast('fontSizeChanged', newPara.config.fontSize); } /** push the rest */ - $scope.paragraph.aborted = newPara.aborted - $scope.paragraph.user = newPara.user - $scope.paragraph.dateUpdated = newPara.dateUpdated - $scope.paragraph.dateCreated = newPara.dateCreated - $scope.paragraph.dateFinished = newPara.dateFinished - $scope.paragraph.dateStarted = newPara.dateStarted - $scope.paragraph.errorMessage = newPara.errorMessage - $scope.paragraph.jobName = newPara.jobName - $scope.paragraph.title = newPara.title - $scope.paragraph.lineNumbers = newPara.lineNumbers - $scope.paragraph.status = newPara.status - $scope.paragraph.fontSize = newPara.fontSize + $scope.paragraph.aborted = newPara.aborted; + $scope.paragraph.user = newPara.user; + $scope.paragraph.dateUpdated = newPara.dateUpdated; + $scope.paragraph.dateCreated = newPara.dateCreated; + $scope.paragraph.dateFinished = newPara.dateFinished; + $scope.paragraph.dateStarted = newPara.dateStarted; + $scope.paragraph.errorMessage = newPara.errorMessage; + $scope.paragraph.jobName = newPara.jobName; + $scope.paragraph.title = newPara.title; + $scope.paragraph.lineNumbers = newPara.lineNumbers; + $scope.paragraph.status = newPara.status; + $scope.paragraph.fontSize = newPara.fontSize; if (newPara.status !== ParagraphStatus.RUNNING) { - $scope.paragraph.results = newPara.results + $scope.paragraph.results = newPara.results; } - $scope.paragraph.settings = newPara.settings - $scope.paragraph.runtimeInfos = newPara.runtimeInfos + $scope.paragraph.settings = newPara.settings; + $scope.paragraph.runtimeInfos = newPara.runtimeInfos; if ($scope.editor) { - $scope.editor.setReadOnly($scope.isRunning(newPara)) + $scope.editor.setReadOnly($scope.isRunning(newPara)); } if (!$scope.asIframe) { - $scope.paragraph.config = newPara.config - initializeDefault(newPara.config) + $scope.paragraph.config = newPara.config; + initializeDefault(newPara.config); } else { - newPara.config.editorHide = true - newPara.config.tableHide = false - $scope.paragraph.config = newPara.config + newPara.config.editorHide = true; + newPara.config.tableHide = false; + $scope.paragraph.config = newPara.config; } - } + }; - $scope.updateParagraph = function (oldPara, newPara, updateCallback) { + $scope.updateParagraph = function(oldPara, newPara, updateCallback) { // 1. can't update on revision view if ($scope.revisionView === true) { - return + return; } // 2. get status, refreshed - const statusChanged = (newPara.status !== oldPara.status) + const statusChanged = (newPara.status !== oldPara.status); const resultRefreshed = (newPara.dateFinished !== oldPara.dateFinished) || isEmpty(newPara.results) !== isEmpty(oldPara.results) || newPara.status === ParagraphStatus.ERROR || - (newPara.status === ParagraphStatus.FINISHED && statusChanged) + (newPara.status === ParagraphStatus.FINISHED && statusChanged); // 3. update texts managed by $scope - $scope.updateAllScopeTexts(oldPara, newPara) + $scope.updateAllScopeTexts(oldPara, newPara); // 4. execute callback to update result - updateCallback() + updateCallback(); // 5. update remaining paragraph objects - $scope.updateParagraphObjectWhenUpdated(newPara) + $scope.updateParagraphObjectWhenUpdated(newPara); // 6. handle scroll down by key properly if new paragraph is added if (statusChanged || resultRefreshed) { // when last paragraph runs, zeppelin automatically appends new paragraph. // this broadcast will focus to the newly inserted paragraph - const paragraphs = angular.element('div[id$="_paragraphColumn_main"]') + const paragraphs = angular.element('div[id$="_paragraphColumn_main"]'); if (paragraphs.length >= 2 && paragraphs[paragraphs.length - 2].id.indexOf($scope.paragraph.id) === 0) { // rendering output can took some time. So delay scrolling event firing for sometime. - setTimeout(() => { $rootScope.$broadcast('scrollToCursor') }, 500) + setTimeout(() => { + $rootScope.$broadcast('scrollToCursor'); + }, 500); } } - } + }; /** $scope.$on */ - $scope.$on('runParagraphUsingSpell', function (event, data) { - const oldPara = $scope.paragraph - let newPara = data.paragraph + $scope.$on('runParagraphUsingSpell', function(event, data) { + const oldPara = $scope.paragraph; + let newPara = data.paragraph; const updateCallback = () => { - $scope.runParagraph(newPara.text, true, true) - } + $scope.runParagraph(newPara.text, true, true); + }; if (!isUpdateRequired(oldPara, newPara)) { - return + return; } - $scope.updateParagraph(oldPara, newPara, updateCallback) - }) + $scope.updateParagraph(oldPara, newPara, updateCallback); + }); - $scope.$on('updateParagraph', function (event, data) { - const oldPara = $scope.paragraph - const newPara = data.paragraph + $scope.$on('updateParagraph', function(event, data) { + const oldPara = $scope.paragraph; + const newPara = data.paragraph; if (!isUpdateRequired(oldPara, newPara)) { - return + return; } const updateCallback = () => { // broadcast `updateResult` message to trigger result update if (newPara.results && newPara.results.msg) { for (let i in newPara.results.msg) { - const newResult = newPara.results.msg ? newPara.results.msg[i] : {} - const oldResult = (oldPara.results && oldPara.results.msg) - ? oldPara.results.msg[i] : {} - const newConfig = newPara.config.results ? newPara.config.results[i] : {} - const oldConfig = oldPara.config.results ? oldPara.config.results[i] : {} - if (!angular.equals(newResult, oldResult) || - !angular.equals(newConfig, oldConfig)) { - $rootScope.$broadcast('updateResult', newResult, newConfig, newPara, parseInt(i)) + if (newPara.results.msg.hasOwnProperty(i)) { + const newResult = newPara.results.msg ? newPara.results.msg[i] : {}; + const oldResult = (oldPara.results && oldPara.results.msg) + ? oldPara.results.msg[i] : {}; + const newConfig = newPara.config.results ? newPara.config.results[i] : {}; + const oldConfig = oldPara.config.results ? oldPara.config.results[i] : {}; + if (!angular.equals(newResult, oldResult) || + !angular.equals(newConfig, oldConfig)) { + $rootScope.$broadcast('updateResult', newResult, newConfig, newPara, parseInt(i)); + } } } } - } + }; - $scope.updateParagraph(oldPara, newPara, updateCallback) - }) + $scope.updateParagraph(oldPara, newPara, updateCallback); + }); - $scope.$on('updateProgress', function (event, data) { + $scope.$on('updateProgress', function(event, data) { if (data.id === $scope.paragraph.id) { - $scope.currentProgress = data.progress + $scope.currentProgress = data.progress; } - }) + }); - $scope.$on('appendParagraphOutput', function (event, data) { + $scope.$on('appendParagraphOutput', function(event, data) { if (data.paragraphId === $scope.paragraph.id) { if (!$scope.paragraph.results) { - $scope.paragraph.results = {} + $scope.paragraph.results = {}; if (!$scope.paragraph.results.msg) { - $scope.paragraph.results.msg = [] + $scope.paragraph.results.msg = []; } $scope.paragraph.results.msg[data.index] = { data: data.data, - type: data.type - } + type: data.type, + }; $rootScope.$broadcast( 'updateResult', $scope.paragraph.results.msg[data.index], $scope.paragraph.config.results[data.index], $scope.paragraph, - data.index) + data.index); } } - }) + }); - $scope.$on('keyEvent', function (event, keyEvent) { + $scope.$on('keyEvent', function(event, keyEvent) { if ($scope.paragraphFocused) { - let paragraphId = $scope.paragraph.id - let keyCode = keyEvent.keyCode - let noShortcutDefined = false - let editorHide = $scope.paragraph.config.editorHide + let paragraphId = $scope.paragraph.id; + let keyCode = keyEvent.keyCode; + let noShortcutDefined = false; + let editorHide = $scope.paragraph.config.editorHide; if (editorHide && (keyCode === 38 || (keyCode === 80 && keyEvent.ctrlKey && !keyEvent.altKey))) { // up // move focus to previous paragraph - $scope.$emit('moveFocusToPreviousParagraph', paragraphId) + $scope.$emit('moveFocusToPreviousParagraph', paragraphId); } else if (editorHide && (keyCode === 40 || (keyCode === 78 && keyEvent.ctrlKey && !keyEvent.altKey))) { // down // move focus to next paragraph // $timeout stops chaining effect of focus propogation - $timeout(() => $scope.$emit('moveFocusToNextParagraph', paragraphId)) + $timeout(() => $scope.$emit('moveFocusToNextParagraph', paragraphId)); } else if (!keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 13) { // Shift + Enter - $scope.runParagraphFromShortcut($scope.getEditorValue()) + $scope.runParagraphFromShortcut($scope.getEditorValue()); } else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 13) { // Ctrl + Shift + Enter - $scope.runAllToOrFromThis($scope.paragraph) + $scope.runAllToOrFromThis($scope.paragraph); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 67) { // Ctrl + Alt + c - $scope.cancelParagraph($scope.paragraph) + $scope.cancelParagraph($scope.paragraph); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 68) { // Ctrl + Alt + d - $scope.removeParagraph($scope.paragraph) + $scope.removeParagraph($scope.paragraph); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 75) { // Ctrl + Alt + k - $scope.moveUp($scope.paragraph) + $scope.moveUp($scope.paragraph); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 74) { // Ctrl + Alt + j - $scope.moveDown($scope.paragraph) + $scope.moveDown($scope.paragraph); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 65) { // Ctrl + Alt + a - $scope.insertNew('above') + $scope.insertNew('above'); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 66) { // Ctrl + Alt + b - $scope.insertNew('below') + $scope.insertNew('below'); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 79) { // Ctrl + Alt + o - $scope.toggleOutput($scope.paragraph) + $scope.toggleOutput($scope.paragraph); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 82) { // Ctrl + Alt + r - $scope.toggleEnableDisable($scope.paragraph) + $scope.toggleEnableDisable($scope.paragraph); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 69) { // Ctrl + Alt + e - $scope.toggleEditor($scope.paragraph) + $scope.toggleEditor($scope.paragraph); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 77) { // Ctrl + Alt + m if ($scope.paragraph.config.lineNumbers) { - $scope.hideLineNumbers($scope.paragraph) + $scope.hideLineNumbers($scope.paragraph); } else { - $scope.showLineNumbers($scope.paragraph) + $scope.showLineNumbers($scope.paragraph); } } else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 189) { // Ctrl + Shift + - - $scope.changeColWidth($scope.paragraph, Math.max(1, $scope.paragraph.config.colWidth - 1)) + $scope.changeColWidth($scope.paragraph, Math.max(1, $scope.paragraph.config.colWidth - 1)); } else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 187) { // Ctrl + Shift + = - $scope.changeColWidth($scope.paragraph, Math.min(12, $scope.paragraph.config.colWidth + 1)) + $scope.changeColWidth($scope.paragraph, Math.min(12, $scope.paragraph.config.colWidth + 1)); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 84) { // Ctrl + Alt + t if ($scope.paragraph.config.title) { - $scope.hideTitle($scope.paragraph) + $scope.hideTitle($scope.paragraph); } else { - $scope.showTitle($scope.paragraph) + $scope.showTitle($scope.paragraph); } } else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 67) { // Ctrl + Alt + c - $scope.copyPara('below') + $scope.copyPara('below'); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 76) { // Ctrl + Alt + l - $scope.clearParagraphOutput($scope.paragraph) + $scope.clearParagraphOutput($scope.paragraph); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 87) { // Ctrl + Alt + w - $scope.goToSingleParagraph() + $scope.goToSingleParagraph(); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 70) { // Ctrl + f - $scope.$emit('toggleSearchBox') + $scope.$emit('toggleSearchBox'); } else { - noShortcutDefined = true + noShortcutDefined = true; } if (!noShortcutDefined) { - keyEvent.preventDefault() + keyEvent.preventDefault(); } } - }) + }); - $scope.$on('focusParagraph', function (event, paragraphId, cursorPosRow, cursorPosCol, mouseEvent) { + $scope.$on('focusParagraph', function(event, paragraphId, cursorPosRow, cursorPosCol, mouseEvent) { if (cursorPosCol === null || cursorPosCol === undefined) { - cursorPosCol = 0 + cursorPosCol = 0; } if ($scope.paragraph.id === paragraphId) { // focus editor if (!$scope.paragraph.config.editorHide) { if (!mouseEvent) { - $scope.editor.focus() + $scope.editor.focus(); // move cursor to the first row (or the last row) - let row + let row; if (cursorPosRow >= 0) { - row = cursorPosRow - $scope.editor.gotoLine(row, cursorPosCol) + row = cursorPosRow; + $scope.editor.gotoLine(row, cursorPosCol); } else { - row = $scope.editor.session.getLength() - $scope.editor.gotoLine(row, cursorPosCol) + row = $scope.editor.session.getLength(); + $scope.editor.gotoLine(row, cursorPosCol); } - $scope.scrollToCursor($scope.paragraph.id, cursorPosCol) + $scope.scrollToCursor($scope.paragraph.id, cursorPosCol); } } - handleFocus(true) + handleFocus(true); } else { if ($scope.editor !== undefined && $scope.editor !== null) { - $scope.editor.blur() + $scope.editor.blur(); } - let isDigestPass = true - handleFocus(false, isDigestPass) + let isDigestPass = true; + handleFocus(false, isDigestPass); } - }) + }); - $scope.$on('saveInterpreterBindings', function (event, paragraphId) { + $scope.$on('saveInterpreterBindings', function(event, paragraphId) { if ($scope.paragraph.id === paragraphId && $scope.editor) { - setInterpreterBindings = true - setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue()) + setInterpreterBindings = true; + setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue()); } - }) + }); - $scope.$on('doubleClickParagraph', function (event, paragraphId) { + $scope.$on('doubleClickParagraph', function(event, paragraphId) { if ($scope.paragraph.id === paragraphId && $scope.paragraph.config.editorHide && $scope.paragraph.config.editorSetting.editOnDblClick && $scope.revisionView !== true) { - let deferred = $q.defer() - openEditorAndCloseTable($scope.paragraph) + let deferred = $q.defer(); + openEditorAndCloseTable($scope.paragraph); $timeout( - $scope.$on('updateParagraph', function (event, data) { - deferred.resolve(data) + $scope.$on('updateParagraph', function(event, data) { + deferred.resolve(data); } - ), 1000) + ), 1000); - deferred.promise.then(function (data) { + deferred.promise.then(function(data) { if ($scope.editor) { - $scope.editor.focus() - $scope.goToEnd($scope.editor) + $scope.editor.focus(); + $scope.goToEnd($scope.editor); } - }) + }); } - }) + }); - $scope.$on('openEditor', function (event) { - $scope.openEditor($scope.paragraph) - }) + $scope.$on('openEditor', function(event) { + $scope.openEditor($scope.paragraph); + }); - $scope.$on('closeEditor', function (event) { - $scope.closeEditor($scope.paragraph) - }) + $scope.$on('closeEditor', function(event) { + $scope.closeEditor($scope.paragraph); + }); - $scope.$on('openTable', function (event) { - $scope.openTable($scope.paragraph) - }) + $scope.$on('openTable', function(event) { + $scope.openTable($scope.paragraph); + }); - $scope.$on('closeTable', function (event) { - $scope.closeTable($scope.paragraph) - }) + $scope.$on('closeTable', function(event) { + $scope.closeTable($scope.paragraph); + }); - $scope.$on('resultRendered', function (event, paragraphId) { + $scope.$on('resultRendered', function(event, paragraphId) { if ($scope.paragraph.id !== paragraphId) { - return + return; } /** increase spell result count and return if not finished */ if (!$scope.increaseSpellTransactionResultCount()) { - return + return; } - $scope.cleanupSpellTransaction() - }) + $scope.cleanupSpellTransaction(); + }); - $scope.$on('fontSizeChanged', function (event, fontSize) { + $scope.$on('fontSizeChanged', function(event, fontSize) { if ($scope.editor) { $scope.editor.setOptions({ - fontSize: fontSize + 'pt' - }) + fontSize: fontSize + 'pt', + }); } - }) + }); const clearSearchSelection = function() { for (let i = 0; i < searchRanges.length; ++i) { - $scope.editor.session.removeMarker(searchRanges[i].markerId) + $scope.editor.session.removeMarker(searchRanges[i].markerId); } - searchRanges = [] + searchRanges = []; if (currentRange.id !== -1) { - $scope.editor.session.removeMarker(currentRange.markerId) + $scope.editor.session.removeMarker(currentRange.markerId); } - currentRange = getCurrentRangeDefault() - } + currentRange = getCurrentRangeDefault(); + }; $scope.onEditorClick = function() { - $scope.$emit('editorClicked') - } + $scope.$emit('editorClicked'); + }; $scope.$on('unmarkAll', function() { - clearSearchSelection() - }) + clearSearchSelection(); + }); const markAllOccurrences = function(text) { - clearSearchSelection() + clearSearchSelection(); if (text === '') { - return + return; } if ($scope.editor.findAll(text) === 0) { - return + return; } - let ranges = $scope.editor.selection.getAllRanges() - $scope.editor.selection.toSingleRange() - $scope.editor.selection.clearSelection() + let ranges = $scope.editor.selection.getAllRanges(); + $scope.editor.selection.toSingleRange(); + $scope.editor.selection.clearSelection(); for (let i = 0; i < ranges.length; ++i) { - let id = $scope.editor.session.addMarker(ranges[i], 'ace_selected-word', 'text') - searchRanges.push({markerId: id, range: ranges[i]}) + let id = $scope.editor.session.addMarker(ranges[i], 'ace_selected-word', 'text'); + searchRanges.push({markerId: id, range: ranges[i]}); } - } + }; $scope.$on('markAllOccurrences', function(event, text) { - markAllOccurrences(text) + markAllOccurrences(text); if (searchRanges.length > 0) { - $scope.$emit('occurrencesExists', searchRanges.length) + $scope.$emit('occurrencesExists', searchRanges.length); } - }) + }); $scope.$on('nextOccurrence', function(event, paragraphId) { if ($scope.paragraph.id !== paragraphId) { - return + return; } - let highlightedRangeExists = currentRange.id !== -1 + let highlightedRangeExists = currentRange.id !== -1; if (highlightedRangeExists) { - $scope.editor.session.removeMarker(currentRange.markerId) - currentRange.markerId = -1 + $scope.editor.session.removeMarker(currentRange.markerId); + currentRange.markerId = -1; } - ++currentRange.id + ++currentRange.id; if (currentRange.id >= searchRanges.length) { - currentRange.id = -1 - $scope.$emit('noNextOccurrence') - return + currentRange.id = -1; + $scope.$emit('noNextOccurrence'); + return; } currentRange.markerId = $scope.editor.session.addMarker( - searchRanges[currentRange.id].range, 'ace_selection', 'text') - }) + searchRanges[currentRange.id].range, 'ace_selection', 'text'); + }); $scope.$on('prevOccurrence', function(event, paragraphId) { if ($scope.paragraph.id !== paragraphId) { - return + return; } - let highlightedRangeExists = currentRange.id !== -1 + let highlightedRangeExists = currentRange.id !== -1; if (highlightedRangeExists) { - $scope.editor.session.removeMarker(currentRange.markerId) - currentRange.markerId = -1 + $scope.editor.session.removeMarker(currentRange.markerId); + currentRange.markerId = -1; } if (currentRange.id === -1) { - currentRange.id = searchRanges.length + currentRange.id = searchRanges.length; } - --currentRange.id + --currentRange.id; if (currentRange.id === -1) { - $scope.$emit('noPrevOccurrence') - return + $scope.$emit('noPrevOccurrence'); + return; } currentRange.markerId = $scope.editor.session.addMarker( - searchRanges[currentRange.id].range, 'ace_selection', 'text') - }) + searchRanges[currentRange.id].range, 'ace_selection', 'text'); + }); $scope.$on('replaceCurrent', function(event, from, to) { if (currentRange.id === -1) { - return - } - let indexFromEnd = searchRanges.length - currentRange.id - 1 - let prevId = currentRange.id - $scope.editor.session.removeMarker(currentRange.markerId) - $scope.editor.session.replace(searchRanges[currentRange.id].range, to) - markAllOccurrences(from) - let currentIndex = searchRanges.length - indexFromEnd - $scope.$emit('occurrencesCountChanged', currentIndex - prevId - 1) - currentRange.id = currentIndex + return; + } + let indexFromEnd = searchRanges.length - currentRange.id - 1; + let prevId = currentRange.id; + $scope.editor.session.removeMarker(currentRange.markerId); + $scope.editor.session.replace(searchRanges[currentRange.id].range, to); + markAllOccurrences(from); + let currentIndex = searchRanges.length - indexFromEnd; + $scope.$emit('occurrencesCountChanged', currentIndex - prevId - 1); + currentRange.id = currentIndex; if (currentRange.id === searchRanges.length) { - currentRange.id = -1 - $scope.$emit('noNextOccurrenceAfterReplace') + currentRange.id = -1; + $scope.$emit('noNextOccurrenceAfterReplace'); } else { currentRange.markerId = $scope.editor.session.addMarker( - searchRanges[currentRange.id].range, 'ace_selection', 'text') + searchRanges[currentRange.id].range, 'ace_selection', 'text'); } - }) + }); $scope.$on('replaceAll', function(event, from, to) { - clearSearchSelection() - $scope.editor.replaceAll(to, {needle: from}) - }) + clearSearchSelection(); + $scope.editor.replaceAll(to, {needle: from}); + }); $scope.$on('checkOccurrences', function() { if (searchRanges.length > 0) { - $scope.$emit('occurrencesExists', searchRanges.length) + $scope.$emit('occurrencesExists', searchRanges.length); } - }) + }); } diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.test.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.test.js index 94230de7f97..38d5480e021 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.test.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.test.js @@ -1,53 +1,53 @@ -describe('Controller: ParagraphCtrl', function () { - beforeEach(angular.mock.module('zeppelinWebApp')) +describe('Controller: ParagraphCtrl', function() { + beforeEach(angular.mock.module('zeppelinWebApp')); - let scope - let websocketMsgSrvMock = {} + let scope; + let websocketMsgSrvMock = {}; let paragraphMock = { config: {}, settings: { - forms: {} - } - } + forms: {}, + }, + }; let route = { current: { pathParams: { - noteId: 'noteId' - } - } - } + noteId: 'noteId', + }, + }, + }; - beforeEach(inject(function ($controller, $rootScope) { - scope = $rootScope.$new() - $rootScope.notebookScope = $rootScope.$new(true, $rootScope) + beforeEach(inject(function($controller, $rootScope) { + scope = $rootScope.$new(); + $rootScope.notebookScope = $rootScope.$new(true, $rootScope); $controller('ParagraphCtrl', { $scope: scope, websocketMsgSrv: websocketMsgSrvMock, $element: {}, - $route: route - }) + $route: route, + }); - scope.init(paragraphMock) - })) + scope.init(paragraphMock); + })); let functions = ['isRunning', 'getIframeDimensions', 'cancelParagraph', 'runParagraph', 'saveParagraph', 'moveUp', 'moveDown', 'insertNew', 'removeParagraph', 'toggleEditor', 'closeEditor', 'openEditor', 'closeTable', 'openTable', 'showTitle', 'hideTitle', 'setTitle', 'showLineNumbers', 'hideLineNumbers', 'changeColWidth', 'columnWidthClass', 'toggleOutput', - 'aceChanged', 'aceLoaded', 'getEditorValue', 'getProgress', 'getExecutionTime', 'isResultOutdated'] - - functions.forEach(function (fn) { - it('check for scope functions to be defined : ' + fn, function () { - expect(scope[fn]).toBeDefined() - }) - }) - - it('should have this array of values for "colWidthOption"', function () { - expect(scope.colWidthOption).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) - }) - - it('should set default value of "paragraphFocused" as false', function () { - expect(scope.paragraphFocused).toEqual(false) - }) -}) + 'aceChanged', 'aceLoaded', 'getEditorValue', 'getProgress', 'getExecutionTime', 'isResultOutdated']; + + functions.forEach(function(fn) { + it('check for scope functions to be defined : ' + fn, function() { + expect(scope[fn]).toBeDefined(); + }); + }); + + it('should have this array of values for "colWidthOption"', function() { + expect(scope.colWidthOption).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + }); + + it('should set default value of "paragraphFocused" as false', function() { + expect(scope.paragraphFocused).toEqual(false); + }); +}); diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.status.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.status.js index f839eeeb219..bd70de3da59 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.status.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.status.js @@ -19,12 +19,16 @@ export const ParagraphStatus = { FINISHED: 'FINISHED', ABORT: 'ABORT', ERROR: 'ERROR', -} +}; -export function isParagraphRunning (paragraph) { - if (!paragraph) { return false } - const status = paragraph.status - if (!status) { return false } +export function isParagraphRunning(paragraph) { + if (!paragraph) { + return false; + } + const status = paragraph.status; + if (!status) { + return false; + } - return status === ParagraphStatus.PENDING || status === ParagraphStatus.RUNNING + return status === ParagraphStatus.PENDING || status === ParagraphStatus.RUNNING; } diff --git a/zeppelin-web/src/app/notebook/paragraph/resizable.directive.js b/zeppelin-web/src/app/notebook/paragraph/resizable.directive.js index 2893cd5d088..874f9d82154 100644 --- a/zeppelin-web/src/app/notebook/paragraph/resizable.directive.js +++ b/zeppelin-web/src/app/notebook/paragraph/resizable.directive.js @@ -12,58 +12,58 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').directive('resizable', ResizableDirective) +angular.module('zeppelinWebApp').directive('resizable', ResizableDirective); -function ResizableDirective () { +function ResizableDirective() { let resizableConfig = { autoHide: true, handles: 'se', helper: 'resizable-helper', - stop: function () { - angular.element(this).css({'width': '100%', 'height': '100%'}) - } - } + stop: function() { + angular.element(this).css({'width': '100%', 'height': '100%'}); + }, + }; return { restrict: 'A', scope: { - callback: '&onResize' + callback: '&onResize', }, - link: function postLink (scope, elem, attrs) { - attrs.$observe('resize', function (resize) { - let resetResize = function (elem, resize) { - let colStep = window.innerWidth / 12 - elem.off('resizestop') - let conf = angular.copy(resizableConfig) + link: function postLink(scope, elem, attrs) { + attrs.$observe('resize', function(resize) { + let resetResize = function(elem, resize) { + let colStep = window.innerWidth / 12; + elem.off('resizestop'); + let conf = angular.copy(resizableConfig); if (resize.graphType === 'TABLE' || resize.graphType === 'NETWORK' || resize.graphType === 'TEXT') { - conf.grid = [colStep, 10] - conf.minHeight = 100 + conf.grid = [colStep, 10]; + conf.minHeight = 100; } else { - conf.grid = [colStep, 10000] - conf.minHeight = 0 + conf.grid = [colStep, 10000]; + conf.minHeight = 0; } - conf.maxWidth = window.innerWidth + conf.maxWidth = window.innerWidth; - elem.resizable(conf) - elem.on('resizestop', function () { + elem.resizable(conf); + elem.on('resizestop', function() { if (scope.callback) { - let height = elem.height() + let height = elem.height(); if (height < 50) { - height = 300 + height = 300; } - scope.callback({width: Math.ceil(elem.width() / colStep), height: height}) + scope.callback({width: Math.ceil(elem.width() / colStep), height: height}); } - }) - } + }); + }; - resize = JSON.parse(resize) + resize = JSON.parse(resize); if (resize.allowresize === 'true') { - resetResize(elem, resize) - angular.element(window).resize(function () { - resetResize(elem, resize) - }) + resetResize(elem, resize); + angular.element(window).resize(function() { + resetResize(elem, resize); + }); } - }) - } - } + }); + }, + }; } diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js index 5dfe3143971..5bf77dcd71e 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -12,30 +12,30 @@ * limitations under the License. */ -import moment from 'moment' - -import DatasetFactory from '../../../tabledata/datasetfactory' -import TableVisualization from '../../../visualization/builtins/visualization-table' -import BarchartVisualization from '../../../visualization/builtins/visualization-barchart' -import PiechartVisualization from '../../../visualization/builtins/visualization-piechart' -import AreachartVisualization from '../../../visualization/builtins/visualization-areachart' -import LinechartVisualization from '../../../visualization/builtins/visualization-linechart' -import ScatterchartVisualization from '../../../visualization/builtins/visualization-scatterchart' -import NetworkVisualization from '../../../visualization/builtins/visualization-d3network' -import {DefaultDisplayType, SpellResult} from '../../../spell' -import {ParagraphStatus} from '../paragraph.status' - -const AnsiUp = require('ansi_up') -const AnsiUpConverter = new AnsiUp.default // eslint-disable-line new-parens,new-cap -const TableGridFilterTemplate = require('../../../visualization/builtins/visualization-table-grid-filter.html') - -angular.module('zeppelinWebApp').controller('ResultCtrl', ResultCtrl) - -function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $location, +import moment from 'moment'; + +import DatasetFactory from '../../../tabledata/datasetfactory'; +import TableVisualization from '../../../visualization/builtins/visualization-table'; +import BarchartVisualization from '../../../visualization/builtins/visualization-barchart'; +import PiechartVisualization from '../../../visualization/builtins/visualization-piechart'; +import AreachartVisualization from '../../../visualization/builtins/visualization-areachart'; +import LinechartVisualization from '../../../visualization/builtins/visualization-linechart'; +import ScatterchartVisualization from '../../../visualization/builtins/visualization-scatterchart'; +import NetworkVisualization from '../../../visualization/builtins/visualization-d3network'; +import {DefaultDisplayType, SpellResult} from '../../../spell'; +import {ParagraphStatus} from '../paragraph.status'; + +const AnsiUp = require('ansi_up'); +const AnsiUpConverter = new AnsiUp.default; // eslint-disable-line new-parens,new-cap +const TableGridFilterTemplate = require('../../../visualization/builtins/visualization-table-grid-filter.html'); + +angular.module('zeppelinWebApp').controller('ResultCtrl', ResultCtrl); + +function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location, $timeout, $compile, $http, $q, $templateCache, $templateRequest, $sce, websocketMsgSrv, baseUrlSrv, ngToast, saveAsService, noteVarShareService, heliumService, uiGridConstants) { - 'ngInject' + 'ngInject'; /** * Built-in visualizations @@ -45,49 +45,49 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio id: 'table', // paragraph.config.graph.mode name: 'Table', // human readable name. tooltip icon: '', - supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK] + supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], }, { id: 'multiBarChart', name: 'Bar Chart', icon: '', transformation: 'pivot', - supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK] + supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], }, { id: 'pieChart', name: 'Pie Chart', icon: '', transformation: 'pivot', - supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK] + supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], }, { id: 'stackedAreaChart', name: 'Area Chart', icon: '', transformation: 'pivot', - supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK] + supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], }, { id: 'lineChart', name: 'Line Chart', icon: '', transformation: 'pivot', - supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK] + supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], }, { id: 'scatterChart', name: 'Scatter Chart', icon: '', - supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK] + supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], }, { id: 'network', name: 'Network', icon: '', - supports: [DefaultDisplayType.NETWORK] - } - ] + supports: [DefaultDisplayType.NETWORK], + }, + ]; /** * Holds class and actual runtime instance and related infos of built-in visualizations @@ -95,137 +95,143 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio let builtInVisualizations = { 'table': { class: TableVisualization, - instance: undefined // created from setGraphMode() + instance: undefined, // created from setGraphMode() }, 'multiBarChart': { class: BarchartVisualization, - instance: undefined + instance: undefined, }, 'pieChart': { class: PiechartVisualization, - instance: undefined + instance: undefined, }, 'stackedAreaChart': { class: AreachartVisualization, - instance: undefined + instance: undefined, }, 'lineChart': { class: LinechartVisualization, - instance: undefined + instance: undefined, }, 'scatterChart': { class: ScatterchartVisualization, - instance: undefined + instance: undefined, }, 'network': { class: NetworkVisualization, - instance: undefined - } - } + instance: undefined, + }, + }; // type - $scope.type = null + $scope.type = null; // Data of the result - let data + let data; // config - $scope.config = null + $scope.config = null; // resultId = paragraph.id + index - $scope.id = null + $scope.id = null; // referece to paragraph - let paragraph + let paragraph; // index of the result - let resultIndex + let resultIndex; // TableData instance - let tableData + let tableData; // available columns in tabledata - $scope.tableDataColumns = [] + $scope.tableDataColumns = []; // enable helium - let enableHelium = false + let enableHelium = false; // graphMode - $scope.graphMode = null + $scope.graphMode = null; // image data - $scope.imageData = null + $scope.imageData = null; // queue for append output - const textResultQueueForAppend = [] + const textResultQueueForAppend = []; // prevent body area scrollbar from blocking due to scroll in paragraph results - $scope.mouseOver = false - $scope.onMouseOver = function() { $scope.mouseOver = true } - $scope.onMouseOut = function() { $scope.mouseOver = false } + $scope.mouseOver = false; + $scope.onMouseOver = function() { + $scope.mouseOver = true; + }; + $scope.onMouseOut = function() { + $scope.mouseOver = false; + }; $scope.getPointerEvent = function() { - return ($scope.mouseOver) ? {'pointer-events': 'auto' } - : {'pointer-events': 'none' } - } + return ($scope.mouseOver) ? {'pointer-events': 'auto'} + : {'pointer-events': 'none'}; + }; - $scope.init = function (result, config, paragraph, index) { + $scope.init = function(result, config, paragraph, index) { // register helium plugin vis packages - let visPackages = heliumService.getVisualizationCachedPackages() - const visPackageOrder = heliumService.getVisualizationCachedPackageOrder() + let visPackages = heliumService.getVisualizationCachedPackages(); + const visPackageOrder = heliumService.getVisualizationCachedPackageOrder(); // push the helium vis packages following the order - visPackageOrder.map(visName => { - visPackages.map(vis => { - if (vis.name !== visName) { return } + visPackageOrder.map((visName) => { + visPackages.map((vis) => { + if (vis.name !== visName) { + return; + } $scope.builtInTableDataVisualizationList.push({ id: vis.id, name: vis.name, icon: $sce.trustAsHtml(vis.icon), - supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK] - }) + supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], + }); builtInVisualizations[vis.id] = { - class: vis.class - } - }) - }) - - updateData(result, config, paragraph, index) - renderResult($scope.type) - } - - function isDOMLoaded (targetElemId) { - const elem = angular.element(`#${targetElemId}`) - return elem.length + class: vis.class, + }; + }); + }); + + updateData(result, config, paragraph, index); + renderResult($scope.type); + }; + + function isDOMLoaded(targetElemId) { + const elem = angular.element(`#${targetElemId}`); + return elem.length; } - function retryUntilElemIsLoaded (targetElemId, callback) { - function retry () { + function retryUntilElemIsLoaded(targetElemId, callback) { + function retry() { if (!isDOMLoaded(targetElemId)) { - $timeout(retry, 10) - return + $timeout(retry, 10); + return; } - const elem = angular.element(`#${targetElemId}`) - callback(elem) + const elem = angular.element(`#${targetElemId}`); + callback(elem); } - $timeout(retry) + $timeout(retry); } - $scope.$on('updateResult', function (event, result, newConfig, paragraphRef, index) { + $scope.$on('updateResult', function(event, result, newConfig, paragraphRef, index) { if (paragraph.id !== paragraphRef.id || index !== resultIndex) { - return + return; } let refresh = !angular.equals(newConfig, $scope.config) || !angular.equals(result.type, $scope.type) || - !angular.equals(result.data, data) + !angular.equals(result.data, data); - updateData(result, newConfig, paragraph, resultIndex) - renderResult($scope.type, refresh) - }) + updateData(result, newConfig, paragraph, resultIndex); + renderResult($scope.type, refresh); + }); - $scope.$on('appendParagraphOutput', function (event, data) { + $scope.$on('appendParagraphOutput', function(event, data) { /* It has been observed that append events * can be errorneously called even if paragraph * execution has ended, and in that case, no append @@ -238,162 +244,162 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio resultIndex === data.index && (paragraph.status === ParagraphStatus.PENDING || paragraph.status === ParagraphStatus.RUNNING)) { if (DefaultDisplayType.TEXT !== $scope.type) { - $scope.type = DefaultDisplayType.TEXT + $scope.type = DefaultDisplayType.TEXT; } - appendTextOutput(data.data) + appendTextOutput(data.data); } - }) + }); - const updateData = function (result, config, paragraphRef, index) { - data = result.data - paragraph = paragraphRef - resultIndex = parseInt(index) + const updateData = function(result, config, paragraphRef, index) { + data = result.data; + paragraph = paragraphRef; + resultIndex = parseInt(index); - $scope.id = paragraph.id + '_' + index - $scope.type = result.type - config = config ? config : {} + $scope.id = paragraph.id + '_' + index; + $scope.type = result.type; + config = config ? config : {}; // initialize default config values if (!config.graph) { - config.graph = {} + config.graph = {}; } if (!config.graph.mode) { - config.graph.mode = 'table' + config.graph.mode = 'table'; } if (!config.graph.height) { - config.graph.height = 300 + config.graph.height = 300; } if (!config.graph.optionOpen) { - config.graph.optionOpen = false + config.graph.optionOpen = false; } - $scope.graphMode = config.graph.mode - $scope.config = angular.copy(config) + $scope.graphMode = config.graph.mode; + $scope.config = angular.copy(config); // enable only when it is last result - enableHelium = (index === paragraphRef.results.msg.length - 1) + enableHelium = (index === paragraphRef.results.msg.length - 1); if ($scope.type === 'TABLE' || $scope.type === 'NETWORK') { - tableData = new DatasetFactory().createDataset($scope.type) - tableData.loadParagraphResult({type: $scope.type, msg: data}) - $scope.tableDataColumns = tableData.columns - $scope.tableDataComment = tableData.comment + tableData = new DatasetFactory().createDataset($scope.type); + tableData.loadParagraphResult({type: $scope.type, msg: data}); + $scope.tableDataColumns = tableData.columns; + $scope.tableDataComment = tableData.comment; if ($scope.type === 'NETWORK') { - $scope.networkNodes = tableData.networkNodes - $scope.networkRelationships = tableData.networkRelationships - $scope.networkProperties = tableData.networkProperties + $scope.networkNodes = tableData.networkNodes; + $scope.networkRelationships = tableData.networkRelationships; + $scope.networkProperties = tableData.networkProperties; } } else if ($scope.type === 'IMG') { - $scope.imageData = data + $scope.imageData = data; } - } + }; - $scope.createDisplayDOMId = function (baseDOMId, type) { + $scope.createDisplayDOMId = function(baseDOMId, type) { if (type === DefaultDisplayType.TABLE || type === DefaultDisplayType.NETWORK) { - return `${baseDOMId}_graph` + return `${baseDOMId}_graph`; } else if (type === DefaultDisplayType.HTML) { - return `${baseDOMId}_html` + return `${baseDOMId}_html`; } else if (type === DefaultDisplayType.ANGULAR) { - return `${baseDOMId}_angular` + return `${baseDOMId}_angular`; } else if (type === DefaultDisplayType.TEXT) { - return `${baseDOMId}_text` + return `${baseDOMId}_text`; } else if (type === DefaultDisplayType.ELEMENT) { - return `${baseDOMId}_elem` + return `${baseDOMId}_elem`; } else { - console.error(`Cannot create display DOM Id due to unknown display type: ${type}`) + console.error(`Cannot create display DOM Id due to unknown display type: ${type}`); } - } + }; - $scope.renderDefaultDisplay = function (targetElemId, type, data, refresh) { + $scope.renderDefaultDisplay = function(targetElemId, type, data, refresh) { const afterLoaded = () => { if (type === DefaultDisplayType.TABLE || type === DefaultDisplayType.NETWORK) { - renderGraph(targetElemId, $scope.graphMode, refresh) + renderGraph(targetElemId, $scope.graphMode, refresh); } else if (type === DefaultDisplayType.HTML) { - renderHtml(targetElemId, data) + renderHtml(targetElemId, data); } else if (type === DefaultDisplayType.ANGULAR) { - renderAngular(targetElemId, data) + renderAngular(targetElemId, data); } else if (type === DefaultDisplayType.TEXT) { - renderText(targetElemId, data, refresh) + renderText(targetElemId, data, refresh); } else if (type === DefaultDisplayType.ELEMENT) { - renderElem(targetElemId, data) + renderElem(targetElemId, data); } else { - console.error(`Unknown Display Type: ${type}`) + console.error(`Unknown Display Type: ${type}`); } - } + }; - retryUntilElemIsLoaded(targetElemId, afterLoaded) + retryUntilElemIsLoaded(targetElemId, afterLoaded); // send message to parent that this result is rendered - const paragraphId = $scope.$parent.paragraph.id - $scope.$emit('resultRendered', paragraphId) - } + const paragraphId = $scope.$parent.paragraph.id; + $scope.$emit('resultRendered', paragraphId); + }; - const renderResult = function (type, refresh) { - let activeApp + const renderResult = function(type, refresh) { + let activeApp; if (enableHelium) { - getSuggestions() - getApplicationStates() - activeApp = _.get($scope.config, 'helium.activeApp') + getSuggestions(); + getApplicationStates(); + activeApp = _.get($scope.config, 'helium.activeApp'); } if (activeApp) { - const appState = _.find($scope.apps, {id: activeApp}) - renderApp(`p${appState.id}`, appState) + const appState = _.find($scope.apps, {id: activeApp}); + renderApp(`p${appState.id}`, appState); } else { if (!DefaultDisplayType[type]) { - $scope.renderCustomDisplay(type, data) + $scope.renderCustomDisplay(type, data); } else { - const targetElemId = $scope.createDisplayDOMId(`p${$scope.id}`, type) - $scope.renderDefaultDisplay(targetElemId, type, data, refresh) + const targetElemId = $scope.createDisplayDOMId(`p${$scope.id}`, type); + $scope.renderDefaultDisplay(targetElemId, type, data, refresh); } } - } + }; - $scope.isDefaultDisplay = function () { - return DefaultDisplayType[$scope.type] - } + $scope.isDefaultDisplay = function() { + return DefaultDisplayType[$scope.type]; + }; /** * Render multiple sub results for custom display */ - $scope.renderCustomDisplay = function (type, data) { + $scope.renderCustomDisplay = function(type, data) { // get result from intp if (!heliumService.getSpellByMagic(type)) { - console.error(`Can't execute spell due to unknown display type: ${type}`) - return + console.error(`Can't execute spell due to unknown display type: ${type}`); + return; } // custom display result can include multiple subset results heliumService.executeSpellAsDisplaySystem(type, data) - .then(dataWithTypes => { - const containerDOMId = `p${$scope.id}_custom` + .then((dataWithTypes) => { + const containerDOMId = `p${$scope.id}_custom`; const afterLoaded = () => { - const containerDOM = angular.element(`#${containerDOMId}`) + const containerDOM = angular.element(`#${containerDOMId}`); // Spell.interpret() can create multiple outputs for (let i = 0; i < dataWithTypes.length; i++) { - const dt = dataWithTypes[i] - const data = dt.data - const type = dt.type + const dt = dataWithTypes[i]; + const data = dt.data; + const type = dt.type; // prepare each DOM to be filled - const subResultDOMId = $scope.createDisplayDOMId(`p${$scope.id}_custom_${i}`, type) - const subResultDOM = document.createElement('div') - containerDOM.append(subResultDOM) - subResultDOM.setAttribute('id', subResultDOMId) + const subResultDOMId = $scope.createDisplayDOMId(`p${$scope.id}_custom_${i}`, type); + const subResultDOM = document.createElement('div'); + containerDOM.append(subResultDOM); + subResultDOM.setAttribute('id', subResultDOMId); - $scope.renderDefaultDisplay(subResultDOMId, type, data, true) + $scope.renderDefaultDisplay(subResultDOMId, type, data, true); } - } + }; - retryUntilElemIsLoaded(containerDOMId, afterLoaded) - }) - .catch(error => { - console.error(`Failed to render custom display: ${$scope.type}\n` + error) + retryUntilElemIsLoaded(containerDOMId, afterLoaded); }) - } + .catch((error) => { + console.error(`Failed to render custom display: ${$scope.type}\n` + error); + }); + }; /** * generates actually object which will be consumed from `data` property @@ -405,688 +411,710 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio * @param successCallback * @param failureCallback */ - const handleData = function (data, type, successCallback, failureCallback) { + const handleData = function(data, type, successCallback, failureCallback) { if (SpellResult.isFunction(data)) { try { - successCallback(data()) + successCallback(data()); } catch (error) { - failureCallback(error) - console.error(`Failed to handle ${type} type, function data\n`, error) + failureCallback(error); + console.error(`Failed to handle ${type} type, function data\n`, error); } } else if (SpellResult.isObject(data)) { try { - successCallback(data) + successCallback(data); } catch (error) { - console.error(`Failed to handle ${type} type, object data\n`, error) + console.error(`Failed to handle ${type} type, object data\n`, error); } } - } + }; - const renderElem = function (targetElemId, data) { - const elem = angular.element(`#${targetElemId}`) - handleData(() => { data(targetElemId) }, DefaultDisplayType.ELEMENT, + const renderElem = function(targetElemId, data) { + const elem = angular.element(`#${targetElemId}`); + handleData(() => { + data(targetElemId); + }, DefaultDisplayType.ELEMENT, () => {}, /** HTML element will be filled with data. thus pass empty success callback */ - (error) => { elem.html(`${error.stack}`) } - ) - } + (error) => { + elem.html(`${error.stack}`); + } + ); + }; - const renderHtml = function (targetElemId, data) { - const elem = angular.element(`#${targetElemId}`) + const renderHtml = function(targetElemId, data) { + const elem = angular.element(`#${targetElemId}`); handleData(data, DefaultDisplayType.HTML, (generated) => { - elem.html(generated) - elem.find('pre code').each(function (i, e) { - hljs.highlightBlock(e) - }) + elem.html(generated); + elem.find('pre code').each(function(i, e) { + hljs.highlightBlock(e); + }); /* eslint new-cap: [2, {"capIsNewExceptions": ["MathJax.Hub.Queue"]}] */ - MathJax.Hub.Queue(['Typeset', MathJax.Hub, elem[0]]) + MathJax.Hub.Queue(['Typeset', MathJax.Hub, elem[0]]); }, - (error) => { elem.html(`${error.stack}`) } - ) - } + (error) => { + elem.html(`${error.stack}`); + } + ); + }; - const renderAngular = function (targetElemId, data) { - const elem = angular.element(`#${targetElemId}`) - const paragraphScope = noteVarShareService.get(`${paragraph.id}_paragraphScope`) + const renderAngular = function(targetElemId, data) { + const elem = angular.element(`#${targetElemId}`); + const paragraphScope = noteVarShareService.get(`${paragraph.id}_paragraphScope`); handleData(data, DefaultDisplayType.ANGULAR, (generated) => { - elem.html(generated) - $compile(elem.contents())(paragraphScope) + elem.html(generated); + $compile(elem.contents())(paragraphScope); }, - (error) => { elem.html(`${error.stack}`) } - ) - } + (error) => { + elem.html(`${error.stack}`); + } + ); + }; - const getTextResultElemId = function (resultId) { - return `p${resultId}_text` - } + const getTextResultElemId = function(resultId) { + return `p${resultId}_text`; + }; - const checkAndReplaceCarriageReturn = function (str) { + const checkAndReplaceCarriageReturn = function(str) { if (/\r/.test(str)) { - let newGenerated = '' - let strArr = str.split('\n') + let newGenerated = ''; + let strArr = str.split('\n'); for (let str of strArr) { if (/\r/.test(str)) { - let splitCR = str.split('\r') - newGenerated += splitCR[splitCR.length - 1] + '\n' + let splitCR = str.split('\r'); + newGenerated += splitCR[splitCR.length - 1] + '\n'; } else { - newGenerated += str + '\n' + newGenerated += str + '\n'; } } // remove last "\n" character - return newGenerated.slice(0, -1) + return newGenerated.slice(0, -1); } else { - return str + return str; } - } + }; - const renderText = function (targetElemId, data, refresh) { - const elem = angular.element(`#${targetElemId}`) + const renderText = function(targetElemId, data, refresh) { + const elem = angular.element(`#${targetElemId}`); handleData(data, DefaultDisplayType.TEXT, (generated) => { // clear all lines before render - removeChildrenDOM(targetElemId) + removeChildrenDOM(targetElemId); if (generated) { - generated = checkAndReplaceCarriageReturn(generated) - const escaped = AnsiUpConverter.ansi_to_html(generated) - const divDOM = angular.element('
    ').innerHTML = escaped + generated = checkAndReplaceCarriageReturn(generated); + const escaped = AnsiUpConverter.ansi_to_html(generated); + const divDOM = angular.element('
    ').innerHTML = escaped; if (refresh) { - elem.html(divDOM) + elem.html(divDOM); } else { - elem.append(divDOM) + elem.append(divDOM); } } else if (refresh) { - elem.html('') + elem.html(''); } - elem.bind('mousewheel', (e) => { $scope.keepScrollDown = false }) + elem.bind('mousewheel', (e) => { + $scope.keepScrollDown = false; + }); }, - (error) => { elem.html(`${error.stack}`) } - ) - } + (error) => { + elem.html(`${error.stack}`); + } + ); + }; - const removeChildrenDOM = function (targetElemId) { - const elem = angular.element(`#${targetElemId}`) + const removeChildrenDOM = function(targetElemId) { + const elem = angular.element(`#${targetElemId}`); if (elem.length) { - elem.children().remove() + elem.children().remove(); } - } + }; - function appendTextOutput (data) { - const elemId = getTextResultElemId($scope.id) - textResultQueueForAppend.push(data) + function appendTextOutput(data) { + const elemId = getTextResultElemId($scope.id); + textResultQueueForAppend.push(data); // if DOM is not loaded, just push data and return if (!isDOMLoaded(elemId)) { - return + return; } - const elem = angular.element(`#${elemId}`) + const elem = angular.element(`#${elemId}`); // pop all stacked data and append to the DOM while (textResultQueueForAppend.length > 0) { - const line = elem.html() + AnsiUpConverter.ansi_to_html(textResultQueueForAppend.pop()) - elem.html(checkAndReplaceCarriageReturn(line)) + const line = elem.html() + AnsiUpConverter.ansi_to_html(textResultQueueForAppend.pop()); + elem.html(checkAndReplaceCarriageReturn(line)); if ($scope.keepScrollDown) { - const doc = angular.element(`#${elemId}`) - doc[0].scrollTop = doc[0].scrollHeight + const doc = angular.element(`#${elemId}`); + doc[0].scrollTop = doc[0].scrollHeight; } } } - const getTrSettingElem = function (scopeId, graphMode) { - return angular.element('#trsetting' + scopeId + '_' + graphMode) - } + const getTrSettingElem = function(scopeId, graphMode) { + return angular.element('#trsetting' + scopeId + '_' + graphMode); + }; - const getVizSettingElem = function (scopeId, graphMode) { - return angular.element('#vizsetting' + scopeId + '_' + graphMode) - } + const getVizSettingElem = function(scopeId, graphMode) { + return angular.element('#vizsetting' + scopeId + '_' + graphMode); + }; - const renderGraph = function (graphElemId, graphMode, refresh) { + const renderGraph = function(graphElemId, graphMode, refresh) { // set graph height - const height = $scope.config.graph.height - const graphElem = angular.element(`#${graphElemId}`) - graphElem.height(height) + const height = $scope.config.graph.height; + const graphElem = angular.element(`#${graphElemId}`); + graphElem.height(height); - if (!graphMode) { graphMode = 'table' } + if (!graphMode) { + graphMode = 'table'; + } - let builtInViz = builtInVisualizations[graphMode] + let builtInViz = builtInVisualizations[graphMode]; if (!builtInViz) { /** helium package is not available, fallback to table vis */ - graphMode = 'table' - $scope.graphMode = graphMode /** html depends on this scope value */ - builtInViz = builtInVisualizations[graphMode] + graphMode = 'table'; + $scope.graphMode = graphMode; /** html depends on this scope value */ + builtInViz = builtInVisualizations[graphMode]; } // deactive previsouly active visualization for (let t in builtInVisualizations) { - const v = builtInVisualizations[t].instance + if (builtInVisualizations.hasOwnProperty(t)) { + const v = builtInVisualizations[t].instance; - if (t !== graphMode && v && v.isActive()) { - v.deactivate() - break + if (t !== graphMode && v && v.isActive()) { + v.deactivate(); + break; + } } } - let afterLoaded = function () { /** will be overwritten */ } + let afterLoaded = function() { /** will be overwritten */ }; if (!builtInViz.instance) { // not instantiated yet // render when targetEl is available - afterLoaded = function (loadedElem) { + afterLoaded = function(loadedElem) { try { - const transformationSettingTargetEl = getTrSettingElem($scope.id, graphMode) - const visualizationSettingTargetEl = getVizSettingElem($scope.id, graphMode) + const transformationSettingTargetEl = getTrSettingElem($scope.id, graphMode); + const visualizationSettingTargetEl = getVizSettingElem($scope.id, graphMode); // set height - loadedElem.height(height) + loadedElem.height(height); // instantiate visualization - const config = getVizConfig(graphMode) - const Visualization = builtInViz.class - builtInViz.instance = new Visualization(loadedElem, config) + const config = getVizConfig(graphMode); + const Visualization = builtInViz.class; + builtInViz.instance = new Visualization(loadedElem, config); // inject emitter, $templateRequest - const emitter = function (graphSetting) { - commitVizConfigChange(graphSetting, graphMode) - } - builtInViz.instance._emitter = emitter - builtInViz.instance._compile = $compile + const emitter = function(graphSetting) { + commitVizConfigChange(graphSetting, graphMode); + }; + builtInViz.instance._emitter = emitter; + builtInViz.instance._compile = $compile; // ui-grid related - $templateCache.put('ui-grid/ui-grid-filter', TableGridFilterTemplate) - builtInViz.instance._uiGridConstants = uiGridConstants - builtInViz.instance._timeout = $timeout - - builtInViz.instance._createNewScope = createNewScope - builtInViz.instance._templateRequest = $templateRequest - const transformation = builtInViz.instance.getTransformation() - transformation._emitter = emitter - transformation._templateRequest = $templateRequest - transformation._compile = $compile - transformation._createNewScope = createNewScope + $templateCache.put('ui-grid/ui-grid-filter', TableGridFilterTemplate); + builtInViz.instance._uiGridConstants = uiGridConstants; + builtInViz.instance._timeout = $timeout; + + builtInViz.instance._createNewScope = createNewScope; + builtInViz.instance._templateRequest = $templateRequest; + const transformation = builtInViz.instance.getTransformation(); + transformation._emitter = emitter; + transformation._templateRequest = $templateRequest; + transformation._compile = $compile; + transformation._createNewScope = createNewScope; // render - const transformed = transformation.transform(tableData) - transformation.renderSetting(transformationSettingTargetEl) - builtInViz.instance.render(transformed) - builtInViz.instance.renderSetting(visualizationSettingTargetEl) - builtInViz.instance.activate() + const transformed = transformation.transform(tableData); + transformation.renderSetting(transformationSettingTargetEl); + builtInViz.instance.render(transformed); + builtInViz.instance.renderSetting(visualizationSettingTargetEl); + builtInViz.instance.activate(); angular.element(window).resize(() => { - builtInViz.instance.resize() - }) + builtInViz.instance.resize(); + }); } catch (err) { - console.error('Graph drawing error %o', err) + console.error('Graph drawing error %o', err); } - } + }; } else if (refresh) { // when graph options or data are changed - console.log('Refresh data %o', tableData) - - afterLoaded = function (loadedElem) { - const transformationSettingTargetEl = getTrSettingElem($scope.id, graphMode) - const visualizationSettingTargetEl = getVizSettingElem($scope.id, graphMode) - const config = getVizConfig(graphMode) - loadedElem.height(height) - const transformation = builtInViz.instance.getTransformation() - transformation.setConfig(config) - const transformed = transformation.transform(tableData) - transformation.renderSetting(transformationSettingTargetEl) - builtInViz.instance.setConfig(config) - builtInViz.instance.render(transformed) - builtInViz.instance.renderSetting(visualizationSettingTargetEl) - builtInViz.instance.activate() - } + console.log('Refresh data %o', tableData); + + afterLoaded = function(loadedElem) { + const transformationSettingTargetEl = getTrSettingElem($scope.id, graphMode); + const visualizationSettingTargetEl = getVizSettingElem($scope.id, graphMode); + const config = getVizConfig(graphMode); + loadedElem.height(height); + const transformation = builtInViz.instance.getTransformation(); + transformation.setConfig(config); + const transformed = transformation.transform(tableData); + transformation.renderSetting(transformationSettingTargetEl); + builtInViz.instance.setConfig(config); + builtInViz.instance.render(transformed); + builtInViz.instance.renderSetting(visualizationSettingTargetEl); + builtInViz.instance.activate(); + }; } else { - afterLoaded = function (loadedElem) { - loadedElem.height(height) - builtInViz.instance.activate() - } + afterLoaded = function(loadedElem) { + loadedElem.height(height); + builtInViz.instance.activate(); + }; } - const tableElemId = `p${$scope.id}_${graphMode}` - retryUntilElemIsLoaded(tableElemId, afterLoaded) - } + const tableElemId = `p${$scope.id}_${graphMode}`; + retryUntilElemIsLoaded(tableElemId, afterLoaded); + }; - $scope.switchViz = function (newMode) { - let newConfig = angular.copy($scope.config) - let newParams = angular.copy(paragraph.settings.params) + $scope.switchViz = function(newMode) { + let newConfig = angular.copy($scope.config); + let newParams = angular.copy(paragraph.settings.params); // graph options - newConfig.graph.mode = newMode + newConfig.graph.mode = newMode; // see switchApp() - _.set(newConfig, 'helium.activeApp', undefined) + _.set(newConfig, 'helium.activeApp', undefined); - commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams) - } + commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams); + }; - const createNewScope = function () { - return $rootScope.$new(true) - } + const createNewScope = function() { + return $rootScope.$new(true); + }; - const commitParagraphResult = function (title, text, config, params) { - let newParagraphConfig = angular.copy(paragraph.config) - newParagraphConfig.results = newParagraphConfig.results || [] - newParagraphConfig.results[resultIndex] = config + const commitParagraphResult = function(title, text, config, params) { + let newParagraphConfig = angular.copy(paragraph.config); + newParagraphConfig.results = newParagraphConfig.results || []; + newParagraphConfig.results[resultIndex] = config; if ($scope.revisionView === true) { // local update without commit updateData({ type: $scope.type, - data: data - }, newParagraphConfig.results[resultIndex], paragraph, resultIndex) - renderResult($scope.type, true) + data: data, + }, newParagraphConfig.results[resultIndex], paragraph, resultIndex); + renderResult($scope.type, true); } else { - return websocketMsgSrv.commitParagraph(paragraph.id, title, text, newParagraphConfig, params) + return websocketMsgSrv.commitParagraph(paragraph.id, title, text, newParagraphConfig, params); } - } + }; - $scope.toggleGraphSetting = function () { - let newConfig = angular.copy($scope.config) + $scope.toggleGraphSetting = function() { + let newConfig = angular.copy($scope.config); if (newConfig.graph.optionOpen) { - newConfig.graph.optionOpen = false + newConfig.graph.optionOpen = false; } else { - newConfig.graph.optionOpen = true + newConfig.graph.optionOpen = true; } - let newParams = angular.copy(paragraph.settings.params) - commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams) - } + let newParams = angular.copy(paragraph.settings.params); + commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams); + }; - const getVizConfig = function (vizId) { - let config - let graph = $scope.config.graph + const getVizConfig = function(vizId) { + let config; + let graph = $scope.config.graph; if (graph) { // copy setting for vizId if (graph.setting) { - config = angular.copy(graph.setting[vizId]) + config = angular.copy(graph.setting[vizId]); } if (!config) { - config = {} + config = {}; } // copy common setting - config.common = angular.copy(graph.commonSetting) || {} + config.common = angular.copy(graph.commonSetting) || {}; // copy pivot setting if (graph.keys) { config.common.pivot = { keys: angular.copy(graph.keys), groups: angular.copy(graph.groups), - values: angular.copy(graph.values) - } + values: angular.copy(graph.values), + }; } } - console.debug('getVizConfig', config) - return config - } + console.debug('getVizConfig', config); + return config; + }; - const commitVizConfigChange = function (config, vizId) { - let newConfig = angular.copy($scope.config) + const commitVizConfigChange = function(config, vizId) { + let newConfig = angular.copy($scope.config); if (!newConfig.graph) { - newConfig.graph = {} + newConfig.graph = {}; } // copy setting for vizId if (!newConfig.graph.setting) { - newConfig.graph.setting = {} + newConfig.graph.setting = {}; } - newConfig.graph.setting[vizId] = angular.copy(config) + newConfig.graph.setting[vizId] = angular.copy(config); // copy common setting if (newConfig.graph.setting[vizId]) { - newConfig.graph.commonSetting = newConfig.graph.setting[vizId].common - delete newConfig.graph.setting[vizId].common + newConfig.graph.commonSetting = newConfig.graph.setting[vizId].common; + delete newConfig.graph.setting[vizId].common; } // copy pivot setting if (newConfig.graph.commonSetting && newConfig.graph.commonSetting.pivot) { - newConfig.graph.keys = newConfig.graph.commonSetting.pivot.keys - newConfig.graph.groups = newConfig.graph.commonSetting.pivot.groups - newConfig.graph.values = newConfig.graph.commonSetting.pivot.values - delete newConfig.graph.commonSetting.pivot + newConfig.graph.keys = newConfig.graph.commonSetting.pivot.keys; + newConfig.graph.groups = newConfig.graph.commonSetting.pivot.groups; + newConfig.graph.values = newConfig.graph.commonSetting.pivot.values; + delete newConfig.graph.commonSetting.pivot; } - console.debug('committVizConfig', newConfig) - let newParams = angular.copy(paragraph.settings.params) - commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams) - } + console.debug('committVizConfig', newConfig); + let newParams = angular.copy(paragraph.settings.params); + commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams); + }; - $scope.$on('paragraphResized', function (event, paragraphId) { + $scope.$on('paragraphResized', function(event, paragraphId) { // paragraph col width changed if (paragraphId === paragraph.id) { - let builtInViz = builtInVisualizations[$scope.graphMode] + let builtInViz = builtInVisualizations[$scope.graphMode]; if (builtInViz && builtInViz.instance) { - $timeout(_ => builtInViz.instance.resize(), 200) + $timeout(() => builtInViz.instance.resize(), 200); } } - }) + }); - $scope.resize = function (width, height) { - $timeout(function () { - changeHeight(width, height) - }, 200) - } + $scope.resize = function(width, height) { + $timeout(function() { + changeHeight(width, height); + }, 200); + }; - const changeHeight = function (width, height) { - let newParams = angular.copy(paragraph.settings.params) - let newConfig = angular.copy($scope.config) + const changeHeight = function(width, height) { + let newParams = angular.copy(paragraph.settings.params); + let newConfig = angular.copy($scope.config); - newConfig.graph.height = height - paragraph.config.colWidth = width + newConfig.graph.height = height; + paragraph.config.colWidth = width; - commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams) - } + commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams); + }; - $scope.exportToDSV = function (delimiter) { - let dsv = '' - let dateFinished = moment(paragraph.dateFinished).format('YYYY-MM-DD hh:mm:ss A') - let exportedFileName = paragraph.title ? paragraph.title + '_' + dateFinished : 'data_' + dateFinished + $scope.exportToDSV = function(delimiter) { + let dsv = ''; + let dateFinished = moment(paragraph.dateFinished).format('YYYY-MM-DD hh:mm:ss A'); + let exportedFileName = paragraph.title ? paragraph.title + '_' + dateFinished : 'data_' + dateFinished; for (let titleIndex in tableData.columns) { - dsv += tableData.columns[titleIndex].name + delimiter + if (tableData.columns.hasOwnProperty(titleIndex)) { + dsv += tableData.columns[titleIndex].name + delimiter; + } } - dsv = dsv.substring(0, dsv.length - 1) + '\n' + dsv = dsv.substring(0, dsv.length - 1) + '\n'; for (let r in tableData.rows) { - let row = tableData.rows[r] - let dsvRow = '' - for (let index in row) { - let stringValue = (row[index]).toString() - if (stringValue.indexOf(delimiter) > -1) { - dsvRow += '"' + stringValue + '"' + delimiter - } else { - dsvRow += row[index] + delimiter + if (tableData.rows.hasOwnProperty(r)) { + let row = tableData.rows[r]; + let dsvRow = ''; + for (let index in row) { + if (row.hasOwnProperty(index)) { + let stringValue = (row[index]).toString(); + if (stringValue.indexOf(delimiter) > -1) { + dsvRow += '"' + stringValue + '"' + delimiter; + } else { + dsvRow += row[index] + delimiter; + } + } } + dsv += dsvRow.substring(0, dsvRow.length - 1) + '\n'; } - dsv += dsvRow.substring(0, dsvRow.length - 1) + '\n' } - let extension = '' + let extension = ''; if (delimiter === '\t') { - extension = 'tsv' + extension = 'tsv'; } else if (delimiter === ',') { - extension = 'csv' + extension = 'csv'; } - saveAsService.saveAs(dsv, exportedFileName, extension) - } + saveAsService.saveAs(dsv, exportedFileName, extension); + }; - $scope.getBase64ImageSrc = function (base64Data) { - return 'data:image/png;base64,' + base64Data - } + $scope.getBase64ImageSrc = function(base64Data) { + return 'data:image/png;base64,' + base64Data; + }; // Helium ---------------- - let ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_' + let ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_'; // app states - $scope.apps = [] + $scope.apps = []; // suggested apps - $scope.suggestion = {} + $scope.suggestion = {}; - $scope.switchApp = function (appId) { - let newConfig = angular.copy($scope.config) - let newParams = angular.copy(paragraph.settings.params) + $scope.switchApp = function(appId) { + let newConfig = angular.copy($scope.config); + let newParams = angular.copy(paragraph.settings.params); // 'helium.activeApp' can be cleared by switchViz() - _.set(newConfig, 'helium.activeApp', appId) + _.set(newConfig, 'helium.activeApp', appId); - commitConfig(newConfig, newParams) - } + commitConfig(newConfig, newParams); + }; - $scope.loadApp = function (heliumPackage) { - let noteId = $route.current.pathParams.noteId + $scope.loadApp = function(heliumPackage) { + let noteId = $route.current.pathParams.noteId; $http.post(baseUrlSrv.getRestApiBase() + '/helium/load/' + noteId + '/' + paragraph.id, heliumPackage) - .success(function (data, status, headers, config) { - console.log('Load app %o', data) + .success(function(data, status, headers, config) { + console.log('Load app %o', data); }) - .error(function (err, status, headers, config) { - console.log('Error %o', err) - }) - } + .error(function(err, status, headers, config) { + console.log('Error %o', err); + }); + }; - const commitConfig = function (config, params) { - commitParagraphResult(paragraph.title, paragraph.text, config, params) - } + const commitConfig = function(config, params) { + commitParagraphResult(paragraph.title, paragraph.text, config, params); + }; - const getApplicationStates = function () { - let appStates = [] + const getApplicationStates = function() { + let appStates = []; // Display ApplicationState if (paragraph.apps) { - _.forEach(paragraph.apps, function (app) { + _.forEach(paragraph.apps, function(app) { appStates.push({ id: app.id, pkg: app.pkg, status: app.status, - output: app.output - }) - }) + output: app.output, + }); + }); } // update or remove app states no longer exists - _.forEach($scope.apps, function (currentAppState, idx) { - let newAppState = _.find(appStates, {id: currentAppState.id}) + _.forEach($scope.apps, function(currentAppState, idx) { + let newAppState = _.find(appStates, {id: currentAppState.id}); if (newAppState) { - angular.extend($scope.apps[idx], newAppState) + angular.extend($scope.apps[idx], newAppState); } else { - $scope.apps.splice(idx, 1) + $scope.apps.splice(idx, 1); } - }) + }); // add new app states - _.forEach(appStates, function (app, idx) { + _.forEach(appStates, function(app, idx) { if ($scope.apps.length <= idx || $scope.apps[idx].id !== app.id) { - $scope.apps.splice(idx, 0, app) + $scope.apps.splice(idx, 0, app); } - }) - } + }); + }; - const getSuggestions = function () { + const getSuggestions = function() { // Get suggested apps - let noteId = $route.current.pathParams.noteId + let noteId = $route.current.pathParams.noteId; if (!noteId) { - return + return; } $http.get(baseUrlSrv.getRestApiBase() + '/helium/suggest/' + noteId + '/' + paragraph.id) - .success(function (data, status, headers, config) { - $scope.suggestion = data.body - }) - .error(function (err, status, headers, config) { - console.log('Error %o', err) + .success(function(data, status, headers, config) { + $scope.suggestion = data.body; }) - } + .error(function(err, status, headers, config) { + console.log('Error %o', err); + }); + }; - const renderApp = function (targetElemId, appState) { + const renderApp = function(targetElemId, appState) { const afterLoaded = (loadedElem) => { try { - console.log('renderApp %o', appState) - loadedElem.html(appState.output) - $compile(loadedElem.contents())(getAppScope(appState)) + console.log('renderApp %o', appState); + loadedElem.html(appState.output); + $compile(loadedElem.contents())(getAppScope(appState)); } catch (err) { - console.log('App rendering error %o', err) + console.log('App rendering error %o', err); } - } - retryUntilElemIsLoaded(targetElemId, afterLoaded) - } + }; + retryUntilElemIsLoaded(targetElemId, afterLoaded); + }; /* ** $scope.$on functions below */ - $scope.$on('appendAppOutput', function (event, data) { + $scope.$on('appendAppOutput', function(event, data) { if (paragraph.id === data.paragraphId) { - let app = _.find($scope.apps, {id: data.appId}) + let app = _.find($scope.apps, {id: data.appId}); if (app) { - app.output += data.data + app.output += data.data; - let paragraphAppState = _.find(paragraph.apps, {id: data.appId}) - paragraphAppState.output = app.output + let paragraphAppState = _.find(paragraph.apps, {id: data.appId}); + paragraphAppState.output = app.output; - let 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) + let 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) { + $scope.$on('updateAppOutput', function(event, data) { if (paragraph.id === data.paragraphId) { - let app = _.find($scope.apps, {id: data.appId}) + let app = _.find($scope.apps, {id: data.appId}); if (app) { - app.output = data.data + app.output = data.data; - let paragraphAppState = _.find(paragraph.apps, {id: data.appId}) - paragraphAppState.output = app.output + let paragraphAppState = _.find(paragraph.apps, {id: data.appId}); + paragraphAppState.output = app.output; - let targetEl = angular.element(document.getElementById('p' + app.id)) - targetEl.html(app.output) - $compile(targetEl.contents())(getAppScope(app)) - console.log('append app output') + let 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) { + $scope.$on('appLoad', function(event, data) { if (paragraph.id === data.paragraphId) { - let app = _.find($scope.apps, {id: data.appId}) + let app = _.find($scope.apps, {id: data.appId}); if (!app) { app = { id: data.appId, pkg: data.pkg, status: 'UNLOADED', - output: '' - } + output: '', + }; - $scope.apps.push(app) - paragraph.apps.push(app) - $scope.switchApp(app.id) + $scope.apps.push(app); + paragraph.apps.push(app); + $scope.switchApp(app.id); } } - }) + }); - $scope.$on('appStatusChange', function (event, data) { + $scope.$on('appStatusChange', function(event, data) { if (paragraph.id === data.paragraphId) { - let app = _.find($scope.apps, {id: data.appId}) + let app = _.find($scope.apps, {id: data.appId}); if (app) { - app.status = data.status - let paragraphAppState = _.find(paragraph.apps, {id: data.appId}) - paragraphAppState.status = app.status + app.status = data.status; + let paragraphAppState = _.find(paragraph.apps, {id: data.appId}); + paragraphAppState.status = app.status; } } - }) + }); - let getAppRegistry = function (appState) { + let getAppRegistry = function(appState) { if (!appState.registry) { - appState.registry = {} + appState.registry = {}; } - return appState.registry - } + return appState.registry; + }; - const getAppScope = function (appState) { + const getAppScope = function(appState) { if (!appState.scope) { - appState.scope = $rootScope.$new(true, $rootScope) + appState.scope = $rootScope.$new(true, $rootScope); } - return appState.scope - } + return appState.scope; + }; - $scope.$on('angularObjectUpdate', function (event, data) { - let noteId = $route.current.pathParams.noteId + $scope.$on('angularObjectUpdate', function(event, data) { + let noteId = $route.current.pathParams.noteId; if (!data.noteId || data.noteId === noteId) { - let scope - let registry + let scope; + let registry; - let app = _.find($scope.apps, {id: data.paragraphId}) + let app = _.find($scope.apps, {id: data.paragraphId}); if (app) { - scope = getAppScope(app) - registry = getAppRegistry(app) + scope = getAppScope(app); + registry = getAppRegistry(app); } else { // no matching app in this paragraph - return + return; } - let varName = data.angularObject.name + let varName = data.angularObject.name; if (angular.equals(data.angularObject.object, scope[varName])) { // return when update has no change - return + return; } if (!registry[varName]) { registry[varName] = { interpreterGroupId: data.interpreterGroupId, noteId: data.noteId, - paragraphId: data.paragraphId - } + paragraphId: data.paragraphId, + }; } else { - registry[varName].noteId = registry[varName].noteId || data.noteId - registry[varName].paragraphId = registry[varName].paragraphId || data.paragraphId + registry[varName].noteId = registry[varName].noteId || data.noteId; + registry[varName].paragraphId = registry[varName].paragraphId || data.paragraphId; } - registry[varName].skipEmit = true + registry[varName].skipEmit = true; if (!registry[varName].clearWatcher) { - registry[varName].clearWatcher = scope.$watch(varName, function (newValue, oldValue) { - console.log('angular object (paragraph) updated %o %o', varName, registry[varName]) + 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 + registry[varName].skipEmit = false; + return; } websocketMsgSrv.updateAngularObject( registry[varName].noteId, registry[varName].paragraphId, varName, newValue, - registry[varName].interpreterGroupId) - }) + registry[varName].interpreterGroupId); + }); } - console.log('angular object (paragraph) created %o', varName) - scope[varName] = data.angularObject.object + console.log('angular object (paragraph) created %o', varName); + scope[varName] = data.angularObject.object; // create proxy for AngularFunction if (varName.indexOf(ANGULAR_FUNCTION_OBJECT_NAME_PREFIX) === 0) { - let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length) - scope[funcName] = function () { + let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length); + scope[funcName] = function() { // eslint-disable-next-line prefer-rest-params - scope[varName] = arguments + scope[varName] = arguments; // eslint-disable-next-line prefer-rest-params - console.log('angular function (paragraph) invoked %o', arguments) - } + console.log('angular function (paragraph) invoked %o', arguments); + }; - console.log('angular function (paragraph) created %o', scope[funcName]) + console.log('angular function (paragraph) created %o', scope[funcName]); } } - }) + }); - $scope.$on('angularObjectRemove', function (event, data) { - let noteId = $route.current.pathParams.noteId + $scope.$on('angularObjectRemove', function(event, data) { + let noteId = $route.current.pathParams.noteId; if (!data.noteId || data.noteId === noteId) { - let scope - let registry + let scope; + let registry; - let app = _.find($scope.apps, {id: data.paragraphId}) + let app = _.find($scope.apps, {id: data.paragraphId}); if (app) { - scope = getAppScope(app) - registry = getAppRegistry(app) + scope = getAppScope(app); + registry = getAppRegistry(app); } else { // no matching app in this paragraph - return + return; } - let varName = data.name + let varName = data.name; // clear watcher if (registry[varName]) { - registry[varName].clearWatcher() - registry[varName] = undefined + registry[varName].clearWatcher(); + registry[varName] = undefined; } // remove scope variable - scope[varName] = undefined + scope[varName] = undefined; // remove proxy for AngularFunction if (varName.indexOf(ANGULAR_FUNCTION_OBJECT_NAME_PREFIX) === 0) { - let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length) - scope[funcName] = undefined + let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length); + scope[funcName] = undefined; } } - }) + }); } diff --git a/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.component.js b/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.component.js index 45db38a44f8..debdb6e0fd7 100644 --- a/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.component.js +++ b/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.component.js @@ -12,81 +12,81 @@ * limitations under the License. */ -import revisionsComparatorTemplate from './revisions-comparator.html' -import './revisions-comparator.css' -import moment from 'moment' +import revisionsComparatorTemplate from './revisions-comparator.html'; +import './revisions-comparator.css'; +import moment from 'moment'; function RevisionsComparatorController($scope, websocketMsgSrv, $routeParams) { - 'ngInject' + 'ngInject'; - $scope.firstNoteRevisionForCompare = null - $scope.secondNoteRevisionForCompare = null - $scope.mergeNoteRevisionsForCompare = null - $scope.currentParagraphDiffDisplay = null - $scope.currentFirstRevisionForCompare = 'Choose...' - $scope.currentSecondRevisionForCompare = 'Choose...' + $scope.firstNoteRevisionForCompare = null; + $scope.secondNoteRevisionForCompare = null; + $scope.mergeNoteRevisionsForCompare = null; + $scope.currentParagraphDiffDisplay = null; + $scope.currentFirstRevisionForCompare = 'Choose...'; + $scope.currentSecondRevisionForCompare = 'Choose...'; - $scope.getNoteRevisionForReview = function (revision, position) { + $scope.getNoteRevisionForReview = function(revision, position) { if (position) { if (position === 'first') { - $scope.currentFirstRevisionForCompare = revision.message + $scope.currentFirstRevisionForCompare = revision.message; } else { - $scope.currentSecondRevisionForCompare = revision.message + $scope.currentSecondRevisionForCompare = revision.message; } - websocketMsgSrv.getNoteByRevisionForCompare($routeParams.noteId, revision.id, position) + websocketMsgSrv.getNoteByRevisionForCompare($routeParams.noteId, revision.id, position); } - } + }; // compare revisions - $scope.compareRevisions = function () { + $scope.compareRevisions = function() { if ($scope.firstNoteRevisionForCompare && $scope.secondNoteRevisionForCompare) { - let paragraphs1 = $scope.firstNoteRevisionForCompare.note.paragraphs - let paragraphs2 = $scope.secondNoteRevisionForCompare.note.paragraphs - let added = 'added' - let deleted = 'deleted' - let compared = 'compared' - let merge = [] + let paragraphs1 = $scope.firstNoteRevisionForCompare.note.paragraphs; + let paragraphs2 = $scope.secondNoteRevisionForCompare.note.paragraphs; + let added = 'added'; + let deleted = 'deleted'; + let compared = 'compared'; + let merge = []; for (let p1 of paragraphs1) { - let p2 = null + let p2 = null; for (let p of paragraphs2) { if (p1.id === p.id) { - p2 = p - break + p2 = p; + break; } } if (p2 === null) { - merge.push({paragraph: p1, firstString: (p1.text || '').split('\n')[0], type: deleted}) + merge.push({paragraph: p1, firstString: (p1.text || '').split('\n')[0], type: deleted}); } else { - let colorClass = '' - let span = null - let text1 = p1.text || '' - let text2 = p2.text || '' - - let diff = window.JsDiff.diffLines(text1, text2) - let diffHtml = document.createDocumentFragment() - let identical = true - let identicalClass = 'color-black' - - diff.forEach(function (part) { - colorClass = part.added ? 'color-green-row' : part.removed ? 'color-red-row' : identicalClass - span = document.createElement('span') - span.className = colorClass + let colorClass = ''; + let span = null; + let text1 = p1.text || ''; + let text2 = p2.text || ''; + + let diff = window.JsDiff.diffLines(text1, text2); + let diffHtml = document.createDocumentFragment(); + let identical = true; + let identicalClass = 'color-black'; + + diff.forEach(function(part) { + colorClass = part.added ? 'color-green-row' : part.removed ? 'color-red-row' : identicalClass; + span = document.createElement('span'); + span.className = colorClass; if (identical && colorClass !== identicalClass) { - identical = false + identical = false; } - let str = part.value + let str = part.value; if (str[str.length - 1] !== '\n') { - str = str + '\n' + str = str + '\n'; } - span.appendChild(document.createTextNode(str)) - diffHtml.appendChild(span) - }) + span.appendChild(document.createTextNode(str)); + diffHtml.appendChild(span); + }); - let pre = document.createElement('pre') - pre.appendChild(diffHtml) + let pre = document.createElement('pre'); + pre.appendChild(diffHtml); merge.push( { @@ -94,88 +94,88 @@ function RevisionsComparatorController($scope, websocketMsgSrv, $routeParams) { diff: pre.innerHTML, identical: identical, firstString: (p1.text || '').split('\n')[0], - type: compared - }) + type: compared, + }); } } for (let p2 of paragraphs2) { - let p1 = null + let p1 = null; for (let p of paragraphs1) { if (p2.id === p.id) { - p1 = p - break + p1 = p; + break; } } if (p1 === null) { - merge.push({paragraph: p2, firstString: (p2.text || '').split('\n')[0], type: added}) + merge.push({paragraph: p2, firstString: (p2.text || '').split('\n')[0], type: added}); } } - merge.sort(function (a, b) { + merge.sort(function(a, b) { if (a.type === added) { - return -1 + return -1; } if (a.type === compared) { - return 1 + return 1; } if (a.type === deleted) { if (b.type === compared) { - return -1 + return -1; } else { - return 1 + return 1; } } - }) + }); - $scope.mergeNoteRevisionsForCompare = merge + $scope.mergeNoteRevisionsForCompare = merge; if ($scope.currentParagraphDiffDisplay !== null) { - $scope.changeCurrentParagraphDiffDisplay($scope.currentParagraphDiffDisplay.paragraph.id) + $scope.changeCurrentParagraphDiffDisplay($scope.currentParagraphDiffDisplay.paragraph.id); } } - } + }; - $scope.$on('noteRevisionForCompare', function (event, data) { - console.debug('received note revision for compare %o', data) + $scope.$on('noteRevisionForCompare', function(event, data) { + console.debug('received note revision for compare %o', data); if (data.note && data.position) { if (data.position === 'first') { - $scope.firstNoteRevisionForCompare = data + $scope.firstNoteRevisionForCompare = data; } else { - $scope.secondNoteRevisionForCompare = data + $scope.secondNoteRevisionForCompare = data; } if ($scope.firstNoteRevisionForCompare !== null && $scope.secondNoteRevisionForCompare !== null && $scope.firstNoteRevisionForCompare.revisionId !== $scope.secondNoteRevisionForCompare.revisionId) { - $scope.compareRevisions() + $scope.compareRevisions(); } } - }) + }); - $scope.formatRevisionDate = function (date) { - return moment.unix(date).format('MMMM Do YYYY, h:mm:ss a') - } + $scope.formatRevisionDate = function(date) { + return moment.unix(date).format('MMMM Do YYYY, h:mm:ss a'); + }; - $scope.changeCurrentParagraphDiffDisplay = function (paragraphId) { + $scope.changeCurrentParagraphDiffDisplay = function(paragraphId) { for (let p of $scope.mergeNoteRevisionsForCompare) { if (p.paragraph.id === paragraphId) { - $scope.currentParagraphDiffDisplay = p - return + $scope.currentParagraphDiffDisplay = p; + return; } } - $scope.currentParagraphDiffDisplay = null - } + $scope.currentParagraphDiffDisplay = null; + }; } export const RevisionsComparatorComponent = { template: revisionsComparatorTemplate, controller: RevisionsComparatorController, bindings: { - noteRevisions: '<' - } -} + noteRevisions: '<', + }, +}; export const RevisionsComparatorModule = angular .module('zeppelinWebApp') .component('revisionsComparator', RevisionsComparatorComponent) - .name + .name; diff --git a/zeppelin-web/src/app/notebook/save-as/browser-detect.service.js b/zeppelin-web/src/app/notebook/save-as/browser-detect.service.js index a31e9afa2da..0c74b452204 100644 --- a/zeppelin-web/src/app/notebook/save-as/browser-detect.service.js +++ b/zeppelin-web/src/app/notebook/save-as/browser-detect.service.js @@ -12,28 +12,28 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').service('browserDetectService', BrowserDetectService) +angular.module('zeppelinWebApp').service('browserDetectService', BrowserDetectService); -function BrowserDetectService () { - this.detectIE = function () { - let ua = window.navigator.userAgent - let msie = ua.indexOf('MSIE ') +function BrowserDetectService() { + this.detectIE = function() { + let ua = window.navigator.userAgent; + let msie = ua.indexOf('MSIE '); if (msie > 0) { // IE 10 or older => return version number - return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10) + return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); } - let trident = ua.indexOf('Trident/') + let trident = ua.indexOf('Trident/'); if (trident > 0) { // IE 11 => return version number - let rv = ua.indexOf('rv:') - return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10) + let rv = ua.indexOf('rv:'); + return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); } - let edge = ua.indexOf('Edge/') + let edge = ua.indexOf('Edge/'); if (edge > 0) { // IE 12 (aka Edge) => return version number - return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10) + return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); } // other browser - return false - } + return false; + }; } diff --git a/zeppelin-web/src/app/notebook/save-as/save-as.service.js b/zeppelin-web/src/app/notebook/save-as/save-as.service.js index ff463c85289..72a54d150a2 100644 --- a/zeppelin-web/src/app/notebook/save-as/save-as.service.js +++ b/zeppelin-web/src/app/notebook/save-as/save-as.service.js @@ -12,45 +12,45 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').service('saveAsService', SaveAsService) +angular.module('zeppelinWebApp').service('saveAsService', SaveAsService); -function SaveAsService (browserDetectService) { - 'ngInject' +function SaveAsService(browserDetectService) { + 'ngInject'; - this.saveAs = function (content, filename, extension) { - let BOM = '\uFEFF' + this.saveAs = function(content, filename, extension) { + let BOM = '\uFEFF'; if (browserDetectService.detectIE()) { - angular.element('body').append('') - let frameSaveAs = angular.element('body > iframe#SaveAsId')[0].contentWindow - content = BOM + content - frameSaveAs.document.open('text/json', 'replace') - frameSaveAs.document.write(content) - frameSaveAs.document.close() - frameSaveAs.focus() - let t1 = Date.now() - frameSaveAs.document.execCommand('SaveAs', false, filename + '.' + extension) - let t2 = Date.now() + angular.element('body').append(''); + let frameSaveAs = angular.element('body > iframe#SaveAsId')[0].contentWindow; + content = BOM + content; + frameSaveAs.document.open('text/json', 'replace'); + frameSaveAs.document.write(content); + frameSaveAs.document.close(); + frameSaveAs.focus(); + let t1 = Date.now(); + frameSaveAs.document.execCommand('SaveAs', false, filename + '.' + extension); + let t2 = Date.now(); // This means, this version of IE dosen't support auto download of a file with extension provided in param // falling back to ".txt" if (t1 === t2) { - frameSaveAs.document.execCommand('SaveAs', true, filename + '.txt') + frameSaveAs.document.execCommand('SaveAs', true, filename + '.txt'); } - angular.element('body > iframe#SaveAsId').remove() + angular.element('body > iframe#SaveAsId').remove(); } else { - let binaryData = [] - binaryData.push(BOM) - binaryData.push(content) - content = window.URL.createObjectURL(new Blob(binaryData)) + let binaryData = []; + binaryData.push(BOM); + binaryData.push(content); + content = window.URL.createObjectURL(new Blob(binaryData)); - angular.element('body').append('') - let saveAsElement = angular.element('body > a#SaveAsId') - saveAsElement.attr('href', content) - saveAsElement.attr('download', filename + '.' + extension) - saveAsElement.attr('target', '_blank') - saveAsElement[0].click() - saveAsElement.remove() - window.URL.revokeObjectURL(content) + angular.element('body').append(''); + let saveAsElement = angular.element('body > a#SaveAsId'); + saveAsElement.attr('href', content); + saveAsElement.attr('download', filename + '.' + extension); + saveAsElement.attr('target', '_blank'); + saveAsElement[0].click(); + saveAsElement.remove(); + window.URL.revokeObjectURL(content); } - } + }; } diff --git a/zeppelin-web/src/app/search/result-list.controller.js b/zeppelin-web/src/app/search/result-list.controller.js index 05be721dee0..65c10b1f7bf 100644 --- a/zeppelin-web/src/app/search/result-list.controller.js +++ b/zeppelin-web/src/app/search/result-list.controller.js @@ -12,107 +12,107 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').controller('SearchResultCtrl', SearchResultCtrl) +angular.module('zeppelinWebApp').controller('SearchResultCtrl', SearchResultCtrl); -function SearchResultCtrl ($scope, $routeParams, searchService) { - 'ngInject' +function SearchResultCtrl($scope, $routeParams, searchService) { + 'ngInject'; - $scope.isResult = true - $scope.searchTerm = $routeParams.searchTerm - let results = searchService.search({'q': $routeParams.searchTerm}).query() + $scope.isResult = true; + $scope.searchTerm = $routeParams.searchTerm; + let results = searchService.search({'q': $routeParams.searchTerm}).query(); - results.$promise.then(function (result) { - $scope.notes = result.body.map(function (note) { + results.$promise.then(function(result) { + $scope.notes = result.body.map(function(note) { // redirect to notebook when search result is a notebook itself, // not a paragraph if (!/\/paragraph\//.test(note.id)) { - return note + return note; } note.id = note.id.replace('paragraph/', '?paragraph=') + - '&term=' + $routeParams.searchTerm + '&term=' + $routeParams.searchTerm; - return note - }) + return note; + }); if ($scope.notes.length === 0) { - $scope.isResult = false + $scope.isResult = false; } else { - $scope.isResult = true + $scope.isResult = true; } - $scope.$on('$routeChangeStart', function (event, next, current) { + $scope.$on('$routeChangeStart', function(event, next, current) { if (next.originalPath !== '/search/:searchTerm') { - searchService.searchTerm = '' + searchService.searchTerm = ''; } - }) - }) + }); + }); - $scope.page = 0 - $scope.allResults = false + $scope.page = 0; + $scope.allResults = false; - $scope.highlightSearchResults = function (note) { - return function (_editor) { - function getEditorMode (text) { + $scope.highlightSearchResults = function(note) { + return function(_editor) { + function getEditorMode(text) { let editorModes = { 'ace/mode/scala': /^%(\w*\.)?spark/, 'ace/mode/python': /^%(\w*\.)?(pyspark|python)/, 'ace/mode/r': /^%(\w*\.)?(r|sparkr|knitr)/, 'ace/mode/sql': /^%(\w*\.)?\wql/, 'ace/mode/markdown': /^%md/, - 'ace/mode/sh': /^%sh/ - } + 'ace/mode/sh': /^%sh/, + }; - return Object.keys(editorModes).reduce(function (res, mode) { - return editorModes[mode].test(text) ? mode : res - }, 'ace/mode/scala') + return Object.keys(editorModes).reduce(function(res, mode) { + return editorModes[mode].test(text) ? mode : res; + }, 'ace/mode/scala'); } - let Range = ace.require('ace/range').Range + let Range = ace.require('ace/range').Range; - _editor.setOption('highlightActiveLine', false) - _editor.$blockScrolling = Infinity - _editor.setReadOnly(true) - _editor.renderer.setShowGutter(false) - _editor.setTheme('ace/theme/chrome') - _editor.getSession().setMode(getEditorMode(note.text)) + _editor.setOption('highlightActiveLine', false); + _editor.$blockScrolling = Infinity; + _editor.setReadOnly(true); + _editor.renderer.setShowGutter(false); + _editor.setTheme('ace/theme/chrome'); + _editor.getSession().setMode(getEditorMode(note.text)); - function getIndeces (term) { - return function (str) { - let indeces = [] - let i = -1 + function getIndeces(term) { + return function(str) { + let indeces = []; + let i = -1; while ((i = str.indexOf(term, i + 1)) >= 0) { - indeces.push(i) + indeces.push(i); } - return indeces - } + return indeces; + }; } - let result = '' + let result = ''; if (note.header !== '') { - result = note.header + '\n\n' + note.snippet + result = note.header + '\n\n' + note.snippet; } else { - result = note.snippet + result = note.snippet; } let lines = result .split('\n') - .map(function (line, row) { - let match = line.match(/(.+?)<\/B>/) + .map(function(line, row) { + let match = line.match(/(.+?)<\/B>/); // return early if nothing to highlight if (!match) { - return line + return line; } - let term = match[1] + let term = match[1]; let __line = line .replace(//g, '') - .replace(/<\/B>/g, '') + .replace(/<\/B>/g, ''); - let indeces = getIndeces(term)(__line) + let indeces = getIndeces(term)(__line); - indeces.forEach(function (start) { - let end = start + term.length + indeces.forEach(function(start) { + let end = start + term.length; if (note.header !== '' && row === 0) { _editor .getSession() @@ -120,14 +120,14 @@ function SearchResultCtrl ($scope, $routeParams, searchService) { new Range(row, 0, row, line.length), 'search-results-highlight-header', 'background' - ) + ); _editor .getSession() .addMarker( new Range(row, start, row, end), 'search-results-highlight', 'line' - ) + ); } else { _editor .getSession() @@ -135,20 +135,22 @@ function SearchResultCtrl ($scope, $routeParams, searchService) { new Range(row, start, row, end), 'search-results-highlight', 'line' - ) + ); } - }) - return __line - }) + }); + return __line; + }); // resize editor based on content length _editor.setOption( 'maxLines', - lines.reduce(function (len, line) { return len + line.length }, 0) - ) - - _editor.getSession().setValue(lines.join('\n')) - note.searchResult = lines - } - } + lines.reduce(function(len, line) { + return len + line.length; + }, 0) + ); + + _editor.getSession().setValue(lines.join('\n')); + note.searchResult = lines; + }; + }; } diff --git a/zeppelin-web/src/app/search/search.service.js b/zeppelin-web/src/app/search/search.service.js index fe4b6664363..248bacd2b98 100644 --- a/zeppelin-web/src/app/search/search.service.js +++ b/zeppelin-web/src/app/search/search.service.js @@ -12,22 +12,22 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').service('searchService', SearchService) +angular.module('zeppelinWebApp').service('searchService', SearchService); -function SearchService ($resource, baseUrlSrv) { - 'ngInject' +function SearchService($resource, baseUrlSrv) { + 'ngInject'; - this.search = function (term) { - this.searchTerm = term.q - console.log('Searching for: %o', term.q) + this.search = function(term) { + this.searchTerm = term.q; + console.log('Searching for: %o', term.q); if (!term.q) { // TODO(bzz): empty string check - return + return; } - let encQuery = window.encodeURIComponent(term.q) + let encQuery = window.encodeURIComponent(term.q); return $resource(baseUrlSrv.getRestApiBase() + '/notebook/search?q=' + encQuery, {}, { - query: {method: 'GET'} - }) - } + query: {method: 'GET'}, + }); + }; - this.searchTerm = '' + this.searchTerm = ''; } diff --git a/zeppelin-web/src/app/spell/index.js b/zeppelin-web/src/app/spell/index.js index ac4343c443e..8ec47532316 100644 --- a/zeppelin-web/src/app/spell/index.js +++ b/zeppelin-web/src/app/spell/index.js @@ -18,8 +18,8 @@ export { DefaultDisplayType, SpellResult, -} from './spell-result' +} from './spell-result'; export { SpellBase, -} from './spell-base' +} from './spell-base'; diff --git a/zeppelin-web/src/app/spell/spell-base.js b/zeppelin-web/src/app/spell/spell-base.js index 0b4216f6214..16e0553edc7 100644 --- a/zeppelin-web/src/app/spell/spell-base.js +++ b/zeppelin-web/src/app/spell/spell-base.js @@ -19,12 +19,12 @@ import { DefaultDisplayType, SpellResult, -} from './spell-result' +} from './spell-result'; /* eslint-enable no-unused-vars */ export class SpellBase { - constructor (magic) { - this.magic = magic + constructor(magic) { + this.magic = magic; } /** @@ -34,8 +34,8 @@ export class SpellBase { * @param config {Object} * @return {SpellResult} */ - interpret (paragraphText, config) { - throw new Error('SpellBase.interpret() should be overrided') + interpret(paragraphText, config) { + throw new Error('SpellBase.interpret() should be overrided'); } /** @@ -43,7 +43,7 @@ export class SpellBase { * (e.g `%flowchart`) * @return {string} */ - getMagic () { - return this.magic + getMagic() { + return this.magic; } } diff --git a/zeppelin-web/src/app/spell/spell-result.js b/zeppelin-web/src/app/spell/spell-result.js index 5ba65c28936..52bcdc1cdab 100644 --- a/zeppelin-web/src/app/spell/spell-result.js +++ b/zeppelin-web/src/app/spell/spell-result.js @@ -21,8 +21,8 @@ export const DefaultDisplayType = { HTML: 'HTML', ANGULAR: 'ANGULAR', TEXT: 'TEXT', - NETWORK: 'NETWORK' -} + NETWORK: 'NETWORK', +}; export const DefaultDisplayMagic = { '%element': DefaultDisplayType.ELEMENT, @@ -31,12 +31,12 @@ export const DefaultDisplayMagic = { '%angular': DefaultDisplayType.ANGULAR, '%text': DefaultDisplayType.TEXT, '%network': DefaultDisplayType.NETWORK, -} +}; export class DataWithType { - constructor (data, type, magic, text) { - this.data = data - this.type = type + constructor(data, type, magic, text) { + this.data = data; + this.type = type; /** * keep for `DefaultDisplayType.ELEMENT` (function data type) @@ -46,29 +46,29 @@ export class DataWithType { * since they don't have context where they are created. */ - this.magic = magic - this.text = text + this.magic = magic; + this.text = text; } - static handleDefaultMagic (m) { + static handleDefaultMagic(m) { // let's use default display type instead of magic in case of default // to keep consistency with backend interpreter if (DefaultDisplayMagic[m]) { - return DefaultDisplayMagic[m] + return DefaultDisplayMagic[m]; } else { - return m + return m; } } - static createPropagable (dataWithType) { + static createPropagable(dataWithType) { if (!SpellResult.isFunction(dataWithType.data)) { - return dataWithType + return dataWithType; } - const data = dataWithType.getText() - const type = dataWithType.getMagic() + const data = dataWithType.getText(); + const type = dataWithType.getMagic(); - return new DataWithType(data, type) + return new DataWithType(data, type); } /** @@ -77,45 +77,45 @@ export class DataWithType { * @param customDisplayType * @return {Array} */ - static parseStringData (data, customDisplayMagic) { - function availableMagic (magic) { - return magic && (DefaultDisplayMagic[magic] || customDisplayMagic[magic]) + static parseStringData(data, customDisplayMagic) { + function availableMagic(magic) { + return magic && (DefaultDisplayMagic[magic] || customDisplayMagic[magic]); } - const splited = data.split('\n') + const splited = data.split('\n'); - const gensWithTypes = [] - let mergedGens = [] - let previousMagic = DefaultDisplayType.TEXT + const gensWithTypes = []; + let mergedGens = []; + let previousMagic = DefaultDisplayType.TEXT; // create `DataWithType` whenever see available display type. for (let i = 0; i < splited.length; i++) { - const g = splited[i] - const magic = SpellResult.extractMagic(g) + const g = splited[i]; + const magic = SpellResult.extractMagic(g); // create `DataWithType` only if see new magic if (availableMagic(magic) && mergedGens.length > 0) { - gensWithTypes.push(new DataWithType(mergedGens.join(''), previousMagic)) - mergedGens = [] + gensWithTypes.push(new DataWithType(mergedGens.join(''), previousMagic)); + mergedGens = []; } // accumulate `data` to mergedGens if (availableMagic(magic)) { - const withoutMagic = g.split(magic)[1] - mergedGens.push(`${withoutMagic}\n`) - previousMagic = DataWithType.handleDefaultMagic(magic) + const withoutMagic = g.split(magic)[1]; + mergedGens.push(`${withoutMagic}\n`); + previousMagic = DataWithType.handleDefaultMagic(magic); } else { - mergedGens.push(`${g}\n`) + mergedGens.push(`${g}\n`); } } // cleanup the last `DataWithType` if (mergedGens.length > 0) { - previousMagic = DataWithType.handleDefaultMagic(previousMagic) - gensWithTypes.push(new DataWithType(mergedGens.join(''), previousMagic)) + previousMagic = DataWithType.handleDefaultMagic(previousMagic); + gensWithTypes.push(new DataWithType(mergedGens.join(''), previousMagic)); } - return gensWithTypes + return gensWithTypes; } /** @@ -128,44 +128,46 @@ export class DataWithType { * @param textWithoutMagic * @return {Promise>} */ - static produceMultipleData (dataWithType, customDisplayType, + static produceMultipleData(dataWithType, customDisplayType, magic, textWithoutMagic) { - const data = dataWithType.getData() - const type = dataWithType.getType() + const data = dataWithType.getData(); + const type = dataWithType.getType(); // if the type is specified, just return it // handle non-specified dataWithTypes only if (type) { - return new Promise((resolve) => { resolve([dataWithType]) }) + return new Promise((resolve) => { + resolve([dataWithType]); + }); } - let wrapped + let wrapped; if (SpellResult.isFunction(data)) { // if data is a function, we consider it as ELEMENT type. wrapped = new Promise((resolve) => { const dt = new DataWithType( - data, DefaultDisplayType.ELEMENT, magic, textWithoutMagic) - const result = [dt] - return resolve(result) - }) + data, DefaultDisplayType.ELEMENT, magic, textWithoutMagic); + const result = [dt]; + return resolve(result); + }); } else if (SpellResult.isPromise(data)) { // if data is a promise, - wrapped = data.then(generated => { + wrapped = data.then((generated) => { const result = - DataWithType.parseStringData(generated, customDisplayType) - return result - }) + DataWithType.parseStringData(generated, customDisplayType); + return result; + }); } else { // if data is a object, parse it to multiples wrapped = new Promise((resolve) => { const result = - DataWithType.parseStringData(data, customDisplayType) - return resolve(result) - }) + DataWithType.parseStringData(data, customDisplayType); + return resolve(result); + }); } - return wrapped + return wrapped; } /** @@ -177,8 +179,8 @@ export class DataWithType { * will be called in `then()` of this promise. * @returns {*} `data` which can be object, function or promise. */ - getData () { - return this.data + getData() { + return this.data; } /** @@ -187,66 +189,66 @@ export class DataWithType { * by `SpellResult.parseStringData()` * @returns {string} */ - getType () { - return this.type + getType() { + return this.type; } - getMagic () { - return this.magic + getMagic() { + return this.magic; } - getText () { - return this.text + getText() { + return this.text; } } export class SpellResult { - constructor (resultData, resultType) { - this.dataWithTypes = [] - this.add(resultData, resultType) + constructor(resultData, resultType) { + this.dataWithTypes = []; + this.add(resultData, resultType); } - static isFunction (data) { - return (data && typeof data === 'function') + static isFunction(data) { + return (data && typeof data === 'function'); } - static isPromise (data) { - return (data && typeof data.then === 'function') + static isPromise(data) { + return (data && typeof data.then === 'function'); } - static isObject (data) { + static isObject(data) { return (data && !SpellResult.isFunction(data) && - !SpellResult.isPromise(data)) + !SpellResult.isPromise(data)); } - static extractMagic (allParagraphText) { - const pattern = /^\s*%(\S+)\s*/g + static extractMagic(allParagraphText) { + const pattern = /^\s*%(\S+)\s*/g; try { - let match = pattern.exec(allParagraphText) + let match = pattern.exec(allParagraphText); if (match) { - return `%${match[1].trim()}` + return `%${match[1].trim()}`; } } catch (error) { // failed to parse, ignore } - return undefined + return undefined; } - static createPropagable (resultMsg) { - return resultMsg.map(dt => { - return DataWithType.createPropagable(dt) - }) + static createPropagable(resultMsg) { + return resultMsg.map((dt) => { + return DataWithType.createPropagable(dt); + }); } - add (resultData, resultType) { + add(resultData, resultType) { if (resultData) { this.dataWithTypes.push( - new DataWithType(resultData, resultType)) + new DataWithType(resultData, resultType)); } - return this + return this; } /** @@ -254,23 +256,23 @@ export class SpellResult { * @param textWithoutMagic * @return {Promise>} */ - getAllParsedDataWithTypes (customDisplayType, magic, textWithoutMagic) { - const promises = this.dataWithTypes.map(dt => { + getAllParsedDataWithTypes(customDisplayType, magic, textWithoutMagic) { + const promises = this.dataWithTypes.map((dt) => { return DataWithType.produceMultipleData( - dt, customDisplayType, magic, textWithoutMagic) - }) + dt, customDisplayType, magic, textWithoutMagic); + }); // some promises can include an array so we need to flatten them - const flatten = Promise.all(promises).then(values => { + const flatten = Promise.all(promises).then((values) => { return values.reduce((acc, cur) => { if (Array.isArray(cur)) { - return acc.concat(cur) + return acc.concat(cur); } else { - return acc.concat([cur]) + return acc.concat([cur]); } - }) - }) + }); + }); - return flatten + return flatten; } } diff --git a/zeppelin-web/src/app/tabledata/advanced-transformation-util.js b/zeppelin-web/src/app/tabledata/advanced-transformation-util.js index 0d1c2f657e0..97c1b2c1561 100644 --- a/zeppelin-web/src/app/tabledata/advanced-transformation-util.js +++ b/zeppelin-web/src/app/tabledata/advanced-transformation-util.js @@ -13,40 +13,40 @@ */ export function getCurrentChart(config) { - return config.chart.current + return config.chart.current; } export function getCurrentChartTransform(config) { - return config.spec.charts[getCurrentChart(config)].transform + return config.spec.charts[getCurrentChart(config)].transform; } export function getCurrentChartAxis(config) { - return config.axis[getCurrentChart(config)] + return config.axis[getCurrentChart(config)]; } export function getCurrentChartParam(config) { - return config.parameter[getCurrentChart(config)] + return config.parameter[getCurrentChart(config)]; } export function getCurrentChartAxisSpecs(config) { - return config.axisSpecs[getCurrentChart(config)] + return config.axisSpecs[getCurrentChart(config)]; } export function getCurrentChartParamSpecs(config) { - return config.paramSpecs[getCurrentChart(config)] + return config.paramSpecs[getCurrentChart(config)]; } export function useSharedAxis(config, chart) { - return config.spec.charts[chart].sharedAxis + return config.spec.charts[chart].sharedAxis; } export function serializeSharedAxes(config) { - const availableCharts = getAvailableChartNames(config.spec.charts) + const availableCharts = getAvailableChartNames(config.spec.charts); for (let i = 0; i < availableCharts.length; i++) { - const chartName = availableCharts[i] + const chartName = availableCharts[i]; if (useSharedAxis(config, chartName)) { /** use reference :) in case of sharedAxis */ - config.axis[chartName] = config.sharedAxis + config.axis[chartName] = config.sharedAxis; } } } @@ -56,22 +56,22 @@ export const Widget = { OPTION: 'option', CHECKBOX: 'checkbox', TEXTAREA: 'textarea', -} +}; export function isInputWidget(paramSpec) { - return (paramSpec && !paramSpec.widget) || (paramSpec && paramSpec.widget === Widget.INPUT) + return (paramSpec && !paramSpec.widget) || (paramSpec && paramSpec.widget === Widget.INPUT); } export function isOptionWidget(paramSpec) { - return paramSpec && paramSpec.widget === Widget.OPTION + return paramSpec && paramSpec.widget === Widget.OPTION; } export function isCheckboxWidget(paramSpec) { - return paramSpec && paramSpec.widget === Widget.CHECKBOX + return paramSpec && paramSpec.widget === Widget.CHECKBOX; } export function isTextareaWidget(paramSpec) { - return paramSpec && paramSpec.widget === Widget.TEXTAREA + return paramSpec && paramSpec.widget === Widget.TEXTAREA; } export const ParameterValueType = { @@ -80,59 +80,71 @@ export const ParameterValueType = { BOOLEAN: 'boolean', STRING: 'string', JSON: 'JSON', -} +}; export function parseParameter(paramSpecs, param) { /** copy original params */ - const parsed = JSON.parse(JSON.stringify(param)) + const parsed = JSON.parse(JSON.stringify(param)); for (let i = 0; i < paramSpecs.length; i++) { - const paramSpec = paramSpecs[i] - const name = paramSpec.name + const paramSpec = paramSpecs[i]; + const name = paramSpec.name; if (paramSpec.valueType === ParameterValueType.INT && typeof parsed[name] !== 'number') { - try { parsed[name] = parseInt(parsed[name]) } catch (error) { parsed[name] = paramSpec.defaultValue } + try { + parsed[name] = parseInt(parsed[name]); + } catch (error) { + parsed[name] = paramSpec.defaultValue; + } } else if (paramSpec.valueType === ParameterValueType.FLOAT && typeof parsed[name] !== 'number') { - try { parsed[name] = parseFloat(parsed[name]) } catch (error) { parsed[name] = paramSpec.defaultValue } + try { + parsed[name] = parseFloat(parsed[name]); + } catch (error) { + parsed[name] = paramSpec.defaultValue; + } } else if (paramSpec.valueType === ParameterValueType.BOOLEAN) { if (parsed[name] === 'false') { - parsed[name] = false + parsed[name] = false; } else if (parsed[name] === 'true') { - parsed[name] = true + parsed[name] = true; } else if (typeof parsed[name] !== 'boolean') { - parsed[name] = paramSpec.defaultValue + parsed[name] = paramSpec.defaultValue; } } else if (paramSpec.valueType === ParameterValueType.JSON) { if (parsed[name] !== null && typeof parsed[name] !== 'object') { - try { parsed[name] = JSON.parse(parsed[name]) } catch (error) { parsed[name] = paramSpec.defaultValue } + try { + parsed[name] = JSON.parse(parsed[name]); + } catch (error) { + parsed[name] = paramSpec.defaultValue; + } } else if (parsed[name] === null) { - parsed[name] = paramSpec.defaultValue + parsed[name] = paramSpec.defaultValue; } } } - return parsed + return parsed; } export const AxisType = { AGGREGATOR: 'aggregator', KEY: 'key', GROUP: 'group', -} +}; export function isAggregatorAxis(axisSpec) { - return axisSpec && axisSpec.axisType === AxisType.AGGREGATOR + return axisSpec && axisSpec.axisType === AxisType.AGGREGATOR; } export function isGroupAxis(axisSpec) { - return axisSpec && axisSpec.axisType === AxisType.GROUP + return axisSpec && axisSpec.axisType === AxisType.GROUP; } export function isKeyAxis(axisSpec) { - return axisSpec && axisSpec.axisType === AxisType.KEY + return axisSpec && axisSpec.axisType === AxisType.KEY; } export function isSingleDimensionAxis(axisSpec) { - return axisSpec && axisSpec.dimension === 'single' + return axisSpec && axisSpec.dimension === 'single'; } /** @@ -142,92 +154,112 @@ export function isSingleDimensionAxis(axisSpec) { * add the `name` field while converting to array to easily manipulate */ export function getSpecs(specObject) { - const specs = [] + const specs = []; for (let name in specObject) { - const singleSpec = specObject[name] - if (!singleSpec) { continue } - singleSpec.name = name - specs.push(singleSpec) + if (specObject.hasOwnProperty(name)) { + const singleSpec = specObject[name]; + if (!singleSpec) { + continue; + } + singleSpec.name = name; + specs.push(singleSpec); + } } - return specs + return specs; } export function getAvailableChartNames(charts) { - const available = [] + const available = []; for (let name in charts) { - available.push(name) + if (charts.hasOwnProperty(name)) { + available.push(name); + } } - return available + return available; } export function applyMaxAxisCount(config, axisSpec) { if (isSingleDimensionAxis(axisSpec) || typeof axisSpec.maxAxisCount === 'undefined') { - return + return; } - const columns = getCurrentChartAxis(config)[axisSpec.name] - if (columns.length <= axisSpec.maxAxisCount) { return } + const columns = getCurrentChartAxis(config)[axisSpec.name]; + if (columns.length <= axisSpec.maxAxisCount) { + return; + } - const sliced = columns.slice(1) - getCurrentChartAxis(config)[axisSpec.name] = sliced + const sliced = columns.slice(1); + getCurrentChartAxis(config)[axisSpec.name] = sliced; } export function removeDuplicatedColumnsInMultiDimensionAxis(config, axisSpec) { - if (isSingleDimensionAxis(axisSpec)) { return config } + if (isSingleDimensionAxis(axisSpec)) { + return config; + } - const columns = getCurrentChartAxis(config)[axisSpec.name] + const columns = getCurrentChartAxis(config)[axisSpec.name]; const uniqObject = columns.reduce((acc, col) => { - if (!acc[`${col.name}(${col.aggr})`]) { acc[`${col.name}(${col.aggr})`] = col } - return acc - }, {}) + if (!acc[`${col.name}(${col.aggr})`]) { + acc[`${col.name}(${col.aggr})`] = col; + } + return acc; + }, {}); - const filtered = [] + const filtered = []; for (let name in uniqObject) { - const col = uniqObject[name] - filtered.push(col) + if (uniqObject.hasOwnProperty(name)) { + const col = uniqObject[name]; + filtered.push(col); + } } - getCurrentChartAxis(config)[axisSpec.name] = filtered - return config + getCurrentChartAxis(config)[axisSpec.name] = filtered; + return config; } export function clearAxisConfig(config) { - delete config.axis /** Object: persisted axis for each chart */ - delete config.sharedAxis + delete config.axis; /** Object: persisted axis for each chart */ + delete config.sharedAxis; } export function initAxisConfig(config) { - if (!config.axis) { config.axis = {} } - if (!config.sharedAxis) { config.sharedAxis = {} } + if (!config.axis) { + config.axis = {}; + } + if (!config.sharedAxis) { + config.sharedAxis = {}; + } - const spec = config.spec - const availableCharts = getAvailableChartNames(spec.charts) + const spec = config.spec; + const availableCharts = getAvailableChartNames(spec.charts); - if (!config.axisSpecs) { config.axisSpecs = {} } + if (!config.axisSpecs) { + config.axisSpecs = {}; + } for (let i = 0; i < availableCharts.length; i++) { - const chartName = availableCharts[i] + const chartName = availableCharts[i]; if (!config.axis[chartName]) { - config.axis[chartName] = {} + config.axis[chartName] = {}; } - const axisSpecs = getSpecs(spec.charts[chartName].axis) + const axisSpecs = getSpecs(spec.charts[chartName].axis); if (!config.axisSpecs[chartName]) { - config.axisSpecs[chartName] = axisSpecs + config.axisSpecs[chartName] = axisSpecs; } /** initialize multi-dimension axes */ for (let i = 0; i < axisSpecs.length; i++) { - const axisSpec = axisSpecs[i] + const axisSpec = axisSpecs[i]; if (isSingleDimensionAxis(axisSpec)) { - continue + continue; } /** intentionally nested if-stmt is used because order of conditions matter here */ if (!useSharedAxis(config, chartName)) { if (!Array.isArray(config.axis[chartName][axisSpec.name])) { - config.axis[chartName][axisSpec.name] = [] + config.axis[chartName][axisSpec.name] = []; } } else if (useSharedAxis(config, chartName)) { /** @@ -235,180 +267,200 @@ export function initAxisConfig(config) { * all charts using shared axis have the same axis specs */ if (!Array.isArray(config.sharedAxis[axisSpec.name])) { - config.sharedAxis[axisSpec.name] = [] + config.sharedAxis[axisSpec.name] = []; } } } } /** this function should be called after initializing */ - serializeSharedAxes(config) + serializeSharedAxes(config); } export function resetAxisConfig(config) { - clearAxisConfig(config) - initAxisConfig(config) + clearAxisConfig(config); + initAxisConfig(config); } export function clearParameterConfig(config) { - delete config.parameter /** Object: persisted parameter for each chart */ + delete config.parameter; /** Object: persisted parameter for each chart */ } export function initParameterConfig(config) { - if (!config.parameter) { config.parameter = {} } + if (!config.parameter) { + config.parameter = {}; + } - const spec = config.spec - const availableCharts = getAvailableChartNames(spec.charts) + const spec = config.spec; + const availableCharts = getAvailableChartNames(spec.charts); - if (!config.paramSpecs) { config.paramSpecs = {} } + if (!config.paramSpecs) { + config.paramSpecs = {}; + } for (let i = 0; i < availableCharts.length; i++) { - const chartName = availableCharts[i] + const chartName = availableCharts[i]; - if (!config.parameter[chartName]) { config.parameter[chartName] = {} } - const paramSpecs = getSpecs(spec.charts[chartName].parameter) - if (!config.paramSpecs[chartName]) { config.paramSpecs[chartName] = paramSpecs } + if (!config.parameter[chartName]) { + config.parameter[chartName] = {}; + } + const paramSpecs = getSpecs(spec.charts[chartName].parameter); + if (!config.paramSpecs[chartName]) { + config.paramSpecs[chartName] = paramSpecs; + } for (let i = 0; i < paramSpecs.length; i++) { - const paramSpec = paramSpecs[i] + const paramSpec = paramSpecs[i]; if (!config.parameter[chartName][paramSpec.name]) { - config.parameter[chartName][paramSpec.name] = paramSpec.defaultValue + config.parameter[chartName][paramSpec.name] = paramSpec.defaultValue; } } } } export function resetParameterConfig(config) { - clearParameterConfig(config) - initParameterConfig(config) + clearParameterConfig(config); + initParameterConfig(config); } export function getSpecVersion(availableCharts, spec) { - const axisHash = {} - const paramHash = {} + const axisHash = {}; + const paramHash = {}; for (let i = 0; i < availableCharts.length; i++) { - const chartName = availableCharts[i] - const axisSpecs = getSpecs(spec.charts[chartName].axis) - axisHash[chartName] = axisSpecs + const chartName = availableCharts[i]; + const axisSpecs = getSpecs(spec.charts[chartName].axis); + axisHash[chartName] = axisSpecs; - const paramSpecs = getSpecs(spec.charts[chartName].parameter) - paramHash[chartName] = paramSpecs + const paramSpecs = getSpecs(spec.charts[chartName].parameter); + paramHash[chartName] = paramSpecs; } - return { axisVersion: JSON.stringify(axisHash), paramVersion: JSON.stringify(paramHash), } + return {axisVersion: JSON.stringify(axisHash), paramVersion: JSON.stringify(paramHash)}; } export function initializeConfig(config, spec) { - config.chartChanged = true - config.parameterChanged = false + config.chartChanged = true; + config.parameterChanged = false; - let updated = false + let updated = false; - const availableCharts = getAvailableChartNames(spec.charts) - const { axisVersion, paramVersion, } = getSpecVersion(availableCharts, spec) + const availableCharts = getAvailableChartNames(spec.charts); + const {axisVersion, paramVersion} = getSpecVersion(availableCharts, spec); if (!config.spec || !config.spec.version || !config.spec.version.axis || config.spec.version.axis !== axisVersion) { - spec.initialized = true - updated = true + spec.initialized = true; + updated = true; - delete config.chart /** Object: contains current, available chart */ - config.panel = { columnPanelOpened: true, parameterPanelOpened: false, } + delete config.chart; /** Object: contains current, available chart */ + config.panel = {columnPanelOpened: true, parameterPanelOpened: false}; - clearAxisConfig(config) - delete config.axisSpecs /** Object: persisted axisSpecs for each chart */ + clearAxisConfig(config); + delete config.axisSpecs; /** Object: persisted axisSpecs for each chart */ } if (!config.spec || !config.spec.version || !config.spec.version.parameter || config.spec.version.parameter !== paramVersion) { - updated = true + updated = true; - clearParameterConfig(config) - delete config.paramSpecs /** Object: persisted paramSpecs for each chart */ + clearParameterConfig(config); + delete config.paramSpecs; /** Object: persisted paramSpecs for each chart */ } - if (!spec.version) { spec.version = {} } - spec.version.axis = axisVersion - spec.version.parameter = paramVersion + if (!spec.version) { + spec.version = {}; + } + spec.version.axis = axisVersion; + spec.version.parameter = paramVersion; - if (!config.spec || updated) { config.spec = spec } + if (!config.spec || updated) { + config.spec = spec; + } if (!config.chart) { - config.chart = {} - config.chart.current = availableCharts[0] - config.chart.available = availableCharts + config.chart = {}; + config.chart.current = availableCharts[0]; + config.chart.available = availableCharts; } /** initialize config.axis, config.axisSpecs for each chart */ - initAxisConfig(config) + initAxisConfig(config); /** initialize config.parameter for each chart */ - initParameterConfig(config) - return config + initParameterConfig(config); + return config; } export function getColumnsForMultipleAxes(axisType, axisSpecs, axis) { - const axisNames = [] - let column = {} + const axisNames = []; + let column = {}; for (let i = 0; i < axisSpecs.length; i++) { - const axisSpec = axisSpecs[i] + const axisSpec = axisSpecs[i]; if (axisType === AxisType.KEY && isKeyAxis(axisSpec)) { - axisNames.push(axisSpec.name) + axisNames.push(axisSpec.name); } else if (axisType === AxisType.GROUP && isGroupAxis(axisSpec)) { - axisNames.push(axisSpec.name) + axisNames.push(axisSpec.name); } else if (axisType.AGGREGATOR && isAggregatorAxis(axisSpec)) { - axisNames.push(axisSpec.name) + axisNames.push(axisSpec.name); } } for (let axisName of axisNames) { - const columns = axis[axisName] - if (typeof axis[axisName] === 'undefined') { continue } - if (!column[axisName]) { column[axisName] = [] } - column[axisName] = column[axisName].concat(columns) + const columns = axis[axisName]; + if (typeof axis[axisName] === 'undefined') { + continue; + } + if (!column[axisName]) { + column[axisName] = []; + } + column[axisName] = column[axisName].concat(columns); } - return column + return column; } export function getColumnsFromAxis(axisSpecs, axis) { - const keyAxisNames = [] - const groupAxisNames = [] - const aggrAxisNames = [] + const keyAxisNames = []; + const groupAxisNames = []; + const aggrAxisNames = []; for (let i = 0; i < axisSpecs.length; i++) { - const axisSpec = axisSpecs[i] + const axisSpec = axisSpecs[i]; if (isKeyAxis(axisSpec)) { - keyAxisNames.push(axisSpec.name) + keyAxisNames.push(axisSpec.name); } else if (isGroupAxis(axisSpec)) { - groupAxisNames.push(axisSpec.name) + groupAxisNames.push(axisSpec.name); } else if (isAggregatorAxis(axisSpec)) { - aggrAxisNames.push(axisSpec.name) + aggrAxisNames.push(axisSpec.name); } } - let keyColumns = [] - let groupColumns = [] - let aggregatorColumns = [] - let customColumn = {} + let keyColumns = []; + let groupColumns = []; + let aggregatorColumns = []; + let customColumn = {}; for (let axisName in axis) { - const columns = axis[axisName] - if (keyAxisNames.includes(axisName)) { - keyColumns = keyColumns.concat(columns) - } else if (groupAxisNames.includes(axisName)) { - groupColumns = groupColumns.concat(columns) - } else if (aggrAxisNames.includes(axisName)) { - aggregatorColumns = aggregatorColumns.concat(columns) - } else { - const axisType = axisSpecs.filter(s => s.name === axisName)[0].axisType - if (!customColumn[axisType]) { customColumn[axisType] = [] } - customColumn[axisType] = customColumn[axisType].concat(columns) + if (axis.hasOwnProperty(axisName)) { + const columns = axis[axisName]; + if (keyAxisNames.includes(axisName)) { + keyColumns = keyColumns.concat(columns); + } else if (groupAxisNames.includes(axisName)) { + groupColumns = groupColumns.concat(columns); + } else if (aggrAxisNames.includes(axisName)) { + aggregatorColumns = aggregatorColumns.concat(columns); + } else { + const axisType = axisSpecs.filter((s) => s.name === axisName)[0].axisType; + if (!customColumn[axisType]) { + customColumn[axisType] = []; + } + customColumn[axisType] = customColumn[axisType].concat(columns); + } } } @@ -417,7 +469,7 @@ export function getColumnsFromAxis(axisSpecs, axis) { group: groupColumns, aggregator: aggregatorColumns, custom: customColumn, - } + }; } export const Aggregator = { @@ -426,7 +478,7 @@ export const Aggregator = { AVG: 'avg', MIN: 'min', MAX: 'max', -} +}; const TransformMethod = { /** @@ -449,38 +501,42 @@ const TransformMethod = { ARRAY: 'array', ARRAY_2_KEY: 'array:2-key', DRILL_DOWN: 'drill-down', -} +}; /** return function for lazy computation */ export function getTransformer(conf, rows, axisSpecs, axis) { - let transformer = () => {} + let transformer = () => {}; - const transformSpec = getCurrentChartTransform(conf) - if (!transformSpec) { return transformer } + const transformSpec = getCurrentChartTransform(conf); + if (!transformSpec) { + return transformer; + } - const method = transformSpec.method + const method = transformSpec.method; - const columns = getColumnsFromAxis(axisSpecs, axis) - const keyColumns = columns.key - const groupColumns = columns.group - const aggregatorColumns = columns.aggregator - const customColumns = columns.custom + const columns = getColumnsFromAxis(axisSpecs, axis); + const keyColumns = columns.key; + const groupColumns = columns.group; + const aggregatorColumns = columns.aggregator; + const customColumns = columns.custom; let column = { key: keyColumns, group: groupColumns, aggregator: aggregatorColumns, custom: customColumns, - } + }; if (method === TransformMethod.RAW) { - transformer = () => { return rows } + transformer = () => { + return rows; + }; } else if (method === TransformMethod.OBJECT) { transformer = () => { - const { cube, schema, keyColumnName, keyNames, groupNameSet, selectorNameWithIndex, } = - getKGACube(rows, keyColumns, groupColumns, aggregatorColumns) + const {cube, schema, keyColumnName, keyNames, groupNameSet, selectorNameWithIndex} = + getKGACube(rows, keyColumns, groupColumns, aggregatorColumns); const { - transformed, groupNames, sortedSelectors + transformed, groupNames, sortedSelectors, } = getObjectRowsFromKGACube(cube, schema, aggregatorColumns, - keyColumnName, keyNames, groupNameSet, selectorNameWithIndex) + keyColumnName, keyNames, groupNameSet, selectorNameWithIndex); return { rows: transformed, @@ -488,17 +544,17 @@ export function getTransformer(conf, rows, axisSpecs, axis) { keyNames, groupNames: groupNames, selectors: sortedSelectors, - } - } + }; + }; } else if (method === TransformMethod.ARRAY) { transformer = () => { - const { cube, schema, keyColumnName, keyNames, groupNameSet, selectorNameWithIndex, } = - getKGACube(rows, keyColumns, groupColumns, aggregatorColumns) + const {cube, schema, keyColumnName, keyNames, groupNameSet, selectorNameWithIndex} = + getKGACube(rows, keyColumns, groupColumns, aggregatorColumns); const { transformed, groupNames, sortedSelectors, } = getArrayRowsFromKGACube(cube, schema, aggregatorColumns, - keyColumnName, keyNames, groupNameSet, selectorNameWithIndex) + keyColumnName, keyNames, groupNameSet, selectorNameWithIndex); return { rows: transformed, @@ -506,34 +562,40 @@ export function getTransformer(conf, rows, axisSpecs, axis) { keyNames, groupNames: groupNames, selectors: sortedSelectors, - } - } + }; + }; } else if (method === TransformMethod.ARRAY_2_KEY) { - const keyAxisColumn = getColumnsForMultipleAxes(AxisType.KEY, axisSpecs, axis) - column.key = keyAxisColumn + const keyAxisColumn = getColumnsForMultipleAxes(AxisType.KEY, axisSpecs, axis); + column.key = keyAxisColumn; - let key1Columns = [] - let key2Columns = [] + let key1Columns = []; + let key2Columns = []; // since ARRAY_2_KEY :) - let i = 0 + let i = 0; for (let axisName in keyAxisColumn) { - if (i === 2) { break } + if (i === 2) { + break; + } - if (i === 0) { key1Columns = keyAxisColumn[axisName] } else if (i === 1) { key2Columns = keyAxisColumn[axisName] } - i++ + if (i === 0) { + key1Columns = keyAxisColumn[axisName]; + } else if (i === 1) { + key2Columns = keyAxisColumn[axisName]; + } + i++; } - const { cube, schema, + const {cube, schema, key1ColumnName, key1Names, key2ColumnName, key2Names, groupNameSet, selectorNameWithIndex, - } = getKKGACube(rows, key1Columns, key2Columns, groupColumns, aggregatorColumns) + } = getKKGACube(rows, key1Columns, key2Columns, groupColumns, aggregatorColumns); const { transformed, groupNames, sortedSelectors, key1NameWithIndex, key2NameWithIndex, } = getArrayRowsFromKKGACube(cube, schema, aggregatorColumns, - key1Names, key2Names, groupNameSet, selectorNameWithIndex) + key1Names, key2Names, groupNameSet, selectorNameWithIndex); transformer = () => { return { @@ -546,17 +608,17 @@ export function getTransformer(conf, rows, axisSpecs, axis) { key2NameWithIndex: key2NameWithIndex, groupNames: groupNames, selectors: sortedSelectors, - } - } + }; + }; } else if (method === TransformMethod.DRILL_DOWN) { transformer = () => { - const { cube, schema, keyColumnName, keyNames, groupNameSet, selectorNameWithIndex, } = - getKAGCube(rows, keyColumns, groupColumns, aggregatorColumns) + const {cube, schema, keyColumnName, keyNames, groupNameSet, selectorNameWithIndex} = + getKAGCube(rows, keyColumns, groupColumns, aggregatorColumns); const { transformed, groupNames, sortedSelectors, } = getDrilldownRowsFromKAGCube(cube, schema, aggregatorColumns, - keyColumnName, keyNames, groupNameSet, selectorNameWithIndex) + keyColumnName, keyNames, groupNameSet, selectorNameWithIndex); return { rows: transformed, @@ -564,48 +626,48 @@ export function getTransformer(conf, rows, axisSpecs, axis) { keyNames, groupNames: groupNames, selectors: sortedSelectors, - } - } + }; + }; } - return { transformer: transformer, column: column, } + return {transformer: transformer, column: column}; } const AggregatorFunctions = { sum: function(a, b) { - const varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0 - const varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0 - return varA + varB + const varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0; + const varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0; + return varA + varB; }, count: function(a, b) { - const varA = (a !== undefined) ? parseInt(a) : 0 - const varB = (b !== undefined) ? 1 : 0 - return varA + varB + const varA = (a !== undefined) ? parseInt(a) : 0; + const varB = (b !== undefined) ? 1 : 0; + return varA + varB; }, min: function(a, b) { - const varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0 - const varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0 - return Math.min(varA, varB) + const varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0; + const varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0; + return Math.min(varA, varB); }, max: function(a, b) { - const varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0 - const varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0 - return Math.max(varA, varB) + const varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0; + const varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0; + return Math.max(varA, varB); }, avg: function(a, b, c) { - const varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0 - const varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0 - return varA + varB - } -} + const varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0; + const varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0; + return varA + varB; + }, +}; const AggregatorFunctionDiv = { sum: false, min: false, max: false, count: false, - avg: true -} + avg: true, +}; /** nested cube `(key) -> (group) -> aggregator` */ export function getKGACube(rows, keyColumns, groupColumns, aggrColumns) { @@ -613,67 +675,75 @@ export function getKGACube(rows, keyColumns, groupColumns, aggrColumns) { key: keyColumns.length !== 0, group: groupColumns.length !== 0, aggregator: aggrColumns.length !== 0, - } + }; - let cube = {} - const entry = {} + let cube = {}; + const entry = {}; - const keyColumnName = keyColumns.map(c => c.name).join('.') - const groupNameSet = new Set() - const keyNameSet = new Set() - const selectorNameWithIndex = {} /** { selectorName: index } */ - let indexCounter = 0 + const keyColumnName = keyColumns.map((c) => c.name).join('.'); + const groupNameSet = new Set(); + const keyNameSet = new Set(); + const selectorNameWithIndex = {}; /** { selectorName: index } */ + let indexCounter = 0; for (let i = 0; i < rows.length; i++) { - const row = rows[i] - let e = entry - let c = cube + const row = rows[i]; + let e = entry; + let c = cube; // key: add to entry - let mergedKeyName + let mergedKeyName; if (schema.key) { - mergedKeyName = keyColumns.map(c => row[c.index]).join('.') - if (!e[mergedKeyName]) { e[mergedKeyName] = { children: {}, } } - e = e[mergedKeyName].children + mergedKeyName = keyColumns.map((c) => row[c.index]).join('.'); + if (!e[mergedKeyName]) { + e[mergedKeyName] = {children: {}}; + } + e = e[mergedKeyName].children; // key: add to row - if (!c[mergedKeyName]) { c[mergedKeyName] = {} } - c = c[mergedKeyName] + if (!c[mergedKeyName]) { + c[mergedKeyName] = {}; + } + c = c[mergedKeyName]; - keyNameSet.add(mergedKeyName) + keyNameSet.add(mergedKeyName); } - let mergedGroupName + let mergedGroupName; if (schema.group) { - mergedGroupName = groupColumns.map(c => row[c.index]).join('.') + mergedGroupName = groupColumns.map((c) => row[c.index]).join('.'); // add group to entry - if (!e[mergedGroupName]) { e[mergedGroupName] = { children: {}, } } - e = e[mergedGroupName].children + if (!e[mergedGroupName]) { + e[mergedGroupName] = {children: {}}; + } + e = e[mergedGroupName].children; // add group to row - if (!c[mergedGroupName]) { c[mergedGroupName] = {} } - c = c[mergedGroupName] - groupNameSet.add(mergedGroupName) + if (!c[mergedGroupName]) { + c[mergedGroupName] = {}; + } + c = c[mergedGroupName]; + groupNameSet.add(mergedGroupName); } for (let a = 0; a < aggrColumns.length; a++) { - const aggrColumn = aggrColumns[a] - const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})` + const aggrColumn = aggrColumns[a]; + const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`; // update groupNameSet if (!mergedGroupName) { - groupNameSet.add(aggrName) /** aggr column name will be used as group name if group is empty */ + groupNameSet.add(aggrName); /** aggr column name will be used as group name if group is empty */ } // update selectorNameWithIndex - const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName) + const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName); if (typeof selectorNameWithIndex[selector] === 'undefined' /** value might be 0 */) { - selectorNameWithIndex[selector] = indexCounter - indexCounter = indexCounter + 1 + selectorNameWithIndex[selector] = indexCounter; + indexCounter = indexCounter + 1; } // add aggregator to entry if (!e[aggrName]) { - e[aggrName] = { type: 'aggregator', order: aggrColumn, index: aggrColumn.index, } + e[aggrName] = {type: 'aggregator', order: aggrColumn, index: aggrColumn.index}; } // add aggregatorName to row @@ -682,26 +752,26 @@ export function getKGACube(rows, keyColumns, groupColumns, aggrColumns) { aggr: aggrColumn.aggr, value: (aggrColumn.aggr !== 'count') ? row[aggrColumn.index] : 1, count: 1, - } + }; } else { const value = AggregatorFunctions[aggrColumn.aggr]( - c[aggrName].value, row[aggrColumn.index], c[aggrName].count + 1) + c[aggrName].value, row[aggrColumn.index], c[aggrName].count + 1); const count = (AggregatorFunctionDiv[aggrColumn.aggr]) - ? c[aggrName].count + 1 : c[aggrName].count + ? c[aggrName].count + 1 : c[aggrName].count; - c[aggrName].value = value - c[aggrName].count = count + c[aggrName].value = value; + c[aggrName].count = count; } } /** end loop for aggrColumns */ } - let keyNames = null + let keyNames = null; if (!schema.key) { - const mergedGroupColumnName = groupColumns.map(c => c.name).join('.') - cube = { [mergedGroupColumnName]: cube, } - keyNames = [ mergedGroupColumnName, ] + const mergedGroupColumnName = groupColumns.map((c) => c.name).join('.'); + cube = {[mergedGroupColumnName]: cube}; + keyNames = [mergedGroupColumnName]; } else { - keyNames = Object.keys(cube).sort() /** keys should be sorted */ + keyNames = Object.keys(cube).sort(); /** keys should be sorted */ } return { @@ -711,7 +781,7 @@ export function getKGACube(rows, keyColumns, groupColumns, aggrColumns) { keyNames: keyNames, groupNameSet: groupNameSet, selectorNameWithIndex: selectorNameWithIndex, - } + }; } /** nested cube `(key) -> aggregator -> (group)` for drill-down support */ @@ -720,98 +790,100 @@ export function getKAGCube(rows, keyColumns, groupColumns, aggrColumns) { key: keyColumns.length !== 0, group: groupColumns.length !== 0, aggregator: aggrColumns.length !== 0, - } + }; - let cube = {} + let cube = {}; - const keyColumnName = keyColumns.map(c => c.name).join('.') - const groupNameSet = new Set() - const keyNameSet = new Set() - const selectorNameWithIndex = {} /** { selectorName: index } */ - let indexCounter = 0 + const keyColumnName = keyColumns.map((c) => c.name).join('.'); + const groupNameSet = new Set(); + const keyNameSet = new Set(); + const selectorNameWithIndex = {}; /** { selectorName: index } */ + let indexCounter = 0; for (let i = 0; i < rows.length; i++) { - const row = rows[i] - let c = cube + const row = rows[i]; + let c = cube; // key: add to entry - let mergedKeyName + let mergedKeyName; if (schema.key) { - mergedKeyName = keyColumns.map(c => row[c.index]).join('.') + mergedKeyName = keyColumns.map((c) => row[c.index]).join('.'); // key: add to row - if (!c[mergedKeyName]) { c[mergedKeyName] = {} } - c = c[mergedKeyName] + if (!c[mergedKeyName]) { + c[mergedKeyName] = {}; + } + c = c[mergedKeyName]; - keyNameSet.add(mergedKeyName) + keyNameSet.add(mergedKeyName); } - let mergedGroupName + let mergedGroupName; if (schema.group) { - mergedGroupName = groupColumns.map(c => row[c.index]).join('.') - groupNameSet.add(mergedGroupName) + mergedGroupName = groupColumns.map((c) => row[c.index]).join('.'); + groupNameSet.add(mergedGroupName); } for (let a = 0; a < aggrColumns.length; a++) { - const aggrColumn = aggrColumns[a] - const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})` + const aggrColumn = aggrColumns[a]; + const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`; // update groupNameSet if (!mergedGroupName) { - groupNameSet.add(aggrName) /** aggr column name will be used as group name if group is empty */ + groupNameSet.add(aggrName); /** aggr column name will be used as group name if group is empty */ } // update selectorNameWithIndex - const selector = getSelectorName(mergedKeyName, aggrColumns.length, aggrName) + const selector = getSelectorName(mergedKeyName, aggrColumns.length, aggrName); if (typeof selectorNameWithIndex[selector] === 'undefined' /** value might be 0 */) { - selectorNameWithIndex[selector] = indexCounter - indexCounter = indexCounter + 1 + selectorNameWithIndex[selector] = indexCounter; + indexCounter = indexCounter + 1; } // add aggregatorName to row if (!c[aggrName]) { - const value = (aggrColumn.aggr !== 'count') ? row[aggrColumn.index] : 1 - const count = 1 + const value = (aggrColumn.aggr !== 'count') ? row[aggrColumn.index] : 1; + const count = 1; - c[aggrName] = { aggr: aggrColumn.aggr, value: value, count: count, children: {}, } + c[aggrName] = {aggr: aggrColumn.aggr, value: value, count: count, children: {}}; } else { const value = AggregatorFunctions[aggrColumn.aggr]( - c[aggrName].value, row[aggrColumn.index], c[aggrName].count + 1) + c[aggrName].value, row[aggrColumn.index], c[aggrName].count + 1); const count = (AggregatorFunctionDiv[aggrColumn.aggr]) - ? c[aggrName].count + 1 : c[aggrName].count + ? c[aggrName].count + 1 : c[aggrName].count; - c[aggrName].value = value - c[aggrName].count = count + c[aggrName].value = value; + c[aggrName].count = count; } // add aggregated group (for drill-down) to row iff group is enabled if (mergedGroupName) { if (!c[aggrName].children[mergedGroupName]) { - const value = (aggrColumn.aggr !== 'count') ? row[aggrColumn.index] : 1 - const count = 1 + const value = (aggrColumn.aggr !== 'count') ? row[aggrColumn.index] : 1; + const count = 1; - c[aggrName].children[mergedGroupName] = { value: value, count: count, } + c[aggrName].children[mergedGroupName] = {value: value, count: count}; } else { - const drillDownedValue = c[aggrName].children[mergedGroupName].value - const drillDownedCount = c[aggrName].children[mergedGroupName].count + const drillDownedValue = c[aggrName].children[mergedGroupName].value; + const drillDownedCount = c[aggrName].children[mergedGroupName].count; const value = AggregatorFunctions[aggrColumn.aggr]( - drillDownedValue, row[aggrColumn.index], drillDownedCount + 1) + drillDownedValue, row[aggrColumn.index], drillDownedCount + 1); const count = (AggregatorFunctionDiv[aggrColumn.aggr]) - ? drillDownedCount + 1 : drillDownedCount + ? drillDownedCount + 1 : drillDownedCount; - c[aggrName].children[mergedGroupName].value = value - c[aggrName].children[mergedGroupName].count = count + c[aggrName].children[mergedGroupName].value = value; + c[aggrName].children[mergedGroupName].count = count; } } } /** end loop for aggrColumns */ } - let keyNames = null + let keyNames = null; if (!schema.key) { - const mergedGroupColumnName = groupColumns.map(c => c.name).join('.') - cube = { [mergedGroupColumnName]: cube, } - keyNames = [ mergedGroupColumnName, ] + const mergedGroupColumnName = groupColumns.map((c) => c.name).join('.'); + cube = {[mergedGroupColumnName]: cube}; + keyNames = [mergedGroupColumnName]; } else { - keyNames = Object.keys(cube).sort() /** keys should be sorted */ + keyNames = Object.keys(cube).sort(); /** keys should be sorted */ } return { @@ -821,7 +893,7 @@ export function getKAGCube(rows, keyColumns, groupColumns, aggrColumns) { keyNames: keyNames, groupNameSet: groupNameSet, selectorNameWithIndex: selectorNameWithIndex, - } + }; } /** nested cube `(key1) -> (key2) -> (group) -> aggregator` */ export function getKKGACube(rows, key1Columns, key2Columns, groupColumns, aggrColumns) { @@ -830,82 +902,98 @@ export function getKKGACube(rows, key1Columns, key2Columns, groupColumns, aggrCo key2: key2Columns.length !== 0, group: groupColumns.length !== 0, aggregator: aggrColumns.length !== 0, - } + }; - let cube = {} - const entry = {} + let cube = {}; + const entry = {}; - const key1ColumnName = key1Columns.map(c => c.name).join('.') - const key1NameSet = {} - const key2ColumnName = key2Columns.map(c => c.name).join('.') - const key2NameSet = {} - const groupNameSet = new Set() - const selectorNameWithIndex = {} /** { selectorName: index } */ - let indexCounter = 0 + const key1ColumnName = key1Columns.map((c) => c.name).join('.'); + const key1NameSet = {}; + const key2ColumnName = key2Columns.map((c) => c.name).join('.'); + const key2NameSet = {}; + const groupNameSet = new Set(); + const selectorNameWithIndex = {}; /** { selectorName: index } */ + let indexCounter = 0; for (let i = 0; i < rows.length; i++) { - const row = rows[i] - let e = entry - let c = cube + const row = rows[i]; + let e = entry; + let c = cube; // key1: add to entry - let mergedKey1Name + let mergedKey1Name; if (schema.key1) { - mergedKey1Name = key1Columns.map(c => row[c.index]).join('.') - if (!e[mergedKey1Name]) { e[mergedKey1Name] = { children: {}, } } - e = e[mergedKey1Name].children + mergedKey1Name = key1Columns.map((c) => row[c.index]).join('.'); + if (!e[mergedKey1Name]) { + e[mergedKey1Name] = {children: {}}; + } + e = e[mergedKey1Name].children; // key1: add to row - if (!c[mergedKey1Name]) { c[mergedKey1Name] = {} } - c = c[mergedKey1Name] + if (!c[mergedKey1Name]) { + c[mergedKey1Name] = {}; + } + c = c[mergedKey1Name]; - if (!key1NameSet[mergedKey1Name]) { key1NameSet[mergedKey1Name] = true } + if (!key1NameSet[mergedKey1Name]) { + key1NameSet[mergedKey1Name] = true; + } } // key2: add to entry - let mergedKey2Name + let mergedKey2Name; if (schema.key2) { - mergedKey2Name = key2Columns.map(c => row[c.index]).join('.') - if (!e[mergedKey2Name]) { e[mergedKey2Name] = { children: {}, } } - e = e[mergedKey2Name].children + mergedKey2Name = key2Columns.map((c) => row[c.index]).join('.'); + if (!e[mergedKey2Name]) { + e[mergedKey2Name] = {children: {}}; + } + e = e[mergedKey2Name].children; // key2: add to row - if (!c[mergedKey2Name]) { c[mergedKey2Name] = {} } - c = c[mergedKey2Name] + if (!c[mergedKey2Name]) { + c[mergedKey2Name] = {}; + } + c = c[mergedKey2Name]; - if (!key2NameSet[mergedKey2Name]) { key2NameSet[mergedKey2Name] = true } + if (!key2NameSet[mergedKey2Name]) { + key2NameSet[mergedKey2Name] = true; + } } - let mergedGroupName + let mergedGroupName; if (schema.group) { - mergedGroupName = groupColumns.map(c => row[c.index]).join('.') + mergedGroupName = groupColumns.map((c) => row[c.index]).join('.'); // add group to entry - if (!e[mergedGroupName]) { e[mergedGroupName] = { children: {}, } } - e = e[mergedGroupName].children + if (!e[mergedGroupName]) { + e[mergedGroupName] = {children: {}}; + } + e = e[mergedGroupName].children; // add group to row - if (!c[mergedGroupName]) { c[mergedGroupName] = {} } - c = c[mergedGroupName] - groupNameSet.add(mergedGroupName) + if (!c[mergedGroupName]) { + c[mergedGroupName] = {}; + } + c = c[mergedGroupName]; + groupNameSet.add(mergedGroupName); } for (let a = 0; a < aggrColumns.length; a++) { - const aggrColumn = aggrColumns[a] - const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})` + const aggrColumn = aggrColumns[a]; + const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`; // update groupNameSet if (!mergedGroupName) { - groupNameSet.add(aggrName) /** aggr column name will be used as group name if group is empty */ + groupNameSet.add(aggrName); /** aggr column name will be used as group name if group is empty */ } // update selectorNameWithIndex - const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName) + const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName); if (typeof selectorNameWithIndex[selector] === 'undefined' /** value might be 0 */) { - selectorNameWithIndex[selector] = indexCounter - indexCounter = indexCounter + 1 + selectorNameWithIndex[selector] = indexCounter; + indexCounter = indexCounter + 1; } // add aggregator to entry if (!e[aggrName]) { - e[aggrName] = { type: 'aggregator', order: aggrColumn, index: aggrColumn.index, } + e[aggrName] = {type: 'aggregator', order: aggrColumn, index: aggrColumn.index}; } // add aggregatorName to row @@ -914,21 +1002,21 @@ export function getKKGACube(rows, key1Columns, key2Columns, groupColumns, aggrCo aggr: aggrColumn.aggr, value: (aggrColumn.aggr !== 'count') ? row[aggrColumn.index] : 1, count: 1, - } + }; } else { const value = AggregatorFunctions[aggrColumn.aggr]( - c[aggrName].value, row[aggrColumn.index], c[aggrName].count + 1) + c[aggrName].value, row[aggrColumn.index], c[aggrName].count + 1); const count = (AggregatorFunctionDiv[aggrColumn.aggr]) - ? c[aggrName].count + 1 : c[aggrName].count + ? c[aggrName].count + 1 : c[aggrName].count; - c[aggrName].value = value - c[aggrName].count = count + c[aggrName].value = value; + c[aggrName].count = count; } } /** end loop for aggrColumns */ } - let key1Names = Object.keys(key1NameSet).sort() /** keys should be sorted */ - let key2Names = Object.keys(key2NameSet).sort() /** keys should be sorted */ + let key1Names = Object.keys(key1NameSet).sort(); /** keys should be sorted */ + let key2Names = Object.keys(key2NameSet).sort(); /** keys should be sorted */ return { cube: cube, @@ -939,59 +1027,61 @@ export function getKKGACube(rows, key1Columns, key2Columns, groupColumns, aggrCo key2Names: key2Names, groupNameSet: groupNameSet, selectorNameWithIndex: selectorNameWithIndex, - } + }; } export function getSelectorName(mergedGroupName, aggrColumnLength, aggrColumnName) { if (!mergedGroupName) { - return aggrColumnName + return aggrColumnName; } else { return (aggrColumnLength > 1) - ? `${mergedGroupName} / ${aggrColumnName}` : mergedGroupName + ? `${mergedGroupName} / ${aggrColumnName}` : mergedGroupName; } } export function getCubeValue(obj, aggregator, aggrColumnName) { - let value = null /** default is null */ + let value = null; /** default is null */ try { /** if AVG or COUNT, calculate it now, previously we can't because we were doing accumulation */ if (aggregator === Aggregator.AVG) { - value = obj[aggrColumnName].value / obj[aggrColumnName].count + value = obj[aggrColumnName].value / obj[aggrColumnName].count; } else if (aggregator === Aggregator.COUNT) { - value = obj[aggrColumnName].value + value = obj[aggrColumnName].value; } else { - value = obj[aggrColumnName].value + value = obj[aggrColumnName].value; } - if (typeof value === 'undefined') { value = null } + if (typeof value === 'undefined') { + value = null; + } } catch (error) { /** iognore */ } - return value + return value; } export function getNameWithIndex(names) { - const nameWithIndex = {} + const nameWithIndex = {}; for (let i = 0; i < names.length; i++) { - const name = names[i] - nameWithIndex[name] = i + const name = names[i]; + nameWithIndex[name] = i; } - return nameWithIndex + return nameWithIndex; } export function getArrayRowsFromKKGACube(cube, schema, aggregatorColumns, key1Names, key2Names, groupNameSet, selectorNameWithIndex) { - const sortedSelectors = Object.keys(selectorNameWithIndex).sort() - const sortedSelectorNameWithIndex = getNameWithIndex(sortedSelectors) + const sortedSelectors = Object.keys(selectorNameWithIndex).sort(); + const sortedSelectorNameWithIndex = getNameWithIndex(sortedSelectors); - const selectorRows = new Array(sortedSelectors.length) - const key1NameWithIndex = getNameWithIndex(key1Names) - const key2NameWithIndex = getNameWithIndex(key2Names) + const selectorRows = new Array(sortedSelectors.length); + const key1NameWithIndex = getNameWithIndex(key1Names); + const key2NameWithIndex = getNameWithIndex(key2Names); fillSelectorRows(schema, cube, selectorRows, aggregatorColumns, sortedSelectorNameWithIndex, - key1Names, key2Names, key1NameWithIndex, key2NameWithIndex) + key1Names, key2Names, key1NameWithIndex, key2NameWithIndex); return { key1NameWithIndex: key1NameWithIndex, @@ -999,7 +1089,7 @@ export function getArrayRowsFromKKGACube(cube, schema, aggregatorColumns, transformed: selectorRows, groupNames: Array.from(groupNameSet).sort(), sortedSelectors: sortedSelectors, - } + }; } /** truly mutable style func. will return nothing */ @@ -1009,90 +1099,94 @@ export function fillSelectorRows(schema, cube, selectorRows, function fill(grouped, mergedGroupName, key1Name, key2Name) { // should iterate aggrColumns in the most nested loop to utilize memory locality for (let aggrColumn of aggrColumns) { - const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})` - const value = getCubeValue(grouped, aggrColumn.aggr, aggrName) - const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName) - const selectorIndex = selectorNameWithIndex[selector] + const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`; + const value = getCubeValue(grouped, aggrColumn.aggr, aggrName); + const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName); + const selectorIndex = selectorNameWithIndex[selector]; if (typeof selectorRows[selectorIndex] === 'undefined') { - selectorRows[selectorIndex] = { selector: selector, value: [], } + selectorRows[selectorIndex] = {selector: selector, value: []}; } - const row = { aggregated: value, } + const row = {aggregated: value}; - if (typeof key1Name !== 'undefined') { row.key1 = key1Name } - if (typeof key2Name !== 'undefined') { row.key2 = key2Name } + if (typeof key1Name !== 'undefined') { + row.key1 = key1Name; + } + if (typeof key2Name !== 'undefined') { + row.key2 = key2Name; + } - selectorRows[selectorIndex].value.push(row) + selectorRows[selectorIndex].value.push(row); } } function iterateGroupNames(keyed, key1Name, key2Name) { if (!schema.group) { - fill(keyed, undefined, key1Name, key2Name) + fill(keyed, undefined, key1Name, key2Name); } else { // assuming sparse distribution (usual case) // otherwise we need to iterate using `groupNameSet` - const availableGroupNames = Object.keys(keyed) + const availableGroupNames = Object.keys(keyed); for (let groupName of availableGroupNames) { - const grouped = keyed[groupName] - fill(grouped, groupName, key1Name, key2Name) + const grouped = keyed[groupName]; + fill(grouped, groupName, key1Name, key2Name); } } } if (schema.key1 && schema.key2) { for (let key1Name of key1Names) { - const key1ed = cube[key1Name] + const key1ed = cube[key1Name]; // assuming sparse distribution (usual case) // otherwise we need to iterate using `key2Names` - const availableKey2Names = Object.keys(key1ed) + const availableKey2Names = Object.keys(key1ed); for (let key2Name of availableKey2Names) { - const keyed = key1ed[key2Name] - iterateGroupNames(keyed, key1Name, key2Name) + const keyed = key1ed[key2Name]; + iterateGroupNames(keyed, key1Name, key2Name); } } } else if (schema.key1 && !schema.key2) { for (let key1Name of key1Names) { - const keyed = cube[key1Name] - iterateGroupNames(keyed, key1Name, undefined) + const keyed = cube[key1Name]; + iterateGroupNames(keyed, key1Name, undefined); } } else if (!schema.key1 && schema.key2) { for (let key2Name of key2Names) { - const keyed = cube[key2Name] - iterateGroupNames(keyed, undefined, key2Name) + const keyed = cube[key2Name]; + iterateGroupNames(keyed, undefined, key2Name); } } else { - iterateGroupNames(cube, undefined, undefined) + iterateGroupNames(cube, undefined, undefined); } } export function getArrayRowsFromKGACube(cube, schema, aggregatorColumns, keyColumnName, keyNames, groupNameSet, selectorNameWithIndex) { - const sortedSelectors = Object.keys(selectorNameWithIndex).sort() - const sortedSelectorNameWithIndex = getNameWithIndex(sortedSelectors) + const sortedSelectors = Object.keys(selectorNameWithIndex).sort(); + const sortedSelectorNameWithIndex = getNameWithIndex(sortedSelectors); - const keyArrowRows = new Array(sortedSelectors.length) - const keyNameWithIndex = getNameWithIndex(keyNames) + const keyArrowRows = new Array(sortedSelectors.length); + const keyNameWithIndex = getNameWithIndex(keyNames); for (let i = 0; i < keyNames.length; i++) { - const key = keyNames[i] + const key = keyNames[i]; - const obj = cube[key] + const obj = cube[key]; fillArrayRow(schema, aggregatorColumns, obj, groupNameSet, sortedSelectorNameWithIndex, - key, keyNames, keyArrowRows, keyNameWithIndex) + key, keyNames, keyArrowRows, keyNameWithIndex); } return { transformed: keyArrowRows, groupNames: Array.from(groupNameSet).sort(), sortedSelectors: sortedSelectors, - } + }; } /** truly mutable style func. will return nothing, just modify `keyArrayRows` */ @@ -1100,34 +1194,34 @@ export function fillArrayRow(schema, aggrColumns, obj, groupNameSet, selectorNameWithIndex, keyName, keyNames, keyArrayRows, keyNameWithIndex) { function fill(target, mergedGroupName, aggr, aggrName) { - const value = getCubeValue(target, aggr, aggrName) - const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName) - const selectorIndex = selectorNameWithIndex[selector] - const keyIndex = keyNameWithIndex[keyName] + const value = getCubeValue(target, aggr, aggrName); + const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName); + const selectorIndex = selectorNameWithIndex[selector]; + const keyIndex = keyNameWithIndex[keyName]; if (typeof keyArrayRows[selectorIndex] === 'undefined') { keyArrayRows[selectorIndex] = { - selector: selector, value: new Array(keyNames.length) - } + selector: selector, value: new Array(keyNames.length), + }; } - keyArrayRows[selectorIndex].value[keyIndex] = value + keyArrayRows[selectorIndex].value[keyIndex] = value; } /** when group is empty */ if (!schema.group) { for (let i = 0; i < aggrColumns.length; i++) { - const aggrColumn = aggrColumns[i] - const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})` - fill(obj, undefined, aggrColumn.aggr, aggrName) + const aggrColumn = aggrColumns[i]; + const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`; + fill(obj, undefined, aggrColumn.aggr, aggrName); } } else { for (let i = 0; i < aggrColumns.length; i++) { - const aggrColumn = aggrColumns[i] - const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})` + const aggrColumn = aggrColumns[i]; + const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`; for (let groupName of groupNameSet) { - const grouped = obj[groupName] - fill(grouped, groupName, aggrColumn.aggr, aggrName) + const grouped = obj[groupName]; + fill(grouped, groupName, aggrColumn.aggr, aggrName); } } } @@ -1137,81 +1231,83 @@ export function getObjectRowsFromKGACube(cube, schema, aggregatorColumns, keyColumnName, keyNames, groupNameSet, selectorNameWithIndex) { const rows = keyNames.reduce((acc, key) => { - const obj = cube[key] - const row = getObjectRow(schema, aggregatorColumns, obj, groupNameSet) + const obj = cube[key]; + const row = getObjectRow(schema, aggregatorColumns, obj, groupNameSet); - if (schema.key) { row[keyColumnName] = key } - acc.push(row) + if (schema.key) { + row[keyColumnName] = key; + } + acc.push(row); - return acc - }, []) + return acc; + }, []); return { transformed: rows, sortedSelectors: Object.keys(selectorNameWithIndex).sort(), groupNames: Array.from(groupNameSet).sort(), - } + }; } export function getObjectRow(schema, aggrColumns, obj, groupNameSet) { - const row = {} + const row = {}; function fill(row, target, mergedGroupName, aggr, aggrName) { - const value = getCubeValue(target, aggr, aggrName) - const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName) - row[selector] = value + const value = getCubeValue(target, aggr, aggrName); + const selector = getSelectorName(mergedGroupName, aggrColumns.length, aggrName); + row[selector] = value; } /** when group is empty */ if (!schema.group) { for (let i = 0; i < aggrColumns.length; i++) { - const aggrColumn = aggrColumns[i] - const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})` + const aggrColumn = aggrColumns[i]; + const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`; - fill(row, obj, undefined, aggrColumn.aggr, aggrName) + fill(row, obj, undefined, aggrColumn.aggr, aggrName); } - return row + return row; } /** when group is specified */ for (let i = 0; i < aggrColumns.length; i++) { - const aggrColumn = aggrColumns[i] - const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})` + const aggrColumn = aggrColumns[i]; + const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`; for (let groupName of groupNameSet) { - const grouped = obj[groupName] + const grouped = obj[groupName]; if (grouped) { - fill(row, grouped, groupName, aggrColumn.aggr, aggrName) + fill(row, grouped, groupName, aggrColumn.aggr, aggrName); } } } - return row + return row; } export function getDrilldownRowsFromKAGCube(cube, schema, aggregatorColumns, keyColumnName, keyNames, groupNameSet, selectorNameWithIndex) { - const sortedSelectors = Object.keys(selectorNameWithIndex).sort() - const sortedSelectorNameWithIndex = getNameWithIndex(sortedSelectors) + const sortedSelectors = Object.keys(selectorNameWithIndex).sort(); + const sortedSelectorNameWithIndex = getNameWithIndex(sortedSelectors); - const rows = new Array(sortedSelectors.length) + const rows = new Array(sortedSelectors.length); - const groupNames = Array.from(groupNameSet).sort() + const groupNames = Array.from(groupNameSet).sort(); - keyNames.map(key => { - const obj = cube[key] + keyNames.map((key) => { + const obj = cube[key]; fillDrillDownRow(schema, obj, rows, key, - sortedSelectorNameWithIndex, aggregatorColumns, groupNames) - }) + sortedSelectorNameWithIndex, aggregatorColumns, groupNames); + }); return { transformed: rows, groupNames: groupNames, sortedSelectors: sortedSelectors, sortedSelectorNameWithIndex: sortedSelectorNameWithIndex, - } + }; } /** truly mutable style func. will return nothing, just modify `rows` */ @@ -1219,27 +1315,27 @@ export function fillDrillDownRow(schema, obj, rows, key, selectorNameWithIndex, aggrColumns, groupNames) { /** when group is empty */ for (let i = 0; i < aggrColumns.length; i++) { - const row = {} - const aggrColumn = aggrColumns[i] - const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})` + const row = {}; + const aggrColumn = aggrColumns[i]; + const aggrName = `${aggrColumn.name}(${aggrColumn.aggr})`; - const value = getCubeValue(obj, aggrColumn.aggr, aggrName) - const selector = getSelectorName((schema.key) ? key : undefined, aggrColumns.length, aggrName) + const value = getCubeValue(obj, aggrColumn.aggr, aggrName); + const selector = getSelectorName((schema.key) ? key : undefined, aggrColumns.length, aggrName); - const selectorIndex = selectorNameWithIndex[selector] - row.value = value - row.drillDown = [] - row.selector = selector + const selectorIndex = selectorNameWithIndex[selector]; + row.value = value; + row.drillDown = []; + row.selector = selector; if (schema.group) { - row.drillDown = [] + row.drillDown = []; for (let groupName of groupNames) { - const value = getCubeValue(obj[aggrName].children, aggrColumn.aggr, groupName) - row.drillDown.push({ group: groupName, value: value, }) + const value = getCubeValue(obj[aggrName].children, aggrColumn.aggr, groupName); + row.drillDown.push({group: groupName, value: value}); } } - rows[selectorIndex] = row + rows[selectorIndex] = row; } } diff --git a/zeppelin-web/src/app/tabledata/advanced-transformation-util.test.js b/zeppelin-web/src/app/tabledata/advanced-transformation-util.test.js index 90f569fade4..84ea4419dbc 100644 --- a/zeppelin-web/src/app/tabledata/advanced-transformation-util.test.js +++ b/zeppelin-web/src/app/tabledata/advanced-transformation-util.test.js @@ -12,1728 +12,1730 @@ * limitations under the License. */ -import * as Util from './advanced-transformation-util.js' +import * as Util from './advanced-transformation-util.js'; /* eslint-disable max-len */ const MockParameter = { - 'floatParam': { valueType: 'float', defaultValue: 10, description: '', }, - 'intParam': { valueType: 'int', defaultValue: 50, description: '', }, - 'jsonParam': { valueType: 'JSON', defaultValue: '', description: '', widget: 'textarea', }, - 'stringParam1': { valueType: 'string', defaultValue: '', description: '', }, - 'stringParam2': { valueType: 'string', defaultValue: '', description: '', widget: 'input', }, - 'boolParam': { valueType: 'boolean', defaultValue: false, description: '', widget: 'checkbox', }, - 'optionParam': { valueType: 'string', defaultValue: 'line', description: '', widget: 'option', optionValues: [ 'line', 'smoothedLine', ], }, -} + 'floatParam': {valueType: 'float', defaultValue: 10, description: ''}, + 'intParam': {valueType: 'int', defaultValue: 50, description: ''}, + 'jsonParam': {valueType: 'JSON', defaultValue: '', description: '', widget: 'textarea'}, + 'stringParam1': {valueType: 'string', defaultValue: '', description: ''}, + 'stringParam2': {valueType: 'string', defaultValue: '', description: '', widget: 'input'}, + 'boolParam': {valueType: 'boolean', defaultValue: false, description: '', widget: 'checkbox'}, + 'optionParam': {valueType: 'string', defaultValue: 'line', description: '', widget: 'option', optionValues: ['line', 'smoothedLine']}, +}; /* eslint-enable max-len */ const MockAxis1 = { - 'keyAxis': { dimension: 'multiple', axisType: 'key', }, - 'aggrAxis': { dimension: 'multiple', axisType: 'aggregator', }, - 'groupAxis': { dimension: 'multiple', axisType: 'group', }, -} + 'keyAxis': {dimension: 'multiple', axisType: 'key'}, + 'aggrAxis': {dimension: 'multiple', axisType: 'aggregator'}, + 'groupAxis': {dimension: 'multiple', axisType: 'group'}, +}; const MockAxis2 = { - 'singleKeyAxis': { dimension: 'single', axisType: 'key', }, - 'limitedAggrAxis': { dimension: 'multiple', axisType: 'aggregator', maxAxisCount: 2, }, - 'groupAxis': { dimension: 'multiple', axisType: 'group', }, -} + 'singleKeyAxis': {dimension: 'single', axisType: 'key'}, + 'limitedAggrAxis': {dimension: 'multiple', axisType: 'aggregator', maxAxisCount: 2}, + 'groupAxis': {dimension: 'multiple', axisType: 'group'}, +}; const MockAxis3 = { - 'customAxis1': { dimension: 'single', axisType: 'unique', }, - 'customAxis2': { dimension: 'multiple', axisType: 'value', }, -} + 'customAxis1': {dimension: 'single', axisType: 'unique'}, + 'customAxis2': {dimension: 'multiple', axisType: 'value'}, +}; const MockAxis4 = { - 'key1Axis': { dimension: 'multiple', axisType: 'key', }, - 'key2Axis': { dimension: 'multiple', axisType: 'key', }, - 'aggrAxis': { dimension: 'multiple', axisType: 'aggregator', }, - 'groupAxis': { dimension: 'multiple', axisType: 'group', }, -} + 'key1Axis': {dimension: 'multiple', axisType: 'key'}, + 'key2Axis': {dimension: 'multiple', axisType: 'key'}, + 'aggrAxis': {dimension: 'multiple', axisType: 'aggregator'}, + 'groupAxis': {dimension: 'multiple', axisType: 'group'}, +}; // test spec for axis, param, widget const MockSpec = { charts: { 'object-chart': { - transform: { method: 'object', }, + transform: {method: 'object'}, sharedAxis: true, axis: JSON.parse(JSON.stringify(MockAxis1)), parameter: MockParameter, }, 'array-chart': { - transform: { method: 'array', }, + transform: {method: 'array'}, sharedAxis: true, axis: JSON.parse(JSON.stringify(MockAxis1)), parameter: { - 'arrayChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', }, + 'arrayChartParam0': {valueType: 'string', defaultValue: '', description: 'param0'}, }, }, 'drillDown-chart': { - transform: { method: 'drill-down', }, + transform: {method: 'drill-down'}, axis: JSON.parse(JSON.stringify(MockAxis2)), parameter: { - 'drillDownChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', }, + 'drillDownChartParam0': {valueType: 'string', defaultValue: '', description: 'param0'}, }, }, 'raw-chart': { - transform: { method: 'raw', }, + transform: {method: 'raw'}, axis: JSON.parse(JSON.stringify(MockAxis3)), parameter: { - 'rawChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', }, + 'rawChartParam0': {valueType: 'string', defaultValue: '', description: 'param0'}, }, }, }, -} +}; // test spec for transformation const MockSpec2 = { charts: { 'object-chart': { - transform: { method: 'object', }, + transform: {method: 'object'}, sharedAxis: false, axis: JSON.parse(JSON.stringify(MockAxis1)), parameter: MockParameter, }, 'array-chart': { - transform: { method: 'array', }, + transform: {method: 'array'}, sharedAxis: false, axis: JSON.parse(JSON.stringify(MockAxis1)), parameter: { - 'arrayChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', }, + 'arrayChartParam0': {valueType: 'string', defaultValue: '', description: 'param0'}, }, }, 'drillDown-chart': { - transform: { method: 'drill-down', }, + transform: {method: 'drill-down'}, sharedAxis: false, axis: JSON.parse(JSON.stringify(MockAxis1)), parameter: { - 'drillDownChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', }, + 'drillDownChartParam0': {valueType: 'string', defaultValue: '', description: 'param0'}, }, }, 'array2Key-chart': { - transform: { method: 'array:2-key', }, + transform: {method: 'array:2-key'}, sharedAxis: false, axis: JSON.parse(JSON.stringify(MockAxis4)), parameter: { - 'drillDownChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', }, + 'drillDownChartParam0': {valueType: 'string', defaultValue: '', description: 'param0'}, }, }, 'raw-chart': { - transform: { method: 'raw', }, + transform: {method: 'raw'}, sharedAxis: false, axis: JSON.parse(JSON.stringify(MockAxis3)), parameter: { - 'rawChartParam0': { valueType: 'string', defaultValue: '', description: 'param0', }, + 'rawChartParam0': {valueType: 'string', defaultValue: '', description: 'param0'}, }, }, }, -} +}; /* eslint-disable max-len */ const MockTableDataColumn = [ - {'name': 'age', 'index': 0, 'aggr': 'sum', }, - {'name': 'job', 'index': 1, 'aggr': 'sum', }, - {'name': 'marital', 'index': 2, 'aggr': 'sum', }, - {'name': 'education', 'index': 3, 'aggr': 'sum', }, - {'name': 'default', 'index': 4, 'aggr': 'sum', }, - {'name': 'balance', 'index': 5, 'aggr': 'sum', }, - {'name': 'housing', 'index': 6, 'aggr': 'sum', }, - {'name': 'loan', 'index': 7, 'aggr': 'sum', }, - {'name': 'contact', 'index': 8, 'aggr': 'sum', }, - {'name': 'day', 'index': 9, 'aggr': 'sum', }, - {'name': 'month', 'index': 10, 'aggr': 'sum', }, - {'name': 'duration', 'index': 11, 'aggr': 'sum', }, - {'name': 'campaign', 'index': 12, 'aggr': 'sum', }, - {'name': 'pdays', 'index': 13, 'aggr': 'sum', }, - {'name': 'previous', 'index': 14, 'aggr': 'sum', }, - {'name': 'poutcome', 'index': 15, 'aggr': 'sum', }, - {'name': 'y', 'index': 16, 'aggr': 'sum', } -] + {'name': 'age', 'index': 0, 'aggr': 'sum'}, + {'name': 'job', 'index': 1, 'aggr': 'sum'}, + {'name': 'marital', 'index': 2, 'aggr': 'sum'}, + {'name': 'education', 'index': 3, 'aggr': 'sum'}, + {'name': 'default', 'index': 4, 'aggr': 'sum'}, + {'name': 'balance', 'index': 5, 'aggr': 'sum'}, + {'name': 'housing', 'index': 6, 'aggr': 'sum'}, + {'name': 'loan', 'index': 7, 'aggr': 'sum'}, + {'name': 'contact', 'index': 8, 'aggr': 'sum'}, + {'name': 'day', 'index': 9, 'aggr': 'sum'}, + {'name': 'month', 'index': 10, 'aggr': 'sum'}, + {'name': 'duration', 'index': 11, 'aggr': 'sum'}, + {'name': 'campaign', 'index': 12, 'aggr': 'sum'}, + {'name': 'pdays', 'index': 13, 'aggr': 'sum'}, + {'name': 'previous', 'index': 14, 'aggr': 'sum'}, + {'name': 'poutcome', 'index': 15, 'aggr': 'sum'}, + {'name': 'y', 'index': 16, 'aggr': 'sum'}, +]; const MockTableDataRows1 = [ - [ '44', 'services', 'single', 'tertiary', 'no', '106', 'no', 'no', 'unknown', '12', 'jun', '109', '2', '-1', '0', 'unknown', 'no' ], - [ '43', 'services', 'married', 'primary', 'no', '-88', 'yes', 'yes', 'cellular', '17', 'apr', '313', '1', '147', '2', 'failure', 'no' ], - [ '39', 'services', 'married', 'secondary', 'no', '9374', 'yes', 'no', 'unknown', '20', 'may', '273', '1', '-1', '0', 'unknown', 'no' ], - [ '33', 'services', 'single', 'tertiary', 'no', '4789', 'yes', 'yes', 'cellular', '11', 'may', '220', '1', '339', '4', 'failure', 'no' ], -] + ['44', 'services', 'single', 'tertiary', 'no', '106', 'no', 'no', 'unknown', '12', 'jun', '109', '2', '-1', '0', 'unknown', 'no'], + ['43', 'services', 'married', 'primary', 'no', '-88', 'yes', 'yes', 'cellular', '17', 'apr', '313', '1', '147', '2', 'failure', 'no'], + ['39', 'services', 'married', 'secondary', 'no', '9374', 'yes', 'no', 'unknown', '20', 'may', '273', '1', '-1', '0', 'unknown', 'no'], + ['33', 'services', 'single', 'tertiary', 'no', '4789', 'yes', 'yes', 'cellular', '11', 'may', '220', '1', '339', '4', 'failure', 'no'], +]; /* eslint-enable max-len */ describe('advanced-transformation-util', () => { describe('getCurrent* funcs', () => { it('should set return proper value of the current chart', () => { - const config = {} - const spec = JSON.parse(JSON.stringify(MockSpec)) - Util.initializeConfig(config, spec) - expect(Util.getCurrentChart(config)).toEqual('object-chart') - expect(Util.getCurrentChartTransform(config)).toEqual({method: 'object'}) + const config = {}; + const spec = JSON.parse(JSON.stringify(MockSpec)); + Util.initializeConfig(config, spec); + expect(Util.getCurrentChart(config)).toEqual('object-chart'); + expect(Util.getCurrentChartTransform(config)).toEqual({method: 'object'}); // use `toBe` to compare reference - expect(Util.getCurrentChartAxis(config)).toBe(config.axis['object-chart']) + expect(Util.getCurrentChartAxis(config)).toBe(config.axis['object-chart']); // use `toBe` to compare reference - expect(Util.getCurrentChartParam(config)).toBe(config.parameter['object-chart']) - }) - }) + expect(Util.getCurrentChartParam(config)).toBe(config.parameter['object-chart']); + }); + }); describe('useSharedAxis', () => { it('should set chartChanged for initial drawing', () => { - const config = {} - const spec = JSON.parse(JSON.stringify(MockSpec)) - Util.initializeConfig(config, spec) - expect(Util.useSharedAxis(config, 'object-chart')).toEqual(true) - expect(Util.useSharedAxis(config, 'array-chart')).toEqual(true) - expect(Util.useSharedAxis(config, 'drillDown-chart')).toBeUndefined() - expect(Util.useSharedAxis(config, 'raw-chart')).toBeUndefined() - }) - }) + const config = {}; + const spec = JSON.parse(JSON.stringify(MockSpec)); + Util.initializeConfig(config, spec); + expect(Util.useSharedAxis(config, 'object-chart')).toEqual(true); + expect(Util.useSharedAxis(config, 'array-chart')).toEqual(true); + expect(Util.useSharedAxis(config, 'drillDown-chart')).toBeUndefined(); + expect(Util.useSharedAxis(config, 'raw-chart')).toBeUndefined(); + }); + }); describe('initializeConfig', () => { - const config = {} - const spec = JSON.parse(JSON.stringify(MockSpec)) - Util.initializeConfig(config, spec) + const config = {}; + const spec = JSON.parse(JSON.stringify(MockSpec)); + Util.initializeConfig(config, spec); it('should set chartChanged for initial drawing', () => { - expect(config.chartChanged).toBe(true) - expect(config.parameterChanged).toBe(false) - }) + expect(config.chartChanged).toBe(true); + expect(config.parameterChanged).toBe(false); + }); it('should set panel toggles ', () => { - expect(config.panel.columnPanelOpened).toBe(true) - expect(config.panel.parameterPanelOpened).toBe(false) - }) + expect(config.panel.columnPanelOpened).toBe(true); + expect(config.panel.parameterPanelOpened).toBe(false); + }); it('should set version and initialized', () => { - expect(config.spec.version).toBeDefined() - expect(config.spec.initialized).toBe(true) - }) + expect(config.spec.version).toBeDefined(); + expect(config.spec.initialized).toBe(true); + }); it('should set chart', () => { - expect(config.chart.current).toBe('object-chart') + expect(config.chart.current).toBe('object-chart'); expect(config.chart.available).toEqual([ 'object-chart', 'array-chart', 'drillDown-chart', 'raw-chart', - ]) - }) + ]); + }); it('should set sharedAxis', () => { expect(config.sharedAxis).toEqual({ keyAxis: [], aggrAxis: [], groupAxis: [], - }) + }); // should use `toBe` to compare object reference - expect(config.sharedAxis).toBe(config.axis['object-chart']) + expect(config.sharedAxis).toBe(config.axis['object-chart']); // should use `toBe` to compare object reference - expect(config.sharedAxis).toBe(config.axis['array-chart']) - }) + expect(config.sharedAxis).toBe(config.axis['array-chart']); + }); it('should set paramSpecs', () => { - const expected = Util.getSpecs(MockParameter) - expect(config.paramSpecs['object-chart']).toEqual(expected) - expect(config.paramSpecs['array-chart'].length).toEqual(1) - expect(config.paramSpecs['drillDown-chart'].length).toEqual(1) - expect(config.paramSpecs['raw-chart'].length).toEqual(1) - }) + const expected = Util.getSpecs(MockParameter); + expect(config.paramSpecs['object-chart']).toEqual(expected); + expect(config.paramSpecs['array-chart'].length).toEqual(1); + expect(config.paramSpecs['drillDown-chart'].length).toEqual(1); + expect(config.paramSpecs['raw-chart'].length).toEqual(1); + }); it('should set parameter with default value', () => { - expect(Object.keys(MockParameter).length).toBeGreaterThan(0) // length > 0 + expect(Object.keys(MockParameter).length).toBeGreaterThan(0); // length > 0 for (let paramName in MockParameter) { - expect(config.parameter['object-chart'][paramName]) - .toEqual(MockParameter[paramName].defaultValue) + if (MockParameter.hasOwnProperty(paramName)) { + expect(config.parameter['object-chart'][paramName]) + .toEqual(MockParameter[paramName].defaultValue); + } } - }) + }); it('should set axisSpecs', () => { - const expected = Util.getSpecs(MockAxis1) - expect(config.axisSpecs['object-chart']).toEqual(expected) - expect(config.axisSpecs['array-chart'].length).toEqual(3) - expect(config.axisSpecs['drillDown-chart'].length).toEqual(3) - expect(config.axisSpecs['raw-chart'].length).toEqual(2) - }) + const expected = Util.getSpecs(MockAxis1); + expect(config.axisSpecs['object-chart']).toEqual(expected); + expect(config.axisSpecs['array-chart'].length).toEqual(3); + expect(config.axisSpecs['drillDown-chart'].length).toEqual(3); + expect(config.axisSpecs['raw-chart'].length).toEqual(2); + }); it('should prepare axis depending on dimension', () => { expect(config.axis['object-chart']).toEqual({ keyAxis: [], aggrAxis: [], groupAxis: [], - }) + }); expect(config.axis['array-chart']).toEqual({ keyAxis: [], aggrAxis: [], groupAxis: [], - }) + }); // it's ok not to set single dimension axis - expect(config.axis['drillDown-chart']).toEqual({ limitedAggrAxis: [], groupAxis: [], }) + expect(config.axis['drillDown-chart']).toEqual({limitedAggrAxis: [], groupAxis: []}); // it's ok not to set single dimension axis - expect(config.axis['raw-chart']).toEqual({ customAxis2: [], }) - }) - }) + expect(config.axis['raw-chart']).toEqual({customAxis2: []}); + }); + }); describe('axis', () => { - }) + }); describe('parameter:widget', () => { it('isInputWidget', () => { - expect(Util.isInputWidget(MockParameter.stringParam1)).toBe(true) - expect(Util.isInputWidget(MockParameter.stringParam2)).toBe(true) + expect(Util.isInputWidget(MockParameter.stringParam1)).toBe(true); + expect(Util.isInputWidget(MockParameter.stringParam2)).toBe(true); - expect(Util.isInputWidget(MockParameter.boolParam)).toBe(false) - expect(Util.isInputWidget(MockParameter.jsonParam)).toBe(false) - expect(Util.isInputWidget(MockParameter.optionParam)).toBe(false) - }) + expect(Util.isInputWidget(MockParameter.boolParam)).toBe(false); + expect(Util.isInputWidget(MockParameter.jsonParam)).toBe(false); + expect(Util.isInputWidget(MockParameter.optionParam)).toBe(false); + }); it('isOptionWidget', () => { - expect(Util.isOptionWidget(MockParameter.optionParam)).toBe(true) + expect(Util.isOptionWidget(MockParameter.optionParam)).toBe(true); - expect(Util.isOptionWidget(MockParameter.stringParam1)).toBe(false) - expect(Util.isOptionWidget(MockParameter.stringParam2)).toBe(false) - expect(Util.isOptionWidget(MockParameter.boolParam)).toBe(false) - expect(Util.isOptionWidget(MockParameter.jsonParam)).toBe(false) - }) + expect(Util.isOptionWidget(MockParameter.stringParam1)).toBe(false); + expect(Util.isOptionWidget(MockParameter.stringParam2)).toBe(false); + expect(Util.isOptionWidget(MockParameter.boolParam)).toBe(false); + expect(Util.isOptionWidget(MockParameter.jsonParam)).toBe(false); + }); it('isCheckboxWidget', () => { - expect(Util.isCheckboxWidget(MockParameter.boolParam)).toBe(true) + expect(Util.isCheckboxWidget(MockParameter.boolParam)).toBe(true); - expect(Util.isCheckboxWidget(MockParameter.stringParam1)).toBe(false) - expect(Util.isCheckboxWidget(MockParameter.stringParam2)).toBe(false) - expect(Util.isCheckboxWidget(MockParameter.jsonParam)).toBe(false) - expect(Util.isCheckboxWidget(MockParameter.optionParam)).toBe(false) - }) + expect(Util.isCheckboxWidget(MockParameter.stringParam1)).toBe(false); + expect(Util.isCheckboxWidget(MockParameter.stringParam2)).toBe(false); + expect(Util.isCheckboxWidget(MockParameter.jsonParam)).toBe(false); + expect(Util.isCheckboxWidget(MockParameter.optionParam)).toBe(false); + }); it('isTextareaWidget', () => { - expect(Util.isTextareaWidget(MockParameter.jsonParam)).toBe(true) + expect(Util.isTextareaWidget(MockParameter.jsonParam)).toBe(true); - expect(Util.isTextareaWidget(MockParameter.stringParam1)).toBe(false) - expect(Util.isTextareaWidget(MockParameter.stringParam2)).toBe(false) - expect(Util.isTextareaWidget(MockParameter.boolParam)).toBe(false) - expect(Util.isTextareaWidget(MockParameter.optionParam)).toBe(false) - }) - }) + expect(Util.isTextareaWidget(MockParameter.stringParam1)).toBe(false); + expect(Util.isTextareaWidget(MockParameter.stringParam2)).toBe(false); + expect(Util.isTextareaWidget(MockParameter.boolParam)).toBe(false); + expect(Util.isTextareaWidget(MockParameter.optionParam)).toBe(false); + }); + }); describe('parameter:parseParameter', () => { - const paramSpec = Util.getSpecs(MockParameter) + const paramSpec = Util.getSpecs(MockParameter); it('should parse number', () => { - const params = { intParam: '3', } - const parsed = Util.parseParameter(paramSpec, params) - expect(parsed.intParam).toBe(3) - }) + const params = {intParam: '3'}; + const parsed = Util.parseParameter(paramSpec, params); + expect(parsed.intParam).toBe(3); + }); it('should parse float', () => { - const params = { floatParam: '0.10', } - const parsed = Util.parseParameter(paramSpec, params) - expect(parsed.floatParam).toBe(0.10) - }) + const params = {floatParam: '0.10'}; + const parsed = Util.parseParameter(paramSpec, params); + expect(parsed.floatParam).toBe(0.10); + }); it('should parse boolean', () => { - const params1 = { boolParam: 'true', } - const parsed1 = Util.parseParameter(paramSpec, params1) - expect(typeof parsed1.boolParam).toBe('boolean') - expect(parsed1.boolParam).toBe(true) + const params1 = {boolParam: 'true'}; + const parsed1 = Util.parseParameter(paramSpec, params1); + expect(typeof parsed1.boolParam).toBe('boolean'); + expect(parsed1.boolParam).toBe(true); - const params2 = { boolParam: 'false', } - const parsed2 = Util.parseParameter(paramSpec, params2) - expect(typeof parsed2.boolParam).toBe('boolean') - expect(parsed2.boolParam).toBe(false) - }) + const params2 = {boolParam: 'false'}; + const parsed2 = Util.parseParameter(paramSpec, params2); + expect(typeof parsed2.boolParam).toBe('boolean'); + expect(parsed2.boolParam).toBe(false); + }); it('should parse JSON', () => { - const params = { jsonParam: '{ "a": 3 }', } - const parsed = Util.parseParameter(paramSpec, params) - expect(typeof parsed.jsonParam).toBe('object') - expect(JSON.stringify(parsed.jsonParam)).toBe('{"a":3}') - }) + const params = {jsonParam: '{ "a": 3 }'}; + const parsed = Util.parseParameter(paramSpec, params); + expect(typeof parsed.jsonParam).toBe('object'); + expect(JSON.stringify(parsed.jsonParam)).toBe('{"a":3}'); + }); it('should not parse string', () => { - const params = { stringParam: 'example', } - const parsed = Util.parseParameter(paramSpec, params) - expect(typeof parsed.stringParam).toBe('string') - expect(parsed.stringParam).toBe('example') - }) - }) + const params = {stringParam: 'example'}; + const parsed = Util.parseParameter(paramSpec, params); + expect(typeof parsed.stringParam).toBe('string'); + expect(parsed.stringParam).toBe('example'); + }); + }); describe('removeDuplicatedColumnsInMultiDimensionAxis', () => { - let config = {} + let config = {}; beforeEach(() => { - config = {} - const spec = JSON.parse(JSON.stringify(MockSpec)) - Util.initializeConfig(config, spec) - config.chart.current = 'drillDown-chart' // set non-sharedAxis chart - }) + config = {}; + const spec = JSON.parse(JSON.stringify(MockSpec)); + Util.initializeConfig(config, spec); + config.chart.current = 'drillDown-chart'; // set non-sharedAxis chart + }); it('should remove duplicated axis names in config when axis is not aggregator', () => { const addColumn = function(config, col) { - const axis = Util.getCurrentChartAxis(config)['groupAxis'] - axis.push(col) - const axisSpecs = Util.getCurrentChartAxisSpecs(config) - Util.removeDuplicatedColumnsInMultiDimensionAxis(config, axisSpecs[2]) - } + const axis = Util.getCurrentChartAxis(config)['groupAxis']; + axis.push(col); + const axisSpecs = Util.getCurrentChartAxisSpecs(config); + Util.removeDuplicatedColumnsInMultiDimensionAxis(config, axisSpecs[2]); + }; - addColumn(config, { name: 'columnA', aggr: 'sum', index: 0, }) - addColumn(config, { name: 'columnA', aggr: 'sum', index: 0, }) - addColumn(config, { name: 'columnA', aggr: 'sum', index: 0, }) + addColumn(config, {name: 'columnA', aggr: 'sum', index: 0}); + addColumn(config, {name: 'columnA', aggr: 'sum', index: 0}); + addColumn(config, {name: 'columnA', aggr: 'sum', index: 0}); - expect(Util.getCurrentChartAxis(config)['groupAxis'].length).toEqual(1) - }) + expect(Util.getCurrentChartAxis(config)['groupAxis'].length).toEqual(1); + }); it('should remove duplicated axis names in config when axis is aggregator', () => { const addColumn = function(config, value) { - const axis = Util.getCurrentChartAxis(config)['limitedAggrAxis'] - axis.push(value) - const axisSpecs = Util.getCurrentChartAxisSpecs(config) - Util.removeDuplicatedColumnsInMultiDimensionAxis(config, axisSpecs[1]) - } + const axis = Util.getCurrentChartAxis(config)['limitedAggrAxis']; + axis.push(value); + const axisSpecs = Util.getCurrentChartAxisSpecs(config); + Util.removeDuplicatedColumnsInMultiDimensionAxis(config, axisSpecs[1]); + }; - config.chart.current = 'drillDown-chart' // set non-sharedAxis chart - addColumn(config, { name: 'columnA', aggr: 'sum', index: 0, }) - addColumn(config, { name: 'columnA', aggr: 'aggr', index: 0, }) - addColumn(config, { name: 'columnA', aggr: 'sum', index: 0, }) + config.chart.current = 'drillDown-chart'; // set non-sharedAxis chart + addColumn(config, {name: 'columnA', aggr: 'sum', index: 0}); + addColumn(config, {name: 'columnA', aggr: 'aggr', index: 0}); + addColumn(config, {name: 'columnA', aggr: 'sum', index: 0}); - expect(Util.getCurrentChartAxis(config)['limitedAggrAxis'].length).toEqual(2) - }) - }) + expect(Util.getCurrentChartAxis(config)['limitedAggrAxis'].length).toEqual(2); + }); + }); describe('applyMaxAxisCount', () => { - const config = {} - const spec = JSON.parse(JSON.stringify(MockSpec)) - Util.initializeConfig(config, spec) + const config = {}; + const spec = JSON.parse(JSON.stringify(MockSpec)); + Util.initializeConfig(config, spec); const addColumn = function(config, value) { - const axis = Util.getCurrentChartAxis(config)['limitedAggrAxis'] - axis.push(value) - const axisSpecs = Util.getCurrentChartAxisSpecs(config) - Util.applyMaxAxisCount(config, axisSpecs[1]) - } + const axis = Util.getCurrentChartAxis(config)['limitedAggrAxis']; + axis.push(value); + const axisSpecs = Util.getCurrentChartAxisSpecs(config); + Util.applyMaxAxisCount(config, axisSpecs[1]); + }; it('should remove duplicated axis names in config', () => { - config.chart.current = 'drillDown-chart' // set non-sharedAxis chart + config.chart.current = 'drillDown-chart'; // set non-sharedAxis chart - addColumn(config, 'columnA') - addColumn(config, 'columnB') - addColumn(config, 'columnC') - addColumn(config, 'columnD') + addColumn(config, 'columnA'); + addColumn(config, 'columnB'); + addColumn(config, 'columnC'); + addColumn(config, 'columnD'); expect(Util.getCurrentChartAxis(config)['limitedAggrAxis']).toEqual([ 'columnC', 'columnD', - ]) - }) - }) + ]); + }); + }); describe('getColumnsFromAxis', () => { it('should return proper value for regular axis spec (key, aggr, group)', () => { - const config = {} - - const spec = JSON.parse(JSON.stringify(MockSpec)) - Util.initializeConfig(config, spec) - const chart = 'object-chart' - config.chart.current = chart - - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - axis['keyAxis'].push('columnA') - axis['keyAxis'].push('columnB') - axis['aggrAxis'].push('columnC') - axis['groupAxis'].push('columnD') - axis['groupAxis'].push('columnE') - axis['groupAxis'].push('columnF') - - const column = Util.getColumnsFromAxis(axisSpecs, axis) - expect(column.key).toEqual([ 'columnA', 'columnB', ]) - expect(column.aggregator).toEqual([ 'columnC', ]) - expect(column.group).toEqual([ 'columnD', 'columnE', 'columnF', ]) - }) + const config = {}; + + const spec = JSON.parse(JSON.stringify(MockSpec)); + Util.initializeConfig(config, spec); + const chart = 'object-chart'; + config.chart.current = chart; + + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + axis['keyAxis'].push('columnA'); + axis['keyAxis'].push('columnB'); + axis['aggrAxis'].push('columnC'); + axis['groupAxis'].push('columnD'); + axis['groupAxis'].push('columnE'); + axis['groupAxis'].push('columnF'); + + const column = Util.getColumnsFromAxis(axisSpecs, axis); + expect(column.key).toEqual(['columnA', 'columnB']); + expect(column.aggregator).toEqual(['columnC']); + expect(column.group).toEqual(['columnD', 'columnE', 'columnF']); + }); it('should return proper value for custom axis spec', () => { - const config = {} - const spec = JSON.parse(JSON.stringify(MockSpec)) - Util.initializeConfig(config, spec) - const chart = 'raw-chart' // for test custom columns - config.chart.current = chart - - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - axis['customAxis1'] = ['columnA'] - axis['customAxis2'].push('columnB') - axis['customAxis2'].push('columnC') - axis['customAxis2'].push('columnD') - - const column = Util.getColumnsFromAxis(axisSpecs, axis) - expect(column.custom.unique).toEqual([ 'columnA', ]) - expect(column.custom.value).toEqual([ 'columnB', 'columnC', 'columnD', ]) - }) - }) + const config = {}; + const spec = JSON.parse(JSON.stringify(MockSpec)); + Util.initializeConfig(config, spec); + const chart = 'raw-chart'; // for test custom columns + config.chart.current = chart; + + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + axis['customAxis1'] = ['columnA']; + axis['customAxis2'].push('columnB'); + axis['customAxis2'].push('columnC'); + axis['customAxis2'].push('columnD'); + + const column = Util.getColumnsFromAxis(axisSpecs, axis); + expect(column.custom.unique).toEqual(['columnA']); + expect(column.custom.value).toEqual(['columnB', 'columnC', 'columnD']); + }); + }); // it's hard to test all methods for transformation. // so let's do behavioral (black-box) test instead of describe('getTransformer', () => { describe('method: raw', () => { - let config = {} - const spec = JSON.parse(JSON.stringify(MockSpec2)) - Util.initializeConfig(config, spec) + let config = {}; + const spec = JSON.parse(JSON.stringify(MockSpec2)); + Util.initializeConfig(config, spec); it('should return original rows when transform.method is `raw`', () => { - const chart = 'raw-chart' - config.chart.current = chart + const chart = 'raw-chart'; + config.chart.current = chart; - const rows = [ { 'r1': 1, }, ] - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, rows, axisSpecs, axis).transformer - const transformed = transformer() + const rows = [{'r1': 1}]; + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, rows, axisSpecs, axis).transformer; + const transformed = transformer(); - expect(transformed).toBe(rows) - }) - }) + expect(transformed).toBe(rows); + }); + }); describe('array method', () => { - let config = {} - const chart = 'array-chart' - let ageColumn = null - let balanceColumn = null - let educationColumn = null - let martialColumn = null - let tableDataRows = [] + let config = {}; + const chart = 'array-chart'; + let ageColumn = null; + let balanceColumn = null; + let educationColumn = null; + let martialColumn = null; + let tableDataRows = []; beforeEach(() => { - const spec = JSON.parse(JSON.stringify(MockSpec2)) - config = {} - Util.initializeConfig(config, spec) - config.chart.current = chart - tableDataRows = JSON.parse(JSON.stringify(MockTableDataRows1)) - ageColumn = JSON.parse(JSON.stringify(MockTableDataColumn[0])) - balanceColumn = JSON.parse(JSON.stringify(MockTableDataColumn[5])) - educationColumn = JSON.parse(JSON.stringify(MockTableDataColumn[3])) - martialColumn = JSON.parse(JSON.stringify(MockTableDataColumn[2])) - }) + const spec = JSON.parse(JSON.stringify(MockSpec2)); + config = {}; + Util.initializeConfig(config, spec); + config.chart.current = chart; + tableDataRows = JSON.parse(JSON.stringify(MockTableDataRows1)); + ageColumn = JSON.parse(JSON.stringify(MockTableDataColumn[0])); + balanceColumn = JSON.parse(JSON.stringify(MockTableDataColumn[5])); + educationColumn = JSON.parse(JSON.stringify(MockTableDataColumn[3])); + martialColumn = JSON.parse(JSON.stringify(MockTableDataColumn[2])); + }); it('should transform properly: 0 key, 0 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer - - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() - - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ '', ]) - expect(groupNames).toEqual([ 'age(sum)', ]) - expect(selectors).toEqual([ 'age(sum)', ]) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; + + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); + + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['']); + expect(groupNames).toEqual(['age(sum)']); + expect(selectors).toEqual(['age(sum)']); expect(rows).toEqual([ - { selector: 'age(sum)', value: [ 159, ], } - ]) - }) + {selector: 'age(sum)', value: [159]}, + ]); + }); it('should transform properly: 0 key, 0 group, 1 aggr(count)', () => { - ageColumn.aggr = 'count' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'count'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - let { rows, } = transformer() + let {rows} = transformer(); expect(rows).toEqual([ - { selector: 'age(count)', value: [ 4, ], } - ]) - }) + {selector: 'age(count)', value: [4]}, + ]); + }); it('should transform properly: 0 key, 0 group, 1 aggr(avg)', () => { - ageColumn.aggr = 'avg' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'avg'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, } = transformer() + const {rows} = transformer(); expect(rows).toEqual([ - { selector: 'age(avg)', value: [ (44 + 43 + 39 + 33) / 4.0, ], } - ]) - }) + {selector: 'age(avg)', value: [(44 + 43 + 39 + 33) / 4.0]}, + ]); + }); it('should transform properly: 0 key, 0 group, 1 aggr(max)', () => { - ageColumn.aggr = 'max' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'max'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, } = transformer() + const {rows} = transformer(); expect(rows).toEqual([ - { selector: 'age(max)', value: [ 44, ], } - ]) - }) + {selector: 'age(max)', value: [44]}, + ]); + }); it('should transform properly: 0 key, 0 group, 1 aggr(min)', () => { - ageColumn.aggr = 'min' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'min'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, } = transformer() + const {rows} = transformer(); expect(rows).toEqual([ - { selector: 'age(min)', value: [ 33, ], } - ]) - }) + {selector: 'age(min)', value: [33]}, + ]); + }); it('should transform properly: 0 key, 0 group, 2 aggr(sum)', () => { - ageColumn.aggr = 'sum' - balanceColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].aggrAxis.push(balanceColumn) + ageColumn.aggr = 'sum'; + balanceColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].aggrAxis.push(balanceColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ '', ]) - expect(groupNames).toEqual([ 'age(sum)', 'balance(sum)', ]) - expect(selectors).toEqual([ 'age(sum)', 'balance(sum)', ]) + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['']); + expect(groupNames).toEqual(['age(sum)', 'balance(sum)']); + expect(selectors).toEqual(['age(sum)', 'balance(sum)']); expect(rows).toEqual([ - { selector: 'age(sum)', value: [ 159, ], }, - { selector: 'balance(sum)', value: [ 14181, ], }, - ]) - }) + {selector: 'age(sum)', value: [159]}, + {selector: 'balance(sum)', value: [14181]}, + ]); + }); it('should transform properly: 0 key, 1 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].groupAxis.push(martialColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].groupAxis.push(martialColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ 'marital', ]) - expect(groupNames).toEqual([ 'married', 'single', ]) - expect(selectors).toEqual([ 'married', 'single', ]) + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['marital']); + expect(groupNames).toEqual(['married', 'single']); + expect(selectors).toEqual(['married', 'single']); expect(rows).toEqual([ - { selector: 'married', value: [ 82, ], }, - { selector: 'single', value: [ 77, ], }, - ]) - }) + {selector: 'married', value: [82]}, + {selector: 'single', value: [77]}, + ]); + }); it('should transform properly: 0 key, 1 group, 2 aggr(sum)', () => { - ageColumn.aggr = 'sum' - balanceColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].aggrAxis.push(balanceColumn) - config.axis[chart].groupAxis.push(martialColumn) + ageColumn.aggr = 'sum'; + balanceColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].aggrAxis.push(balanceColumn); + config.axis[chart].groupAxis.push(martialColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ 'marital', ]) - expect(groupNames).toEqual([ 'married', 'single', ]) + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['marital']); + expect(groupNames).toEqual(['married', 'single']); expect(selectors).toEqual([ 'married / age(sum)', 'married / balance(sum)', 'single / age(sum)', 'single / balance(sum)', - ]) + ]); expect(rows).toEqual([ - { selector: 'married / age(sum)', value: [ 82 ] }, - { selector: 'married / balance(sum)', value: [ 9286 ] }, - { selector: 'single / age(sum)', value: [ 77 ] }, - { selector: 'single / balance(sum)', value: [ 4895 ] }, - ]) - }) + {selector: 'married / age(sum)', value: [82]}, + {selector: 'married / balance(sum)', value: [9286]}, + {selector: 'single / age(sum)', value: [77]}, + {selector: 'single / balance(sum)', value: [4895]}, + ]); + }); it('should transform properly: 0 key, 2 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].groupAxis.push(martialColumn) - config.axis[chart].groupAxis.push(educationColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].groupAxis.push(martialColumn); + config.axis[chart].groupAxis.push(educationColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ 'marital.education', ]) - expect(groupNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ]) - expect(selectors).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ]) + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['marital.education']); + expect(groupNames).toEqual(['married.primary', 'married.secondary', 'single.tertiary']); + expect(selectors).toEqual(['married.primary', 'married.secondary', 'single.tertiary']); expect(rows).toEqual([ - { selector: 'married.primary', value: [ '43' ] }, - { selector: 'married.secondary', value: [ '39' ] }, - { selector: 'single.tertiary', value: [ 77 ] }, - ]) - }) + {selector: 'married.primary', value: ['43']}, + {selector: 'married.secondary', value: ['39']}, + {selector: 'single.tertiary', value: [77]}, + ]); + }); it('should transform properly: 1 key, 0 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].keyAxis.push(martialColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].keyAxis.push(martialColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('marital') - expect(keyNames).toEqual([ 'married', 'single', ]) - expect(groupNames).toEqual([ 'age(sum)', ]) - expect(selectors).toEqual([ 'age(sum)', ]) + expect(keyColumnName).toEqual('marital'); + expect(keyNames).toEqual(['married', 'single']); + expect(groupNames).toEqual(['age(sum)']); + expect(selectors).toEqual(['age(sum)']); expect(rows).toEqual([ - { selector: 'age(sum)', value: [ 82, 77, ] }, - ]) - }) + {selector: 'age(sum)', value: [82, 77]}, + ]); + }); it('should transform properly: 2 key, 0 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].keyAxis.push(martialColumn) - config.axis[chart].keyAxis.push(educationColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].keyAxis.push(martialColumn); + config.axis[chart].keyAxis.push(educationColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('marital.education') - expect(keyNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ]) - expect(groupNames).toEqual([ 'age(sum)', ]) - expect(selectors).toEqual([ 'age(sum)', ]) + expect(keyColumnName).toEqual('marital.education'); + expect(keyNames).toEqual(['married.primary', 'married.secondary', 'single.tertiary']); + expect(groupNames).toEqual(['age(sum)']); + expect(selectors).toEqual(['age(sum)']); expect(rows).toEqual([ - { selector: 'age(sum)', value: [ '43', '39', 77, ] }, - ]) - }) + {selector: 'age(sum)', value: ['43', '39', 77]}, + ]); + }); it('should transform properly: 1 key, 1 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].keyAxis.push(martialColumn) - config.axis[chart].groupAxis.push(educationColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].keyAxis.push(martialColumn); + config.axis[chart].groupAxis.push(educationColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('marital') - expect(keyNames).toEqual([ 'married', 'single', ]) - expect(groupNames).toEqual([ 'primary', 'secondary', 'tertiary', ]) - expect(selectors).toEqual([ 'primary', 'secondary', 'tertiary', ]) + expect(keyColumnName).toEqual('marital'); + expect(keyNames).toEqual(['married', 'single']); + expect(groupNames).toEqual(['primary', 'secondary', 'tertiary']); + expect(selectors).toEqual(['primary', 'secondary', 'tertiary']); expect(rows).toEqual([ - { selector: 'primary', value: [ '43', null, ] }, - { selector: 'secondary', value: [ '39', null, ] }, - { selector: 'tertiary', value: [ null, 77, ] }, - ]) - }) - }) // end: describe('method: array') + {selector: 'primary', value: ['43', null]}, + {selector: 'secondary', value: ['39', null]}, + {selector: 'tertiary', value: [null, 77]}, + ]); + }); + }); // end: describe('method: array') describe('method: object', () => { - let config = {} - const chart = 'object-chart' - let ageColumn = null - let balanceColumn = null - let educationColumn = null - let martialColumn = null - const tableDataRows = JSON.parse(JSON.stringify(MockTableDataRows1)) + let config = {}; + const chart = 'object-chart'; + let ageColumn = null; + let balanceColumn = null; + let educationColumn = null; + let martialColumn = null; + const tableDataRows = JSON.parse(JSON.stringify(MockTableDataRows1)); beforeEach(() => { - const spec = JSON.parse(JSON.stringify(MockSpec2)) - config = {} - Util.initializeConfig(config, spec) - config.chart.current = chart - ageColumn = JSON.parse(JSON.stringify(MockTableDataColumn[0])) - balanceColumn = JSON.parse(JSON.stringify(MockTableDataColumn[5])) - educationColumn = JSON.parse(JSON.stringify(MockTableDataColumn[3])) - martialColumn = JSON.parse(JSON.stringify(MockTableDataColumn[2])) - }) + const spec = JSON.parse(JSON.stringify(MockSpec2)); + config = {}; + Util.initializeConfig(config, spec); + config.chart.current = chart; + ageColumn = JSON.parse(JSON.stringify(MockTableDataColumn[0])); + balanceColumn = JSON.parse(JSON.stringify(MockTableDataColumn[5])); + educationColumn = JSON.parse(JSON.stringify(MockTableDataColumn[3])); + martialColumn = JSON.parse(JSON.stringify(MockTableDataColumn[2])); + }); it('should transform properly: 0 key, 0 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ '', ]) - expect(groupNames).toEqual([ 'age(sum)', ]) - expect(selectors).toEqual([ 'age(sum)', ]) - expect(rows).toEqual([{ 'age(sum)': 44 + 43 + 39 + 33, }]) - }) + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['']); + expect(groupNames).toEqual(['age(sum)']); + expect(selectors).toEqual(['age(sum)']); + expect(rows).toEqual([{'age(sum)': 44 + 43 + 39 + 33}]); + }); it('should transform properly: 0 key, 0 group, 1 aggr(count)', () => { - ageColumn.aggr = 'count' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'count'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, } = transformer() - expect(rows).toEqual([{ 'age(count)': 4, }]) - }) + const {rows} = transformer(); + expect(rows).toEqual([{'age(count)': 4}]); + }); it('should transform properly: 0 key, 0 group, 1 aggr(avg)', () => { - ageColumn.aggr = 'avg' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'avg'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, } = transformer() + const {rows} = transformer(); expect(rows).toEqual([ - { 'age(avg)': (44 + 43 + 39 + 33) / 4.0, } - ]) - }) + {'age(avg)': (44 + 43 + 39 + 33) / 4.0}, + ]); + }); it('should transform properly: 0 key, 0 group, 1 aggr(max)', () => { - ageColumn.aggr = 'max' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'max'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, } = transformer() - expect(rows).toEqual([{ 'age(max)': 44, }]) - }) + const {rows} = transformer(); + expect(rows).toEqual([{'age(max)': 44}]); + }); it('should transform properly: 0 key, 0 group, 1 aggr(min)', () => { - ageColumn.aggr = 'min' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'min'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, } = transformer() - expect(rows).toEqual([{ 'age(min)': 33, }]) - }) + const {rows} = transformer(); + expect(rows).toEqual([{'age(min)': 33}]); + }); it('should transform properly: 0 key, 0 group, 2 aggr(sum)', () => { - ageColumn.aggr = 'sum' - balanceColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].aggrAxis.push(balanceColumn) + ageColumn.aggr = 'sum'; + balanceColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].aggrAxis.push(balanceColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ '', ]) - expect(groupNames).toEqual([ 'age(sum)', 'balance(sum)', ]) - expect(selectors).toEqual([ 'age(sum)', 'balance(sum)', ]) - expect(rows).toEqual([{ 'age(sum)': 159, 'balance(sum)': 14181, }]) - }) + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['']); + expect(groupNames).toEqual(['age(sum)', 'balance(sum)']); + expect(selectors).toEqual(['age(sum)', 'balance(sum)']); + expect(rows).toEqual([{'age(sum)': 159, 'balance(sum)': 14181}]); + }); it('should transform properly: 0 key, 1 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].groupAxis.push(martialColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].groupAxis.push(martialColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ 'marital', ]) - expect(groupNames).toEqual([ 'married', 'single', ]) - expect(selectors).toEqual([ 'married', 'single', ]) + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['marital']); + expect(groupNames).toEqual(['married', 'single']); + expect(selectors).toEqual(['married', 'single']); expect(rows).toEqual([ - { single: 77, married: 82, } - ]) - }) + {single: 77, married: 82}, + ]); + }); it('should transform properly: 0 key, 1 group, 2 aggr(sum)', () => { - ageColumn.aggr = 'sum' - balanceColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].aggrAxis.push(balanceColumn) - config.axis[chart].groupAxis.push(martialColumn) + ageColumn.aggr = 'sum'; + balanceColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].aggrAxis.push(balanceColumn); + config.axis[chart].groupAxis.push(martialColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ 'marital', ]) - expect(groupNames).toEqual([ 'married', 'single', ]) + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['marital']); + expect(groupNames).toEqual(['married', 'single']); expect(selectors).toEqual([ 'married / age(sum)', 'married / balance(sum)', 'single / age(sum)', 'single / balance(sum)', - ]) + ]); expect(rows).toEqual([{ 'married / age(sum)': 82, 'single / age(sum)': 77, 'married / balance(sum)': 9286, 'single / balance(sum)': 4895, - }]) - }) + }]); + }); it('should transform properly: 0 key, 2 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].groupAxis.push(martialColumn) - config.axis[chart].groupAxis.push(educationColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].groupAxis.push(martialColumn); + config.axis[chart].groupAxis.push(educationColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ 'marital.education', ]) - expect(groupNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ]) - expect(selectors).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ]) + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['marital.education']); + expect(groupNames).toEqual(['married.primary', 'married.secondary', 'single.tertiary']); + expect(selectors).toEqual(['married.primary', 'married.secondary', 'single.tertiary']); expect(rows).toEqual([{ 'married.primary': '43', 'married.secondary': '39', 'single.tertiary': 77, - }]) - }) + }]); + }); it('should transform properly: 1 key, 0 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].keyAxis.push(martialColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].keyAxis.push(martialColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('marital') - expect(keyNames).toEqual([ 'married', 'single', ]) - expect(groupNames).toEqual([ 'age(sum)', ]) - expect(selectors).toEqual([ 'age(sum)', ]) + expect(keyColumnName).toEqual('marital'); + expect(keyNames).toEqual(['married', 'single']); + expect(groupNames).toEqual(['age(sum)']); + expect(selectors).toEqual(['age(sum)']); expect(rows).toEqual([ - { 'age(sum)': 82, 'marital': 'married', }, - { 'age(sum)': 77, 'marital': 'single', }, - ]) - }) + {'age(sum)': 82, 'marital': 'married'}, + {'age(sum)': 77, 'marital': 'single'}, + ]); + }); it('should transform properly: 2 key, 0 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].keyAxis.push(martialColumn) - config.axis[chart].keyAxis.push(educationColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].keyAxis.push(martialColumn); + config.axis[chart].keyAxis.push(educationColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('marital.education') - expect(keyNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ]) - expect(groupNames).toEqual([ 'age(sum)', ]) - expect(selectors).toEqual([ 'age(sum)', ]) + expect(keyColumnName).toEqual('marital.education'); + expect(keyNames).toEqual(['married.primary', 'married.secondary', 'single.tertiary']); + expect(groupNames).toEqual(['age(sum)']); + expect(selectors).toEqual(['age(sum)']); expect(rows).toEqual([ - { 'age(sum)': '43', 'marital.education': 'married.primary' }, - { 'age(sum)': '39', 'marital.education': 'married.secondary' }, - { 'age(sum)': 77, 'marital.education': 'single.tertiary' }, - ]) - }) + {'age(sum)': '43', 'marital.education': 'married.primary'}, + {'age(sum)': '39', 'marital.education': 'married.secondary'}, + {'age(sum)': 77, 'marital.education': 'single.tertiary'}, + ]); + }); it('should transform properly: 1 key, 1 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].keyAxis.push(martialColumn) - config.axis[chart].groupAxis.push(educationColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].keyAxis.push(martialColumn); + config.axis[chart].groupAxis.push(educationColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('marital') - expect(keyNames).toEqual([ 'married', 'single', ]) - expect(groupNames).toEqual([ 'primary', 'secondary', 'tertiary', ]) - expect(selectors).toEqual([ 'primary', 'secondary', 'tertiary', ]) + expect(keyColumnName).toEqual('marital'); + expect(keyNames).toEqual(['married', 'single']); + expect(groupNames).toEqual(['primary', 'secondary', 'tertiary']); + expect(selectors).toEqual(['primary', 'secondary', 'tertiary']); expect(rows).toEqual([ - { primary: '43', secondary: '39', marital: 'married' }, - { tertiary: 44 + 33, marital: 'single' }, - ]) - }) - }) // end: describe('method: object') + {primary: '43', secondary: '39', marital: 'married'}, + {tertiary: 44 + 33, marital: 'single'}, + ]); + }); + }); // end: describe('method: object') describe('method: drill-down', () => { - let config = {} - const chart = 'drillDown-chart' - let ageColumn = null - let balanceColumn = null - let educationColumn = null - let martialColumn = null - const tableDataRows = JSON.parse(JSON.stringify(MockTableDataRows1)) + let config = {}; + const chart = 'drillDown-chart'; + let ageColumn = null; + let balanceColumn = null; + let educationColumn = null; + let martialColumn = null; + const tableDataRows = JSON.parse(JSON.stringify(MockTableDataRows1)); beforeEach(() => { - const spec = JSON.parse(JSON.stringify(MockSpec2)) - config = {} - Util.initializeConfig(config, spec) - config.chart.current = chart - ageColumn = JSON.parse(JSON.stringify(MockTableDataColumn[0])) - balanceColumn = JSON.parse(JSON.stringify(MockTableDataColumn[5])) - educationColumn = JSON.parse(JSON.stringify(MockTableDataColumn[3])) - martialColumn = JSON.parse(JSON.stringify(MockTableDataColumn[2])) - }) + const spec = JSON.parse(JSON.stringify(MockSpec2)); + config = {}; + Util.initializeConfig(config, spec); + config.chart.current = chart; + ageColumn = JSON.parse(JSON.stringify(MockTableDataColumn[0])); + balanceColumn = JSON.parse(JSON.stringify(MockTableDataColumn[5])); + educationColumn = JSON.parse(JSON.stringify(MockTableDataColumn[3])); + martialColumn = JSON.parse(JSON.stringify(MockTableDataColumn[2])); + }); it('should transform properly: 0 key, 0 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ '', ]) - expect(groupNames).toEqual([ 'age(sum)', ]) - expect(selectors).toEqual([ 'age(sum)', ]) + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['']); + expect(groupNames).toEqual(['age(sum)']); + expect(selectors).toEqual(['age(sum)']); expect(rows).toEqual([ - { selector: 'age(sum)', value: 44 + 43 + 39 + 33, drillDown: [ ], }, - ]) - }) + {selector: 'age(sum)', value: 44 + 43 + 39 + 33, drillDown: []}, + ]); + }); it('should transform properly: 0 key, 0 group, 1 aggr(count)', () => { - ageColumn.aggr = 'count' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'count'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, } = transformer() + const {rows} = transformer(); expect(rows).toEqual([ - { selector: 'age(count)', value: 4, drillDown: [ ], }, - ]) - }) + {selector: 'age(count)', value: 4, drillDown: []}, + ]); + }); it('should transform properly: 0 key, 0 group, 1 aggr(avg)', () => { - ageColumn.aggr = 'avg' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'avg'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, } = transformer() + const {rows} = transformer(); expect(rows).toEqual([ - { selector: 'age(avg)', value: (44 + 43 + 39 + 33) / 4.0, drillDown: [ ], }, - ]) - }) + {selector: 'age(avg)', value: (44 + 43 + 39 + 33) / 4.0, drillDown: []}, + ]); + }); it('should transform properly: 0 key, 0 group, 1 aggr(max)', () => { - ageColumn.aggr = 'max' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'max'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, } = transformer() + const {rows} = transformer(); expect(rows).toEqual([ - { selector: 'age(max)', value: 44, drillDown: [ ], }, - ]) - }) + {selector: 'age(max)', value: 44, drillDown: []}, + ]); + }); it('should transform properly: 0 key, 0 group, 1 aggr(min)', () => { - ageColumn.aggr = 'min' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'min'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, } = transformer() + const {rows} = transformer(); expect(rows).toEqual([ - { selector: 'age(min)', value: 33, drillDown: [ ], }, - ]) - }) + {selector: 'age(min)', value: 33, drillDown: []}, + ]); + }); it('should transform properly: 0 key, 0 group, 2 aggr(sum)', () => { - ageColumn.aggr = 'sum' - balanceColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].aggrAxis.push(balanceColumn) + ageColumn.aggr = 'sum'; + balanceColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].aggrAxis.push(balanceColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ '', ]) - expect(groupNames).toEqual([ 'age(sum)', 'balance(sum)', ]) - expect(selectors).toEqual([ 'age(sum)', 'balance(sum)', ]) + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['']); + expect(groupNames).toEqual(['age(sum)', 'balance(sum)']); + expect(selectors).toEqual(['age(sum)', 'balance(sum)']); expect(rows).toEqual([ - { selector: 'age(sum)', value: 159, drillDown: [ ], }, - { selector: 'balance(sum)', value: 14181, drillDown: [ ], }, - ]) - }) + {selector: 'age(sum)', value: 159, drillDown: []}, + {selector: 'balance(sum)', value: 14181, drillDown: []}, + ]); + }); it('should transform properly: 0 key, 1 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].groupAxis.push(martialColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].groupAxis.push(martialColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ 'marital', ]) - expect(groupNames).toEqual([ 'married', 'single', ]) - expect(selectors).toEqual([ 'age(sum)', ]) + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['marital']); + expect(groupNames).toEqual(['married', 'single']); + expect(selectors).toEqual(['age(sum)']); expect(rows).toEqual([ { selector: 'age(sum)', value: 159, drillDown: [ - { group: 'married', value: 82 }, - { group: 'single', value: 77 }, + {group: 'married', value: 82}, + {group: 'single', value: 77}, ], }, - ]) - }) + ]); + }); it('should transform properly: 0 key, 1 group, 2 aggr(sum)', () => { - ageColumn.aggr = 'sum' - balanceColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].aggrAxis.push(balanceColumn) - config.axis[chart].groupAxis.push(martialColumn) - - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer - - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() - - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ 'marital', ]) - expect(groupNames).toEqual([ 'married', 'single', ]) - expect(selectors).toEqual([ 'age(sum)', 'balance(sum)' ]) + ageColumn.aggr = 'sum'; + balanceColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].aggrAxis.push(balanceColumn); + config.axis[chart].groupAxis.push(martialColumn); + + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; + + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); + + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['marital']); + expect(groupNames).toEqual(['married', 'single']); + expect(selectors).toEqual(['age(sum)', 'balance(sum)']); expect(rows).toEqual([ { selector: 'age(sum)', value: 159, drillDown: [ - { group: 'married', value: 82 }, - { group: 'single', value: 77 }, + {group: 'married', value: 82}, + {group: 'single', value: 77}, ], }, { selector: 'balance(sum)', value: 14181, drillDown: [ - { group: 'married', value: 9286 }, - { group: 'single', value: 4895 }, + {group: 'married', value: 9286}, + {group: 'single', value: 4895}, ], }, - ]) - }) + ]); + }); it('should transform properly: 0 key, 2 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].groupAxis.push(martialColumn) - config.axis[chart].groupAxis.push(educationColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].groupAxis.push(martialColumn); + config.axis[chart].groupAxis.push(educationColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('') - expect(keyNames).toEqual([ 'marital.education', ]) - expect(groupNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ]) - expect(selectors).toEqual([ 'age(sum)', ]) + expect(keyColumnName).toEqual(''); + expect(keyNames).toEqual(['marital.education']); + expect(groupNames).toEqual(['married.primary', 'married.secondary', 'single.tertiary']); + expect(selectors).toEqual(['age(sum)']); expect(rows).toEqual([ { selector: 'age(sum)', value: 159, drillDown: [ - { group: 'married.primary', value: '43' }, - { group: 'married.secondary', value: '39' }, - { group: 'single.tertiary', value: 77 }, + {group: 'married.primary', value: '43'}, + {group: 'married.secondary', value: '39'}, + {group: 'single.tertiary', value: 77}, ], }, - ]) - }) + ]); + }); it('should transform properly: 1 key, 0 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].keyAxis.push(martialColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].keyAxis.push(martialColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('marital') - expect(keyNames).toEqual([ 'married', 'single', ]) - expect(groupNames).toEqual([ 'age(sum)', ]) - expect(selectors).toEqual([ 'married', 'single', ]) + expect(keyColumnName).toEqual('marital'); + expect(keyNames).toEqual(['married', 'single']); + expect(groupNames).toEqual(['age(sum)']); + expect(selectors).toEqual(['married', 'single']); expect(rows).toEqual([ - { selector: 'married', value: 82, drillDown: [ ], }, - { selector: 'single', value: 77, drillDown: [ ], }, - ]) - }) + {selector: 'married', value: 82, drillDown: []}, + {selector: 'single', value: 77, drillDown: []}, + ]); + }); it('should transform properly: 2 key, 0 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].keyAxis.push(martialColumn) - config.axis[chart].keyAxis.push(educationColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].keyAxis.push(martialColumn); + config.axis[chart].keyAxis.push(educationColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('marital.education') - expect(keyNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ]) - expect(groupNames).toEqual([ 'age(sum)', ]) - expect(selectors).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ]) + expect(keyColumnName).toEqual('marital.education'); + expect(keyNames).toEqual(['married.primary', 'married.secondary', 'single.tertiary']); + expect(groupNames).toEqual(['age(sum)']); + expect(selectors).toEqual(['married.primary', 'married.secondary', 'single.tertiary']); expect(rows).toEqual([ - { selector: 'married.primary', value: '43', drillDown: [ ], }, - { selector: 'married.secondary', value: '39', drillDown: [ ], }, - { selector: 'single.tertiary', value: 77, drillDown: [ ], }, - ]) - }) + {selector: 'married.primary', value: '43', drillDown: []}, + {selector: 'married.secondary', value: '39', drillDown: []}, + {selector: 'single.tertiary', value: 77, drillDown: []}, + ]); + }); it('should transform properly: 1 key, 1 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].keyAxis.push(martialColumn) - config.axis[chart].groupAxis.push(educationColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].keyAxis.push(martialColumn); + config.axis[chart].groupAxis.push(educationColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, keyColumnName, keyNames, groupNames, selectors, } = transformer() + const {rows, keyColumnName, keyNames, groupNames, selectors} = transformer(); - expect(keyColumnName).toEqual('marital') - expect(keyNames).toEqual([ 'married', 'single', ]) - expect(groupNames).toEqual([ 'primary', 'secondary', 'tertiary', ]) - expect(selectors).toEqual([ 'married', 'single', ]) + expect(keyColumnName).toEqual('marital'); + expect(keyNames).toEqual(['married', 'single']); + expect(groupNames).toEqual(['primary', 'secondary', 'tertiary']); + expect(selectors).toEqual(['married', 'single']); expect(rows).toEqual([ { selector: 'married', value: 82, drillDown: [ - { group: 'primary', value: '43' }, - { group: 'secondary', value: '39' }, - { group: 'tertiary', value: null }, + {group: 'primary', value: '43'}, + {group: 'secondary', value: '39'}, + {group: 'tertiary', value: null}, ], }, { selector: 'single', value: 77, drillDown: [ - { group: 'primary', value: null }, - { group: 'secondary', value: null }, - { group: 'tertiary', value: 77 }, + {group: 'primary', value: null}, + {group: 'secondary', value: null}, + {group: 'tertiary', value: 77}, ], }, - ]) - }) - }) // end: describe('method: drill-down') + ]); + }); + }); // end: describe('method: drill-down') describe('method: array:2-key', () => { - let config = {} - const chart = 'array2Key-chart' - let ageColumn = null - let balanceColumn = null - let educationColumn = null - let martialColumn = null - let daysColumn = null - let pDaysColumn = null - const tableDataRows = JSON.parse(JSON.stringify(MockTableDataRows1)) + let config = {}; + const chart = 'array2Key-chart'; + let ageColumn = null; + let balanceColumn = null; + let educationColumn = null; + let martialColumn = null; + let daysColumn = null; + let pDaysColumn = null; + const tableDataRows = JSON.parse(JSON.stringify(MockTableDataRows1)); beforeEach(() => { - const spec = JSON.parse(JSON.stringify(MockSpec2)) - config = {} - Util.initializeConfig(config, spec) - config.chart.current = chart - ageColumn = JSON.parse(JSON.stringify(MockTableDataColumn[0])) - martialColumn = JSON.parse(JSON.stringify(MockTableDataColumn[2])) - educationColumn = JSON.parse(JSON.stringify(MockTableDataColumn[3])) - balanceColumn = JSON.parse(JSON.stringify(MockTableDataColumn[5])) - daysColumn = JSON.parse(JSON.stringify(MockTableDataColumn[9])) - pDaysColumn = JSON.parse(JSON.stringify(MockTableDataColumn[13])) - }) + const spec = JSON.parse(JSON.stringify(MockSpec2)); + config = {}; + Util.initializeConfig(config, spec); + config.chart.current = chart; + ageColumn = JSON.parse(JSON.stringify(MockTableDataColumn[0])); + martialColumn = JSON.parse(JSON.stringify(MockTableDataColumn[2])); + educationColumn = JSON.parse(JSON.stringify(MockTableDataColumn[3])); + balanceColumn = JSON.parse(JSON.stringify(MockTableDataColumn[5])); + daysColumn = JSON.parse(JSON.stringify(MockTableDataColumn[9])); + pDaysColumn = JSON.parse(JSON.stringify(MockTableDataColumn[13])); + }); it('should transform properly: 0 key1, 0 key2, 0 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, key1Names, key2Names, selectors, } = transformer() + const {rows, key1Names, key2Names, selectors} = transformer(); - expect(key1Names).toEqual([]) - expect(key2Names).toEqual([]) - expect(selectors).toEqual([ 'age(sum)', ]) + expect(key1Names).toEqual([]); + expect(key2Names).toEqual([]); + expect(selectors).toEqual(['age(sum)']); expect(rows).toEqual([ - { selector: 'age(sum)', value: [ { aggregated: 44 + 43 + 39 + 33, }, ] }, - ]) - }) + {selector: 'age(sum)', value: [{aggregated: 44 + 43 + 39 + 33}]}, + ]); + }); it('should transform properly: 0 key1, 0 key2, 0 group, 1 aggr(count)', () => { - ageColumn.aggr = 'count' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'count'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, key1Names, key2Names, selectors, } = transformer() + const {rows, key1Names, key2Names, selectors} = transformer(); - expect(key1Names).toEqual([]) - expect(key2Names).toEqual([]) - expect(selectors).toEqual([ 'age(count)', ]) + expect(key1Names).toEqual([]); + expect(key2Names).toEqual([]); + expect(selectors).toEqual(['age(count)']); expect(rows).toEqual([ - { selector: 'age(count)', value: [ { aggregated: 4, }, ] }, - ]) - }) + {selector: 'age(count)', value: [{aggregated: 4}]}, + ]); + }); it('should transform properly: 0 key1, 0 key2, 0 group, 1 aggr(avg)', () => { - ageColumn.aggr = 'avg' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'avg'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, key1Names, key2Names, selectors, } = transformer() + const {rows, key1Names, key2Names, selectors} = transformer(); - expect(key1Names).toEqual([]) - expect(key2Names).toEqual([]) - expect(selectors).toEqual([ 'age(avg)', ]) + expect(key1Names).toEqual([]); + expect(key2Names).toEqual([]); + expect(selectors).toEqual(['age(avg)']); expect(rows).toEqual([ - { selector: 'age(avg)', value: [ { aggregated: (44 + 43 + 39 + 33) / 4.0, }, ] }, - ]) - }) + {selector: 'age(avg)', value: [{aggregated: (44 + 43 + 39 + 33) / 4.0}]}, + ]); + }); it('should transform properly: 0 key1, 0 key2, 0 group, 1 aggr(max)', () => { - ageColumn.aggr = 'max' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'max'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, key1Names, key2Names, selectors, } = transformer() + const {rows, key1Names, key2Names, selectors} = transformer(); - expect(key1Names).toEqual([]) - expect(key2Names).toEqual([]) - expect(selectors).toEqual([ 'age(max)', ]) + expect(key1Names).toEqual([]); + expect(key2Names).toEqual([]); + expect(selectors).toEqual(['age(max)']); expect(rows).toEqual([ - { selector: 'age(max)', value: [ { aggregated: 44, }, ] }, - ]) - }) + {selector: 'age(max)', value: [{aggregated: 44}]}, + ]); + }); it('should transform properly: 0 key1, 0 key2, 0 group, 1 aggr(min)', () => { - ageColumn.aggr = 'min' - config.axis[chart].aggrAxis.push(ageColumn) + ageColumn.aggr = 'min'; + config.axis[chart].aggrAxis.push(ageColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, key1Names, key2Names, selectors, } = transformer() + const {rows, key1Names, key2Names, selectors} = transformer(); - expect(key1Names).toEqual([]) - expect(key2Names).toEqual([]) - expect(selectors).toEqual([ 'age(min)', ]) + expect(key1Names).toEqual([]); + expect(key2Names).toEqual([]); + expect(selectors).toEqual(['age(min)']); expect(rows).toEqual([ - { selector: 'age(min)', value: [ { aggregated: 33, }, ] }, - ]) - }) + {selector: 'age(min)', value: [{aggregated: 33}]}, + ]); + }); it('should transform properly: 0 key1, 0 key2, 0 group, 2 aggr(sum)', () => { - ageColumn.aggr = 'sum' - balanceColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].aggrAxis.push(balanceColumn) + ageColumn.aggr = 'sum'; + balanceColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].aggrAxis.push(balanceColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, groupNames, selectors, } = transformer() + const {rows, groupNames, selectors} = transformer(); - expect(groupNames).toEqual([ 'age(sum)', 'balance(sum)', ]) - expect(selectors).toEqual([ 'age(sum)', 'balance(sum)', ]) + expect(groupNames).toEqual(['age(sum)', 'balance(sum)']); + expect(selectors).toEqual(['age(sum)', 'balance(sum)']); expect(rows).toEqual([ - { selector: 'age(sum)', value: [ { aggregated: 159 } ] }, - { selector: 'balance(sum)', value: [ { aggregated: 14181 }, ] }, - ]) - }) + {selector: 'age(sum)', value: [{aggregated: 159}]}, + {selector: 'balance(sum)', value: [{aggregated: 14181}]}, + ]); + }); it('should transform properly: 0 key1, 0 key2, 1 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].groupAxis.push(martialColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].groupAxis.push(martialColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, groupNames, selectors, } = transformer() + const {rows, groupNames, selectors} = transformer(); - expect(groupNames).toEqual([ 'married', 'single', ]) - expect(selectors).toEqual([ 'married', 'single', ]) + expect(groupNames).toEqual(['married', 'single']); + expect(selectors).toEqual(['married', 'single']); expect(rows).toEqual([ - { selector: 'married', value: [ { aggregated: 82 }, ] }, - { selector: 'single', value: [ { aggregated: 77 }, ] }, - ]) - }) + {selector: 'married', value: [{aggregated: 82}]}, + {selector: 'single', value: [{aggregated: 77}]}, + ]); + }); it('should transform properly: 0 key1, 0 key2, 1 group, 2 aggr(sum)', () => { - ageColumn.aggr = 'sum' - balanceColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].aggrAxis.push(balanceColumn) - config.axis[chart].groupAxis.push(martialColumn) + ageColumn.aggr = 'sum'; + balanceColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].aggrAxis.push(balanceColumn); + config.axis[chart].groupAxis.push(martialColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, groupNames, selectors, } = transformer() + const {rows, groupNames, selectors} = transformer(); - expect(groupNames).toEqual([ 'married', 'single', ]) + expect(groupNames).toEqual(['married', 'single']); expect(selectors).toEqual([ 'married / age(sum)', 'married / balance(sum)', 'single / age(sum)', 'single / balance(sum)', - ]) + ]); expect(rows).toEqual([ - { selector: 'married / age(sum)', value: [ { aggregated: 82 }, ] }, - { selector: 'married / balance(sum)', value: [ { aggregated: 9286 }, ] }, - { selector: 'single / age(sum)', value: [ { aggregated: 77 }, ] }, - { selector: 'single / balance(sum)', value: [ { aggregated: 4895 }, ] }, - ]) - }) + {selector: 'married / age(sum)', value: [{aggregated: 82}]}, + {selector: 'married / balance(sum)', value: [{aggregated: 9286}]}, + {selector: 'single / age(sum)', value: [{aggregated: 77}]}, + {selector: 'single / balance(sum)', value: [{aggregated: 4895}]}, + ]); + }); it('should transform properly: 0 key1, 0 key2, 2 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].groupAxis.push(martialColumn) - config.axis[chart].groupAxis.push(educationColumn) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].groupAxis.push(martialColumn); + config.axis[chart].groupAxis.push(educationColumn); - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; - const { rows, groupNames, selectors, } = transformer() + const {rows, groupNames, selectors} = transformer(); - expect(groupNames).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ]) - expect(selectors).toEqual([ 'married.primary', 'married.secondary', 'single.tertiary', ]) + expect(groupNames).toEqual(['married.primary', 'married.secondary', 'single.tertiary']); + expect(selectors).toEqual(['married.primary', 'married.secondary', 'single.tertiary']); expect(rows).toEqual([ - { selector: 'married.primary', value: [ { aggregated: '43' }, ] }, - { selector: 'married.secondary', value: [ { aggregated: '39' }, ] }, - { selector: 'single.tertiary', value: [ { aggregated: 77 }, ] }, - ]) - }) + {selector: 'married.primary', value: [{aggregated: '43'}]}, + {selector: 'married.secondary', value: [{aggregated: '39'}]}, + {selector: 'single.tertiary', value: [{aggregated: 77}]}, + ]); + }); it('should transform properly: 1 key1, 0 key2, 0 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].key1Axis.push(balanceColumn) - - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer - - const { rows, key1Names, key1ColumnName, - key2Names, key2ColumnName, groupNames, selectors, } = transformer() - - expect(key1Names).toEqual([ '-88', '106', '4789', '9374' ]) - expect(key1ColumnName).toEqual('balance') - expect(key2Names).toEqual([]) - expect(key2ColumnName).toEqual('') - expect(groupNames).toEqual([ 'age(sum)', ]) - expect(selectors).toEqual([ 'age(sum)', ]) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].key1Axis.push(balanceColumn); + + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; + + const {rows, key1Names, key1ColumnName, + key2Names, key2ColumnName, groupNames, selectors} = transformer(); + + expect(key1Names).toEqual(['-88', '106', '4789', '9374']); + expect(key1ColumnName).toEqual('balance'); + expect(key2Names).toEqual([]); + expect(key2ColumnName).toEqual(''); + expect(groupNames).toEqual(['age(sum)']); + expect(selectors).toEqual(['age(sum)']); expect(rows).toEqual([ { selector: 'age(sum)', value: [ - { aggregated: '43', key1: '-88' }, - { aggregated: '44', key1: '106' }, - { aggregated: '33', key1: '4789' }, - { aggregated: '39', key1: '9374' }, - ] - } - ]) - }) + {aggregated: '43', key1: '-88'}, + {aggregated: '44', key1: '106'}, + {aggregated: '33', key1: '4789'}, + {aggregated: '39', key1: '9374'}, + ], + }, + ]); + }); it('should transform properly: 0 key1, 1 key2, 0 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].key2Axis.push(balanceColumn) - - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer - - const { rows, key1Names, key1ColumnName, - key2Names, key2ColumnName, groupNames, selectors, } = transformer() - - expect(key1Names).toEqual([]) - expect(key1ColumnName).toEqual('') - expect(key2Names).toEqual([ '-88', '106', '4789', '9374' ]) - expect(key2ColumnName).toEqual('balance') - expect(groupNames).toEqual([ 'age(sum)', ]) - expect(selectors).toEqual([ 'age(sum)', ]) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].key2Axis.push(balanceColumn); + + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; + + const {rows, key1Names, key1ColumnName, + key2Names, key2ColumnName, groupNames, selectors} = transformer(); + + expect(key1Names).toEqual([]); + expect(key1ColumnName).toEqual(''); + expect(key2Names).toEqual(['-88', '106', '4789', '9374']); + expect(key2ColumnName).toEqual('balance'); + expect(groupNames).toEqual(['age(sum)']); + expect(selectors).toEqual(['age(sum)']); expect(rows).toEqual([ { selector: 'age(sum)', value: [ - { aggregated: '43', key2: '-88' }, - { aggregated: '44', key2: '106' }, - { aggregated: '33', key2: '4789' }, - { aggregated: '39', key2: '9374' }, - ] + {aggregated: '43', key2: '-88'}, + {aggregated: '44', key2: '106'}, + {aggregated: '33', key2: '4789'}, + {aggregated: '39', key2: '9374'}, + ], }, - ]) - }) + ]); + }); it('should transform properly: 1 key1, 0 key2, 1 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].key1Axis.push(balanceColumn) - config.axis[chart].groupAxis.push(educationColumn) - - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer - - const { rows, key1Names, key1ColumnName, - key2Names, key2ColumnName, groupNames, selectors, } = transformer() - - expect(key1Names).toEqual([ '-88', '106', '4789', '9374' ]) - expect(key1ColumnName).toEqual('balance') - expect(key2Names).toEqual([]) - expect(key2ColumnName).toEqual('') - expect(groupNames).toEqual([ 'primary', 'secondary', 'tertiary', ]) - expect(selectors).toEqual([ 'primary', 'secondary', 'tertiary', ]) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].key1Axis.push(balanceColumn); + config.axis[chart].groupAxis.push(educationColumn); + + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; + + const {rows, key1Names, key1ColumnName, + key2Names, key2ColumnName, groupNames, selectors} = transformer(); + + expect(key1Names).toEqual(['-88', '106', '4789', '9374']); + expect(key1ColumnName).toEqual('balance'); + expect(key2Names).toEqual([]); + expect(key2ColumnName).toEqual(''); + expect(groupNames).toEqual(['primary', 'secondary', 'tertiary']); + expect(selectors).toEqual(['primary', 'secondary', 'tertiary']); expect(rows).toEqual([ - { selector: 'primary', value: [ { aggregated: '43', key1: '-88' }, ] }, - { selector: 'secondary', value: [ { aggregated: '39', key1: '9374' }, ] }, + {selector: 'primary', value: [{aggregated: '43', key1: '-88'}]}, + {selector: 'secondary', value: [{aggregated: '39', key1: '9374'}]}, { selector: 'tertiary', value: [ - { aggregated: '44', key1: '106' }, - { aggregated: '33', key1: '4789' }, - ] + {aggregated: '44', key1: '106'}, + {aggregated: '33', key1: '4789'}, + ], }, - ]) - }) + ]); + }); it('should transform properly: 0 key1, 1 key2, 1 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].key2Axis.push(balanceColumn) - config.axis[chart].groupAxis.push(educationColumn) - - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer - - const { rows, key1Names, key1ColumnName, - key2Names, key2ColumnName, groupNames, selectors, } = transformer() - - expect(key1Names).toEqual([]) - expect(key1ColumnName).toEqual('') - expect(key2Names).toEqual([ '-88', '106', '4789', '9374' ]) - expect(key2ColumnName).toEqual('balance') - expect(groupNames).toEqual([ 'primary', 'secondary', 'tertiary', ]) - expect(selectors).toEqual([ 'primary', 'secondary', 'tertiary', ]) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].key2Axis.push(balanceColumn); + config.axis[chart].groupAxis.push(educationColumn); + + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; + + const {rows, key1Names, key1ColumnName, + key2Names, key2ColumnName, groupNames, selectors} = transformer(); + + expect(key1Names).toEqual([]); + expect(key1ColumnName).toEqual(''); + expect(key2Names).toEqual(['-88', '106', '4789', '9374']); + expect(key2ColumnName).toEqual('balance'); + expect(groupNames).toEqual(['primary', 'secondary', 'tertiary']); + expect(selectors).toEqual(['primary', 'secondary', 'tertiary']); expect(rows).toEqual([ - { selector: 'primary', value: [ { aggregated: '43', key2: '-88' }, ] }, - { selector: 'secondary', value: [ { aggregated: '39', key2: '9374' }, ] }, + {selector: 'primary', value: [{aggregated: '43', key2: '-88'}]}, + {selector: 'secondary', value: [{aggregated: '39', key2: '9374'}]}, { selector: 'tertiary', value: [ - { aggregated: '44', key2: '106' }, - { aggregated: '33', key2: '4789' }, - ] + {aggregated: '44', key2: '106'}, + {aggregated: '33', key2: '4789'}, + ], }, - ]) - }) + ]); + }); it('should transform properly: 1 key1, 1 key2, 1 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].key1Axis.push(pDaysColumn) - config.axis[chart].key2Axis.push(balanceColumn) - config.axis[chart].groupAxis.push(educationColumn) - - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer - - const { rows, key1Names, key1ColumnName, - key2Names, key2ColumnName, groupNames, selectors, } = transformer() - - expect(key1Names).toEqual([ '-1', '147', '339', ]) - expect(key1ColumnName).toEqual('pdays') - expect(key2Names).toEqual([ '-88', '106', '4789', '9374' ]) - expect(key2ColumnName).toEqual('balance') - expect(groupNames).toEqual([ 'primary', 'secondary', 'tertiary', ]) - expect(selectors).toEqual([ 'primary', 'secondary', 'tertiary', ]) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].key1Axis.push(pDaysColumn); + config.axis[chart].key2Axis.push(balanceColumn); + config.axis[chart].groupAxis.push(educationColumn); + + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; + + const {rows, key1Names, key1ColumnName, + key2Names, key2ColumnName, groupNames, selectors} = transformer(); + + expect(key1Names).toEqual(['-1', '147', '339']); + expect(key1ColumnName).toEqual('pdays'); + expect(key2Names).toEqual(['-88', '106', '4789', '9374']); + expect(key2ColumnName).toEqual('balance'); + expect(groupNames).toEqual(['primary', 'secondary', 'tertiary']); + expect(selectors).toEqual(['primary', 'secondary', 'tertiary']); expect(rows).toEqual([ { selector: 'primary', - value: [ { aggregated: '43', key1: '147', key2: '-88' }, ] + value: [{aggregated: '43', key1: '147', key2: '-88'}], }, { selector: 'secondary', - value: [ { aggregated: '39', key1: '-1', key2: '9374' }, ] + value: [{aggregated: '39', key1: '-1', key2: '9374'}], }, { selector: 'tertiary', value: [ - { aggregated: '44', key1: '-1', key2: '106' }, - { aggregated: '33', key1: '339', key2: '4789' }, - ] + {aggregated: '44', key1: '-1', key2: '106'}, + {aggregated: '33', key1: '339', key2: '4789'}, + ], }, - ]) - }) + ]); + }); it('should transform properly: 1 key1, 1 key2, 2 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'sum' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].key1Axis.push(pDaysColumn) - config.axis[chart].key2Axis.push(balanceColumn) - config.axis[chart].groupAxis.push(educationColumn) - config.axis[chart].groupAxis.push(martialColumn) - - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer - - const { rows, key1Names, key1ColumnName, - key2Names, key2ColumnName, groupNames, selectors, } = transformer() - - expect(key1Names).toEqual([ '-1', '147', '339', ]) - expect(key1ColumnName).toEqual('pdays') - expect(key2Names).toEqual([ '-88', '106', '4789', '9374' ]) - expect(key2ColumnName).toEqual('balance') - expect(groupNames).toEqual([ 'primary.married', 'secondary.married', 'tertiary.single', ]) - expect(selectors).toEqual([ 'primary.married', 'secondary.married', 'tertiary.single', ]) + ageColumn.aggr = 'sum'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].key1Axis.push(pDaysColumn); + config.axis[chart].key2Axis.push(balanceColumn); + config.axis[chart].groupAxis.push(educationColumn); + config.axis[chart].groupAxis.push(martialColumn); + + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; + + const {rows, key1Names, key1ColumnName, + key2Names, key2ColumnName, groupNames, selectors} = transformer(); + + expect(key1Names).toEqual(['-1', '147', '339']); + expect(key1ColumnName).toEqual('pdays'); + expect(key2Names).toEqual(['-88', '106', '4789', '9374']); + expect(key2ColumnName).toEqual('balance'); + expect(groupNames).toEqual(['primary.married', 'secondary.married', 'tertiary.single']); + expect(selectors).toEqual(['primary.married', 'secondary.married', 'tertiary.single']); expect(rows).toEqual([ { selector: 'primary.married', - value: [ { aggregated: '43', key1: '147', key2: '-88'}, ] + value: [{aggregated: '43', key1: '147', key2: '-88'}], }, { selector: 'secondary.married', - value: [ { aggregated: '39', key1: '-1', key2: '9374' }, ] + value: [{aggregated: '39', key1: '-1', key2: '9374'}], }, { selector: 'tertiary.single', value: [ - { aggregated: '44', key1: '-1', key2: '106' }, - { aggregated: '33', key1: '339', key2: '4789' }, - ] + {aggregated: '44', key1: '-1', key2: '106'}, + {aggregated: '33', key1: '339', key2: '4789'}, + ], }, - ]) - }) + ]); + }); it('should transform properly: 1 key1, 1 key2, 2 group, 1 aggr(sum)', () => { - ageColumn.aggr = 'min' - daysColumn.aggr = 'max' - config.axis[chart].aggrAxis.push(ageColumn) - config.axis[chart].aggrAxis.push(daysColumn) - config.axis[chart].key1Axis.push(pDaysColumn) - config.axis[chart].key2Axis.push(balanceColumn) - config.axis[chart].groupAxis.push(martialColumn) - - const axisSpecs = config.axisSpecs[chart] - const axis = config.axis[chart] - const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer - - const { rows, key1Names, key1ColumnName, - key2Names, key2ColumnName, groupNames, selectors, } = transformer() - - expect(key1Names).toEqual([ '-1', '147', '339', ]) - expect(key1ColumnName).toEqual('pdays') - expect(key2Names).toEqual([ '-88', '106', '4789', '9374' ]) - expect(key2ColumnName).toEqual('balance') - expect(groupNames).toEqual([ 'married', 'single', ]) + ageColumn.aggr = 'min'; + daysColumn.aggr = 'max'; + config.axis[chart].aggrAxis.push(ageColumn); + config.axis[chart].aggrAxis.push(daysColumn); + config.axis[chart].key1Axis.push(pDaysColumn); + config.axis[chart].key2Axis.push(balanceColumn); + config.axis[chart].groupAxis.push(martialColumn); + + const axisSpecs = config.axisSpecs[chart]; + const axis = config.axis[chart]; + const transformer = Util.getTransformer(config, tableDataRows, axisSpecs, axis).transformer; + + const {rows, key1Names, key1ColumnName, + key2Names, key2ColumnName, groupNames, selectors} = transformer(); + + expect(key1Names).toEqual(['-1', '147', '339']); + expect(key1ColumnName).toEqual('pdays'); + expect(key2Names).toEqual(['-88', '106', '4789', '9374']); + expect(key2ColumnName).toEqual('balance'); + expect(groupNames).toEqual(['married', 'single']); expect(selectors).toEqual( - [ 'married / age(min)', 'married / day(max)', 'single / age(min)', 'single / day(max)', ] - ) + ['married / age(min)', 'married / day(max)', 'single / age(min)', 'single / day(max)'] + ); expect(rows).toEqual([ { selector: 'married / age(min)', value: [ - { aggregated: '39', key1: '-1', key2: '9374' }, - { aggregated: '43', key1: '147', key2: '-88' }, - ] + {aggregated: '39', key1: '-1', key2: '9374'}, + {aggregated: '43', key1: '147', key2: '-88'}, + ], }, { selector: 'married / day(max)', value: [ - { aggregated: '20', key1: '-1', key2: '9374' }, - { aggregated: '17', key1: '147', key2: '-88' }, - ] + {aggregated: '20', key1: '-1', key2: '9374'}, + {aggregated: '17', key1: '147', key2: '-88'}, + ], }, { selector: 'single / age(min)', value: [ - { aggregated: '44', key1: '-1', key2: '106' }, - { aggregated: '33', key1: '339', key2: '4789' }, - ] + {aggregated: '44', key1: '-1', key2: '106'}, + {aggregated: '33', key1: '339', key2: '4789'}, + ], }, { selector: 'single / day(max)', value: [ - { aggregated: '12', key1: '-1', key2: '106' }, - { aggregated: '11', key1: '339', key2: '4789' }, - ] + {aggregated: '12', key1: '-1', key2: '106'}, + {aggregated: '11', key1: '339', key2: '4789'}, + ], }, - ]) - }) - }) // end: describe('method: array:2-key') - }) // end: describe('getTransformer') -}) + ]); + }); + }); // end: describe('method: array:2-key') + }); // end: describe('getTransformer') +}); diff --git a/zeppelin-web/src/app/tabledata/advanced-transformation.js b/zeppelin-web/src/app/tabledata/advanced-transformation.js index 8650de53046..7420bede811 100644 --- a/zeppelin-web/src/app/tabledata/advanced-transformation.js +++ b/zeppelin-web/src/app/tabledata/advanced-transformation.js @@ -12,7 +12,7 @@ * limitations under the License. */ -import Transformation from './transformation' +import Transformation from './transformation'; import { getCurrentChart, getCurrentChartAxis, getCurrentChartParam, @@ -23,46 +23,46 @@ import { removeDuplicatedColumnsInMultiDimensionAxis, applyMaxAxisCount, isInputWidget, isOptionWidget, isCheckboxWidget, isTextareaWidget, parseParameter, getTransformer, -} from './advanced-transformation-util' +} from './advanced-transformation-util'; -const SETTING_TEMPLATE = 'app/tabledata/advanced-transformation-setting.html' +const SETTING_TEMPLATE = 'app/tabledata/advanced-transformation-setting.html'; export default class AdvancedTransformation extends Transformation { constructor(config, spec) { - super(config) + super(config); - this.columns = [] /** [{ name, index, comment }] */ - this.props = {} - this.spec = spec + this.columns = []; /** [{ name, index, comment }] */ + this.props = {}; + this.spec = spec; - initializeConfig(config, spec) + initializeConfig(config, spec); } emitConfigChange(conf) { - conf.chartChanged = false - conf.parameterChanged = false - this.emitConfig(conf) + conf.chartChanged = false; + conf.parameterChanged = false; + this.emitConfig(conf); } emitChartChange(conf) { - conf.chartChanged = true - conf.parameterChanged = false - this.emitConfig(conf) + conf.chartChanged = true; + conf.parameterChanged = false; + this.emitConfig(conf); } emitParameterChange(conf) { - conf.chartChanged = false - conf.parameterChanged = true - this.emitConfig(conf) + conf.chartChanged = false; + conf.parameterChanged = true; + this.emitConfig(conf); } getSetting() { - const self = this /** for closure */ - const configInstance = self.config /** for closure */ + const self = this; /** for closure */ + const configInstance = self.config; /** for closure */ if (self.spec.initialized) { - self.spec.initialized = false - self.emitConfig(configInstance) + self.spec.initialized = false; + self.emitConfig(configInstance); } return { @@ -71,148 +71,174 @@ export default class AdvancedTransformation extends Transformation { config: configInstance, columns: self.columns, resetAxisConfig: () => { - resetAxisConfig(configInstance) - self.emitChartChange(configInstance) + resetAxisConfig(configInstance); + self.emitChartChange(configInstance); }, resetParameterConfig: () => { - resetParameterConfig(configInstance) - self.emitParameterChange(configInstance) + resetParameterConfig(configInstance); + self.emitParameterChange(configInstance); }, toggleColumnPanel: () => { - configInstance.panel.columnPanelOpened = !configInstance.panel.columnPanelOpened - self.emitConfigChange(configInstance) + configInstance.panel.columnPanelOpened = !configInstance.panel.columnPanelOpened; + self.emitConfigChange(configInstance); }, toggleParameterPanel: () => { - configInstance.panel.parameterPanelOpened = !configInstance.panel.parameterPanelOpened - self.emitConfigChange(configInstance) + configInstance.panel.parameterPanelOpened = !configInstance.panel.parameterPanelOpened; + self.emitConfigChange(configInstance); }, getAxisAnnotation: (axisSpec) => { - let anno = `${axisSpec.name}` + let anno = `${axisSpec.name}`; if (axisSpec.valueType) { - anno = `${anno} (${axisSpec.valueType})` + anno = `${anno} (${axisSpec.valueType})`; } - return anno + return anno; }, getAxisTypeAnnotation: (axisSpec) => { - let anno = '' + let anno = ''; - let minAxisCount = axisSpec.minAxisCount - let maxAxisCount = axisSpec.maxAxisCount + let minAxisCount = axisSpec.minAxisCount; + let maxAxisCount = axisSpec.maxAxisCount; if (isSingleDimensionAxis(axisSpec)) { - maxAxisCount = 1 + maxAxisCount = 1; } - let comment = '' - if (minAxisCount) { comment = `min: ${minAxisCount}` } - if (minAxisCount && maxAxisCount) { comment = `${comment}, ` } - if (maxAxisCount) { comment = `${comment}max: ${maxAxisCount}` } + let comment = ''; + if (minAxisCount) { + comment = `min: ${minAxisCount}`; + } + if (minAxisCount && maxAxisCount) { + comment = `${comment}, `; + } + if (maxAxisCount) { + comment = `${comment}max: ${maxAxisCount}`; + } if (comment !== '') { - anno = `${anno} (${comment})` + anno = `${anno} (${comment})`; } - return anno + return anno; }, getAxisAnnotationColor: (axisSpec) => { if (isAggregatorAxis(axisSpec)) { - return { 'background-color': '#5782bd' } + return {'background-color': '#5782bd'}; } else if (isGroupAxis(axisSpec)) { - return { 'background-color': '#cd5c5c' } + return {'background-color': '#cd5c5c'}; } else if (isKeyAxis(axisSpec)) { - return { 'background-color': '#906ebd' } + return {'background-color': '#906ebd'}; } else { - return { 'background-color': '#62bda9' } + return {'background-color': '#62bda9'}; } }, - useSharedAxis: (chartName) => { return useSharedAxis(configInstance, chartName) }, - isGroupAxis: (axisSpec) => { return isGroupAxis(axisSpec) }, - isKeyAxis: (axisSpec) => { return isKeyAxis(axisSpec) }, - isAggregatorAxis: (axisSpec) => { return isAggregatorAxis(axisSpec) }, - isSingleDimensionAxis: (axisSpec) => { return isSingleDimensionAxis(axisSpec) }, - getSingleDimensionAxis: (axisSpec) => { return getCurrentChartAxis(configInstance)[axisSpec.name] }, + useSharedAxis: (chartName) => { + return useSharedAxis(configInstance, chartName); + }, + isGroupAxis: (axisSpec) => { + return isGroupAxis(axisSpec); + }, + isKeyAxis: (axisSpec) => { + return isKeyAxis(axisSpec); + }, + isAggregatorAxis: (axisSpec) => { + return isAggregatorAxis(axisSpec); + }, + isSingleDimensionAxis: (axisSpec) => { + return isSingleDimensionAxis(axisSpec); + }, + getSingleDimensionAxis: (axisSpec) => { + return getCurrentChartAxis(configInstance)[axisSpec.name]; + }, chartChanged: (selected) => { - configInstance.chart.current = selected - self.emitChartChange(configInstance) + configInstance.chart.current = selected; + self.emitChartChange(configInstance); }, axisChanged: function(e, ui, axisSpec) { - removeDuplicatedColumnsInMultiDimensionAxis(configInstance, axisSpec) - applyMaxAxisCount(configInstance, axisSpec) + removeDuplicatedColumnsInMultiDimensionAxis(configInstance, axisSpec); + applyMaxAxisCount(configInstance, axisSpec); - self.emitChartChange(configInstance) + self.emitChartChange(configInstance); }, aggregatorChanged: (colIndex, axisSpec, aggregator) => { if (isSingleDimensionAxis(axisSpec)) { - getCurrentChartAxis(configInstance)[axisSpec.name].aggr = aggregator + getCurrentChartAxis(configInstance)[axisSpec.name].aggr = aggregator; } else { - getCurrentChartAxis(configInstance)[axisSpec.name][colIndex].aggr = aggregator - removeDuplicatedColumnsInMultiDimensionAxis(configInstance, axisSpec) + getCurrentChartAxis(configInstance)[axisSpec.name][colIndex].aggr = aggregator; + removeDuplicatedColumnsInMultiDimensionAxis(configInstance, axisSpec); } - self.emitChartChange(configInstance) + self.emitChartChange(configInstance); }, removeFromAxis: function(colIndex, axisSpec) { if (isSingleDimensionAxis(axisSpec)) { - getCurrentChartAxis(configInstance)[axisSpec.name] = null + getCurrentChartAxis(configInstance)[axisSpec.name] = null; } else { - getCurrentChartAxis(configInstance)[axisSpec.name].splice(colIndex, 1) + getCurrentChartAxis(configInstance)[axisSpec.name].splice(colIndex, 1); } - self.emitChartChange(configInstance) + self.emitChartChange(configInstance); }, - isInputWidget: function(paramSpec) { return isInputWidget(paramSpec) }, - isCheckboxWidget: function(paramSpec) { return isCheckboxWidget(paramSpec) }, - isOptionWidget: function(paramSpec) { return isOptionWidget(paramSpec) }, - isTextareaWidget: function(paramSpec) { return isTextareaWidget(paramSpec) }, + isInputWidget: function(paramSpec) { + return isInputWidget(paramSpec); + }, + isCheckboxWidget: function(paramSpec) { + return isCheckboxWidget(paramSpec); + }, + isOptionWidget: function(paramSpec) { + return isOptionWidget(paramSpec); + }, + isTextareaWidget: function(paramSpec) { + return isTextareaWidget(paramSpec); + }, parameterChanged: (paramSpec) => { - configInstance.chartChanged = false - configInstance.parameterChanged = true - self.emitParameterChange(configInstance) + configInstance.chartChanged = false; + configInstance.parameterChanged = true; + self.emitParameterChange(configInstance); }, parameterOnKeyDown: function(event, paramSpec) { - const code = event.keyCode || event.which + const code = event.keyCode || event.which; if (code === 13 && isInputWidget(paramSpec)) { - self.emitParameterChange(configInstance) + self.emitParameterChange(configInstance); } else if (code === 13 && event.shiftKey && isTextareaWidget(paramSpec)) { - self.emitParameterChange(configInstance) + self.emitParameterChange(configInstance); } - event.stopPropagation() /** avoid to conflict with paragraph shortcuts */ + event.stopPropagation(); /** avoid to conflict with paragraph shortcuts */ }, - } - } + }, + }; } transform(tableData) { - this.columns = tableData.columns /** used in `getSetting` */ + this.columns = tableData.columns; /** used in `getSetting` */ /** initialize in `transform` instead of `getSetting` because this method is called before */ - serializeSharedAxes(this.config) + serializeSharedAxes(this.config); - const conf = this.config - const chart = getCurrentChart(conf) - const axis = getCurrentChartAxis(conf) - const axisSpecs = getCurrentChartAxisSpecs(conf) - const param = getCurrentChartParam(conf) - const paramSpecs = getCurrentChartParamSpecs(conf) - const parsedParam = parseParameter(paramSpecs, param) + const conf = this.config; + const chart = getCurrentChart(conf); + const axis = getCurrentChartAxis(conf); + const axisSpecs = getCurrentChartAxisSpecs(conf); + const param = getCurrentChartParam(conf); + const paramSpecs = getCurrentChartParamSpecs(conf); + const parsedParam = parseParameter(paramSpecs, param); - let { transformer, column, } = getTransformer(conf, tableData.rows, axisSpecs, axis) + let {transformer, column} = getTransformer(conf, tableData.rows, axisSpecs, axis); return { chartChanged: conf.chartChanged, @@ -224,6 +250,6 @@ export default class AdvancedTransformation extends Transformation { column: column, transformer: transformer, - } + }; } } diff --git a/zeppelin-web/src/app/tabledata/columnselector.js b/zeppelin-web/src/app/tabledata/columnselector.js index 9fcf2f17369..1998f062b09 100644 --- a/zeppelin-web/src/app/tabledata/columnselector.js +++ b/zeppelin-web/src/app/tabledata/columnselector.js @@ -12,7 +12,7 @@ * limitations under the License. */ -import Transformation from './transformation' +import Transformation from './transformation'; /** * select columns @@ -26,55 +26,55 @@ import Transformation from './transformation' * ] */ export default class ColumnselectorTransformation extends Transformation { - constructor (config, columnSelectorProp) { - super(config) - this.props = columnSelectorProp + constructor(config, columnSelectorProp) { + super(config); + this.props = columnSelectorProp; } - getSetting () { - let self = this - let configObj = self.config + getSetting() { + let self = this; + let configObj = self.config; return { template: 'app/tabledata/columnselector_settings.html', scope: { config: self.config, props: self.props, tableDataColumns: self.tableDataColumns, - save: function () { - self.emitConfig(configObj) + save: function() { + self.emitConfig(configObj); }, - remove: function (selectorName) { - configObj[selectorName] = null - self.emitConfig(configObj) - } - } - } + remove: function(selectorName) { + configObj[selectorName] = null; + self.emitConfig(configObj); + }, + }, + }; } /** * Method will be invoked when tableData or config changes */ - transform (tableData) { - this.tableDataColumns = tableData.columns - this.removeUnknown() - return tableData + transform(tableData) { + this.tableDataColumns = tableData.columns; + this.removeUnknown(); + return tableData; } - removeUnknown () { - let fields = this.config + removeUnknown() { + let fields = this.config; for (let f in fields) { if (fields[f]) { - let found = false + let found = false; for (let i = 0; i < this.tableDataColumns.length; i++) { - let a = fields[f] - let b = this.tableDataColumns[i] + let a = fields[f]; + let b = this.tableDataColumns[i]; if (a.index === b.index && a.name === b.name) { - found = true - break + found = true; + break; } } if (!found && (fields[f] instanceof Object) && !(fields[f] instanceof Array)) { - fields[f] = null + fields[f] = null; } } } diff --git a/zeppelin-web/src/app/tabledata/dataset.js b/zeppelin-web/src/app/tabledata/dataset.js index 762e3008906..ba3ee7d3957 100644 --- a/zeppelin-web/src/app/tabledata/dataset.js +++ b/zeppelin-web/src/app/tabledata/dataset.js @@ -30,7 +30,7 @@ class Dataset { */ const DatasetType = Object.freeze({ NETWORK: 'NETWORK', - TABLE: 'TABLE' -}) + TABLE: 'TABLE', +}); -export {Dataset, DatasetType} +export {Dataset, DatasetType}; diff --git a/zeppelin-web/src/app/tabledata/datasetfactory.js b/zeppelin-web/src/app/tabledata/datasetfactory.js index f2f69c90856..6d19a989c82 100644 --- a/zeppelin-web/src/app/tabledata/datasetfactory.js +++ b/zeppelin-web/src/app/tabledata/datasetfactory.js @@ -12,9 +12,9 @@ * limitations under the License. */ -import TableData from './tabledata' -import NetworkData from './networkdata' -import {DatasetType} from './dataset' +import TableData from './tabledata'; +import NetworkData from './networkdata'; +import {DatasetType} from './dataset'; /** * Create table data object from paragraph table type result @@ -23,11 +23,11 @@ export default class DatasetFactory { createDataset(datasetType) { switch (datasetType) { case DatasetType.NETWORK: - return new NetworkData() + return new NetworkData(); case DatasetType.TABLE: - return new TableData() + return new TableData(); default: - throw new Error('Dataset type not found') + throw new Error('Dataset type not found'); } } } diff --git a/zeppelin-web/src/app/tabledata/datasetfactory.test.js b/zeppelin-web/src/app/tabledata/datasetfactory.test.js index 0beb137e020..807456ab4d2 100644 --- a/zeppelin-web/src/app/tabledata/datasetfactory.test.js +++ b/zeppelin-web/src/app/tabledata/datasetfactory.test.js @@ -12,35 +12,37 @@ * limitations under the License. */ -import NetworkData from './networkdata.js' -import TableData from './tabledata.js' -import {DatasetType} from './dataset.js' -import DatasetFactory from './datasetfactory.js' +import NetworkData from './networkdata.js'; +import TableData from './tabledata.js'; +import {DatasetType} from './dataset.js'; +import DatasetFactory from './datasetfactory.js'; describe('DatasetFactory build', function() { - let df + let df; beforeAll(function() { - df = new DatasetFactory() - }) + df = new DatasetFactory(); + }); it('should create a TableData instance', function() { - let td = df.createDataset(DatasetType.TABLE) - expect(td instanceof TableData).toBeTruthy() - expect(td.columns.length).toBe(0) - expect(td.rows.length).toBe(0) - }) + let td = df.createDataset(DatasetType.TABLE); + expect(td instanceof TableData).toBeTruthy(); + expect(td.columns.length).toBe(0); + expect(td.rows.length).toBe(0); + }); it('should create a NetworkData instance', function() { - let nd = df.createDataset(DatasetType.NETWORK) - expect(nd instanceof NetworkData).toBeTruthy() - expect(nd.columns.length).toBe(0) - expect(nd.rows.length).toBe(0) - expect(nd.graph).toEqual({}) - }) + let nd = df.createDataset(DatasetType.NETWORK); + expect(nd instanceof NetworkData).toBeTruthy(); + expect(nd.columns.length).toBe(0); + expect(nd.rows.length).toBe(0); + expect(nd.graph).toEqual({}); + }); it('should thrown an Error', function() { - expect(function() { df.createDataset('text') }) - .toThrow(new Error('Dataset type not found')) - }) -}) + expect(function() { + df.createDataset('text'); + }) + .toThrow(new Error('Dataset type not found')); + }); +}); diff --git a/zeppelin-web/src/app/tabledata/network.js b/zeppelin-web/src/app/tabledata/network.js index 403ea5b64d5..3566722930d 100644 --- a/zeppelin-web/src/app/tabledata/network.js +++ b/zeppelin-web/src/app/tabledata/network.js @@ -12,37 +12,37 @@ * limitations under the License. */ -import Transformation from './transformation' +import Transformation from './transformation'; /** * trasformation settings for network visualization */ export default class NetworkTransformation extends Transformation { getSetting() { - let self = this - let configObj = self.config + let self = this; + let configObj = self.config; return { template: 'app/tabledata/network_settings.html', scope: { config: configObj, isEmptyObject: function(obj) { - obj = obj || {} - return angular.equals(obj, {}) + obj = obj || {}; + return angular.equals(obj, {}); }, setNetworkLabel: function(label, value) { - configObj.properties[label].selected = value + configObj.properties[label].selected = value; }, saveConfig: function() { - self.emitConfig(configObj) - } - } - } + self.emitConfig(configObj); + }, + }, + }; } setConfig(config) { } transform(networkData) { - return networkData + return networkData; } } diff --git a/zeppelin-web/src/app/tabledata/networkdata.js b/zeppelin-web/src/app/tabledata/networkdata.js index 70cd86ba5b4..368254d3f22 100644 --- a/zeppelin-web/src/app/tabledata/networkdata.js +++ b/zeppelin-web/src/app/tabledata/networkdata.js @@ -12,74 +12,74 @@ * limitations under the License. */ -import TableData from './tabledata' -import {DatasetType} from './dataset' +import TableData from './tabledata'; +import {DatasetType} from './dataset'; /** * Create network data object from paragraph graph type result */ export default class NetworkData extends TableData { constructor(graph) { - super() - this.graph = graph || {} + super(); + this.graph = graph || {}; if (this.graph.nodes) { - this.loadParagraphResult({msg: JSON.stringify(graph), type: DatasetType.NETWORK}) + this.loadParagraphResult({msg: JSON.stringify(graph), type: DatasetType.NETWORK}); } } loadParagraphResult(paragraphResult) { if (!paragraphResult || paragraphResult.type !== DatasetType.NETWORK) { - console.log('Can not load paragraph result') - return + console.log('Can not load paragraph result'); + return; } - this.graph = JSON.parse(paragraphResult.msg.trim() || '{}') + this.graph = JSON.parse(paragraphResult.msg.trim() || '{}'); if (!this.graph.nodes) { - console.log('Graph result is empty') - return + console.log('Graph result is empty'); + return; } - this.graph.edges = this.graph.edges || [] + this.graph.edges = this.graph.edges || []; this.networkNodes = angular.equals({}, this.graph.labels || {}) - ? null : {count: this.graph.nodes.length, labels: this.graph.labels} + ? null : {count: this.graph.nodes.length, labels: this.graph.labels}; this.networkRelationships = angular.equals([], this.graph.types || []) - ? null : {count: this.graph.edges.length, types: this.graph.types} + ? null : {count: this.graph.edges.length, types: this.graph.types}; - const rows = [] - const comment = '' - const entities = this.graph.nodes.concat(this.graph.edges) - const baseColumnNames = [{name: 'id', index: 0, aggr: 'sum'}] - const containsLabelField = _.find(entities, (entity) => 'label' in entity) != null + const rows = []; + const comment = ''; + const entities = this.graph.nodes.concat(this.graph.edges); + const baseColumnNames = [{name: 'id', index: 0, aggr: 'sum'}]; + const containsLabelField = _.find(entities, (entity) => 'label' in entity) !== undefined; if (this.graph.labels || this.graph.types || containsLabelField) { - baseColumnNames.push({name: 'label', index: 1, aggr: 'sum'}) + baseColumnNames.push({name: 'label', index: 1, aggr: 'sum'}); } const internalFieldsToJump = ['count', 'size', 'totalCount', - 'data', 'x', 'y', 'labels', 'source', 'target'] - const baseCols = _.map(baseColumnNames, (col) => col.name) - let keys = _.map(entities, (elem) => Object.keys(elem.data || {})) - keys = _.flatten(keys) - keys = _.uniq(keys).filter((key) => baseCols.indexOf(key) === -1) + 'data', 'x', 'y', 'labels', 'source', 'target']; + const baseCols = _.map(baseColumnNames, (col) => col.name); + let keys = _.map(entities, (elem) => Object.keys(elem.data || {})); + keys = _.flatten(keys); + keys = _.uniq(keys).filter((key) => baseCols.indexOf(key) === -1); const entityColumnNames = _.map(keys, (elem, i) => { - return {name: elem, index: i + baseColumnNames.length, aggr: 'sum'} - }) - const columnNames = baseColumnNames.concat(entityColumnNames) + return {name: elem, index: i + baseColumnNames.length, aggr: 'sum'}; + }); + const columnNames = baseColumnNames.concat(entityColumnNames); for (let i = 0; i < entities.length; i++) { - const entity = entities[i] - const col = [] - entity.data = entity.data || {} + const entity = entities[i]; + const col = []; + entity.data = entity.data || {}; for (let j = 0; j < columnNames.length; j++) { - const name = columnNames[j].name + const name = columnNames[j].name; const value = name in entity && internalFieldsToJump.indexOf(name) === -1 - ? entity[name] : entity.data[name] - const parsedValue = value === null || value === undefined ? '' : value - col.push(parsedValue) + ? entity[name] : entity.data[name]; + const parsedValue = value === null || value === undefined ? '' : value; + col.push(parsedValue); } - rows.push(col) + rows.push(col); } - this.comment = comment - this.columns = columnNames - this.rows = rows + this.comment = comment; + this.columns = columnNames; + this.rows = rows; } } diff --git a/zeppelin-web/src/app/tabledata/networkdata.test.js b/zeppelin-web/src/app/tabledata/networkdata.test.js index 739ac19fe36..cd3a12f29c8 100644 --- a/zeppelin-web/src/app/tabledata/networkdata.test.js +++ b/zeppelin-web/src/app/tabledata/networkdata.test.js @@ -12,56 +12,56 @@ * limitations under the License. */ -import NetworkData from './networkdata.js' -import {DatasetType} from './dataset.js' +import NetworkData from './networkdata.js'; +import {DatasetType} from './dataset.js'; describe('NetworkData build', function() { - let nd + let nd; beforeEach(function() { - nd = new NetworkData() - }) + nd = new NetworkData(); + }); it('should initialize the default value', function() { - expect(nd.columns.length).toBe(0) - expect(nd.rows.length).toBe(0) - expect(nd.graph).toEqual({}) - }) + expect(nd.columns.length).toBe(0); + expect(nd.rows.length).toBe(0); + expect(nd.graph).toEqual({}); + }); it('should able to create NetowkData from paragraph result', function() { - let jsonExpected = {nodes: [{id: 1}, {id: 2}], edges: [{source: 2, target: 1, id: 1}]} + let jsonExpected = {nodes: [{id: 1}, {id: 2}], edges: [{source: 2, target: 1, id: 1}]}; nd.loadParagraphResult({ type: DatasetType.NETWORK, - msg: JSON.stringify(jsonExpected) - }) + msg: JSON.stringify(jsonExpected), + }); - expect(nd.columns.length).toBe(1) - expect(nd.rows.length).toBe(3) - expect(nd.graph.nodes[0].id).toBe(jsonExpected.nodes[0].id) - expect(nd.graph.nodes[1].id).toBe(jsonExpected.nodes[1].id) - expect(nd.graph.edges[0].id).toBe(jsonExpected.edges[0].id) - expect(nd.graph.edges[0].source).toBe(jsonExpected.edges[0].source) - expect(nd.graph.edges[0].target).toBe(jsonExpected.edges[0].target) - }) + expect(nd.columns.length).toBe(1); + expect(nd.rows.length).toBe(3); + expect(nd.graph.nodes[0].id).toBe(jsonExpected.nodes[0].id); + expect(nd.graph.nodes[1].id).toBe(jsonExpected.nodes[1].id); + expect(nd.graph.edges[0].id).toBe(jsonExpected.edges[0].id); + expect(nd.graph.edges[0].source).toBe(jsonExpected.edges[0].source); + expect(nd.graph.edges[0].target).toBe(jsonExpected.edges[0].target); + }); it('should able to show data fields source and target', function() { let jsonExpected = {nodes: [{id: 1, data: {source: 'Source'}}, {id: 2, data: {target: 'Target'}}], - edges: [{source: 2, target: 1, id: 1, data: {source: 'Source Edge Data', target: 'Target Edge Data'}}]} + edges: [{source: 2, target: 1, id: 1, data: {source: 'Source Edge Data', target: 'Target Edge Data'}}]}; nd.loadParagraphResult({ type: DatasetType.NETWORK, - msg: JSON.stringify(jsonExpected) - }) + msg: JSON.stringify(jsonExpected), + }); - expect(nd.columns.length).toBe(3) - expect(nd.rows.length).toBe(3) - expect(nd.graph.nodes[0].id).toBe(jsonExpected.nodes[0].id) - expect(nd.graph.nodes[1].id).toBe(jsonExpected.nodes[1].id) - expect(nd.graph.edges[0].id).toBe(jsonExpected.edges[0].id) - expect(nd.graph.edges[0].source).toBe(jsonExpected.edges[0].source) - expect(nd.graph.edges[0].target).toBe(jsonExpected.edges[0].target) - expect(nd.graph.nodes[0].data.source).toBe(jsonExpected.nodes[0].data.source) - expect(nd.graph.nodes[1].data.target).toBe(jsonExpected.nodes[1].data.target) - expect(nd.graph.edges[0].data.source).toBe(jsonExpected.edges[0].data.source) - expect(nd.graph.edges[0].data.target).toBe(jsonExpected.edges[0].data.target) - }) -}) + expect(nd.columns.length).toBe(3); + expect(nd.rows.length).toBe(3); + expect(nd.graph.nodes[0].id).toBe(jsonExpected.nodes[0].id); + expect(nd.graph.nodes[1].id).toBe(jsonExpected.nodes[1].id); + expect(nd.graph.edges[0].id).toBe(jsonExpected.edges[0].id); + expect(nd.graph.edges[0].source).toBe(jsonExpected.edges[0].source); + expect(nd.graph.edges[0].target).toBe(jsonExpected.edges[0].target); + expect(nd.graph.nodes[0].data.source).toBe(jsonExpected.nodes[0].data.source); + expect(nd.graph.nodes[1].data.target).toBe(jsonExpected.nodes[1].data.target); + expect(nd.graph.edges[0].data.source).toBe(jsonExpected.edges[0].data.source); + expect(nd.graph.edges[0].data.target).toBe(jsonExpected.edges[0].data.target); + }); +}); diff --git a/zeppelin-web/src/app/tabledata/passthrough.js b/zeppelin-web/src/app/tabledata/passthrough.js index e376c43a64b..772b7bee5db 100644 --- a/zeppelin-web/src/app/tabledata/passthrough.js +++ b/zeppelin-web/src/app/tabledata/passthrough.js @@ -12,21 +12,21 @@ * limitations under the License. */ -import Transformation from './transformation' +import Transformation from './transformation'; /** * passthough the data */ export default class PassthroughTransformation extends Transformation { // eslint-disable-next-line no-useless-constructor - constructor (config) { - super(config) + constructor(config) { + super(config); } /** * Method will be invoked when tableData or config changes */ - transform (tableData) { - return tableData + transform(tableData) { + return tableData; } } diff --git a/zeppelin-web/src/app/tabledata/pivot.js b/zeppelin-web/src/app/tabledata/pivot.js index da2990043b0..2baa6b5c8d8 100644 --- a/zeppelin-web/src/app/tabledata/pivot.js +++ b/zeppelin-web/src/app/tabledata/pivot.js @@ -12,192 +12,192 @@ * limitations under the License. */ -import Transformation from './transformation' +import Transformation from './transformation'; /** * pivot table data and return d3 chart data */ export default class PivotTransformation extends Transformation { // eslint-disable-next-line no-useless-constructor - constructor (config) { - super(config) + constructor(config) { + super(config); } - getSetting () { - let self = this + getSetting() { + let self = this; - let configObj = self.config - console.log('getSetting', configObj) + let configObj = self.config; + console.log('getSetting', configObj); return { template: 'app/tabledata/pivot_settings.html', scope: { config: configObj.common.pivot, tableDataColumns: self.tableDataColumns, - save: function () { - self.emitConfig(configObj) + save: function() { + self.emitConfig(configObj); }, - removeKey: function (idx) { - configObj.common.pivot.keys.splice(idx, 1) - self.emitConfig(configObj) + removeKey: function(idx) { + configObj.common.pivot.keys.splice(idx, 1); + self.emitConfig(configObj); }, - removeGroup: function (idx) { - configObj.common.pivot.groups.splice(idx, 1) - self.emitConfig(configObj) + removeGroup: function(idx) { + configObj.common.pivot.groups.splice(idx, 1); + self.emitConfig(configObj); }, - removeValue: function (idx) { - configObj.common.pivot.values.splice(idx, 1) - self.emitConfig(configObj) + removeValue: function(idx) { + configObj.common.pivot.values.splice(idx, 1); + self.emitConfig(configObj); }, - setValueAggr: function (idx, aggr) { - configObj.common.pivot.values[idx].aggr = aggr - self.emitConfig(configObj) - } - } - } + setValueAggr: function(idx, aggr) { + configObj.common.pivot.values[idx].aggr = aggr; + self.emitConfig(configObj); + }, + }, + }; } /** * Method will be invoked when tableData or config changes */ - transform (tableData) { - this.tableDataColumns = tableData.columns - this.config.common = this.config.common || {} - this.config.common.pivot = this.config.common.pivot || {} - let config = this.config.common.pivot - let firstTime = (!config.keys && !config.groups && !config.values) + transform(tableData) { + this.tableDataColumns = tableData.columns; + this.config.common = this.config.common || {}; + this.config.common.pivot = this.config.common.pivot || {}; + let config = this.config.common.pivot; + let firstTime = (!config.keys && !config.groups && !config.values); - config.keys = config.keys || [] - config.groups = config.groups || [] - config.values = config.values || [] + config.keys = config.keys || []; + config.groups = config.groups || []; + config.values = config.values || []; - this.removeUnknown() + this.removeUnknown(); if (firstTime) { - this.selectDefault() + this.selectDefault(); } return this.pivot( tableData, config.keys, config.groups, - config.values) + config.values); } - removeUnknown () { - let config = this.config.common.pivot - let tableDataColumns = this.tableDataColumns - let unique = function (list) { + removeUnknown() { + let config = this.config.common.pivot; + let tableDataColumns = this.tableDataColumns; + let unique = function(list) { for (let i = 0; i < list.length; i++) { for (let j = i + 1; j < list.length; j++) { if (angular.equals(list[i], list[j])) { - list.splice(j, 1) - j-- + list.splice(j, 1); + j--; } } } - } + }; - let removeUnknown = function (list) { + let removeUnknown = function(list) { for (let i = 0; i < list.length; i++) { // remove non existing column - let found = false + let found = false; for (let j = 0; j < tableDataColumns.length; j++) { - let a = list[i] - let b = tableDataColumns[j] + let a = list[i]; + let b = tableDataColumns[j]; if (a.index === b.index && a.name === b.name) { - found = true - break + found = true; + break; } } if (!found) { - list.splice(i, 1) + list.splice(i, 1); } } - } + }; - unique(config.keys) - removeUnknown(config.keys) - unique(config.groups) - removeUnknown(config.groups) - removeUnknown(config.values) + unique(config.keys); + removeUnknown(config.keys); + unique(config.groups); + removeUnknown(config.groups); + removeUnknown(config.values); } - selectDefault () { - let config = this.config.common.pivot + selectDefault() { + let config = this.config.common.pivot; if (config.keys.length === 0 && config.groups.length === 0 && config.values.length === 0) { if (config.keys.length === 0 && this.tableDataColumns.length > 0) { - config.keys.push(this.tableDataColumns[0]) + config.keys.push(this.tableDataColumns[0]); } if (config.values.length === 0 && this.tableDataColumns.length > 1) { - config.values.push(this.tableDataColumns[1]) + config.values.push(this.tableDataColumns[1]); } } } - pivot (data, keys, groups, values) { + pivot(data, keys, groups, values) { let aggrFunc = { - sum: function (a, b) { - let varA = (a !== undefined) ? (isNaN(a) ? 0 : parseFloat(a)) : 0 - let varB = (b !== undefined) ? (isNaN(b) ? 0 : parseFloat(b)) : 0 - return varA + varB + sum: function(a, b) { + let varA = (a !== undefined) ? (isNaN(a) ? 0 : parseFloat(a)) : 0; + let varB = (b !== undefined) ? (isNaN(b) ? 0 : parseFloat(b)) : 0; + return varA + varB; }, - count: function (a, b) { - let varA = (a !== undefined) ? parseInt(a) : 0 - let varB = (b !== undefined) ? 1 : 0 - return varA + varB + count: function(a, b) { + let varA = (a !== undefined) ? parseInt(a) : 0; + let varB = (b !== undefined) ? 1 : 0; + return varA + varB; }, - min: function (a, b) { - let aIsValid = isValidNumber(a) - let bIsValid = isValidNumber(b) + min: function(a, b) { + let aIsValid = isValidNumber(a); + let bIsValid = isValidNumber(b); if (!aIsValid) { - return parseFloat(b) + return parseFloat(b); } else if (!bIsValid) { - return parseFloat(a) + return parseFloat(a); } else { - return Math.min(parseFloat(a), parseFloat(b)) + return Math.min(parseFloat(a), parseFloat(b)); } }, - max: function (a, b) { - let aIsValid = isValidNumber(a) - let bIsValid = isValidNumber(b) + max: function(a, b) { + let aIsValid = isValidNumber(a); + let bIsValid = isValidNumber(b); if (!aIsValid) { - return parseFloat(b) + return parseFloat(b); } else if (!bIsValid) { - return parseFloat(a) + return parseFloat(a); } else { - return Math.max(parseFloat(a), parseFloat(b)) + return Math.max(parseFloat(a), parseFloat(b)); } }, - avg: function (a, b, c) { - let varA = (a !== undefined) ? (isNaN(a) ? 0 : parseFloat(a)) : 0 - let varB = (b !== undefined) ? (isNaN(b) ? 0 : parseFloat(b)) : 0 - return varA + varB - } - } + avg: function(a, b, c) { + let varA = (a !== undefined) ? (isNaN(a) ? 0 : parseFloat(a)) : 0; + let varB = (b !== undefined) ? (isNaN(b) ? 0 : parseFloat(b)) : 0; + return varA + varB; + }, + }; let isValidNumber = function(num) { - return num !== undefined && !isNaN(num) - } + return num !== undefined && !isNaN(num); + }; let aggrFuncDiv = { sum: false, count: false, min: false, max: false, - avg: true - } + avg: true, + }; - let schema = {} - let rows = {} + let schema = {}; + let rows = {}; for (let i = 0; i < data.rows.length; i++) { - let row = data.rows[i] - let s = schema - let p = rows + let row = data.rows[i]; + let s = schema; + let p = rows; for (let k = 0; k < keys.length; k++) { - let key = keys[k] + let key = keys[k]; // add key to schema if (!s[key.name]) { @@ -205,22 +205,22 @@ export default class PivotTransformation extends Transformation { order: k, index: key.index, type: 'key', - children: {} - } + children: {}, + }; } - s = s[key.name].children + s = s[key.name].children; // add key to row - let keyKey = row[key.index] + let keyKey = row[key.index]; if (!p[keyKey]) { - p[keyKey] = {} + p[keyKey] = {}; } - p = p[keyKey] + p = p[keyKey]; } for (let g = 0; g < groups.length; g++) { - let group = groups[g] - let groupKey = row[group.index] + let group = groups[g]; + let groupKey = row[group.index]; // add group to schema if (!s[groupKey]) { @@ -228,42 +228,42 @@ export default class PivotTransformation extends Transformation { order: g, index: group.index, type: 'group', - children: {} - } + children: {}, + }; } - s = s[groupKey].children + s = s[groupKey].children; // add key to row if (!p[groupKey]) { - p[groupKey] = {} + p[groupKey] = {}; } - p = p[groupKey] + p = p[groupKey]; } for (let v = 0; v < values.length; v++) { - let value = values[v] - let valueKey = value.name + '(' + value.aggr + ')' + let value = values[v]; + let valueKey = value.name + '(' + value.aggr + ')'; // add value to schema if (!s[valueKey]) { s[valueKey] = { type: 'value', order: v, - index: value.index - } + index: value.index, + }; } // add value to row if (!p[valueKey]) { p[valueKey] = { value: (value.aggr !== 'count') ? row[value.index] : 1, - count: 1 - } + count: 1, + }; } else { p[valueKey] = { value: aggrFunc[value.aggr](p[valueKey].value, row[value.index], p[valueKey].count + 1), - count: (aggrFuncDiv[value.aggr]) ? p[valueKey].count + 1 : p[valueKey].count - } + count: (aggrFuncDiv[value.aggr]) ? p[valueKey].count + 1 : p[valueKey].count, + }; } } } @@ -274,7 +274,7 @@ export default class PivotTransformation extends Transformation { groups: groups, values: values, schema: schema, - rows: rows - } + rows: rows, + }; } } diff --git a/zeppelin-web/src/app/tabledata/tabledata.js b/zeppelin-web/src/app/tabledata/tabledata.js index 3fe01b7791c..745ab179050 100644 --- a/zeppelin-web/src/app/tabledata/tabledata.js +++ b/zeppelin-web/src/app/tabledata/tabledata.js @@ -11,65 +11,65 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {Dataset, DatasetType} from './dataset' +import {Dataset, DatasetType} from './dataset'; /** * Create table data object from paragraph table type result */ export default class TableData extends Dataset { - constructor (columns, rows, comment) { - super() - this.columns = columns || [] - this.rows = rows || [] - this.comment = comment || '' + constructor(columns, rows, comment) { + super(); + this.columns = columns || []; + this.rows = rows || []; + this.comment = comment || ''; } - loadParagraphResult (paragraphResult) { + loadParagraphResult(paragraphResult) { if (!paragraphResult || paragraphResult.type !== DatasetType.TABLE) { - console.log('Can not load paragraph result') - return + console.log('Can not load paragraph result'); + return; } - let columnNames = [] - let rows = [] - let array = [] - let textRows = paragraphResult.msg.split('\n') - let comment = '' - let commentRow = false + let columnNames = []; + let rows = []; + let array = []; + let textRows = paragraphResult.msg.split('\n'); + let comment = ''; + let commentRow = false; for (let i = 0; i < textRows.length; i++) { - let textRow = textRows[i] + let textRow = textRows[i]; if (commentRow) { - comment += textRow - continue + comment += textRow; + continue; } if (textRow === '' || textRow === '') { if (rows.length > 0) { - commentRow = true + commentRow = true; } - continue + continue; } - let textCols = textRow.split('\t') - let cols = [] - let cols2 = [] + let textCols = textRow.split('\t'); + let cols = []; + let cols2 = []; for (let j = 0; j < textCols.length; j++) { - let col = textCols[j] + let col = textCols[j]; if (i === 0) { - columnNames.push({name: col, index: j, aggr: 'sum'}) + columnNames.push({name: col, index: j, aggr: 'sum'}); } else { - cols.push(col) - cols2.push({key: (columnNames[i]) ? columnNames[i].name : undefined, value: col}) + cols.push(col); + cols2.push({key: (columnNames[i]) ? columnNames[i].name : undefined, value: col}); } } if (i !== 0) { - rows.push(cols) - array.push(cols2) + rows.push(cols); + array.push(cols2); } } - this.comment = comment - this.columns = columnNames - this.rows = rows + this.comment = comment; + this.columns = columnNames; + this.rows = rows; } } diff --git a/zeppelin-web/src/app/tabledata/tabledata.test.js b/zeppelin-web/src/app/tabledata/tabledata.test.js index e24b0733924..931cc2d7801 100644 --- a/zeppelin-web/src/app/tabledata/tabledata.test.js +++ b/zeppelin-web/src/app/tabledata/tabledata.test.js @@ -12,42 +12,42 @@ * limitations under the License. */ -import TableData from './tabledata.js' -import PivotTransformation from './pivot.js' +import TableData from './tabledata.js'; +import PivotTransformation from './pivot.js'; -describe('TableData build', function () { - let td +describe('TableData build', function() { + let td; - beforeEach(function () { - console.log(TableData) - td = new TableData() - }) + beforeEach(function() { + console.log(TableData); + td = new TableData(); + }); - it('should initialize the default value', function () { - expect(td.columns.length).toBe(0) - expect(td.rows.length).toBe(0) - expect(td.comment).toBe('') - }) + it('should initialize the default value', function() { + expect(td.columns.length).toBe(0); + expect(td.rows.length).toBe(0); + expect(td.comment).toBe(''); + }); - it('should able to create Tabledata from paragraph result', function () { + it('should able to create Tabledata from paragraph result', function() { td.loadParagraphResult({ type: 'TABLE', - msg: 'key\tvalue\na\t10\nb\t20\n\nhello' - }) + msg: 'key\tvalue\na\t10\nb\t20\n\nhello', + }); - expect(td.columns.length).toBe(2) - expect(td.rows.length).toBe(2) - expect(td.comment).toBe('hello') - }) -}) + expect(td.columns.length).toBe(2); + expect(td.rows.length).toBe(2); + expect(td.comment).toBe('hello'); + }); +}); describe('PivotTransformation build', function() { - let pt + let pt; - beforeEach(function () { - console.log(PivotTransformation) - pt = new PivotTransformation() - }) + beforeEach(function() { + console.log(PivotTransformation); + pt = new PivotTransformation(); + }); it('check the result of keys, groups and values unique', function() { // set inited mock data @@ -63,33 +63,33 @@ describe('PivotTransformation build', function() { {index: 3, name: '3'}, {index: 5, name: '5'}], groups: [], - values: [] - } - } - } + values: [], + }, + }, + }; pt.tableDataColumns = [ {index: 1, name: '1'}, {index: 2, name: '2'}, {index: 3, name: '3'}, {index: 4, name: '4'}, - {index: 5, name: '5'}] + {index: 5, name: '5'}]; - pt.setConfig(config) + pt.setConfig(config); - pt.removeUnknown() + pt.removeUnknown(); - expect(config.common.pivot.keys.length).toBe(3) - expect(config.common.pivot.keys[0].index).toBe(4) - expect(config.common.pivot.keys[1].index).toBe(3) - expect(config.common.pivot.keys[2].index).toBe(5) - }) + expect(config.common.pivot.keys.length).toBe(3); + expect(config.common.pivot.keys[0].index).toBe(4); + expect(config.common.pivot.keys[1].index).toBe(3); + expect(config.common.pivot.keys[2].index).toBe(5); + }); it('should aggregate values correctly', function() { - let td = new TableData() + let td = new TableData(); td.loadParagraphResult({ type: 'TABLE', - msg: 'key\tvalue\na\t10\na\tnull\na\t0\na\t1\n' - }) + msg: 'key\tvalue\na\t10\na\tnull\na\t0\na\t1\n', + }); let config = { common: { @@ -98,34 +98,34 @@ describe('PivotTransformation build', function() { { 'name': 'key', 'index': 0.0, - } + }, ], groups: [], values: [ { 'name': 'value', 'index': 1.0, - 'aggr': 'sum' - } - ] - } - } - } - - pt.setConfig(config) - let transformed = pt.transform(td) - expect(transformed.rows['a']['value(sum)'].value).toBe(11) - - pt.config.common.pivot.values[0].aggr = 'max' - transformed = pt.transform(td) - expect(transformed.rows['a']['value(max)'].value).toBe(10) - - pt.config.common.pivot.values[0].aggr = 'min' - transformed = pt.transform(td) - expect(transformed.rows['a']['value(min)'].value).toBe(0) - - pt.config.common.pivot.values[0].aggr = 'count' - transformed = pt.transform(td) - expect(transformed.rows['a']['value(count)'].value).toBe(4) - }) -}) + 'aggr': 'sum', + }, + ], + }, + }, + }; + + pt.setConfig(config); + let transformed = pt.transform(td); + expect(transformed.rows['a']['value(sum)'].value).toBe(11); + + pt.config.common.pivot.values[0].aggr = 'max'; + transformed = pt.transform(td); + expect(transformed.rows['a']['value(max)'].value).toBe(10); + + pt.config.common.pivot.values[0].aggr = 'min'; + transformed = pt.transform(td); + expect(transformed.rows['a']['value(min)'].value).toBe(0); + + pt.config.common.pivot.values[0].aggr = 'count'; + transformed = pt.transform(td); + expect(transformed.rows['a']['value(count)'].value).toBe(4); + }); +}); diff --git a/zeppelin-web/src/app/tabledata/transformation.js b/zeppelin-web/src/app/tabledata/transformation.js index f142618283c..a15e12b3a51 100644 --- a/zeppelin-web/src/app/tabledata/transformation.js +++ b/zeppelin-web/src/app/tabledata/transformation.js @@ -16,9 +16,9 @@ * Base class for visualization */ export default class Transformation { - constructor (config) { - this.config = config - this._emitter = () => {} + constructor(config) { + this.config = config; + this._emitter = () => {}; } /** @@ -27,77 +27,81 @@ export default class Transformation { * scope : an object to bind to template scope * } */ - getSetting () { + getSetting() { // override this } /** * Method will be invoked when tableData or config changes */ - transform (tableData) { + transform(tableData) { // override this } /** * render setting */ - renderSetting (targetEl) { - let setting = this.getSetting() + renderSetting(targetEl) { + let setting = this.getSetting(); if (!setting) { - return + return; } // already readered if (this._scope) { - let self = this - this._scope.$apply(function () { + let self = this; + this._scope.$apply(function() { for (let k in setting.scope) { - self._scope[k] = setting.scope[k] + if(setting.scope.hasOwnProperty(k)) { + self._scope[k] = setting.scope[k]; + } } for (let k in self._prevSettingScope) { if (!setting.scope[k]) { - self._scope[k] = setting.scope[k] + self._scope[k] = setting.scope[k]; } } - }) - return + }); + return; } else { - this._prevSettingScope = setting.scope + this._prevSettingScope = setting.scope; } - let scope = this._createNewScope() + let scope = this._createNewScope(); for (let k in setting.scope) { - scope[k] = setting.scope[k] + if(setting.scope.hasOwnProperty(k)) { + scope[k] = setting.scope[k]; + } } - let template = setting.template + let template = setting.template; if (template.split('\n').length === 1 && template.endsWith('.html')) { // template is url - let self = this - this._templateRequest(template).then(function (t) { - self._render(targetEl, t, scope) - }) + let self = this; + this._templateRequest(template).then(function(t) { + self._render(targetEl, t, scope); + }); } else { - this._render(targetEl, template, scope) + this._render(targetEl, template, scope); } } - _render (targetEl, template, scope) { - this._targetEl = targetEl - targetEl.html(template) - this._compile(targetEl.contents())(scope) - this._scope = scope + _render(targetEl, template, scope) { + this._targetEl = targetEl; + targetEl.html(template); + this._compile(targetEl.contents())(scope); + this._scope = scope; } - setConfig (config) { - this.config = config + setConfig(config) { + this.config = config; } /** * Emit config. config will sent to server and saved. */ - emitConfig (config) { - this._emitter(config) + emitConfig(config) { + this._emitter(config); } } diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-areachart.js b/zeppelin-web/src/app/visualization/builtins/visualization-areachart.js index 494f8ae67f7..886aec9a339 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-areachart.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-areachart.js @@ -12,34 +12,34 @@ * limitations under the License. */ -import Nvd3ChartVisualization from './visualization-nvd3chart' -import PivotTransformation from '../../tabledata/pivot' +import Nvd3ChartVisualization from './visualization-nvd3chart'; +import PivotTransformation from '../../tabledata/pivot'; /** * Visualize data in area chart */ export default class AreachartVisualization extends Nvd3ChartVisualization { - constructor (targetEl, config) { - super(targetEl, config) + constructor(targetEl, config) { + super(targetEl, config); - this.pivot = new PivotTransformation(config) + this.pivot = new PivotTransformation(config); try { - this.config.rotate = {degree: config.rotate.degree} + this.config.rotate = {degree: config.rotate.degree}; } catch (e) { - this.config.rotate = {degree: '-45'} + this.config.rotate = {degree: '-45'}; } } - type () { - return 'stackedAreaChart' + type() { + return 'stackedAreaChart'; } - getTransformation () { - return this.pivot + getTransformation() { + return this.pivot; } - render (pivot) { + render(pivot) { let d3Data = this.d3DataFromPivot( pivot.schema, pivot.rows, @@ -48,108 +48,112 @@ export default class AreachartVisualization extends Nvd3ChartVisualization { pivot.values, false, true, - false) + false); - this.xLabels = d3Data.xLabels - super.render(d3Data) - this.config.changeXLabel(this.config.xLabelStatus) + this.xLabels = d3Data.xLabels; + super.render(d3Data); + this.config.changeXLabel(this.config.xLabelStatus); } /** * Set new config */ - setConfig (config) { - super.setConfig(config) - this.pivot.setConfig(config) + setConfig(config) { + super.setConfig(config); + this.pivot.setConfig(config); } - configureChart (chart) { - let self = this - let configObj = self.config + configureChart(chart) { + let self = this; + let configObj = self.config; - chart.xAxis.tickFormat(function (d) { return self.xAxisTickFormat(d, self.xLabels) }) - chart.yAxis.tickFormat(function (d) { return self.yAxisTickFormat(d) }) - chart.yAxis.axisLabelDistance(50) - chart.useInteractiveGuideline(true) // for better UX and performance issue. (https://github.com/novus/nvd3/issues/691) + chart.xAxis.tickFormat(function(d) { + return self.xAxisTickFormat(d, self.xLabels); + }); + chart.yAxis.tickFormat(function(d) { + return self.yAxisTickFormat(d); + }); + chart.yAxis.axisLabelDistance(50); + chart.useInteractiveGuideline(true); // for better UX and performance issue. (https://github.com/novus/nvd3/issues/691) self.config.changeXLabel = function(type) { switch (type) { case 'default': - self.chart._options['showXAxis'] = true - self.chart._options['margin'] = {bottom: 50} - self.chart.xAxis.rotateLabels(0) - configObj.xLabelStatus = 'default' - break + self.chart._options['showXAxis'] = true; + self.chart._options['margin'] = {bottom: 50}; + self.chart.xAxis.rotateLabels(0); + configObj.xLabelStatus = 'default'; + break; case 'rotate': - self.chart._options['showXAxis'] = true - self.chart._options['margin'] = {bottom: 140} - self.chart.xAxis.rotateLabels(configObj.rotate.degree) - configObj.xLabelStatus = 'rotate' - break + self.chart._options['showXAxis'] = true; + self.chart._options['margin'] = {bottom: 140}; + self.chart.xAxis.rotateLabels(configObj.rotate.degree); + configObj.xLabelStatus = 'rotate'; + break; case 'hide': - self.chart._options['showXAxis'] = false - self.chart._options['margin'] = {bottom: 50} - d3.select('#' + self.targetEl[0].id + '> svg').select('g.nv-axis.nv-x').selectAll('*').remove() - configObj.xLabelStatus = 'hide' - break + self.chart._options['showXAxis'] = false; + self.chart._options['margin'] = {bottom: 50}; + d3.select('#' + self.targetEl[0].id + '> svg').select('g.nv-axis.nv-x').selectAll('*').remove(); + configObj.xLabelStatus = 'hide'; + break; } - self.emitConfig(configObj) - } + self.emitConfig(configObj); + }; self.config.isXLabelStatus = function(type) { if (configObj.xLabelStatus === type) { - return true + return true; } else { - return false + return false; } - } + }; self.config.setDegree = function(type) { - configObj.rotate.degree = type - self.chart.xAxis.rotateLabels(type) - self.emitConfig(configObj) - } + configObj.rotate.degree = type; + self.chart.xAxis.rotateLabels(type); + self.emitConfig(configObj); + }; self.config.isDegreeEmpty = function() { if (configObj.rotate.degree.length > 0) { - return true + return true; } else { - configObj.rotate.degree = '-45' - self.emitConfig(configObj) - return false + configObj.rotate.degree = '-45'; + self.emitConfig(configObj); + return false; } - } + }; - this.chart.style(this.config.style || 'stack') + this.chart.style(this.config.style || 'stack'); - this.chart.dispatch.on('stateChange', function (s) { - self.config.style = s.style + this.chart.dispatch.on('stateChange', function(s) { + self.config.style = s.style; // give some time to animation finish - setTimeout(function () { - self.emitConfig(self.config) - }, 500) - }) + setTimeout(function() { + self.emitConfig(self.config); + }, 500); + }); } getSetting(chart) { - let self = this - let configObj = self.config + let self = this; + let configObj = self.config; // default to visualize xLabel if (typeof (configObj.xLabelStatus) === 'undefined') { - configObj.changeXLabel('default') + configObj.changeXLabel('default'); } if (typeof (configObj.rotate.degree) === 'undefined' || configObj.rotate.degree === '') { - configObj.rotate.degree = '-45' - self.emitConfig(configObj) + configObj.rotate.degree = '-45'; + self.emitConfig(configObj); } return { template: 'app/visualization/builtins/visualization-displayXAxis.html', scope: { - config: configObj - } - } + config: configObj, + }, + }; } } diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-barchart.js b/zeppelin-web/src/app/visualization/builtins/visualization-barchart.js index 2653af21e7f..e0279d99759 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-barchart.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-barchart.js @@ -12,34 +12,34 @@ * limitations under the License. */ -import Nvd3ChartVisualization from './visualization-nvd3chart' -import PivotTransformation from '../../tabledata/pivot' +import Nvd3ChartVisualization from './visualization-nvd3chart'; +import PivotTransformation from '../../tabledata/pivot'; /** * Visualize data in bar char */ export default class BarchartVisualization extends Nvd3ChartVisualization { - constructor (targetEl, config) { - super(targetEl, config) + constructor(targetEl, config) { + super(targetEl, config); - this.pivot = new PivotTransformation(config) + this.pivot = new PivotTransformation(config); try { - this.config.rotate = {degree: config.rotate.degree} + this.config.rotate = {degree: config.rotate.degree}; } catch (e) { - this.config.rotate = {degree: '-45'} + this.config.rotate = {degree: '-45'}; } } - type () { - return 'multiBarChart' + type() { + return 'multiBarChart'; } - getTransformation () { - return this.pivot + getTransformation() { + return this.pivot; } - render (pivot) { + render(pivot) { let d3Data = this.d3DataFromPivot( pivot.schema, pivot.rows, @@ -48,96 +48,98 @@ export default class BarchartVisualization extends Nvd3ChartVisualization { pivot.values, true, true, - true) + true); - super.render(d3Data) - this.config.changeXLabel(this.config.xLabelStatus) + super.render(d3Data); + this.config.changeXLabel(this.config.xLabelStatus); } /** * Set new config */ - setConfig (config) { - super.setConfig(config) - this.pivot.setConfig(config) + setConfig(config) { + super.setConfig(config); + this.pivot.setConfig(config); } - configureChart (chart) { - let self = this - let configObj = self.config + configureChart(chart) { + let self = this; + let configObj = self.config; - chart.yAxis.axisLabelDistance(50) - chart.yAxis.tickFormat(function (d) { return self.yAxisTickFormat(d) }) + chart.yAxis.axisLabelDistance(50); + chart.yAxis.tickFormat(function(d) { + return self.yAxisTickFormat(d); + }); - self.chart.stacked(this.config.stacked) + self.chart.stacked(this.config.stacked); self.config.changeXLabel = function(type) { switch (type) { case 'default': - self.chart._options['showXAxis'] = true - self.chart._options['margin'] = {bottom: 50} - self.chart.xAxis.rotateLabels(0) - configObj.xLabelStatus = 'default' - break + self.chart._options['showXAxis'] = true; + self.chart._options['margin'] = {bottom: 50}; + self.chart.xAxis.rotateLabels(0); + configObj.xLabelStatus = 'default'; + break; case 'rotate': - self.chart._options['showXAxis'] = true - self.chart._options['margin'] = {bottom: 140} - self.chart.xAxis.rotateLabels(configObj.rotate.degree) - configObj.xLabelStatus = 'rotate' - break + self.chart._options['showXAxis'] = true; + self.chart._options['margin'] = {bottom: 140}; + self.chart.xAxis.rotateLabels(configObj.rotate.degree); + configObj.xLabelStatus = 'rotate'; + break; case 'hide': - self.chart._options['showXAxis'] = false - self.chart._options['margin'] = {bottom: 50} - d3.select('#' + self.targetEl[0].id + '> svg').select('g.nv-axis.nv-x').selectAll('*').remove() - configObj.xLabelStatus = 'hide' - break + self.chart._options['showXAxis'] = false; + self.chart._options['margin'] = {bottom: 50}; + d3.select('#' + self.targetEl[0].id + '> svg').select('g.nv-axis.nv-x').selectAll('*').remove(); + configObj.xLabelStatus = 'hide'; + break; } - self.emitConfig(configObj) - } + self.emitConfig(configObj); + }; self.config.isXLabelStatus = function(type) { if (configObj.xLabelStatus === type) { - return true + return true; } else { - return false + return false; } - } + }; self.config.setDegree = function(type) { - configObj.rotate.degree = type - self.chart.xAxis.rotateLabels(type) - self.emitConfig(configObj) - } + configObj.rotate.degree = type; + self.chart.xAxis.rotateLabels(type); + self.emitConfig(configObj); + }; this.chart.dispatch.on('stateChange', function(s) { - configObj.stacked = s.stacked + configObj.stacked = s.stacked; // give some time to animation finish setTimeout(function() { - self.emitConfig(configObj) - }, 500) - }) + self.emitConfig(configObj); + }, 500); + }); } getSetting(chart) { - let self = this - let configObj = self.config + let self = this; + let configObj = self.config; // default to visualize xLabel if (typeof (configObj.xLabelStatus) === 'undefined') { - configObj.changeXLabel('default') + configObj.changeXLabel('default'); } if (typeof (configObj.rotate.degree) === 'undefined' || configObj.rotate.degree === '') { - configObj.rotate.degree = '-45' - self.emitConfig(configObj) + configObj.rotate.degree = '-45'; + self.emitConfig(configObj); } return { template: 'app/visualization/builtins/visualization-displayXAxis.html', scope: { - config: configObj - } - } + config: configObj, + }, + }; } } diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js b/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js index 46ee25168d9..749e4344dca 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-d3network.js @@ -12,18 +12,18 @@ * limitations under the License. */ -import Visualization from '../visualization' -import NetworkTransformation from '../../tabledata/network' +import Visualization from '../visualization'; +import NetworkTransformation from '../../tabledata/network'; /** * Visualize data in network format */ export default class NetworkVisualization extends Visualization { constructor(targetEl, config) { - super(targetEl, config) - console.log('Init network viz') + super(targetEl, config); + console.log('Init network viz'); if (!config.properties) { - config.properties = {} + config.properties = {}; } if (!config.d3Graph) { config.d3Graph = { @@ -33,101 +33,101 @@ export default class NetworkVisualization extends Visualization { linkDistance: 80, }, zoom: { - minScale: 1.3 - } - } + minScale: 1.3, + }, + }; } - this.targetEl.addClass('network') - this.containerId = this.targetEl.prop('id') - this.force = null - this.svg = null - this.$timeout = angular.injector(['ng']).get('$timeout') - this.$interpolate = angular.injector(['ng']).get('$interpolate') - this.transformation = new NetworkTransformation(config) + this.targetEl.addClass('network'); + this.containerId = this.targetEl.prop('id'); + this.force = null; + this.svg = null; + this.$timeout = angular.injector(['ng']).get('$timeout'); + this.$interpolate = angular.injector(['ng']).get('$interpolate'); + this.transformation = new NetworkTransformation(config); } refresh() { - console.log('refresh') + console.log('refresh'); } render(networkData) { if (!('graph' in networkData)) { - console.log('graph not found') - return + console.log('graph not found'); + return; } if (!networkData.isRendered) { - networkData.isRendered = true + networkData.isRendered = true; } else { - return + return; } - console.log('Rendering the graph') + console.log('Rendering the graph'); if (networkData.graph.edges.length && !networkData.isDefaultSet) { - networkData.isDefaultSet = true - this._setEdgesDefaults(networkData.graph) + networkData.isDefaultSet = true; + this._setEdgesDefaults(networkData.graph); } - const transformationConfig = this.transformation.getSetting().scope.config - console.log('cfg', transformationConfig) + const transformationConfig = this.transformation.getSetting().scope.config; + console.log('cfg', transformationConfig); if (transformationConfig && angular.equals({}, transformationConfig.properties)) { - transformationConfig.properties = this.getNetworkProperties(networkData.graph) + transformationConfig.properties = this.getNetworkProperties(networkData.graph); } - this.targetEl.empty().append('') + this.targetEl.empty().append(''); - const width = this.targetEl.width() - const height = this.targetEl.height() - const self = this - const defaultOpacity = 0 - const nodeSize = 10 - const textOffset = 3 - const linkSize = 10 + const width = this.targetEl.width(); + const height = this.targetEl.height(); + const self = this; + const defaultOpacity = 0; + const nodeSize = 10; + const textOffset = 3; + const linkSize = 10; const arcPath = (leftHand, d) => { - let start = leftHand ? d.source : d.target - let end = leftHand ? d.target : d.source - let dx = end.x - start.x - let dy = end.y - start.y + let start = leftHand ? d.source : d.target; + let end = leftHand ? d.target : d.source; + let dx = end.x - start.x; + let dy = end.y - start.y; let dr = d.totalCount === 1 - ? 0 : Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) / (1 + (1 / d.totalCount) * (d.count - 1)) - let sweep = leftHand ? 0 : 1 - return `M${start.x},${start.y}A${dr},${dr} 0 0,${sweep} ${end.x},${end.y}` - } + ? 0 : Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) / (1 + (1 / d.totalCount) * (d.count - 1)); + let sweep = leftHand ? 0 : 1; + return `M${start.x},${start.y}A${dr},${dr} 0 0,${sweep} ${end.x},${end.y}`; + }; // Use elliptical arc path segments to doubly-encode directionality. const tick = () => { // Links linkPath.attr('d', function(d) { - return arcPath(true, d) - }) + return arcPath(true, d); + }); textPath.attr('d', function(d) { - return arcPath(d.source.x < d.target.x, d) - }) + return arcPath(d.source.x < d.target.x, d); + }); // Nodes - circle.attr('transform', (d) => `translate(${d.x},${d.y})`) - text.attr('transform', (d) => `translate(${d.x},${d.y})`) - } + circle.attr('transform', (d) => `translate(${d.x},${d.y})`); + text.attr('transform', (d) => `translate(${d.x},${d.y})`); + }; const setOpacity = (scale) => { - let opacity = scale >= +transformationConfig.d3Graph.zoom.minScale ? 1 : 0 + let opacity = scale >= +transformationConfig.d3Graph.zoom.minScale ? 1 : 0; this.svg.selectAll('.nodeLabel') - .style('opacity', opacity) + .style('opacity', opacity); this.svg.selectAll('textPath') - .style('opacity', opacity) - } + .style('opacity', opacity); + }; const zoom = d3.behavior.zoom() .scaleExtent([1, 10]) .on('zoom', () => { - console.log('zoom') - setOpacity(d3.event.scale) - container.attr('transform', `translate(${d3.event.translate})scale(${d3.event.scale})`) - }) + console.log('zoom'); + setOpacity(d3.event.scale); + container.attr('transform', `translate(${d3.event.translate})scale(${d3.event.scale})`); + }); this.svg = d3.select(`#${this.containerId} svg`) .attr('width', width) .attr('height', height) - .call(zoom) + .call(zoom); this.force = d3.layout.force() .charge(transformationConfig.d3Graph.forceLayout.charge) @@ -137,54 +137,56 @@ export default class NetworkVisualization extends Visualization { .links(networkData.graph.edges) .size([width, height]) .on('start', () => { - console.log('force layout start') - this.$timeout(() => { this.force.stop() }, transformationConfig.d3Graph.forceLayout.timeout) + console.log('force layout start'); + this.$timeout(() => { + this.force.stop(); + }, transformationConfig.d3Graph.forceLayout.timeout); }) .on('end', () => { - console.log('force layout stop') - setOpacity(zoom.scale()) + console.log('force layout stop'); + setOpacity(zoom.scale()); }) - .start() + .start(); const renderFooterOnClick = (entity, type) => { - const footerId = this.containerId + '_footer' - const obj = {id: entity.id, label: entity.defaultLabel || entity.label, type: type} - let html = [`
  • ${obj.type}_id: ${obj.id}
  • `] + const footerId = this.containerId + '_footer'; + const obj = {id: entity.id, label: entity.defaultLabel || entity.label, type: type}; + let html = [`
  • ${obj.type}_id: ${obj.id}
  • `]; if (obj.label) { - html.push(`
  • ${obj.type}_type: ${obj.label}
  • `) + html.push(`
  • ${obj.type}_type: ${obj.label}
  • `); } html = html.concat(_.map(entity.data, (v, k) => { - return `
  • ${k}: ${v}
  • ` - })) + return `
  • ${k}: ${v}
  • `; + })); angular.element('#' + footerId) .find('.list-inline') .empty() - .append(html.join('')) - } + .append(html.join('')); + }; const drag = d3.behavior.drag() .origin((d) => d) .on('dragstart', function(d) { - console.log('dragstart') - d3.event.sourceEvent.stopPropagation() - d3.select(this).classed('dragging', true) - self.force.stop() + console.log('dragstart'); + d3.event.sourceEvent.stopPropagation(); + d3.select(this).classed('dragging', true); + self.force.stop(); }) .on('drag', function(d) { - console.log('drag') - d.px += d3.event.dx - d.py += d3.event.dy - d.x += d3.event.dx - d.y += d3.event.dy + console.log('drag'); + d.px += d3.event.dx; + d.py += d3.event.dy; + d.x += d3.event.dx; + d.y += d3.event.dy; }) .on('dragend', function(d) { - console.log('dragend') - d.fixed = true - d3.select(this).classed('dragging', false) - self.force.resume() - }) + console.log('dragend'); + d.fixed = true; + d3.select(this).classed('dragging', false); + self.force.resume(); + }); - const container = this.svg.append('g') + const container = this.svg.append('g'); if (networkData.graph.directed) { container.append('svg:defs').selectAll('marker') .data(['arrowMarker-' + this.containerId]) @@ -198,26 +200,26 @@ export default class NetworkVisualization extends Visualization { .attr('markerHeight', 4) .attr('orient', 'auto') .append('svg:path') - .attr('d', 'M0,-5L10,0L0,5') + .attr('d', 'M0,-5L10,0L0,5'); } // Links const link = container.append('svg:g') .on('click', () => { - renderFooterOnClick(d3.select(d3.event.target).datum(), 'edge') + renderFooterOnClick(d3.select(d3.event.target).datum(), 'edge'); }) .selectAll('g.link') .data(self.force.links()) .enter() - .append('g') - const getPathId = (d) => this.containerId + '_' + d.source.index + '_' + d.target.index + '_' + d.count - const showLabel = (d) => this._showNodeLabel(d) + .append('g'); + const getPathId = (d) => this.containerId + '_' + d.source.index + '_' + d.target.index + '_' + d.count; + const showLabel = (d) => this._showNodeLabel(d); const linkPath = link.append('svg:path') .attr('class', 'link') .attr('size', linkSize) - .attr('marker-end', `url(#arrowMarker-${this.containerId})`) + .attr('marker-end', `url(#arrowMarker-${this.containerId})`); const textPath = link.append('svg:path') .attr('id', getPathId) - .attr('class', 'textpath') + .attr('class', 'textpath'); container.append('svg:g') .selectAll('.pathLabel') .data(self.force.links()) @@ -229,11 +231,11 @@ export default class NetworkVisualization extends Visualization { .attr('text-anchor', 'middle') .attr('xlink:href', (d) => '#' + getPathId(d)) .text((d) => d.label) - .style('opacity', defaultOpacity) + .style('opacity', defaultOpacity); // Nodes const circle = container.append('svg:g') .on('click', () => { - renderFooterOnClick(d3.select(d3.event.target).datum(), 'node') + renderFooterOnClick(d3.select(d3.event.target).datum(), 'node'); }) .selectAll('circle') .data(self.force.nodes()) @@ -241,37 +243,37 @@ export default class NetworkVisualization extends Visualization { .attr('r', (d) => nodeSize) .attr('fill', (d) => networkData.graph.labels && d.label in networkData.graph.labels ? networkData.graph.labels[d.label] : '#000000') - .call(drag) + .call(drag); const text = container.append('svg:g').selectAll('g') .data(self.force.nodes()) - .enter().append('svg:g') + .enter().append('svg:g'); text.append('svg:text') .attr('x', (d) => nodeSize + textOffset) .attr('size', nodeSize) .attr('y', '.31em') .attr('class', (d) => 'nodeLabel shadow label-' + d.label) .text(showLabel) - .style('opacity', defaultOpacity) + .style('opacity', defaultOpacity); text.append('svg:text') .attr('x', (d) => nodeSize + textOffset) .attr('size', nodeSize) .attr('y', '.31em') .attr('class', (d) => 'nodeLabel label-' + d.label) .text(showLabel) - .style('opacity', defaultOpacity) + .style('opacity', defaultOpacity); } destroy() { } _showNodeLabel(d) { - const transformationConfig = this.transformation.getSetting().scope.config - const selectedLabel = (transformationConfig.properties[d.label] || {selected: 'label'}).selected - return d.data[selectedLabel] || d[selectedLabel] + const transformationConfig = this.transformation.getSetting().scope.config; + const selectedLabel = (transformationConfig.properties[d.label] || {selected: 'label'}).selected; + return d.data[selectedLabel] || d[selectedLabel]; } getTransformation() { - return this.transformation + return this.transformation; } setNodesDefaults() { @@ -281,56 +283,56 @@ export default class NetworkVisualization extends Visualization { graph.edges .sort((a, b) => { if (a.source > b.source) { - return 1 + return 1; } else if (a.source < b.source) { - return -1 + return -1; } else if (a.target > b.target) { - return 1 + return 1; } else if (a.target < b.target) { - return -1 + return -1; } else { - return 0 + return 0; } - }) + }); graph.edges .forEach((edge, index) => { - let prevEdge = graph.edges[index - 1] + let prevEdge = graph.edges[index - 1]; edge.count = (index > 0 && +edge.source === +prevEdge.source && +edge.target === +prevEdge.target - ? prevEdge.count : 0) + 1 + ? prevEdge.count : 0) + 1; edge.totalCount = graph.edges .filter((innerEdge) => +edge.source === +innerEdge.source && +edge.target === +innerEdge.target) - .length - }) + .length; + }); graph.edges .forEach((edge) => { if (typeof +edge.source === 'number') { // edge.source = graph.nodes.filter((node) => +edge.source === +node.id)[0] || null - edge.source = _.find(graph.nodes, (node) => +edge.source === +node.id) + edge.source = _.find(graph.nodes, (node) => +edge.source === +node.id); } if (typeof +edge.target === 'number') { // edge.target = graph.nodes.filter((node) => +edge.target === +node.id)[0] || null - edge.target = _.find(graph.nodes, (node) => +edge.target === +node.id) + edge.target = _.find(graph.nodes, (node) => +edge.target === +node.id); } - }) + }); } getNetworkProperties(graph) { - const baseCols = ['id', 'label'] - const properties = {} + const baseCols = ['id', 'label']; + const properties = {}; graph.nodes.forEach(function(node) { - const hasLabel = 'label' in node && node.label !== '' + const hasLabel = 'label' in node && node.label !== ''; if (!hasLabel) { - return + return; } - const label = node.label - const hasKey = hasLabel && label in properties + const label = node.label; + const hasKey = hasLabel && label in properties; const keys = _.uniq(Object.keys(node.data || {}) - .concat(hasKey ? properties[label].keys : baseCols)) + .concat(hasKey ? properties[label].keys : baseCols)); if (!hasKey) { - properties[label] = {selected: 'label'} + properties[label] = {selected: 'label'}; } - properties[label].keys = keys - }) - return properties + properties[label].keys = keys; + }); + return properties; } } diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-linechart.js b/zeppelin-web/src/app/visualization/builtins/visualization-linechart.js index 6d47a9e8d66..df161b98aa8 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-linechart.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-linechart.js @@ -12,39 +12,39 @@ * limitations under the License. */ -import Nvd3ChartVisualization from './visualization-nvd3chart' -import PivotTransformation from '../../tabledata/pivot' -import moment from 'moment' +import Nvd3ChartVisualization from './visualization-nvd3chart'; +import PivotTransformation from '../../tabledata/pivot'; +import moment from 'moment'; /** * Visualize data in line chart */ export default class LinechartVisualization extends Nvd3ChartVisualization { - constructor (targetEl, config) { - super(targetEl, config) + constructor(targetEl, config) { + super(targetEl, config); - this.pivot = new PivotTransformation(config) + this.pivot = new PivotTransformation(config); try { - this.config.rotate = {degree: config.rotate.degree} + this.config.rotate = {degree: config.rotate.degree}; } catch (e) { - this.config.rotate = {degree: '-45'} + this.config.rotate = {degree: '-45'}; } } - type () { + type() { if (this.config.lineWithFocus) { - return 'lineWithFocusChart' + return 'lineWithFocusChart'; } else { - return 'lineChart' + return 'lineChart'; } } - getTransformation () { - return this.pivot + getTransformation() { + return this.pivot; } - render (pivot) { + render(pivot) { let d3Data = this.d3DataFromPivot( pivot.schema, pivot.rows, @@ -53,113 +53,113 @@ export default class LinechartVisualization extends Nvd3ChartVisualization { pivot.values, false, true, - false) + false); - this.xLabels = d3Data.xLabels - super.render(d3Data) - this.config.changeXLabel(this.config.xLabelStatus) + this.xLabels = d3Data.xLabels; + super.render(d3Data); + this.config.changeXLabel(this.config.xLabelStatus); } /** * Set new config */ - setConfig (config) { - super.setConfig(config) - this.pivot.setConfig(config) + setConfig(config) { + super.setConfig(config); + this.pivot.setConfig(config); // change mode if (this.currentMode !== config.lineWithFocus) { - super.destroy() - this.currentMode = config.lineWithFocus + super.destroy(); + this.currentMode = config.lineWithFocus; } } - configureChart (chart) { - let self = this - let configObj = self.config + configureChart(chart) { + let self = this; + let configObj = self.config; - chart.xAxis.tickFormat(function (d) { + chart.xAxis.tickFormat(function(d) { if (self.config.isDateFormat) { if (self.config.dateFormat) { - return moment(new Date(self.xAxisTickFormat(d, self.xLabels))).format(self.config.dateFormat) + return moment(new Date(self.xAxisTickFormat(d, self.xLabels))).format(self.config.dateFormat); } else { - return moment(new Date(self.xAxisTickFormat(d, self.xLabels))).format('YYYY-MM-DD HH:mm:ss') + return moment(new Date(self.xAxisTickFormat(d, self.xLabels))).format('YYYY-MM-DD HH:mm:ss'); } } - return self.xAxisTickFormat(d, self.xLabels) - }) - chart.yAxis.tickFormat(function (d) { + return self.xAxisTickFormat(d, self.xLabels); + }); + chart.yAxis.tickFormat(function(d) { if (d === undefined) { - return 'N/A' + return 'N/A'; } - return self.yAxisTickFormat(d, self.xLabels) - }) - chart.yAxis.axisLabelDistance(50) + return self.yAxisTickFormat(d, self.xLabels); + }); + chart.yAxis.axisLabelDistance(50); if (chart.useInteractiveGuideline) { // lineWithFocusChart hasn't got useInteractiveGuideline - chart.useInteractiveGuideline(true) // for better UX and performance issue. (https://github.com/novus/nvd3/issues/691) + chart.useInteractiveGuideline(true); // for better UX and performance issue. (https://github.com/novus/nvd3/issues/691) } if (this.config.forceY) { - chart.forceY([0]) // force y-axis minimum to 0 for line chart. + chart.forceY([0]); // force y-axis minimum to 0 for line chart. } else { - chart.forceY([]) + chart.forceY([]); } self.config.changeXLabel = function(type) { switch (type) { case 'default': - self.chart._options['showXAxis'] = true - self.chart._options['margin'] = {bottom: 50} - self.chart.xAxis.rotateLabels(0) - configObj.xLabelStatus = 'default' - break + self.chart._options['showXAxis'] = true; + self.chart._options['margin'] = {bottom: 50}; + self.chart.xAxis.rotateLabels(0); + configObj.xLabelStatus = 'default'; + break; case 'rotate': - self.chart._options['showXAxis'] = true - self.chart._options['margin'] = {bottom: 140} - self.chart.xAxis.rotateLabels(configObj.rotate.degree) - configObj.xLabelStatus = 'rotate' - break + self.chart._options['showXAxis'] = true; + self.chart._options['margin'] = {bottom: 140}; + self.chart.xAxis.rotateLabels(configObj.rotate.degree); + configObj.xLabelStatus = 'rotate'; + break; case 'hide': - self.chart._options['showXAxis'] = false - self.chart._options['margin'] = {bottom: 50} - d3.select('#' + self.targetEl[0].id + '> svg').select('g.nv-axis.nv-x').selectAll('*').remove() - configObj.xLabelStatus = 'hide' - break + self.chart._options['showXAxis'] = false; + self.chart._options['margin'] = {bottom: 50}; + d3.select('#' + self.targetEl[0].id + '> svg').select('g.nv-axis.nv-x').selectAll('*').remove(); + configObj.xLabelStatus = 'hide'; + break; } - self.emitConfig(configObj) - } + self.emitConfig(configObj); + }; self.config.isXLabelStatus = function(type) { if (configObj.xLabelStatus === type) { - return true + return true; } else { - return false + return false; } - } + }; self.config.setDegree = function(type) { - configObj.rotate.degree = type - self.chart.xAxis.rotateLabels(type) - self.emitConfig(configObj) - } - - self.config.setDateFormat = function (format) { - configObj.dateFormat = format - self.emitConfig(configObj) - } + configObj.rotate.degree = type; + self.chart.xAxis.rotateLabels(type); + self.emitConfig(configObj); + }; + + self.config.setDateFormat = function(format) { + configObj.dateFormat = format; + self.emitConfig(configObj); + }; } - getSetting (chart) { - let self = this - let configObj = self.config + getSetting(chart) { + let self = this; + let configObj = self.config; // default to visualize xLabel if (typeof (configObj.xLabelStatus) === 'undefined') { - configObj.changeXLabel('default') + configObj.changeXLabel('default'); } if (typeof (configObj.rotate.degree) === 'undefined' || configObj.rotate.degree === '') { - configObj.rotate.degree = '-45' - self.emitConfig(configObj) + configObj.rotate.degree = '-45'; + self.emitConfig(configObj); } return { @@ -199,14 +199,14 @@ export default class LinechartVisualization extends Nvd3ChartVisualization { `, scope: { config: configObj, - save: function () { - self.emitConfig(configObj) - } - } - } + save: function() { + self.emitConfig(configObj); + }, + }, + }; } - defaultY () { - return undefined + defaultY() { + return undefined; } } diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-nvd3chart.js b/zeppelin-web/src/app/visualization/builtins/visualization-nvd3chart.js index f99fa3da7e8..b3e6ec654e2 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-nvd3chart.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-nvd3chart.js @@ -12,42 +12,42 @@ * limitations under the License. */ -import Visualization from '../visualization' +import Visualization from '../visualization'; /** * Visualize data in table format */ export default class Nvd3ChartVisualization extends Visualization { - constructor (targetEl, config) { - super(targetEl, config) - this.targetEl.append('') + constructor(targetEl, config) { + super(targetEl, config); + this.targetEl.append(''); } - refresh () { + refresh() { if (this.chart) { - this.chart.update() + this.chart.update(); } } - render (data) { - let type = this.type() - let d3g = data.d3g + render(data) { + let type = this.type(); + let d3g = data.d3g; if (!this.chart) { - this.chart = nv.models[type]() + this.chart = nv.models[type](); } - this.configureChart(this.chart) + this.configureChart(this.chart); - let animationDuration = 300 - let numberOfDataThreshold = 150 - let height = this.targetEl.height() + let animationDuration = 300; + let numberOfDataThreshold = 150; + let height = this.targetEl.height(); // turn off animation when dataset is too large. (for performance issue) // still, since dataset is large, the chart content sequentially appears like animated try { if (d3g[0].values.length > numberOfDataThreshold) { - animationDuration = 0 + animationDuration = 0; } } catch (err) { /** ignore */ } @@ -56,206 +56,214 @@ export default class Nvd3ChartVisualization extends Visualization { .datum(d3g) .transition() .duration(animationDuration) - .call(this.chart) - d3.select('#' + this.targetEl[0].id + ' svg').style.height = height + 'px' + .call(this.chart); + d3.select('#' + this.targetEl[0].id + ' svg').style.height = height + 'px'; } - type () { + type() { // override this and return chart type } - configureChart (chart) { + configureChart(chart) { // override this to configure chart } - groupedThousandsWith3DigitsFormatter (x) { - return d3.format(',')(d3.round(x, 3)) + groupedThousandsWith3DigitsFormatter(x) { + return d3.format(',')(d3.round(x, 3)); } - customAbbrevFormatter (x) { - let s = d3.format('.3s')(x) + customAbbrevFormatter(x) { + let s = d3.format('.3s')(x); switch (s[s.length - 1]) { - case 'G': return s.slice(0, -1) + 'B' + case 'G': return s.slice(0, -1) + 'B'; } - return s + return s; } - defaultY () { - return 0 + defaultY() { + return 0; } - xAxisTickFormat (d, xLabels) { + xAxisTickFormat(d, xLabels) { if (xLabels[d] && (isNaN(parseFloat(xLabels[d])) || !isFinite(xLabels[d]))) { // to handle string type xlabel - return xLabels[d] + return xLabels[d]; } else { - return d + return d; } } - yAxisTickFormat (d) { + yAxisTickFormat(d) { if (Math.abs(d) >= Math.pow(10, 6)) { - return this.customAbbrevFormatter(d) + return this.customAbbrevFormatter(d); } - return this.groupedThousandsWith3DigitsFormatter(d) + return this.groupedThousandsWith3DigitsFormatter(d); } - d3DataFromPivot ( + d3DataFromPivot( schema, rows, keys, groups, values, allowTextXAxis, fillMissingValues, multiBarChart) { - let self = this + let self = this; // construct table data - let d3g = [] + let d3g = []; - let concat = function (o, n) { + let concat = function(o, n) { if (!o) { - return n + return n; } else { - return o + '.' + n + return o + '.' + n; } - } + }; - const getSchemaUnderKey = function (key, s) { + const getSchemaUnderKey = function(key, s) { for (let c in key.children) { - s[c] = {} - getSchemaUnderKey(key.children[c], s[c]) + if(key.children.hasOwnProperty(c)) { + s[c] = {}; + getSchemaUnderKey(key.children[c], s[c]); + } } - } + }; - const traverse = function (sKey, s, rKey, r, func, rowName, rowValue, colName) { + const traverse = function(sKey, s, rKey, r, func, rowName, rowValue, colName) { // console.log("TRAVERSE sKey=%o, s=%o, rKey=%o, r=%o, rowName=%o, rowValue=%o, colName=%o", sKey, s, rKey, r, rowName, rowValue, colName); if (s.type === 'key') { - rowName = concat(rowName, sKey) - rowValue = concat(rowValue, rKey) + rowName = concat(rowName, sKey); + rowValue = concat(rowValue, rKey); } else if (s.type === 'group') { - colName = concat(colName, rKey) + colName = concat(colName, rKey); } else if (s.type === 'value' && sKey === rKey || valueOnly) { - colName = concat(colName, rKey) - func(rowName, rowValue, colName, r) + colName = concat(colName, rKey); + func(rowName, rowValue, colName, r); } for (let c in s.children) { if (fillMissingValues && s.children[c].type === 'group' && r[c] === undefined) { - let cs = {} - getSchemaUnderKey(s.children[c], cs) - traverse(c, s.children[c], c, cs, func, rowName, rowValue, colName) - continue + let cs = {}; + getSchemaUnderKey(s.children[c], cs); + traverse(c, s.children[c], c, cs, func, rowName, rowValue, colName); + continue; } for (let j in r) { if (s.children[c].type === 'key' || c === j) { - traverse(c, s.children[c], j, r[j], func, rowName, rowValue, colName) + traverse(c, s.children[c], j, r[j], func, rowName, rowValue, colName); } } } - } + }; - const valueOnly = (keys.length === 0 && groups.length === 0 && values.length > 0) - let noKey = (keys.length === 0) - let isMultiBarChart = multiBarChart + const valueOnly = (keys.length === 0 && groups.length === 0 && values.length > 0); + let noKey = (keys.length === 0); + let isMultiBarChart = multiBarChart; - let sKey = Object.keys(schema)[0] + let sKey = Object.keys(schema)[0]; - let rowNameIndex = {} - let rowIdx = 0 - let colNameIndex = {} - let colIdx = 0 - let rowIndexValue = {} + let rowNameIndex = {}; + let rowIdx = 0; + let colNameIndex = {}; + let colIdx = 0; + let rowIndexValue = {}; for (let k in rows) { - traverse(sKey, schema[sKey], k, rows[k], function (rowName, rowValue, colName, value) { - // console.log("RowName=%o, row=%o, col=%o, value=%o", rowName, rowValue, colName, value); - if (rowNameIndex[rowValue] === undefined) { - rowIndexValue[rowIdx] = rowValue - rowNameIndex[rowValue] = rowIdx++ - } + if (rows.hasOwnProperty(k)) { + traverse(sKey, schema[sKey], k, rows[k], function(rowName, rowValue, colName, value) { + // console.log("RowName=%o, row=%o, col=%o, value=%o", rowName, rowValue, colName, value); + if (rowNameIndex[rowValue] === undefined) { + rowIndexValue[rowIdx] = rowValue; + rowNameIndex[rowValue] = rowIdx++; + } - if (colNameIndex[colName] === undefined) { - colNameIndex[colName] = colIdx++ - } - let i = colNameIndex[colName] - if (noKey && isMultiBarChart) { - i = 0 - } + if (colNameIndex[colName] === undefined) { + colNameIndex[colName] = colIdx++; + } + let i = colNameIndex[colName]; + if (noKey && isMultiBarChart) { + i = 0; + } - if (!d3g[i]) { - d3g[i] = { - values: [], - key: (noKey && isMultiBarChart) ? 'values' : colName + if (!d3g[i]) { + d3g[i] = { + values: [], + key: (noKey && isMultiBarChart) ? 'values' : colName, + }; } - } - let xVar = isNaN(rowValue) ? ((allowTextXAxis) ? rowValue : rowNameIndex[rowValue]) : parseFloat(rowValue) - let yVar = self.defaultY() - if (xVar === undefined) { xVar = colName } - if (value !== undefined) { - yVar = isNaN(value.value) ? self.defaultY() : parseFloat(value.value) / parseFloat(value.count) - } - d3g[i].values.push({ - x: xVar, - y: yVar - }) - }) + let xVar = isNaN(rowValue) ? ((allowTextXAxis) ? rowValue : rowNameIndex[rowValue]) : parseFloat(rowValue); + let yVar = self.defaultY(); + if (xVar === undefined) { + xVar = colName; + } + if (value !== undefined) { + yVar = isNaN(value.value) ? self.defaultY() : parseFloat(value.value) / parseFloat(value.count); + } + d3g[i].values.push({ + x: xVar, + y: yVar, + }); + }); + } } // clear aggregation name, if possible - let namesWithoutAggr = {} - let colName - let withoutAggr + let namesWithoutAggr = {}; + let colName; + let withoutAggr; // TODO - This part could use som refactoring - Weird if/else with similar actions and variable names for (colName in colNameIndex) { - withoutAggr = colName.substring(0, colName.lastIndexOf('(')) - if (!namesWithoutAggr[withoutAggr]) { - namesWithoutAggr[withoutAggr] = 1 - } else { - namesWithoutAggr[withoutAggr]++ + if (colNameIndex.hasOwnProperty(colName)) { + withoutAggr = colName.substring(0, colName.lastIndexOf('(')); + if (!namesWithoutAggr[withoutAggr]) { + namesWithoutAggr[withoutAggr] = 1; + } else { + namesWithoutAggr[withoutAggr]++; + } } } if (valueOnly) { for (let valueIndex = 0; valueIndex < d3g[0].values.length; valueIndex++) { - colName = d3g[0].values[valueIndex].x + colName = d3g[0].values[valueIndex].x; if (!colName) { - continue + continue; } - withoutAggr = colName.substring(0, colName.lastIndexOf('(')) + withoutAggr = colName.substring(0, colName.lastIndexOf('(')); if (namesWithoutAggr[withoutAggr] <= 1) { - d3g[0].values[valueIndex].x = withoutAggr + d3g[0].values[valueIndex].x = withoutAggr; } } } else { for (let d3gIndex = 0; d3gIndex < d3g.length; d3gIndex++) { - colName = d3g[d3gIndex].key - withoutAggr = colName.substring(0, colName.lastIndexOf('(')) + colName = d3g[d3gIndex].key; + withoutAggr = colName.substring(0, colName.lastIndexOf('(')); if (namesWithoutAggr[withoutAggr] <= 1) { - d3g[d3gIndex].key = withoutAggr + d3g[d3gIndex].key = withoutAggr; } } // use group name instead of group.value as a column name, if there're only one group and one value selected. if (groups.length === 1 && values.length === 1) { for (let d3gIndex = 0; d3gIndex < d3g.length; d3gIndex++) { - colName = d3g[d3gIndex].key - colName = colName.split('.').slice(0, -1).join('.') - d3g[d3gIndex].key = colName + colName = d3g[d3gIndex].key; + colName = colName.split('.').slice(0, -1).join('.'); + d3g[d3gIndex].key = colName; } } } return { xLabels: rowIndexValue, - d3g: d3g - } + d3g: d3g, + }; } /** * method will be invoked when visualization need to be destroyed. * Don't need to destroy this.targetEl. */ - destroy () { + destroy() { if (this.chart) { - d3.selectAll('#' + this.targetEl[0].id + ' svg > *').remove() - this.chart = undefined + d3.selectAll('#' + this.targetEl[0].id + ' svg > *').remove(); + this.chart = undefined; } } } diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-piechart.js b/zeppelin-web/src/app/visualization/builtins/visualization-piechart.js index 4f80654db1d..84479cb600d 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-piechart.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-piechart.js @@ -12,29 +12,29 @@ * limitations under the License. */ -import Nvd3ChartVisualization from './visualization-nvd3chart' -import PivotTransformation from '../../tabledata/pivot' +import Nvd3ChartVisualization from './visualization-nvd3chart'; +import PivotTransformation from '../../tabledata/pivot'; /** * Visualize data in pie chart */ export default class PiechartVisualization extends Nvd3ChartVisualization { - constructor (targetEl, config) { - super(targetEl, config) - this.pivot = new PivotTransformation(config) + constructor(targetEl, config) { + super(targetEl, config); + this.pivot = new PivotTransformation(config); } - type () { - return 'pieChart' + type() { + return 'pieChart'; } - getTransformation () { - return this.pivot + getTransformation() { + return this.pivot; } - render (pivot) { + render(pivot) { // [ZEPPELIN-2253] New chart function will be created each time inside super.render() - this.chart = null + this.chart = null; const d3Data = this.d3DataFromPivot( pivot.schema, pivot.rows, @@ -43,41 +43,45 @@ export default class PiechartVisualization extends Nvd3ChartVisualization { pivot.values, true, false, - false) - const d = d3Data.d3g + false); + const d = d3Data.d3g; - let generateLabel + let generateLabel; // data is grouped if (pivot.groups && pivot.groups.length > 0) { - generateLabel = (suffix, prefix) => `${prefix}.${suffix}` + generateLabel = (suffix, prefix) => `${prefix}.${suffix}`; } else { // data isn't grouped - generateLabel = suffix => suffix + generateLabel = (suffix) => suffix; } - let d3g = d.map(group => { - return group.values.map(row => ({ + let d3g = d.map((group) => { + return group.values.map((row) => ({ label: generateLabel(row.x, group.key), - value: row.y - })) - }) + value: row.y, + })); + }); // the map function returns d3g as a nested array // [].concat flattens it, http://stackoverflow.com/a/10865042/5154397 - d3g = [].concat.apply([], d3g) // eslint-disable-line prefer-spread - super.render({d3g: d3g}) + d3g = [].concat.apply([], d3g); // eslint-disable-line prefer-spread + super.render({d3g: d3g}); } /** * Set new config */ - setConfig (config) { - super.setConfig(config) - this.pivot.setConfig(config) + setConfig(config) { + super.setConfig(config); + this.pivot.setConfig(config); } - configureChart (chart) { - chart.x(function (d) { return d.label }) - .y(function (d) { return d.value }) - .showLabels(false) - .showTooltipPercent(true) + configureChart(chart) { + chart.x(function(d) { + return d.label; + }) + .y(function(d) { + return d.value; + }) + .showLabels(false) + .showTooltipPercent(true); } } diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-scatterchart.js b/zeppelin-web/src/app/visualization/builtins/visualization-scatterchart.js index d7c00dbc32b..fad7500b962 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-scatterchart.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-scatterchart.js @@ -12,25 +12,25 @@ * limitations under the License. */ -import Nvd3ChartVisualization from './visualization-nvd3chart' -import ColumnselectorTransformation from '../../tabledata/columnselector' +import Nvd3ChartVisualization from './visualization-nvd3chart'; +import ColumnselectorTransformation from '../../tabledata/columnselector'; /** * Visualize data in scatter char */ export default class ScatterchartVisualization extends Nvd3ChartVisualization { - constructor (targetEl, config) { - super(targetEl, config) + constructor(targetEl, config) { + super(targetEl, config); this.columnselectorProps = [ { - name: 'xAxis' + name: 'xAxis', }, { - name: 'yAxis' + name: 'yAxis', }, { - name: 'group' + name: 'group', }, { name: 'size', @@ -39,322 +39,330 @@ export default class ScatterchartVisualization extends Nvd3ChartVisualization { 'number of values in corresponding coordinate' will be used. Zeppelin considers values as discrete when input values contain a string or the number of distinct values is greater than 5% of the total number of values. - This field turns grey when the selected option is invalid.` - } - ] - this.columnselector = new ColumnselectorTransformation(config, this.columnselectorProps) + This field turns grey when the selected option is invalid.`, + }, + ]; + this.columnselector = new ColumnselectorTransformation(config, this.columnselectorProps); } - type () { - return 'scatterChart' + type() { + return 'scatterChart'; } - getTransformation () { - return this.columnselector + getTransformation() { + return this.columnselector; } - render (tableData) { - this.tableData = tableData - this.selectDefault() - let d3Data = this.setScatterChart(tableData, true) - this.xLabels = d3Data.xLabels - this.yLabels = d3Data.yLabels + render(tableData) { + this.tableData = tableData; + this.selectDefault(); + let d3Data = this.setScatterChart(tableData, true); + this.xLabels = d3Data.xLabels; + this.yLabels = d3Data.yLabels; - super.render(d3Data) + super.render(d3Data); } - configureChart (chart) { - let self = this + configureChart(chart) { + let self = this; - chart.xAxis.tickFormat(function (d) { // TODO remove round after bump to nvd3 > 1.8.5 - return self.xAxisTickFormat(Math.round(d * 1e3) / 1e3, self.xLabels) - }) + chart.xAxis.tickFormat(function(d) { // TODO remove round after bump to nvd3 > 1.8.5 + return self.xAxisTickFormat(Math.round(d * 1e3) / 1e3, self.xLabels); + }); - chart.yAxis.tickFormat(function (d) { // TODO remove round after bump to nvd3 > 1.8.5 - return self.yAxisTickFormat(Math.round(d * 1e3) / 1e3, self.yLabels) - }) + chart.yAxis.tickFormat(function(d) { // TODO remove round after bump to nvd3 > 1.8.5 + return self.yAxisTickFormat(Math.round(d * 1e3) / 1e3, self.yLabels); + }); - chart.showDistX(true).showDistY(true) + chart.showDistX(true).showDistY(true); // handle the problem of tooltip not showing when muliple points have same value. } - yAxisTickFormat (d, yLabels) { + yAxisTickFormat(d, yLabels) { if (yLabels[d] && (isNaN(parseFloat(yLabels[d])) || !isFinite(yLabels[d]))) { // to handle string type xlabel - return yLabels[d] + return yLabels[d]; } else { - return super.yAxisTickFormat(d) + return super.yAxisTickFormat(d); } } - selectDefault () { + selectDefault() { if (!this.config.xAxis && !this.config.yAxis) { if (this.tableData.columns.length > 1) { - this.config.xAxis = this.tableData.columns[0] - this.config.yAxis = this.tableData.columns[1] + this.config.xAxis = this.tableData.columns[0]; + this.config.yAxis = this.tableData.columns[1]; } else if (this.tableData.columns.length === 1) { - this.config.xAxis = this.tableData.columns[0] + this.config.xAxis = this.tableData.columns[0]; } } } - setScatterChart (data, refresh) { - let xAxis = this.config.xAxis - let yAxis = this.config.yAxis - let group = this.config.group - let size = this.config.size - - let xValues = [] - let yValues = [] - let rows = {} - let d3g = [] - - let rowNameIndex = {} - let colNameIndex = {} - let grpNameIndex = {} - let rowIndexValue = {} - let colIndexValue = {} - let grpIndexValue = {} - let rowIdx = 0 - let colIdx = 0 - let grpIdx = 0 - let grpName = '' - - let xValue - let yValue - let row + setScatterChart(data, refresh) { + let xAxis = this.config.xAxis; + let yAxis = this.config.yAxis; + let group = this.config.group; + let size = this.config.size; + + let xValues = []; + let yValues = []; + let rows = {}; + let d3g = []; + + let rowNameIndex = {}; + let colNameIndex = {}; + let grpNameIndex = {}; + let rowIndexValue = {}; + let colIndexValue = {}; + let grpIndexValue = {}; + let rowIdx = 0; + let colIdx = 0; + let grpIdx = 0; + let grpName = ''; + + let xValue; + let yValue; + let row; if (!xAxis && !yAxis) { return { - d3g: [] - } + d3g: [], + }; } for (let i = 0; i < data.rows.length; i++) { - row = data.rows[i] + row = data.rows[i]; if (xAxis) { - xValue = row[xAxis.index] - xValues[i] = xValue + xValue = row[xAxis.index]; + xValues[i] = xValue; } if (yAxis) { - yValue = row[yAxis.index] - yValues[i] = yValue + yValue = row[yAxis.index]; + yValues[i] = yValue; } } let isAllDiscrete = ((xAxis && yAxis && this.isDiscrete(xValues) && this.isDiscrete(yValues)) || (!xAxis && this.isDiscrete(yValues)) || - (!yAxis && this.isDiscrete(xValues))) + (!yAxis && this.isDiscrete(xValues))); if (isAllDiscrete) { - rows = this.setDiscreteScatterData(data) + rows = this.setDiscreteScatterData(data); } else { - rows = data.rows + rows = data.rows; } if (!group && isAllDiscrete) { - grpName = 'count' + grpName = 'count'; } else if (!group && !size) { if (xAxis && yAxis) { - grpName = '(' + xAxis.name + ', ' + yAxis.name + ')' + grpName = '(' + xAxis.name + ', ' + yAxis.name + ')'; } else if (xAxis && !yAxis) { - grpName = xAxis.name + grpName = xAxis.name; } else if (!xAxis && yAxis) { - grpName = yAxis.name + grpName = yAxis.name; } } else if (!group && size) { - grpName = size.name + grpName = size.name; } - let epsilon = 1e-4 // TODO remove after bump to nvd3 > 1.8.5 + let epsilon = 1e-4; // TODO remove after bump to nvd3 > 1.8.5 for (let i = 0; i < rows.length; i++) { - row = rows[i] + row = rows[i]; if (xAxis) { - xValue = row[xAxis.index] + xValue = row[xAxis.index]; } if (yAxis) { - yValue = row[yAxis.index] + yValue = row[yAxis.index]; } if (group) { - grpName = row[group.index] + grpName = row[group.index]; } - let sz = (isAllDiscrete) ? row[row.length - 1] : ((size) ? row[size.index] : 1) + let sz = (isAllDiscrete) ? row[row.length - 1] : ((size) ? row[size.index] : 1); if (grpNameIndex[grpName] === undefined) { - grpIndexValue[grpIdx] = grpName - grpNameIndex[grpName] = grpIdx++ + grpIndexValue[grpIdx] = grpName; + grpNameIndex[grpName] = grpIdx++; } if (xAxis && rowNameIndex[xValue] === undefined) { - rowIndexValue[rowIdx] = xValue - rowNameIndex[xValue] = rowIdx++ + rowIndexValue[rowIdx] = xValue; + rowNameIndex[xValue] = rowIdx++; } if (yAxis && colNameIndex[yValue] === undefined) { - colIndexValue[colIdx] = yValue - colNameIndex[yValue] = colIdx++ + colIndexValue[colIdx] = yValue; + colNameIndex[yValue] = colIdx++; } if (!d3g[grpNameIndex[grpName]]) { d3g[grpNameIndex[grpName]] = { key: grpName, - values: [] - } + values: [], + }; } // TODO remove epsilon jitter after bump to nvd3 > 1.8.5 - let xval = 0 - let yval = 0 + let xval = 0; + let yval = 0; if (xAxis) { - xval = (isNaN(xValue) ? rowNameIndex[xValue] : parseFloat(xValue)) + Math.random() * epsilon + xval = (isNaN(xValue) ? rowNameIndex[xValue] : parseFloat(xValue)) + Math.random() * epsilon; } if (yAxis) { - yval = (isNaN(yValue) ? colNameIndex[yValue] : parseFloat(yValue)) + Math.random() * epsilon + yval = (isNaN(yValue) ? colNameIndex[yValue] : parseFloat(yValue)) + Math.random() * epsilon; } d3g[grpNameIndex[grpName]].values.push({ x: xval, y: yval, - size: isNaN(parseFloat(sz)) ? 1 : parseFloat(sz) - }) + size: isNaN(parseFloat(sz)) ? 1 : parseFloat(sz), + }); } // TODO remove sort and dedup after bump to nvd3 > 1.8.5 - let d3gvalues = d3g[grpNameIndex[grpName]].values - d3gvalues.sort(function (a, b) { - return ((a['x'] - b['x']) || (a['y'] - b['y'])) - }) + let d3gvalues = d3g[grpNameIndex[grpName]].values; + d3gvalues.sort(function(a, b) { + return ((a['x'] - b['x']) || (a['y'] - b['y'])); + }); for (let i = 0; i < d3gvalues.length - 1;) { if ((Math.abs(d3gvalues[i]['x'] - d3gvalues[i + 1]['x']) < epsilon) && (Math.abs(d3gvalues[i]['y'] - d3gvalues[i + 1]['y']) < epsilon)) { - d3gvalues.splice(i + 1, 1) + d3gvalues.splice(i + 1, 1); } else { - i++ + i++; } } return { xLabels: rowIndexValue, yLabels: colIndexValue, - d3g: d3g - } + d3g: d3g, + }; } - setDiscreteScatterData (data) { - let xAxis = this.config.xAxis - let yAxis = this.config.yAxis - let group = this.config.group + setDiscreteScatterData(data) { + let xAxis = this.config.xAxis; + let yAxis = this.config.yAxis; + let group = this.config.group; - let xValue - let yValue - let grp + let xValue; + let yValue; + let grp; - let rows = {} + let rows = {}; for (let i = 0; i < data.rows.length; i++) { - let row = data.rows[i] + let row = data.rows[i]; if (xAxis) { - xValue = row[xAxis.index] + xValue = row[xAxis.index]; } if (yAxis) { - yValue = row[yAxis.index] + yValue = row[yAxis.index]; } if (group) { - grp = row[group.index] + grp = row[group.index]; } - let key = xValue + ',' + yValue + ',' + grp + let key = xValue + ',' + yValue + ',' + grp; if (!rows[key]) { rows[key] = { x: xValue, y: yValue, group: grp, - size: 1 - } + size: 1, + }; } else { - rows[key].size++ + rows[key].size++; } } // change object into array - let newRows = [] + let newRows = []; for (let r in rows) { - let newRow = [] - if (xAxis) { newRow[xAxis.index] = rows[r].x } - if (yAxis) { newRow[yAxis.index] = rows[r].y } - if (group) { newRow[group.index] = rows[r].group } - newRow[data.rows[0].length] = rows[r].size - newRows.push(newRow) + if (rows.hasOwnProperty(r)) { + let newRow = []; + if (xAxis) { + newRow[xAxis.index] = rows[r].x; + } + if (yAxis) { + newRow[yAxis.index] = rows[r].y; + } + if (group) { + newRow[group.index] = rows[r].group; + } + newRow[data.rows[0].length] = rows[r].size; + newRows.push(newRow); + } } - return newRows + return newRows; } - isDiscrete (field) { - let getUnique = function (f) { - let uniqObj = {} - let uniqArr = [] - let j = 0 + isDiscrete(field) { + let getUnique = function(f) { + let uniqObj = {}; + let uniqArr = []; + let j = 0; for (let i = 0; i < f.length; i++) { - let item = f[i] + let item = f[i]; if (uniqObj[item] !== 1) { - uniqObj[item] = 1 - uniqArr[j++] = item + uniqObj[item] = 1; + uniqArr[j++] = item; } } - return uniqArr - } + return uniqArr; + }; for (let i = 0; i < field.length; i++) { if (isNaN(parseFloat(field[i])) && (typeof field[i] === 'string' || field[i] instanceof String)) { - return true + return true; } } - let threshold = 0.05 - let unique = getUnique(field) + let threshold = 0.05; + let unique = getUnique(field); if (unique.length / field.length < threshold) { - return true + return true; } else { - return false + return false; } } - isValidSizeOption (options) { - let xValues = [] - let yValues = [] - let rows = this.tableData.rows + isValidSizeOption(options) { + let xValues = []; + let yValues = []; + let rows = this.tableData.rows; for (let i = 0; i < rows.length; i++) { - let row = rows[i] - let size = row[options.size.index] + let row = rows[i]; + let size = row[options.size.index]; // check if the field is numeric if (isNaN(parseFloat(size)) || !isFinite(size)) { - return false + return false; } if (options.xAxis) { - let x = row[options.xAxis.index] - xValues[i] = x + let x = row[options.xAxis.index]; + xValues[i] = x; } if (options.yAxis) { - let y = row[options.yAxis.index] - yValues[i] = y + let y = row[options.yAxis.index]; + yValues[i] = y; } } // check if all existing fields are discrete let isAllDiscrete = ((options.xAxis && options.yAxis && this.isDiscrete(xValues) && this.isDiscrete(yValues)) || (!options.xAxis && this.isDiscrete(yValues)) || - (!options.yAxis && this.isDiscrete(xValues))) + (!options.yAxis && this.isDiscrete(xValues))); if (isAllDiscrete) { - return false + return false; } - return true + return true; } } diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-table.js b/zeppelin-web/src/app/visualization/builtins/visualization-table.js index afb5394610e..d77efbc805a 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-table.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-table.js @@ -12,8 +12,8 @@ * limitations under the License. */ -import Visualization from '../visualization' -import PassthroughTransformation from '../../tabledata/passthrough' +import Visualization from '../visualization'; +import PassthroughTransformation from '../../tabledata/passthrough'; import { Widget, ValueType, @@ -22,9 +22,9 @@ import { initializeTableConfig, resetTableOptionConfig, DefaultTableColumnType, TableColumnType, updateColumnTypeState, parseTableOption, -} from './visualization-util' +} from './visualization-util'; -const SETTING_TEMPLATE = require('./visualization-table-setting.html') +const SETTING_TEMPLATE = require('./visualization-table-setting.html'); const TABLE_OPTION_SPECS = [ { @@ -48,41 +48,43 @@ const TABLE_OPTION_SPECS = [ widget: Widget.CHECKBOX, description: 'Enable a footer for displaying aggregated values', }, -] +]; /** * Visualize data in table format */ export default class TableVisualization extends Visualization { - constructor (targetEl, config) { - super(targetEl, config) - this.passthrough = new PassthroughTransformation(config) - this.emitTimeout = null - this.isRestoring = false + constructor(targetEl, config) { + super(targetEl, config); + this.passthrough = new PassthroughTransformation(config); + this.emitTimeout = null; + this.isRestoring = false; - initializeTableConfig(config, TABLE_OPTION_SPECS) + initializeTableConfig(config, TABLE_OPTION_SPECS); } getColumnMinWidth(colName) { - let width = 150 // default - const calculatedWidth = colName.length * 10 + let width = 150; // default + const calculatedWidth = colName.length * 10; // use the broad one - if (calculatedWidth > width) { width = calculatedWidth } + if (calculatedWidth > width) { + width = calculatedWidth; + } - return width + return width; } createGridOptions(tableData, onRegisterApiCallback, config) { - const rows = tableData.rows - const columnNames = tableData.columns.map(c => c.name) + const rows = tableData.rows; + const columnNames = tableData.columns.map((c) => c.name); - const gridData = rows.map(r => { + const gridData = rows.map((r) => { return columnNames.reduce((acc, colName, index) => { - acc[colName] = r[index] - return acc - }, {}) - }) + acc[colName] = r[index]; + return acc; + }, {}); + }); const gridOptions = { data: gridData, @@ -94,7 +96,7 @@ export default class TableVisualization extends Visualization { fastWatch: false, treeRowHeaderAlwaysVisible: false, - columnDefs: columnNames.map(colName => { + columnDefs: columnNames.map((colName) => { return { displayName: colName, name: colName, @@ -111,7 +113,7 @@ export default class TableVisualization extends Visualization { `, minWidth: this.getColumnMinWidth(colName), width: '*', - } + }; }), rowEditWaitInterval: -1, /** disable saveRow event */ enableRowHashing: true, @@ -126,127 +128,131 @@ export default class TableVisualization extends Visualization { saveTreeView: true, saveFilter: true, saveSelection: false, - } + }; - return gridOptions + return gridOptions; } getGridElemId() { // angular doesn't allow `-` in scope variable name - const gridElemId = `${this.targetEl[0].id}_grid`.replace('-', '_') - return gridElemId + const gridElemId = `${this.targetEl[0].id}_grid`.replace('-', '_'); + return gridElemId; } getGridApiId() { // angular doesn't allow `-` in scope variable name - const gridApiId = `${this.targetEl[0].id}_gridApi`.replace('-', '_') - return gridApiId + const gridApiId = `${this.targetEl[0].id}_gridApi`.replace('-', '_'); + return gridApiId; } refresh() { - const gridElemId = this.getGridElemId() - const gridElem = angular.element(`#${gridElemId}`) + const gridElemId = this.getGridElemId(); + const gridElem = angular.element(`#${gridElemId}`); if (gridElem) { - gridElem.css('height', this.targetEl.height() - 10) + gridElem.css('height', this.targetEl.height() - 10); } } refreshGrid() { - const gridElemId = this.getGridElemId() - const gridElem = angular.element(`#${gridElemId}`) + const gridElemId = this.getGridElemId(); + const gridElem = angular.element(`#${gridElemId}`); if (gridElem) { - const scope = this.getScope() - const gridApiId = this.getGridApiId() - scope[gridApiId].core.notifyDataChange(this._uiGridConstants.dataChange.ALL) + const scope = this.getScope(); + const gridApiId = this.getGridApiId(); + scope[gridApiId].core.notifyDataChange(this._uiGridConstants.dataChange.ALL); } } updateColDefType(colDef, type) { - if (type === colDef.type) { return } + if (type === colDef.type) { + return; + } - colDef.type = type - const colName = colDef.name - const config = this.config + colDef.type = type; + const colName = colDef.name; + const config = this.config; if (config.tableColumnTypeState.names && config.tableColumnTypeState.names[colName]) { - config.tableColumnTypeState.names[colName] = type - this.persistConfigWithGridState(this.config) + config.tableColumnTypeState.names[colName] = type; + this.persistConfigWithGridState(this.config); } } addColumnMenus(gridOptions) { - if (!gridOptions || !gridOptions.columnDefs) { return } + if (!gridOptions || !gridOptions.columnDefs) { + return; + } - const self = this // for closure + const self = this; // for closure // SHOULD use `function() { ... }` syntax for each action to get `this` - gridOptions.columnDefs.map(colDef => { + gridOptions.columnDefs.map((colDef) => { colDef.menuItems = [ { title: 'Type: String', action: function() { - self.updateColDefType(this.context.col.colDef, TableColumnType.STRING) + self.updateColDefType(this.context.col.colDef, TableColumnType.STRING); }, active: function() { - return this.context.col.colDef.type === TableColumnType.STRING + return this.context.col.colDef.type === TableColumnType.STRING; }, }, { title: 'Type: Number', action: function() { - self.updateColDefType(this.context.col.colDef, TableColumnType.NUMBER) + self.updateColDefType(this.context.col.colDef, TableColumnType.NUMBER); }, active: function() { - return this.context.col.colDef.type === TableColumnType.NUMBER + return this.context.col.colDef.type === TableColumnType.NUMBER; }, }, { title: 'Type: Date', action: function() { - self.updateColDefType(this.context.col.colDef, TableColumnType.DATE) + self.updateColDefType(this.context.col.colDef, TableColumnType.DATE); }, active: function() { - return this.context.col.colDef.type === TableColumnType.DATE + return this.context.col.colDef.type === TableColumnType.DATE; }, }, - ] - }) + ]; + }); } setDynamicGridOptions(gridOptions, config) { // parse based on their type definitions - const parsed = parseTableOption(TABLE_OPTION_SPECS, config.tableOptionValue) + const parsed = parseTableOption(TABLE_OPTION_SPECS, config.tableOptionValue); - const { showAggregationFooter, useFilter, showPagination, } = parsed + const {showAggregationFooter, useFilter, showPagination} = parsed; - gridOptions.showGridFooter = false - gridOptions.showColumnFooter = showAggregationFooter - gridOptions.enableFiltering = useFilter + gridOptions.showGridFooter = false; + gridOptions.showColumnFooter = showAggregationFooter; + gridOptions.enableFiltering = useFilter; - gridOptions.enablePagination = showPagination - gridOptions.enablePaginationControls = showPagination + gridOptions.enablePagination = showPagination; + gridOptions.enablePaginationControls = showPagination; if (showPagination) { - gridOptions.paginationPageSize = 50 - gridOptions.paginationPageSizes = [25, 50, 100, 250, 1000] + gridOptions.paginationPageSize = 50; + gridOptions.paginationPageSizes = [25, 50, 100, 250, 1000]; } // selection can't be rendered dynamically in ui-grid 4.0.4 - gridOptions.enableRowSelection = false - gridOptions.enableRowHeaderSelection = false - gridOptions.enableFullRowSelection = false - gridOptions.enableSelectAll = false - gridOptions.enableGroupHeaderSelection = false - gridOptions.enableSelectionBatchEvent = false + gridOptions.enableRowSelection = false; + gridOptions.enableRowHeaderSelection = false; + gridOptions.enableFullRowSelection = false; + gridOptions.enableSelectAll = false; + gridOptions.enableGroupHeaderSelection = false; + gridOptions.enableSelectionBatchEvent = false; } - render (tableData) { - const gridElemId = this.getGridElemId() - let gridElem = document.getElementById(gridElemId) + render(tableData) { + const gridElemId = this.getGridElemId(); + let gridElem = document.getElementById(gridElemId); - const config = this.config - const self = this // for closure + const config = this.config; + const self = this; // for closure if (!gridElem) { // create, compile and append grid elem @@ -261,125 +267,147 @@ export default class TableVisualization extends Visualization { ui-grid-move-columns ui-grid-grouping ui-grid-save-state - ui-grid-exporter>`) + ui-grid-exporter>`); - gridElem.css('height', this.targetEl.height() - 10) - const scope = this.getScope() - gridElem = this._compile(gridElem)(scope) - this.targetEl.append(gridElem) + gridElem.css('height', this.targetEl.height() - 10); + const scope = this.getScope(); + gridElem = this._compile(gridElem)(scope); + this.targetEl.append(gridElem); // set gridOptions for this elem - const gridOptions = this.createGridOptions(tableData, onRegisterApiCallback, config) - this.setDynamicGridOptions(gridOptions, config) - this.addColumnMenus(gridOptions) - scope[gridElemId] = gridOptions + const gridOptions = this.createGridOptions(tableData, onRegisterApiCallback, config); + this.setDynamicGridOptions(gridOptions, config); + this.addColumnMenus(gridOptions); + scope[gridElemId] = gridOptions; // set gridApi for this elem - const gridApiId = this.getGridApiId() + const gridApiId = this.getGridApiId(); const onRegisterApiCallback = (gridApi) => { - scope[gridApiId] = gridApi + scope[gridApiId] = gridApi; // should restore state before registering APIs // register callbacks for change evens // should persist `self.config` instead `config` (closure issue) - gridApi.core.on.columnVisibilityChanged(scope, () => { self.persistConfigWithGridState(self.config) }) - gridApi.colMovable.on.columnPositionChanged(scope, () => { self.persistConfigWithGridState(self.config) }) - gridApi.core.on.sortChanged(scope, () => { self.persistConfigWithGridState(self.config) }) - gridApi.core.on.filterChanged(scope, () => { self.persistConfigWithGridState(self.config) }) - gridApi.grouping.on.aggregationChanged(scope, () => { self.persistConfigWithGridState(self.config) }) - gridApi.grouping.on.groupingChanged(scope, () => { self.persistConfigWithGridState(self.config) }) - gridApi.treeBase.on.rowCollapsed(scope, () => { self.persistConfigWithGridState(self.config) }) - gridApi.treeBase.on.rowExpanded(scope, () => { self.persistConfigWithGridState(self.config) }) - gridApi.colResizable.on.columnSizeChanged(scope, () => { self.persistConfigWithGridState(self.config) }) + gridApi.core.on.columnVisibilityChanged(scope, () => { + self.persistConfigWithGridState(self.config); + }); + gridApi.colMovable.on.columnPositionChanged(scope, () => { + self.persistConfigWithGridState(self.config); + }); + gridApi.core.on.sortChanged(scope, () => { + self.persistConfigWithGridState(self.config); + }); + gridApi.core.on.filterChanged(scope, () => { + self.persistConfigWithGridState(self.config); + }); + gridApi.grouping.on.aggregationChanged(scope, () => { + self.persistConfigWithGridState(self.config); + }); + gridApi.grouping.on.groupingChanged(scope, () => { + self.persistConfigWithGridState(self.config); + }); + gridApi.treeBase.on.rowCollapsed(scope, () => { + self.persistConfigWithGridState(self.config); + }); + gridApi.treeBase.on.rowExpanded(scope, () => { + self.persistConfigWithGridState(self.config); + }); + gridApi.colResizable.on.columnSizeChanged(scope, () => { + self.persistConfigWithGridState(self.config); + }); // pagination doesn't follow usual life-cycle in ui-grid v4.0.4 // gridApi.pagination.on.paginationChanged(scope, () => { self.persistConfigWithGridState(self.config) }) // TBD: do we need to propagate row selection? // gridApi.selection.on.rowSelectionChanged(scope, () => { self.persistConfigWithGridState(self.config) }) // gridApi.selection.on.rowSelectionChangedBatch(scope, () => { self.persistConfigWithGridState(self.config) }) - } - gridOptions.onRegisterApi = onRegisterApiCallback + }; + gridOptions.onRegisterApi = onRegisterApiCallback; } else { // don't need to update gridOptions.data since it's synchronized by paragraph execution - const gridOptions = this.getGridOptions() - this.setDynamicGridOptions(gridOptions, config) - this.refreshGrid() + const gridOptions = this.getGridOptions(); + this.setDynamicGridOptions(gridOptions, config); + this.refreshGrid(); } - const columnDefs = this.getGridOptions().columnDefs - updateColumnTypeState(tableData.columns, config, columnDefs) + const columnDefs = this.getGridOptions().columnDefs; + updateColumnTypeState(tableData.columns, config, columnDefs); // SHOULD restore grid state after columnDefs are updated - this.restoreGridState(config.tableGridState) + this.restoreGridState(config.tableGridState); } restoreGridState(gridState) { - if (!gridState) { return } + if (!gridState) { + return; + } // should set isRestoring to avoid that changed* events are triggered while restoring - this.isRestoring = true - const gridApi = this.getGridApi() + this.isRestoring = true; + const gridApi = this.getGridApi(); // restore grid state when gridApi is available if (!gridApi) { - setTimeout(() => this.restoreGridState(gridState), 100) + setTimeout(() => this.restoreGridState(gridState), 100); } else { - gridApi.saveState.restore(this.getScope(), gridState) - this.isRestoring = false + gridApi.saveState.restore(this.getScope(), gridState); + this.isRestoring = false; } } - destroy () { + destroy() { } - getTransformation () { - return this.passthrough + getTransformation() { + return this.passthrough; } getScope() { - const scope = this.targetEl.scope() - return scope + const scope = this.targetEl.scope(); + return scope; } getGridOptions() { - const scope = this.getScope() - const gridElemId = this.getGridElemId() - return scope[gridElemId] + const scope = this.getScope(); + const gridElemId = this.getGridElemId(); + return scope[gridElemId]; } getGridApi() { - const scope = this.targetEl.scope() - const gridApiId = this.getGridApiId() - return scope[gridApiId] + const scope = this.targetEl.scope(); + const gridApiId = this.getGridApiId(); + return scope[gridApiId]; } persistConfigImmediatelyWithGridState(config) { - this.persistConfigWithGridState(config) + this.persistConfigWithGridState(config); } persistConfigWithGridState(config) { - if (this.isRestoring) { return } + if (this.isRestoring) { + return; + } - const gridApi = this.getGridApi() - config.tableGridState = gridApi.saveState.save() - this.emitConfig(config) + const gridApi = this.getGridApi(); + config.tableGridState = gridApi.saveState.save(); + this.emitConfig(config); } persistConfig(config) { - this.emitConfig(config) + this.emitConfig(config); } - getSetting (chart) { - const self = this // for closure in scope - const configObj = self.config + getSetting(chart) { + const self = this; // for closure in scope + const configObj = self.config; // emit config if it's updated in `render` if (configObj.initialized) { - configObj.initialized = false - this.persistConfig(configObj) // should persist w/o state + configObj.initialized = false; + this.persistConfig(configObj); // should persist w/o state } else if (configObj.tableColumnTypeState && configObj.tableColumnTypeState.updated) { - configObj.tableColumnTypeState.updated = false - this.persistConfig(configObj) // should persist w/o state + configObj.tableColumnTypeState.updated = false; + this.persistConfig(configObj); // should persist w/o state } return { @@ -393,27 +421,27 @@ export default class TableVisualization extends Visualization { isTextareaWidget: isTextareaWidget, isBtnGroupWidget: isBtnGroupWidget, tableOptionValueChanged: () => { - self.persistConfigWithGridState(configObj) + self.persistConfigWithGridState(configObj); }, saveTableOption: () => { - self.persistConfigWithGridState(configObj) + self.persistConfigWithGridState(configObj); }, resetTableOption: () => { - resetTableOptionConfig(configObj) - initializeTableConfig(configObj, TABLE_OPTION_SPECS) - self.persistConfigWithGridState(configObj) + resetTableOptionConfig(configObj); + initializeTableConfig(configObj, TABLE_OPTION_SPECS); + self.persistConfigWithGridState(configObj); }, tableWidgetOnKeyDown: (event, optSpec) => { - const code = event.keyCode || event.which + const code = event.keyCode || event.which; if (code === 13 && isInputWidget(optSpec)) { - self.persistConfigWithGridState(configObj) + self.persistConfigWithGridState(configObj); } else if (code === 13 && event.shiftKey && isTextareaWidget(optSpec)) { - self.persistConfigWithGridState(configObj) + self.persistConfigWithGridState(configObj); } - event.stopPropagation() /** avoid to conflict with paragraph shortcuts */ - } - } - } + event.stopPropagation(); /** avoid to conflict with paragraph shortcuts */ + }, + }, + }; } } diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-util.js b/zeppelin-web/src/app/visualization/builtins/visualization-util.js index cd9cd48b754..a82a18ecceb 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-util.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-util.js @@ -18,7 +18,7 @@ export const Widget = { TEXTAREA: 'textarea', OPTION: 'option', BTN_GROUP: 'btn-group', -} +}; export const ValueType = { INT: 'int', @@ -26,7 +26,7 @@ export const ValueType = { BOOLEAN: 'boolean', STRING: 'string', JSON: 'JSON', -} +}; export const TableColumnType = { STRING: 'string', @@ -35,138 +35,170 @@ export const TableColumnType = { DATE: 'date', OBJECT: 'object', NUMBER_STR: 'numberStr', -} +}; -export const DefaultTableColumnType = TableColumnType.STRING +export const DefaultTableColumnType = TableColumnType.STRING; -export function isInputWidget (spec) { return spec.widget === Widget.INPUT } -export function isOptionWidget (spec) { return spec.widget === Widget.OPTION } -export function isCheckboxWidget (spec) { return spec.widget === Widget.CHECKBOX } -export function isTextareaWidget (spec) { return spec.widget === Widget.TEXTAREA } -export function isBtnGroupWidget (spec) { return spec.widget === Widget.BTN_GROUP } +export function isInputWidget(spec) { + return spec.widget === Widget.INPUT; +} +export function isOptionWidget(spec) { + return spec.widget === Widget.OPTION; +} +export function isCheckboxWidget(spec) { + return spec.widget === Widget.CHECKBOX; +} +export function isTextareaWidget(spec) { + return spec.widget === Widget.TEXTAREA; +} +export function isBtnGroupWidget(spec) { + return spec.widget === Widget.BTN_GROUP; +} export function resetTableOptionConfig(config) { - delete config.tableOptionSpecHash - config.tableOptionSpecHash = {} - delete config.tableOptionValue - config.tableOptionValue = {} - delete config.tableColumnTypeState.names - config.tableColumnTypeState.names = {} - config.updated = false - return config + delete config.tableOptionSpecHash; + config.tableOptionSpecHash = {}; + delete config.tableOptionValue; + config.tableOptionValue = {}; + delete config.tableColumnTypeState.names; + config.tableColumnTypeState.names = {}; + config.updated = false; + return config; } export function initializeTableConfig(config, tableOptionSpecs) { - if (typeof config.tableOptionValue === 'undefined') { config.tableOptionValue = {} } - if (typeof config.tableGridState === 'undefined') { config.tableGridState = {} } - if (typeof config.tableColumnTypeState === 'undefined') { config.tableColumnTypeState = {} } + if (typeof config.tableOptionValue === 'undefined') { + config.tableOptionValue = {}; + } + if (typeof config.tableGridState === 'undefined') { + config.tableGridState = {}; + } + if (typeof config.tableColumnTypeState === 'undefined') { + config.tableColumnTypeState = {}; + } // should remove `$$hashKey` using angular.toJson - const newSpecHash = JSON.stringify(JSON.parse(angular.toJson(tableOptionSpecs))) - const previousSpecHash = config.tableOptionSpecHash + const newSpecHash = JSON.stringify(JSON.parse(angular.toJson(tableOptionSpecs))); + const previousSpecHash = config.tableOptionSpecHash; // check whether spec is updated or not if (typeof previousSpecHash === 'undefined' || (previousSpecHash !== newSpecHash)) { - resetTableOptionConfig(config) + resetTableOptionConfig(config); - config.tableOptionSpecHash = newSpecHash - config.initialized = true + config.tableOptionSpecHash = newSpecHash; + config.initialized = true; // reset all persisted option values if spec is updated for (let i = 0; i < tableOptionSpecs.length; i++) { - const option = tableOptionSpecs[i] - config.tableOptionValue[option.name] = option.defaultValue + const option = tableOptionSpecs[i]; + config.tableOptionValue[option.name] = option.defaultValue; } } - return config + return config; } export function parseTableOption(specs, persistedTableOption) { /** copy original params */ - const parsed = JSON.parse(JSON.stringify(persistedTableOption)) + const parsed = JSON.parse(JSON.stringify(persistedTableOption)); for (let i = 0; i < specs.length; i++) { - const s = specs[i] - const name = s.name + const s = specs[i]; + const name = s.name; if (s.valueType === ValueType.INT && typeof parsed[name] !== 'number') { - try { parsed[name] = parseInt(parsed[name]) } catch (error) { parsed[name] = s.defaultValue } + try { + parsed[name] = parseInt(parsed[name]); + } catch (error) { + parsed[name] = s.defaultValue; + } } else if (s.valueType === ValueType.FLOAT && typeof parsed[name] !== 'number') { - try { parsed[name] = parseFloat(parsed[name]) } catch (error) { parsed[name] = s.defaultValue } + try { + parsed[name] = parseFloat(parsed[name]); + } catch (error) { + parsed[name] = s.defaultValue; + } } else if (s.valueType === ValueType.BOOLEAN) { if (parsed[name] === 'false') { - parsed[name] = false + parsed[name] = false; } else if (parsed[name] === 'true') { - parsed[name] = true + parsed[name] = true; } else if (typeof parsed[name] !== 'boolean') { - parsed[name] = s.defaultValue + parsed[name] = s.defaultValue; } } else if (s.valueType === ValueType.JSON) { if (parsed[name] !== null && typeof parsed[name] !== 'object') { - try { parsed[name] = JSON.parse(parsed[name]) } catch (error) { parsed[name] = s.defaultValue } + try { + parsed[name] = JSON.parse(parsed[name]); + } catch (error) { + parsed[name] = s.defaultValue; + } } else if (parsed[name] === null) { - parsed[name] = s.defaultValue + parsed[name] = s.defaultValue; } } } - return parsed + return parsed; } export function isColumnNameUpdated(prevColumnNames, newColumnNames) { - if (typeof prevColumnNames === 'undefined') { return true } + if (typeof prevColumnNames === 'undefined') { + return true; + } - let columnNameUpdated = false + let columnNameUpdated = false; for (let prevColName in prevColumnNames) { if (!newColumnNames[prevColName]) { - return true + return true; } } if (!columnNameUpdated) { for (let newColName in newColumnNames) { if (!prevColumnNames[newColName]) { - return true + return true; } } } - return false + return false; } export function updateColumnTypeState(columns, config, columnDefs) { - const columnTypeState = config.tableColumnTypeState + const columnTypeState = config.tableColumnTypeState; - if (!columnTypeState) { return } + if (!columnTypeState) { + return; + } // compare objects because order might be changed - const prevColumnNames = columnTypeState.names || {} + const prevColumnNames = columnTypeState.names || {}; const newColumnNames = columns.reduce((acc, c) => { - const prevColumnType = prevColumnNames[c.name] + const prevColumnType = prevColumnNames[c.name]; // use previous column type if exists if (prevColumnType) { - acc[c.name] = prevColumnType + acc[c.name] = prevColumnType; } else { - acc[c.name] = DefaultTableColumnType + acc[c.name] = DefaultTableColumnType; } - return acc - }, {}) + return acc; + }, {}); - let columnNameUpdated = isColumnNameUpdated(prevColumnNames, newColumnNames) + let columnNameUpdated = isColumnNameUpdated(prevColumnNames, newColumnNames); if (columnNameUpdated) { - columnTypeState.names = newColumnNames - columnTypeState.updated = true + columnTypeState.names = newColumnNames; + columnTypeState.updated = true; } // update `columnDefs[n].type` for (let i = 0; i < columnDefs.length; i++) { - const colName = columnDefs[i].name - columnDefs[i].type = columnTypeState.names[colName] + const colName = columnDefs[i].name; + columnDefs[i].type = columnTypeState.names[colName]; } } diff --git a/zeppelin-web/src/app/visualization/visualization.js b/zeppelin-web/src/app/visualization/visualization.js index 6b6e36aa387..f6475cbf523 100644 --- a/zeppelin-web/src/app/visualization/visualization.js +++ b/zeppelin-web/src/app/visualization/visualization.js @@ -16,12 +16,12 @@ * Base class for visualization. */ export default class Visualization { - constructor (targetEl, config) { - this.targetEl = targetEl - this.config = config - this._dirty = false - this._active = false - this._emitter = () => {} + constructor(targetEl, config) { + this.targetEl = targetEl; + this.config = config; + this._dirty = false; + this._active = false; + this._emitter = () => {}; } /** @@ -29,33 +29,33 @@ export default class Visualization { * @abstract * @return {Transformation} */ - getTransformation () { + getTransformation() { // override this - throw new TypeError('Visualization.getTransformation() should be overrided') + throw new TypeError('Visualization.getTransformation() should be overrided'); } /** * Method will be invoked when data or configuration changed. * @abstract */ - render (tableData) { + render(tableData) { // override this - throw new TypeError('Visualization.render() should be overrided') + throw new TypeError('Visualization.render() should be overrided'); } /** * Refresh visualization. */ - refresh () { + refresh() { // override this - console.warn('A chart is missing refresh function, it might not work preperly') + console.warn('A chart is missing refresh function, it might not work preperly'); } /** * Method will be invoked when visualization need to be destroyed. * Don't need to destroy this.targetEl. */ - destroy () { + destroy() { // override this } @@ -65,113 +65,117 @@ export default class Visualization { * scope : an object to bind to template scope * } */ - getSetting () { + getSetting() { // override this } /** * Activate. Invoked when visualization is selected. */ - activate () { + activate() { if (!this._active || this._dirty) { - this.refresh() - this._dirty = false + this.refresh(); + this._dirty = false; } - this._active = true + this._active = true; } /** * Deactivate. Invoked when visualization is de selected. */ - deactivate () { - this._active = false + deactivate() { + this._active = false; } /** * Is active. */ - isActive () { - return this._active + isActive() { + return this._active; } /** * When window or paragraph is resized. */ - resize () { + resize() { if (this.isActive()) { - this.refresh() + this.refresh(); } else { - this._dirty = true + this._dirty = true; } } /** * Set new config. */ - setConfig (config) { - this.config = config + setConfig(config) { + this.config = config; if (this.isActive()) { - this.refresh() + this.refresh(); } else { - this._dirty = true + this._dirty = true; } } /** * Emit config. config will sent to server and saved. */ - emitConfig (config) { - this._emitter(config) + emitConfig(config) { + this._emitter(config); } /** * Render setting. */ - renderSetting (targetEl) { - let setting = this.getSetting() + renderSetting(targetEl) { + let setting = this.getSetting(); if (!setting) { - return + return; } // already readered if (this._scope) { - let self = this - this._scope.$apply(function () { + let self = this; + this._scope.$apply(function() { for (let k in setting.scope) { - self._scope[k] = setting.scope[k] + if (setting.scope.hasOwnProperty(k)) { + self._scope[k] = setting.scope[k]; + } } for (let k in self._prevSettingScope) { if (!setting.scope[k]) { - self._scope[k] = setting.scope[k] + self._scope[k] = setting.scope[k]; } } - }) - return + }); + return; } else { - this._prevSettingScope = setting.scope + this._prevSettingScope = setting.scope; } - let scope = this._createNewScope() + let scope = this._createNewScope(); for (let k in setting.scope) { - scope[k] = setting.scope[k] + if (setting.scope.hasOwnProperty(k)) { + scope[k] = setting.scope[k]; + } } - let template = setting.template + let template = setting.template; if (template.split('\n').length === 1 && template.endsWith('.html')) { // template is url - this._templateRequest(template).then(t => + this._templateRequest(template).then((t) => _renderSetting(this, targetEl, t, scope) - ) + ); } else { - _renderSetting(this, targetEl, template, scope) + _renderSetting(this, targetEl, template, scope); } } } -function _renderSetting (instance, targetEl, template, scope) { - instance._targetEl = targetEl - targetEl.html(template) - instance._compile(targetEl.contents())(scope) - instance._scope = scope +function _renderSetting(instance, targetEl, template, scope) { + instance._targetEl = targetEl; + targetEl.html(template); + instance._compile(targetEl.contents())(scope); + instance._scope = scope; } diff --git a/zeppelin-web/src/components/array-ordering/array-ordering.service.js b/zeppelin-web/src/components/array-ordering/array-ordering.service.js index 6fa1ad9c28a..1f275e691b9 100644 --- a/zeppelin-web/src/components/array-ordering/array-ordering.service.js +++ b/zeppelin-web/src/components/array-ordering/array-ordering.service.js @@ -12,51 +12,51 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').service('arrayOrderingSrv', ArrayOrderingService) +angular.module('zeppelinWebApp').service('arrayOrderingSrv', ArrayOrderingService); function ArrayOrderingService(TRASH_FOLDER_ID) { - 'ngInject' + 'ngInject'; - let arrayOrderingSrv = this + let arrayOrderingSrv = this; - this.noteListOrdering = function (note) { + this.noteListOrdering = function(note) { if (note.id === TRASH_FOLDER_ID) { - return '\uFFFF' + return '\uFFFF'; } - return arrayOrderingSrv.getNoteName(note) - } + return arrayOrderingSrv.getNoteName(note); + }; - this.getNoteName = function (note) { + this.getNoteName = function(note) { if (note.name === undefined || note.name.trim() === '') { - return 'Note ' + note.id + return 'Note ' + note.id; } else { - return note.name + return note.name; } - } + }; - this.noteComparator = function (v1, v2) { - let note1 = v1.value || v1 - let note2 = v2.value || v2 + this.noteComparator = function(v1, v2) { + let note1 = v1.value || v1; + let note2 = v2.value || v2; if (note1.id === TRASH_FOLDER_ID) { - return 1 + return 1; } if (note2.id === TRASH_FOLDER_ID) { - return -1 + return -1; } if (note1.children === undefined && note2.children !== undefined) { - return 1 + return 1; } if (note1.children !== undefined && note2.children === undefined) { - return -1 + return -1; } - let noteName1 = arrayOrderingSrv.getNoteName(note1) - let noteName2 = arrayOrderingSrv.getNoteName(note2) + let noteName1 = arrayOrderingSrv.getNoteName(note1); + let noteName2 = arrayOrderingSrv.getNoteName(note2); - return noteName1.localeCompare(noteName2) - } + return noteName1.localeCompare(noteName2); + }; } diff --git a/zeppelin-web/src/components/base-url/base-url.service.js b/zeppelin-web/src/components/base-url/base-url.service.js index 6ef55b95006..845293c58a0 100644 --- a/zeppelin-web/src/components/base-url/base-url.service.js +++ b/zeppelin-web/src/components/base-url/base-url.service.js @@ -12,39 +12,39 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').service('baseUrlSrv', BaseUrlService) +angular.module('zeppelinWebApp').service('baseUrlSrv', BaseUrlService); function BaseUrlService() { - this.getPort = function () { - let port = Number(location.port) + this.getPort = function() { + let port = Number(location.port); if (!port) { - port = 80 + port = 80; if (location.protocol === 'https:') { - port = 443 + port = 443; } } // Exception for when running locally via grunt if (port === process.env.WEB_PORT) { - port = process.env.SERVER_PORT + port = process.env.SERVER_PORT; } - return port - } + return port; + }; - this.getWebsocketUrl = function () { - let wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:' + this.getWebsocketUrl = function() { + let wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; return wsProtocol + '//' + location.hostname + ':' + this.getPort() + - skipTrailingSlash(location.pathname) + '/ws' - } + skipTrailingSlash(location.pathname) + '/ws'; + }; this.getBase = function() { - return location.protocol + '//' + location.hostname + ':' + this.getPort() + location.pathname - } + return location.protocol + '//' + location.hostname + ':' + this.getPort() + location.pathname; + }; - this.getRestApiBase = function () { - return skipTrailingSlash(this.getBase()) + '/api' - } + this.getRestApiBase = function() { + return skipTrailingSlash(this.getBase()) + '/api'; + }; - const skipTrailingSlash = function (path) { - return path.replace(/\/$/, '') - } + const skipTrailingSlash = function(path) { + return path.replace(/\/$/, ''); + }; } diff --git a/zeppelin-web/src/components/login/login.controller.js b/zeppelin-web/src/components/login/login.controller.js index 919095067fd..9a42d5f62bb 100644 --- a/zeppelin-web/src/components/login/login.controller.js +++ b/zeppelin-web/src/components/login/login.controller.js @@ -12,73 +12,73 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').controller('LoginCtrl', LoginCtrl) +angular.module('zeppelinWebApp').controller('LoginCtrl', LoginCtrl); -function LoginCtrl ($scope, $rootScope, $http, $httpParamSerializer, baseUrlSrv, $location, $timeout) { - 'ngInject' +function LoginCtrl($scope, $rootScope, $http, $httpParamSerializer, baseUrlSrv, $location, $timeout) { + 'ngInject'; - $scope.SigningIn = false - $scope.loginParams = {} - $scope.login = function () { - $scope.SigningIn = true + $scope.SigningIn = false; + $scope.loginParams = {}; + $scope.login = function() { + $scope.SigningIn = true; $http({ method: 'POST', url: baseUrlSrv.getRestApiBase() + '/login', headers: { - 'Content-Type': 'application/x-www-form-urlencoded' + 'Content-Type': 'application/x-www-form-urlencoded', }, data: $httpParamSerializer({ 'userName': $scope.loginParams.userName, - 'password': $scope.loginParams.password - }) - }).then(function successCallback (response) { - $rootScope.ticket = response.data.body - angular.element('#loginModal').modal('toggle') - $rootScope.$broadcast('loginSuccess', true) - $rootScope.userName = $scope.loginParams.userName - $scope.SigningIn = false + 'password': $scope.loginParams.password, + }), + }).then(function successCallback(response) { + $rootScope.ticket = response.data.body; + angular.element('#loginModal').modal('toggle'); + $rootScope.$broadcast('loginSuccess', true); + $rootScope.userName = $scope.loginParams.userName; + $scope.SigningIn = false; // redirect to the page from where the user originally was if ($location.search() && $location.search()['ref']) { - $timeout(function () { - let redirectLocation = $location.search()['ref'] - $location.$$search = {} - $location.path(redirectLocation) - }, 100) + $timeout(function() { + let redirectLocation = $location.search()['ref']; + $location.$$search = {}; + $location.path(redirectLocation); + }, 100); } - }, function errorCallback (errorResponse) { - $scope.loginParams.errorText = 'The username and password that you entered don\'t match.' - $scope.SigningIn = false - }) - } + }, function errorCallback(errorResponse) { + $scope.loginParams.errorText = 'The username and password that you entered don\'t match.'; + $scope.SigningIn = false; + }); + }; - let initValues = function () { + let initValues = function() { $scope.loginParams = { userName: '', - password: '' - } - } + password: '', + }; + }; // handle session logout message received from WebSocket - $rootScope.$on('session_logout', function (event, data) { + $rootScope.$on('session_logout', function(event, data) { if ($rootScope.userName !== '') { - $rootScope.userName = '' - $rootScope.ticket = undefined + $rootScope.userName = ''; + $rootScope.ticket = undefined; - setTimeout(function () { - $scope.loginParams = {} - $scope.loginParams.errorText = data.info - angular.element('.nav-login-btn').click() - }, 1000) - let locationPath = $location.path() - $location.path('/').search('ref', locationPath) + setTimeout(function() { + $scope.loginParams = {}; + $scope.loginParams.errorText = data.info; + angular.element('.nav-login-btn').click(); + }, 1000); + let locationPath = $location.path(); + $location.path('/').search('ref', locationPath); } - }) + }); /* ** $scope.$on functions below */ - $scope.$on('initLoginValues', function () { - initValues() - }) + $scope.$on('initLoginValues', function() { + initValues(); + }); } diff --git a/zeppelin-web/src/components/navbar/expand-collapse/expand-collapse.directive.js b/zeppelin-web/src/components/navbar/expand-collapse/expand-collapse.directive.js index e4280e865a8..58629afdec0 100644 --- a/zeppelin-web/src/components/navbar/expand-collapse/expand-collapse.directive.js +++ b/zeppelin-web/src/components/navbar/expand-collapse/expand-collapse.directive.js @@ -12,37 +12,37 @@ * limitations under the License. */ -import './expand-collapse.css' +import './expand-collapse.css'; -angular.module('zeppelinWebApp').directive('expandCollapse', expandCollapseDirective) +angular.module('zeppelinWebApp').directive('expandCollapse', expandCollapseDirective); function expandCollapseDirective() { return { restrict: 'EA', - link: function (scope, element, attrs) { - angular.element(element).click(function (event) { + link: function(scope, element, attrs) { + angular.element(element).click(function(event) { if (angular.element(element).next('.expandable:visible').length > 1) { - angular.element(element).next('.expandable:visible').slideUp('slow') - angular.element(element).find('i.fa-folder-open').toggleClass('fa-folder fa-folder-open') + angular.element(element).next('.expandable:visible').slideUp('slow'); + angular.element(element).find('i.fa-folder-open').toggleClass('fa-folder fa-folder-open'); } else { - angular.element(element).next('.expandable').first().slideToggle('200', function () { + angular.element(element).next('.expandable').first().slideToggle('200', function() { // do not toggle trash folder if (angular.element(element).find('.fa-trash-o').length === 0) { - angular.element(element).find('i').first().toggleClass('fa-folder fa-folder-open') + angular.element(element).find('i').first().toggleClass('fa-folder fa-folder-open'); } - }) + }); } - let target = event.target + let target = event.target; // add note if (target.classList !== undefined && target.classList.contains('fa-plus') && target.tagName.toLowerCase() === 'i') { - return + return; } - event.stopPropagation() - }) - } - } + event.stopPropagation(); + }); + }, + }; } diff --git a/zeppelin-web/src/components/navbar/navbar.controller.js b/zeppelin-web/src/components/navbar/navbar.controller.js index 139328e11d7..9fb5db2aeec 100644 --- a/zeppelin-web/src/components/navbar/navbar.controller.js +++ b/zeppelin-web/src/components/navbar/navbar.controller.js @@ -12,237 +12,237 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').controller('NavCtrl', NavCtrl) +angular.module('zeppelinWebApp').controller('NavCtrl', NavCtrl); -function NavCtrl ($scope, $rootScope, $http, $routeParams, $location, +function NavCtrl($scope, $rootScope, $http, $routeParams, $location, noteListFactory, baseUrlSrv, websocketMsgSrv, arrayOrderingSrv, searchService, TRASH_FOLDER_ID) { - 'ngInject' - - let vm = this - vm.arrayOrderingSrv = arrayOrderingSrv - vm.connected = websocketMsgSrv.isConnected() - vm.isActive = isActive - vm.logout = logout - vm.notes = noteListFactory - vm.search = search - vm.searchForm = searchService - vm.showLoginWindow = showLoginWindow - vm.TRASH_FOLDER_ID = TRASH_FOLDER_ID - vm.isFilterNote = isFilterNote - vm.numberOfNotesDisplayed = 10 - - $scope.query = {q: ''} - - initController() - - function getZeppelinVersion () { + 'ngInject'; + + let vm = this; + vm.arrayOrderingSrv = arrayOrderingSrv; + vm.connected = websocketMsgSrv.isConnected(); + vm.isActive = isActive; + vm.logout = logout; + vm.notes = noteListFactory; + vm.search = search; + vm.searchForm = searchService; + vm.showLoginWindow = showLoginWindow; + vm.TRASH_FOLDER_ID = TRASH_FOLDER_ID; + vm.isFilterNote = isFilterNote; + vm.numberOfNotesDisplayed = 10; + + $scope.query = {q: ''}; + + initController(); + + function getZeppelinVersion() { $http.get(baseUrlSrv.getRestApiBase() + '/version').success( - function (data, status, headers, config) { - $rootScope.zeppelinVersion = data.body.version + function(data, status, headers, config) { + $rootScope.zeppelinVersion = data.body.version; }).error( - function (data, status, headers, config) { - console.log('Error %o %o', status, data.message) - }) + function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); + }); } - function initController () { - $scope.isDrawNavbarNoteList = false - angular.element('#notebook-list').perfectScrollbar({suppressScrollX: true}) + function initController() { + $scope.isDrawNavbarNoteList = false; + angular.element('#notebook-list').perfectScrollbar({suppressScrollX: true}); - angular.element(document).click(function () { - $scope.query.q = '' - }) + angular.element(document).click(function() { + $scope.query.q = ''; + }); - getZeppelinVersion() - loadNotes() + getZeppelinVersion(); + loadNotes(); } - function isFilterNote (note) { + function isFilterNote(note) { if (!$scope.query.q) { - return true + return true; } - let noteName = note.name + let noteName = note.name; if (noteName.toLowerCase().indexOf($scope.query.q.toLowerCase()) > -1) { - return true + return true; } - return false + return false; } - function isActive (noteId) { - return ($routeParams.noteId === noteId) + function isActive(noteId) { + return ($routeParams.noteId === noteId); } - function listConfigurations () { - websocketMsgSrv.listConfigurations() + function listConfigurations() { + websocketMsgSrv.listConfigurations(); } - function loadNotes () { - websocketMsgSrv.getNoteList() + function loadNotes() { + websocketMsgSrv.getNoteList(); } - function getHomeNote () { - websocketMsgSrv.getHomeNote() + function getHomeNote() { + websocketMsgSrv.getHomeNote(); } function logout() { - let logoutURL = baseUrlSrv.getRestApiBase() + '/login/logout' + let logoutURL = baseUrlSrv.getRestApiBase() + '/login/logout'; - $http.post(logoutURL).then(function () {}, function (response) { + $http.post(logoutURL).then(function() {}, function(response) { if (response.data) { - let res = angular.fromJson(response.data).body + let res = angular.fromJson(response.data).body; if (res['redirectURL']) { if (res['isLogoutAPI'] === 'true') { - $http.get(res['redirectURL']).then(function () { - }, function () { - window.location = baseUrlSrv.getBase() - }) + $http.get(res['redirectURL']).then(function() { + }, function() { + window.location = baseUrlSrv.getBase(); + }); } else { - window.location.href = res['redirectURL'] + window.location.href + window.location.href = res['redirectURL'] + window.location.href; } - return undefined + return undefined; } } // force authcBasic (if configured) to logout if (detectIE()) { - let outcome + let outcome; try { - outcome = document.execCommand('ClearAuthenticationCache') + outcome = document.execCommand('ClearAuthenticationCache'); } catch (e) { - console.log(e) + console.log(e); } if (!outcome) { // Let's create an xmlhttp object - outcome = (function (x) { + outcome = (function(x) { if (x) { // the reason we use "random" value for password is // that browsers cache requests. changing // password effectively behaves like cache-busing. x.open('HEAD', location.href, true, 'logout', - (new Date()).getTime().toString()) - x.send('') + (new Date()).getTime().toString()); + x.send(''); // x.abort() - return 1 // this is **speculative** "We are done." + return 1; // this is **speculative** "We are done." } else { // eslint-disable-next-line no-useless-return - return + return; } })(window.XMLHttpRequest ? new window.XMLHttpRequest() // eslint-disable-next-line no-undef - : (window.ActiveXObject ? new ActiveXObject('Microsoft.XMLHTTP') : u)) + : (window.ActiveXObject ? new ActiveXObject('Microsoft.XMLHTTP') : u)); } if (!outcome) { let m = 'Your browser is too old or too weird to support log out functionality. Close all windows and ' + - 'restart the browser.' - alert(m) + 'restart the browser.'; + alert(m); } } else { // for firefox and safari - logoutURL = logoutURL.replace('//', '//false:false@') + logoutURL = logoutURL.replace('//', '//false:false@'); } - $http.post(logoutURL).error(function () { - $rootScope.userName = '' - $rootScope.ticket.principal = '' - $rootScope.ticket.screenUsername = '' - $rootScope.ticket.ticket = '' - $rootScope.ticket.roles = '' + $http.post(logoutURL).error(function() { + $rootScope.userName = ''; + $rootScope.ticket.principal = ''; + $rootScope.ticket.screenUsername = ''; + $rootScope.ticket.ticket = ''; + $rootScope.ticket.roles = ''; BootstrapDialog.show({ - message: 'Logout Success' - }) - setTimeout(function () { - window.location = baseUrlSrv.getBase() - }, 1000) - }) - }) + message: 'Logout Success', + }); + setTimeout(function() { + window.location = baseUrlSrv.getBase(); + }, 1000); + }); + }); } function detectIE() { - let ua = window.navigator.userAgent + let ua = window.navigator.userAgent; - let msie = ua.indexOf('MSIE ') + let msie = ua.indexOf('MSIE '); if (msie > 0) { // IE 10 or older => return version number - return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10) + return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); } - let trident = ua.indexOf('Trident/') + let trident = ua.indexOf('Trident/'); if (trident > 0) { // IE 11 => return version number - let rv = ua.indexOf('rv:') - return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10) + let rv = ua.indexOf('rv:'); + return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); } - let edge = ua.indexOf('Edge/') + let edge = ua.indexOf('Edge/'); if (edge > 0) { // Edge (IE 12+) => return version number - return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10) + return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); } // other browser - return false + return false; } - function search (searchTerm) { - $location.path('/search/' + searchTerm) + function search(searchTerm) { + $location.path('/search/' + searchTerm); } - function showLoginWindow () { - setTimeout(function () { - angular.element('#userName').focus() - }, 500) + function showLoginWindow() { + setTimeout(function() { + angular.element('#userName').focus(); + }, 500); } /* ** $scope.$on functions below */ - $scope.$on('setNoteMenu', function (event, notes) { - noteListFactory.setNotes(notes) - initNotebookListEventListener() - }) + $scope.$on('setNoteMenu', function(event, notes) { + noteListFactory.setNotes(notes); + initNotebookListEventListener(); + }); - $scope.$on('setConnectedStatus', function (event, param) { - vm.connected = param - }) + $scope.$on('setConnectedStatus', function(event, param) { + vm.connected = param; + }); - $scope.$on('loginSuccess', function (event, param) { - $rootScope.ticket.screenUsername = $rootScope.ticket.principal - listConfigurations() - loadNotes() - getHomeNote() - }) + $scope.$on('loginSuccess', function(event, param) { + $rootScope.ticket.screenUsername = $rootScope.ticket.principal; + listConfigurations(); + loadNotes(); + getHomeNote(); + }); /* ** Performance optimization for Browser Render. */ - function initNotebookListEventListener () { - angular.element(document).ready(function () { - angular.element('.notebook-list-dropdown').on('show.bs.dropdown', function () { - $scope.isDrawNavbarNoteList = true - }) - - angular.element('.notebook-list-dropdown').on('hide.bs.dropdown', function () { - $scope.isDrawNavbarNoteList = false - }) - }) + function initNotebookListEventListener() { + angular.element(document).ready(function() { + angular.element('.notebook-list-dropdown').on('show.bs.dropdown', function() { + $scope.isDrawNavbarNoteList = true; + }); + + angular.element('.notebook-list-dropdown').on('hide.bs.dropdown', function() { + $scope.isDrawNavbarNoteList = false; + }); + }); } - $scope.loadMoreNotes = function () { - vm.numberOfNotesDisplayed += 10 - } + $scope.loadMoreNotes = function() { + vm.numberOfNotesDisplayed += 10; + }; - $scope.calculateTooltipPlacement = function (note) { + $scope.calculateTooltipPlacement = function(note) { if (note !== undefined && note.name !== undefined) { - let length = note.name.length + let length = note.name.length; if (length < 2) { - return 'top-left' + return 'top-left'; } else if (length > 7) { - return 'top-right' + return 'top-right'; } } - return 'top' - } + return 'top'; + }; } diff --git a/zeppelin-web/src/components/navbar/navbar.controller.test.js b/zeppelin-web/src/components/navbar/navbar.controller.test.js index bf29b842bd3..f4bb3bf05e4 100644 --- a/zeppelin-web/src/components/navbar/navbar.controller.test.js +++ b/zeppelin-web/src/components/navbar/navbar.controller.test.js @@ -1,18 +1,18 @@ -describe('Controller: NavCtrl', function () { +describe('Controller: NavCtrl', function() { // load the controller's module - beforeEach(angular.mock.module('zeppelinWebApp')) - let NavCtrl - let scope + beforeEach(angular.mock.module('zeppelinWebApp')); + let NavCtrl; + let scope; // Initialize the controller and a mock scope - beforeEach(inject(function ($controller, $rootScope) { - scope = $rootScope.$new() + beforeEach(inject(function($controller, $rootScope) { + scope = $rootScope.$new(); NavCtrl = $controller('NavCtrl', { - $scope: scope - }) + $scope: scope, + }); - it('NavCtrl to toBeDefined', function () { - expect(NavCtrl).toBeDefined() - expect(NavCtrl.loadNotes).toBeDefined() - }) - })) -}) + it('NavCtrl to toBeDefined', function() { + expect(NavCtrl).toBeDefined(); + expect(NavCtrl.loadNotes).toBeDefined(); + }); + })); +}); diff --git a/zeppelin-web/src/components/ng-enter/ng-enter.directive.js b/zeppelin-web/src/components/ng-enter/ng-enter.directive.js index 98bc067ce15..a4d9219cd46 100644 --- a/zeppelin-web/src/components/ng-enter/ng-enter.directive.js +++ b/zeppelin-web/src/components/ng-enter/ng-enter.directive.js @@ -12,19 +12,19 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').directive('ngEnter', NgEnterDirective) +angular.module('zeppelinWebApp').directive('ngEnter', NgEnterDirective); function NgEnterDirective() { - return function (scope, element, attrs) { - element.bind('keydown keypress', function (event) { + return function(scope, element, attrs) { + element.bind('keydown keypress', function(event) { if (event.which === 13) { if (!event.shiftKey) { - scope.$apply(function () { - scope.$eval(attrs.ngEnter) - }) + scope.$apply(function() { + scope.$eval(attrs.ngEnter); + }); } - event.preventDefault() + event.preventDefault(); } - }) - } + }); + }; } diff --git a/zeppelin-web/src/components/ng-enter/ng-enter.directive.test.js b/zeppelin-web/src/components/ng-enter/ng-enter.directive.test.js index 49f97cca19a..6285b59a499 100644 --- a/zeppelin-web/src/components/ng-enter/ng-enter.directive.test.js +++ b/zeppelin-web/src/components/ng-enter/ng-enter.directive.test.js @@ -1,19 +1,19 @@ -describe('Directive: ngEnter', function () { +describe('Directive: ngEnter', function() { // load the directive's module - beforeEach(angular.mock.module('zeppelinWebApp')) + beforeEach(angular.mock.module('zeppelinWebApp')); - let element - let scope + let element; + let scope; - beforeEach(inject(function ($rootScope) { - scope = $rootScope.$new() - })) + beforeEach(inject(function($rootScope) { + scope = $rootScope.$new(); + })); - it('should be define', inject(function ($compile) { - element = angular.element('') - element = $compile(element)(scope) - expect(element.text()).toBeDefined() - })) + it('should be define', inject(function($compile) { + element = angular.element(''); + element = $compile(element)(scope); + expect(element.text()).toBeDefined(); + })); // Test the rest of function in ngEnter /* it('should make hidden element visible', inject(function ($compile) { @@ -21,4 +21,4 @@ describe('Directive: ngEnter', function () { element = $compile(element)(scope); expect(element.text()).toBe('this is the ngEnter directive'); })); */ -}) +}); diff --git a/zeppelin-web/src/components/ng-escape/ng-escape.directive.js b/zeppelin-web/src/components/ng-escape/ng-escape.directive.js index a3d35ea33f3..bdb76367952 100644 --- a/zeppelin-web/src/components/ng-escape/ng-escape.directive.js +++ b/zeppelin-web/src/components/ng-escape/ng-escape.directive.js @@ -12,17 +12,17 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').directive('ngEscape', NgEscapeDirective) +angular.module('zeppelinWebApp').directive('ngEscape', NgEscapeDirective); function NgEscapeDirective() { - return function (scope, element, attrs) { - element.bind('keydown keyup', function (event) { + return function(scope, element, attrs) { + element.bind('keydown keyup', function(event) { if (event.which === 27) { - scope.$apply(function () { - scope.$eval(attrs.ngEscape) - }) - event.preventDefault() + scope.$apply(function() { + scope.$eval(attrs.ngEscape); + }); + event.preventDefault(); } - }) - } + }); + }; } diff --git a/zeppelin-web/src/components/note-action/note-action.service.js b/zeppelin-web/src/components/note-action/note-action.service.js index 8e00c0fc447..d4bf6f01f93 100644 --- a/zeppelin-web/src/components/note-action/note-action.service.js +++ b/zeppelin-web/src/components/note-action/note-action.service.js @@ -12,172 +12,172 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').service('noteActionService', noteActionService) +angular.module('zeppelinWebApp').service('noteActionService', noteActionService); function noteActionService(websocketMsgSrv, $location, noteRenameService, noteListFactory) { - 'ngInject' + 'ngInject'; - this.moveNoteToTrash = function (noteId, redirectToHome) { + this.moveNoteToTrash = function(noteId, redirectToHome) { BootstrapDialog.confirm({ closable: true, title: 'Move this note to trash?', message: 'This note will be moved to trash.', - callback: function (result) { + callback: function(result) { if (result) { - websocketMsgSrv.moveNoteToTrash(noteId) + websocketMsgSrv.moveNoteToTrash(noteId); if (redirectToHome) { - $location.path('/') + $location.path('/'); } } - } - }) - } + }, + }); + }; - this.moveFolderToTrash = function (folderId) { + this.moveFolderToTrash = function(folderId) { BootstrapDialog.confirm({ closable: true, title: 'Move this folder to trash?', message: 'This folder will be moved to trash.', - callback: function (result) { + callback: function(result) { if (result) { - websocketMsgSrv.moveFolderToTrash(folderId) + websocketMsgSrv.moveFolderToTrash(folderId); } - } - }) - } + }, + }); + }; - this.removeNote = function (noteId, redirectToHome) { + this.removeNote = function(noteId, redirectToHome) { BootstrapDialog.confirm({ type: BootstrapDialog.TYPE_WARNING, closable: true, title: 'WARNING! This note will be removed permanently', message: 'This cannot be undone. Are you sure?', - callback: function (result) { + callback: function(result) { if (result) { - websocketMsgSrv.deleteNote(noteId) + websocketMsgSrv.deleteNote(noteId); if (redirectToHome) { - $location.path('/') + $location.path('/'); } } - } - }) - } + }, + }); + }; - this.removeFolder = function (folderId) { + this.removeFolder = function(folderId) { BootstrapDialog.confirm({ type: BootstrapDialog.TYPE_WARNING, closable: true, title: 'WARNING! This folder will be removed permanently', message: 'This cannot be undone. Are you sure?', - callback: function (result) { + callback: function(result) { if (result) { - websocketMsgSrv.removeFolder(folderId) + websocketMsgSrv.removeFolder(folderId); } - } - }) - } + }, + }); + }; - this.restoreAll = function () { + this.restoreAll = function() { BootstrapDialog.confirm({ closable: true, title: 'Are you sure want to restore all notes in the trash?', message: 'Folders and notes in the trash will be ' + 'merged into their original position.', - callback: function (result) { + callback: function(result) { if (result) { - websocketMsgSrv.restoreAll() + websocketMsgSrv.restoreAll(); } - } - }) - } + }, + }); + }; - this.emptyTrash = function () { + this.emptyTrash = function() { BootstrapDialog.confirm({ type: BootstrapDialog.TYPE_WARNING, closable: true, title: 'WARNING! Notes under trash will be removed permanently', message: 'This cannot be undone. Are you sure?', - callback: function (result) { + callback: function(result) { if (result) { - websocketMsgSrv.emptyTrash() + websocketMsgSrv.emptyTrash(); } - } - }) - } + }, + }); + }; - this.clearAllParagraphOutput = function (noteId) { + this.clearAllParagraphOutput = function(noteId) { BootstrapDialog.confirm({ closable: true, title: '', message: 'Do you want to clear all output?', - callback: function (result) { + callback: function(result) { if (result) { - websocketMsgSrv.clearAllParagraphOutput(noteId) + websocketMsgSrv.clearAllParagraphOutput(noteId); } - } - }) - } + }, + }); + }; - this.renameNote = function (noteId, notePath) { + this.renameNote = function(noteId, notePath) { noteRenameService.openRenameModal({ title: 'Rename note', oldName: notePath, - callback: function (newName) { - websocketMsgSrv.renameNote(noteId, newName) - } - }) - } + callback: function(newName) { + websocketMsgSrv.renameNote(noteId, newName); + }, + }); + }; - this.renameFolder = function (folderId) { + this.renameFolder = function(folderId) { noteRenameService.openRenameModal({ title: 'Rename folder', oldName: folderId, - callback: function (newName) { - let newFolderId = normalizeFolderId(newName) + callback: function(newName) { + let newFolderId = normalizeFolderId(newName); if (_.has(noteListFactory.flatFolderMap, newFolderId)) { BootstrapDialog.confirm({ type: BootstrapDialog.TYPE_WARNING, closable: true, title: 'WARNING! The folder will be MERGED', message: 'The folder will be merged into ' + newFolderId + '. Are you sure?', - callback: function (result) { + callback: function(result) { if (result) { - websocketMsgSrv.renameFolder(folderId, newFolderId) + websocketMsgSrv.renameFolder(folderId, newFolderId); } - } - }) + }, + }); } else { - websocketMsgSrv.renameFolder(folderId, newFolderId) + websocketMsgSrv.renameFolder(folderId, newFolderId); } - } - }) - } + }, + }); + }; - function normalizeFolderId (folderId) { - folderId = folderId.trim() + function normalizeFolderId(folderId) { + folderId = folderId.trim(); while (folderId.indexOf('\\') > -1) { - folderId = folderId.replace('\\', '/') + folderId = folderId.replace('\\', '/'); } while (folderId.indexOf('///') > -1) { - folderId = folderId.replace('///', '/') + folderId = folderId.replace('///', '/'); } - folderId = folderId.replace('//', '/') + folderId = folderId.replace('//', '/'); if (folderId === '/') { - return '/' + return '/'; } if (folderId[0] === '/') { - folderId = folderId.substring(1) + folderId = folderId.substring(1); } if (folderId.slice(-1) === '/') { - folderId = folderId.slice(0, -1) + folderId = folderId.slice(0, -1); } - return folderId + return folderId; } } diff --git a/zeppelin-web/src/components/note-create/note-create.controller.js b/zeppelin-web/src/components/note-create/note-create.controller.js index c999c20271c..a2eb5a6f6d3 100644 --- a/zeppelin-web/src/components/note-create/note-create.controller.js +++ b/zeppelin-web/src/components/note-create/note-create.controller.js @@ -12,95 +12,95 @@ * limitations under the License. */ -import './note-create.css' +import './note-create.css'; -angular.module('zeppelinWebApp').controller('NoteCreateCtrl', NoteCreateCtrl) +angular.module('zeppelinWebApp').controller('NoteCreateCtrl', NoteCreateCtrl); -function NoteCreateCtrl ($scope, noteListFactory, $routeParams, websocketMsgSrv) { - 'ngInject' +function NoteCreateCtrl($scope, noteListFactory, $routeParams, websocketMsgSrv) { + 'ngInject'; - let vm = this - vm.clone = false - vm.notes = noteListFactory - vm.websocketMsgSrv = websocketMsgSrv - $scope.note = {} - $scope.interpreterSettings = {} - $scope.note.defaultInterpreter = null + let vm = this; + vm.clone = false; + vm.notes = noteListFactory; + vm.websocketMsgSrv = websocketMsgSrv; + $scope.note = {}; + $scope.interpreterSettings = {}; + $scope.note.defaultInterpreter = null; - vm.createNote = function () { + vm.createNote = function() { if (!vm.clone) { - let defaultInterpreterId = '' + let defaultInterpreterId = ''; if ($scope.note.defaultInterpreter !== null) { - defaultInterpreterId = $scope.note.defaultInterpreter.id + defaultInterpreterId = $scope.note.defaultInterpreter.id; } - vm.websocketMsgSrv.createNotebook($scope.note.notename, defaultInterpreterId) - $scope.note.defaultInterpreter = $scope.interpreterSettings[0] + vm.websocketMsgSrv.createNotebook($scope.note.notename, defaultInterpreterId); + $scope.note.defaultInterpreter = $scope.interpreterSettings[0]; } else { - let noteId = $routeParams.noteId - vm.websocketMsgSrv.cloneNote(noteId, $scope.note.notename) + let noteId = $routeParams.noteId; + vm.websocketMsgSrv.cloneNote(noteId, $scope.note.notename); } - } + }; - vm.handleNameEnter = function () { - angular.element('#noteCreateModal').modal('toggle') - vm.createNote() - } + vm.handleNameEnter = function() { + angular.element('#noteCreateModal').modal('toggle'); + vm.createNote(); + }; vm.preVisible = function(clone, sourceNoteName, path) { - vm.clone = clone - vm.sourceNoteName = sourceNoteName - $scope.note.notename = vm.clone ? vm.cloneNoteName() : vm.newNoteName(path) - $scope.$apply() - } + vm.clone = clone; + vm.sourceNoteName = sourceNoteName; + $scope.note.notename = vm.clone ? vm.cloneNoteName() : vm.newNoteName(path); + $scope.$apply(); + }; vm.newNoteName = function(path) { - let newCount = 1 - angular.forEach(vm.notes.flatList, function (noteName) { - noteName = noteName.name + let newCount = 1; + angular.forEach(vm.notes.flatList, function(noteName) { + noteName = noteName.name; if (noteName.match(/^Untitled Note [0-9]*$/)) { - let lastCount = noteName.substr(14) * 1 + let lastCount = noteName.substr(14) * 1; if (newCount <= lastCount) { - newCount = lastCount + 1 + newCount = lastCount + 1; } } - }) - return (path ? path + '/' : '') + 'Untitled Note ' + newCount - } + }); + return (path ? path + '/' : '') + 'Untitled Note ' + newCount; + }; - vm.cloneNoteName = function () { - let copyCount = 1 - let newCloneName = '' - let lastIndex = vm.sourceNoteName.lastIndexOf(' ') - let endsWithNumber = !!vm.sourceNoteName.match('^.+?\\s\\d$') - let noteNamePrefix = endsWithNumber ? vm.sourceNoteName.substr(0, lastIndex) : vm.sourceNoteName - let regexp = new RegExp('^' + noteNamePrefix + ' .+') + vm.cloneNoteName = function() { + let copyCount = 1; + let newCloneName = ''; + let lastIndex = vm.sourceNoteName.lastIndexOf(' '); + let endsWithNumber = !!vm.sourceNoteName.match('^.+?\\s\\d$'); + let noteNamePrefix = endsWithNumber ? vm.sourceNoteName.substr(0, lastIndex) : vm.sourceNoteName; + let regexp = new RegExp('^' + noteNamePrefix + ' .+'); - angular.forEach(vm.notes.flatList, function (noteName) { - noteName = noteName.name + angular.forEach(vm.notes.flatList, function(noteName) { + noteName = noteName.name; if (noteName.match(regexp)) { - let lastCopyCount = noteName.substr(lastIndex).trim() - newCloneName = noteNamePrefix - lastCopyCount = parseInt(lastCopyCount) + let lastCopyCount = noteName.substr(lastIndex).trim(); + newCloneName = noteNamePrefix; + lastCopyCount = parseInt(lastCopyCount); if (copyCount <= lastCopyCount) { - copyCount = lastCopyCount + 1 + copyCount = lastCopyCount + 1; } } - }) + }); if (!newCloneName) { - newCloneName = vm.sourceNoteName + newCloneName = vm.sourceNoteName; } - return newCloneName + ' ' + copyCount - } + return newCloneName + ' ' + copyCount; + }; - vm.getInterpreterSettings = function () { - vm.websocketMsgSrv.getInterpreterSettings() - } + vm.getInterpreterSettings = function() { + vm.websocketMsgSrv.getInterpreterSettings(); + }; - $scope.$on('interpreterSettings', function (event, data) { - $scope.interpreterSettings = data.interpreterSettings + $scope.$on('interpreterSettings', function(event, data) { + $scope.interpreterSettings = data.interpreterSettings; // initialize default interpreter with Spark interpreter - $scope.note.defaultInterpreter = data.interpreterSettings[0] - }) + $scope.note.defaultInterpreter = data.interpreterSettings[0]; + }); } diff --git a/zeppelin-web/src/components/note-create/note-create.controller.test.js b/zeppelin-web/src/components/note-create/note-create.controller.test.js index d409a142cd4..59f01d23b33 100644 --- a/zeppelin-web/src/components/note-create/note-create.controller.test.js +++ b/zeppelin-web/src/components/note-create/note-create.controller.test.js @@ -1,39 +1,39 @@ -describe('Controller: NoteCreateCtrl', function () { - beforeEach(angular.mock.module('zeppelinWebApp')) +describe('Controller: NoteCreateCtrl', function() { + beforeEach(angular.mock.module('zeppelinWebApp')); - let scope - let ctrl - let noteList + let scope; + let ctrl; + let noteList; - beforeEach(inject(function ($injector, $rootScope, $controller) { - noteList = $injector.get('noteListFactory') - scope = $rootScope.$new() + beforeEach(inject(function($injector, $rootScope, $controller) { + noteList = $injector.get('noteListFactory'); + scope = $rootScope.$new(); ctrl = $controller('NoteCreateCtrl', { $scope: scope, - noteListFactory: noteList - }) - })) + noteListFactory: noteList, + }); + })); - it('should create a new name from current name when cloneNoteName is called', function () { + it('should create a new name from current name when cloneNoteName is called', function() { let notesList = [ {name: 'dsds 1', id: '1'}, {name: 'dsds 2', id: '2'}, {name: 'test name', id: '3'}, {name: 'aa bb cc', id: '4'}, - {name: 'Untitled Note 6', id: '4'} - ] + {name: 'Untitled Note 6', id: '4'}, + ]; - noteList.setNotes(notesList) + noteList.setNotes(notesList); - ctrl.sourceNoteName = 'test name' - expect(ctrl.cloneNoteName()).toEqual('test name 1') - ctrl.sourceNoteName = 'aa bb cc' - expect(ctrl.cloneNoteName()).toEqual('aa bb cc 1') - ctrl.sourceNoteName = 'Untitled Note 6' - expect(ctrl.cloneNoteName()).toEqual('Untitled Note 7') - ctrl.sourceNoteName = 'My_note' - expect(ctrl.cloneNoteName()).toEqual('My_note 1') - ctrl.sourceNoteName = 'dsds 2' - expect(ctrl.cloneNoteName()).toEqual('dsds 3') - }) -}) + ctrl.sourceNoteName = 'test name'; + expect(ctrl.cloneNoteName()).toEqual('test name 1'); + ctrl.sourceNoteName = 'aa bb cc'; + expect(ctrl.cloneNoteName()).toEqual('aa bb cc 1'); + ctrl.sourceNoteName = 'Untitled Note 6'; + expect(ctrl.cloneNoteName()).toEqual('Untitled Note 7'); + ctrl.sourceNoteName = 'My_note'; + expect(ctrl.cloneNoteName()).toEqual('My_note 1'); + ctrl.sourceNoteName = 'dsds 2'; + expect(ctrl.cloneNoteName()).toEqual('dsds 3'); + }); +}); diff --git a/zeppelin-web/src/components/note-create/visible.directive.js b/zeppelin-web/src/components/note-create/visible.directive.js index 48c170f41a4..7ba8db72f3d 100644 --- a/zeppelin-web/src/components/note-create/visible.directive.js +++ b/zeppelin-web/src/components/note-create/visible.directive.js @@ -12,34 +12,34 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').directive('modalvisible', modalvisible) +angular.module('zeppelinWebApp').directive('modalvisible', modalvisible); -function modalvisible () { +function modalvisible() { return { restrict: 'A', scope: { preVisibleCallback: '&previsiblecallback', postVisibleCallback: '&postvisiblecallback', - targetinput: '@targetinput' + targetinput: '@targetinput', }, - link: function (scope, element, attrs) { + link: function(scope, element, attrs) { // Add some listeners - let previsibleMethod = scope.preVisibleCallback - let postVisibleMethod = scope.postVisibleCallback - element.on('show.bs.modal', function (e) { - let relatedTarget = angular.element(e.relatedTarget) - let clone = relatedTarget.data('clone') - let sourceNoteName = relatedTarget.data('source-note-name') - let path = relatedTarget.data('path') - let cloneNote = clone ? true : false - previsibleMethod()(cloneNote, sourceNoteName, path) - }) - element.on('shown.bs.modal', function (e) { + let previsibleMethod = scope.preVisibleCallback; + let postVisibleMethod = scope.postVisibleCallback; + element.on('show.bs.modal', function(e) { + let relatedTarget = angular.element(e.relatedTarget); + let clone = relatedTarget.data('clone'); + let sourceNoteName = relatedTarget.data('source-note-name'); + let path = relatedTarget.data('path'); + let cloneNote = clone ? true : false; + previsibleMethod()(cloneNote, sourceNoteName, path); + }); + element.on('shown.bs.modal', function(e) { if (scope.targetinput) { - angular.element(e.target).find('input#' + scope.targetinput).select() + angular.element(e.target).find('input#' + scope.targetinput).select(); } - postVisibleMethod() - }) - } - } + postVisibleMethod(); + }); + }, + }; } diff --git a/zeppelin-web/src/components/note-import/note-import.controller.js b/zeppelin-web/src/components/note-import/note-import.controller.js index 8cec8908a02..fbfc5facc82 100644 --- a/zeppelin-web/src/components/note-import/note-import.controller.js +++ b/zeppelin-web/src/components/note-import/note-import.controller.js @@ -12,76 +12,76 @@ * limitations under the License. */ -import './note-import.css' - -angular.module('zeppelinWebApp').controller('NoteImportCtrl', NoteImportCtrl) - -function NoteImportCtrl ($scope, $timeout, websocketMsgSrv) { - 'ngInject' - - let vm = this - $scope.note = {} - $scope.note.step1 = true - $scope.note.step2 = false - $scope.maxLimit = '' - let limit = 0 - - websocketMsgSrv.listConfigurations() - $scope.$on('configurationsInfo', function (scope, event) { - limit = event.configurations['zeppelin.websocket.max.text.message.size'] - $scope.maxLimit = Math.round(limit / 1048576) - }) - - vm.resetFlags = function () { - $scope.note = {} - $scope.note.step1 = true - $scope.note.step2 = false - angular.element('#noteImportFile').val('') - } - - $scope.uploadFile = function () { - angular.element('#noteImportFile').click() - } - - $scope.importFile = function (element) { - $scope.note.errorText = '' - $scope.note.importFile = element.files[0] - let file = $scope.note.importFile - let reader = new FileReader() +import './note-import.css'; + +angular.module('zeppelinWebApp').controller('NoteImportCtrl', NoteImportCtrl); + +function NoteImportCtrl($scope, $timeout, websocketMsgSrv) { + 'ngInject'; + + let vm = this; + $scope.note = {}; + $scope.note.step1 = true; + $scope.note.step2 = false; + $scope.maxLimit = ''; + let limit = 0; + + websocketMsgSrv.listConfigurations(); + $scope.$on('configurationsInfo', function(scope, event) { + limit = event.configurations['zeppelin.websocket.max.text.message.size']; + $scope.maxLimit = Math.round(limit / 1048576); + }); + + vm.resetFlags = function() { + $scope.note = {}; + $scope.note.step1 = true; + $scope.note.step2 = false; + angular.element('#noteImportFile').val(''); + }; + + $scope.uploadFile = function() { + angular.element('#noteImportFile').click(); + }; + + $scope.importFile = function(element) { + $scope.note.errorText = ''; + $scope.note.importFile = element.files[0]; + let file = $scope.note.importFile; + let reader = new FileReader(); if (file.size > limit) { - $scope.note.errorText = 'File size limit Exceeded!' - $scope.$apply() - return + $scope.note.errorText = 'File size limit Exceeded!'; + $scope.$apply(); + return; } - reader.onloadend = function () { - vm.processImportJson(reader.result) - } + reader.onloadend = function() { + vm.processImportJson(reader.result); + }; if (file) { - reader.readAsText(file) + reader.readAsText(file); } - } - - $scope.uploadURL = function () { - $scope.note.errorText = '' - $scope.note.step1 = false - $timeout(function () { - $scope.note.step2 = true - }, 400) - } - - vm.importBack = function () { - $scope.note.errorText = '' - $timeout(function () { - $scope.note.step1 = true - }, 400) - $scope.note.step2 = false - } - - vm.importNote = function () { - $scope.note.errorText = '' + }; + + $scope.uploadURL = function() { + $scope.note.errorText = ''; + $scope.note.step1 = false; + $timeout(function() { + $scope.note.step2 = true; + }, 400); + }; + + vm.importBack = function() { + $scope.note.errorText = ''; + $timeout(function() { + $scope.note.step1 = true; + }, 400); + $scope.note.step2 = false; + }; + + vm.importNote = function() { + $scope.note.errorText = ''; if ($scope.note.importUrl) { jQuery.ajax({ url: $scope.note.importUrl, @@ -89,50 +89,50 @@ function NoteImportCtrl ($scope, $timeout, websocketMsgSrv) { dataType: 'json', jsonp: false, xhrFields: { - withCredentials: false + withCredentials: false, }, - error: function (xhr, ajaxOptions, thrownError) { - $scope.note.errorText = 'Unable to Fetch URL' - $scope.$apply() - }}).done(function (data) { - vm.processImportJson(data) - }) + error: function(xhr, ajaxOptions, thrownError) { + $scope.note.errorText = 'Unable to Fetch URL'; + $scope.$apply(); + }}).done(function(data) { + vm.processImportJson(data); + }); } else { - $scope.note.errorText = 'Enter URL' - $scope.$apply() + $scope.note.errorText = 'Enter URL'; + $scope.$apply(); } - } + }; - vm.processImportJson = function (result) { + vm.processImportJson = function(result) { if (typeof result !== 'object') { try { - result = JSON.parse(result) + result = JSON.parse(result); } catch (e) { - $scope.note.errorText = 'JSON parse exception' - $scope.$apply() - return + $scope.note.errorText = 'JSON parse exception'; + $scope.$apply(); + return; } } if (result.paragraphs && result.paragraphs.length > 0) { if (!$scope.note.noteImportName) { - $scope.note.noteImportName = result.name + $scope.note.noteImportName = result.name; } else { - result.name = $scope.note.noteImportName + result.name = $scope.note.noteImportName; } - websocketMsgSrv.importNote(result) + websocketMsgSrv.importNote(result); // angular.element('#noteImportModal').modal('hide'); } else { - $scope.note.errorText = 'Invalid JSON' + $scope.note.errorText = 'Invalid JSON'; } - $scope.$apply() - } + $scope.$apply(); + }; /* ** $scope.$on functions below */ - $scope.$on('setNoteMenu', function (event, notes) { - vm.resetFlags() - angular.element('#noteImportModal').modal('hide') - }) + $scope.$on('setNoteMenu', function(event, notes) { + vm.resetFlags(); + angular.element('#noteImportModal').modal('hide'); + }); } diff --git a/zeppelin-web/src/components/note-list/note-list.factory.js b/zeppelin-web/src/components/note-list/note-list.factory.js index 5e2c513821c..59662fae35d 100644 --- a/zeppelin-web/src/components/note-list/note-list.factory.js +++ b/zeppelin-web/src/components/note-list/note-list.factory.js @@ -12,71 +12,73 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').factory('noteListFactory', NoteListFactory) +angular.module('zeppelinWebApp').factory('noteListFactory', NoteListFactory); function NoteListFactory(arrayOrderingSrv, TRASH_FOLDER_ID) { - 'ngInject' + 'ngInject'; const notes = { root: {children: []}, flatList: [], flatFolderMap: {}, - setNotes: function (notesList) { + setNotes: function(notesList) { // a flat list to boost searching notes.flatList = _.map(notesList, (note) => { note.isTrash = note.name - ? note.name.split('/')[0] === TRASH_FOLDER_ID : false - return note - }) + ? note.name.split('/')[0] === TRASH_FOLDER_ID : false; + return note; + }); // construct the folder-based tree - notes.root = {children: []} - notes.flatFolderMap = {} - _.reduce(notesList, function (root, note) { - let noteName = note.name || note.id - let nodes = noteName.match(/([^\/][^\/]*)/g) + notes.root = {children: []}; + notes.flatFolderMap = {}; + _.reduce(notesList, function(root, note) { + let noteName = note.name || note.id; + let nodes = noteName.match(/([^\/][^\/]*)/g); // recursively add nodes - addNode(root, nodes, note.id) + addNode(root, nodes, note.id); - return root - }, notes.root) - notes.root.children.sort(arrayOrderingSrv.noteComparator) - } - } + return root; + }, notes.root); + notes.root.children.sort(arrayOrderingSrv.noteComparator); + }, + }; - const addNode = function (curDir, nodes, noteId) { + const addNode = function(curDir, nodes, noteId) { if (nodes.length === 1) { // the leaf curDir.children.push({ name: nodes[0], id: noteId, path: curDir.id ? curDir.id + '/' + nodes[0] : nodes[0], - isTrash: curDir.id ? curDir.id.split('/')[0] === TRASH_FOLDER_ID : false - }) + isTrash: curDir.id ? curDir.id.split('/')[0] === TRASH_FOLDER_ID : false, + }); } else { // a folder node - let node = nodes.shift() + let node = nodes.shift(); let dir = _.find(curDir.children, - function (c) { return c.name === node && c.children !== undefined }) + function(c) { + return c.name === node && c.children !== undefined; + }); if (dir !== undefined) { // found an existing dir - addNode(dir, nodes, noteId) + addNode(dir, nodes, noteId); } else { let newDir = { id: curDir.id ? curDir.id + '/' + node : node, name: node, hidden: true, children: [], - isTrash: curDir.id ? curDir.id.split('/')[0] === TRASH_FOLDER_ID : false - } + isTrash: curDir.id ? curDir.id.split('/')[0] === TRASH_FOLDER_ID : false, + }; // add the folder to flat folder map - notes.flatFolderMap[newDir.id] = newDir + notes.flatFolderMap[newDir.id] = newDir; - curDir.children.push(newDir) - addNode(newDir, nodes, noteId) + curDir.children.push(newDir); + addNode(newDir, nodes, noteId); } } - } + }; - return notes + return notes; } diff --git a/zeppelin-web/src/components/note-list/note-list.factory.test.js b/zeppelin-web/src/components/note-list/note-list.factory.test.js index c16504c8785..2a962d8447c 100644 --- a/zeppelin-web/src/components/note-list/note-list.factory.test.js +++ b/zeppelin-web/src/components/note-list/note-list.factory.test.js @@ -1,15 +1,15 @@ -describe('Factory: NoteList', function () { - let noteList +describe('Factory: NoteList', function() { + let noteList; - beforeEach(function () { - angular.mock.module('zeppelinWebApp') + beforeEach(function() { + angular.mock.module('zeppelinWebApp'); - inject(function ($injector) { - noteList = $injector.get('noteListFactory') - }) - }) + inject(function($injector) { + noteList = $injector.get('noteListFactory'); + }); + }); - it('should generate both flat list and folder-based list properly', function () { + it('should generate both flat list and folder-based list properly', function() { let notesList = [ {name: 'A', id: '000001'}, {name: 'B', id: '000002'}, @@ -19,57 +19,57 @@ describe('Factory: NoteList', function () { {name: '/C/CB/CBA', id: '000006'}, // same name with a dir {name: '/C/CB/CBA', id: '000007'}, // same name with another note {name: 'C///CB//CBB', id: '000008'}, - {name: 'D/D[A/DA]B', id: '000009'} // check if '[' and ']' considered as folder seperator - ] - noteList.setNotes(notesList) + {name: 'D/D[A/DA]B', id: '000009'}, // check if '[' and ']' considered as folder seperator + ]; + noteList.setNotes(notesList); - let flatList = noteList.flatList - expect(flatList.length).toBe(9) - expect(flatList[0].name).toBe('A') - expect(flatList[0].id).toBe('000001') - expect(flatList[1].name).toBe('B') - expect(flatList[2].name).toBeUndefined() - expect(flatList[3].name).toBe('/C/CA') - expect(flatList[4].name).toBe('/C/CB') - expect(flatList[5].name).toBe('/C/CB/CBA') - expect(flatList[6].name).toBe('/C/CB/CBA') - expect(flatList[7].name).toBe('C///CB//CBB') - expect(flatList[8].name).toBe('D/D[A/DA]B') + let flatList = noteList.flatList; + expect(flatList.length).toBe(9); + expect(flatList[0].name).toBe('A'); + expect(flatList[0].id).toBe('000001'); + expect(flatList[1].name).toBe('B'); + expect(flatList[2].name).toBeUndefined(); + expect(flatList[3].name).toBe('/C/CA'); + expect(flatList[4].name).toBe('/C/CB'); + expect(flatList[5].name).toBe('/C/CB/CBA'); + expect(flatList[6].name).toBe('/C/CB/CBA'); + expect(flatList[7].name).toBe('C///CB//CBB'); + expect(flatList[8].name).toBe('D/D[A/DA]B'); - let folderList = noteList.root.children - expect(folderList.length).toBe(5) - expect(folderList[3].name).toBe('A') - expect(folderList[3].id).toBe('000001') - expect(folderList[4].name).toBe('B') - expect(folderList[2].name).toBe('000003') - expect(folderList[0].name).toBe('C') - expect(folderList[0].id).toBe('C') - expect(folderList[0].children.length).toBe(3) - expect(folderList[0].children[0].name).toBe('CA') - expect(folderList[0].children[0].id).toBe('000004') - expect(folderList[0].children[0].children).toBeUndefined() - expect(folderList[0].children[1].name).toBe('CB') - expect(folderList[0].children[1].id).toBe('000005') - expect(folderList[0].children[1].children).toBeUndefined() - expect(folderList[0].children[2].name).toBe('CB') - expect(folderList[0].children[2].id).toBe('C/CB') - expect(folderList[0].children[2].children.length).toBe(3) - expect(folderList[0].children[2].children[0].name).toBe('CBA') - expect(folderList[0].children[2].children[0].id).toBe('000006') - expect(folderList[0].children[2].children[0].children).toBeUndefined() - expect(folderList[0].children[2].children[1].name).toBe('CBA') - expect(folderList[0].children[2].children[1].id).toBe('000007') - expect(folderList[0].children[2].children[1].children).toBeUndefined() - expect(folderList[0].children[2].children[2].name).toBe('CBB') - expect(folderList[0].children[2].children[2].id).toBe('000008') - expect(folderList[0].children[2].children[2].children).toBeUndefined() - expect(folderList[1].name).toBe('D') - expect(folderList[1].id).toBe('D') - expect(folderList[1].children.length).toBe(1) - expect(folderList[1].children[0].name).toBe('D[A') - expect(folderList[1].children[0].id).toBe('D/D[A') - expect(folderList[1].children[0].children[0].name).toBe('DA]B') - expect(folderList[1].children[0].children[0].id).toBe('000009') - expect(folderList[1].children[0].children[0].children).toBeUndefined() - }) -}) + let folderList = noteList.root.children; + expect(folderList.length).toBe(5); + expect(folderList[3].name).toBe('A'); + expect(folderList[3].id).toBe('000001'); + expect(folderList[4].name).toBe('B'); + expect(folderList[2].name).toBe('000003'); + expect(folderList[0].name).toBe('C'); + expect(folderList[0].id).toBe('C'); + expect(folderList[0].children.length).toBe(3); + expect(folderList[0].children[0].name).toBe('CA'); + expect(folderList[0].children[0].id).toBe('000004'); + expect(folderList[0].children[0].children).toBeUndefined(); + expect(folderList[0].children[1].name).toBe('CB'); + expect(folderList[0].children[1].id).toBe('000005'); + expect(folderList[0].children[1].children).toBeUndefined(); + expect(folderList[0].children[2].name).toBe('CB'); + expect(folderList[0].children[2].id).toBe('C/CB'); + expect(folderList[0].children[2].children.length).toBe(3); + expect(folderList[0].children[2].children[0].name).toBe('CBA'); + expect(folderList[0].children[2].children[0].id).toBe('000006'); + expect(folderList[0].children[2].children[0].children).toBeUndefined(); + expect(folderList[0].children[2].children[1].name).toBe('CBA'); + expect(folderList[0].children[2].children[1].id).toBe('000007'); + expect(folderList[0].children[2].children[1].children).toBeUndefined(); + expect(folderList[0].children[2].children[2].name).toBe('CBB'); + expect(folderList[0].children[2].children[2].id).toBe('000008'); + expect(folderList[0].children[2].children[2].children).toBeUndefined(); + expect(folderList[1].name).toBe('D'); + expect(folderList[1].id).toBe('D'); + expect(folderList[1].children.length).toBe(1); + expect(folderList[1].children[0].name).toBe('D[A'); + expect(folderList[1].children[0].id).toBe('D/D[A'); + expect(folderList[1].children[0].children[0].name).toBe('DA]B'); + expect(folderList[1].children[0].children[0].id).toBe('000009'); + expect(folderList[1].children[0].children[0].children).toBeUndefined(); + }); +}); diff --git a/zeppelin-web/src/components/note-rename/note-rename.controller.js b/zeppelin-web/src/components/note-rename/note-rename.controller.js index b950d2b49e8..0fa31c44ee4 100644 --- a/zeppelin-web/src/components/note-rename/note-rename.controller.js +++ b/zeppelin-web/src/components/note-rename/note-rename.controller.js @@ -12,37 +12,37 @@ * limitations under the License. */ -import './note-rename.css' +import './note-rename.css'; -angular.module('zeppelinWebApp').controller('NoteRenameCtrl', NoteRenameController) +angular.module('zeppelinWebApp').controller('NoteRenameCtrl', NoteRenameController); function NoteRenameController($scope) { - 'ngInject' + 'ngInject'; - let self = this + let self = this; - $scope.params = {newName: ''} - $scope.isValid = true + $scope.params = {newName: ''}; + $scope.isValid = true; - $scope.rename = function () { - angular.element('#noteRenameModal').modal('hide') - self.callback($scope.params.newName) - } + $scope.rename = function() { + angular.element('#noteRenameModal').modal('hide'); + self.callback($scope.params.newName); + }; - $scope.$on('openRenameModal', function (event, options) { - self.validator = options.validator || defaultValidator - self.callback = options.callback || function () {} + $scope.$on('openRenameModal', function(event, options) { + self.validator = options.validator || defaultValidator; + self.callback = options.callback || function() {}; - $scope.title = options.title || 'Rename' - $scope.params.newName = options.oldName || '' - $scope.validate = function () { - $scope.isValid = self.validator($scope.params.newName) - } + $scope.title = options.title || 'Rename'; + $scope.params.newName = options.oldName || ''; + $scope.validate = function() { + $scope.isValid = self.validator($scope.params.newName); + }; - angular.element('#noteRenameModal').modal('show') - }) + angular.element('#noteRenameModal').modal('show'); + }); - function defaultValidator (str) { - return !!str.trim() + function defaultValidator(str) { + return !!str.trim(); } } diff --git a/zeppelin-web/src/components/note-rename/note-rename.service.js b/zeppelin-web/src/components/note-rename/note-rename.service.js index 64df82ff951..fd0f3e58e5d 100644 --- a/zeppelin-web/src/components/note-rename/note-rename.service.js +++ b/zeppelin-web/src/components/note-rename/note-rename.service.js @@ -12,12 +12,12 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').service('noteRenameService', NoteRenameService) +angular.module('zeppelinWebApp').service('noteRenameService', NoteRenameService); function NoteRenameService($rootScope) { - 'ngInject' + 'ngInject'; - let self = this + let self = this; /** * @@ -26,7 +26,7 @@ function NoteRenameService($rootScope) { * callback: (newName: string)=>void - callback onButtonClick * validator: (str: string)=>boolean - input validator */ - self.openRenameModal = function (options) { - $rootScope.$broadcast('openRenameModal', options) - } + self.openRenameModal = function(options) { + $rootScope.$broadcast('openRenameModal', options); + }; } diff --git a/zeppelin-web/src/components/websocket/websocket-event.factory.js b/zeppelin-web/src/components/websocket/websocket-event.factory.js index 70d61ecd964..18c704dd6df 100644 --- a/zeppelin-web/src/components/websocket/websocket-event.factory.js +++ b/zeppelin-web/src/components/websocket/websocket-event.factory.js @@ -12,92 +12,92 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').factory('websocketEvents', WebsocketEventFactory) +angular.module('zeppelinWebApp').factory('websocketEvents', WebsocketEventFactory); -function WebsocketEventFactory ($rootScope, $websocket, $location, baseUrlSrv) { - 'ngInject' +function WebsocketEventFactory($rootScope, $websocket, $location, baseUrlSrv) { + 'ngInject'; - let websocketCalls = {} - let pingIntervalId + let websocketCalls = {}; + let pingIntervalId; - websocketCalls.ws = $websocket(baseUrlSrv.getWebsocketUrl()) - websocketCalls.ws.reconnectIfNotNormalClose = true + websocketCalls.ws = $websocket(baseUrlSrv.getWebsocketUrl()); + websocketCalls.ws.reconnectIfNotNormalClose = true; - websocketCalls.ws.onOpen(function () { - console.log('Websocket created') - $rootScope.$broadcast('setConnectedStatus', true) - pingIntervalId = setInterval(function () { - websocketCalls.sendNewEvent({op: 'PING'}) - }, 10000) - }) + websocketCalls.ws.onOpen(function() { + console.log('Websocket created'); + $rootScope.$broadcast('setConnectedStatus', true); + pingIntervalId = setInterval(function() { + websocketCalls.sendNewEvent({op: 'PING'}); + }, 10000); + }); - websocketCalls.sendNewEvent = function (data) { + websocketCalls.sendNewEvent = function(data) { if ($rootScope.ticket !== undefined) { - data.principal = $rootScope.ticket.principal - data.ticket = $rootScope.ticket.ticket - data.roles = $rootScope.ticket.roles + data.principal = $rootScope.ticket.principal; + data.ticket = $rootScope.ticket.ticket; + data.roles = $rootScope.ticket.roles; } else { - data.principal = '' - data.ticket = '' - data.roles = '' + data.principal = ''; + data.ticket = ''; + data.roles = ''; } - console.log('Send >> %o, %o, %o, %o, %o', data.op, data.principal, data.ticket, data.roles, data) - return websocketCalls.ws.send(JSON.stringify(data)) - } + console.log('Send >> %o, %o, %o, %o, %o', data.op, data.principal, data.ticket, data.roles, data); + return websocketCalls.ws.send(JSON.stringify(data)); + }; - websocketCalls.isConnected = function () { - return (websocketCalls.ws.socket.readyState === 1) - } + websocketCalls.isConnected = function() { + return (websocketCalls.ws.socket.readyState === 1); + }; - websocketCalls.ws.onMessage(function (event) { - let payload + websocketCalls.ws.onMessage(function(event) { + let payload; if (event.data) { - payload = angular.fromJson(event.data) + payload = angular.fromJson(event.data); } - console.log('Receive << %o, %o', payload.op, payload) + console.log('Receive << %o, %o', payload.op, payload); - let op = payload.op - let data = payload.data + let op = payload.op; + let data = payload.data; if (op === 'NOTE') { - $rootScope.$broadcast('setNoteContent', data.note) + $rootScope.$broadcast('setNoteContent', data.note); } else if (op === 'NEW_NOTE') { - $location.path('/notebook/' + data.note.id) + $location.path('/notebook/' + data.note.id); } else if (op === 'NOTES_INFO') { - $rootScope.$broadcast('setNoteMenu', data.notes) + $rootScope.$broadcast('setNoteMenu', data.notes); } else if (op === 'LIST_NOTE_JOBS') { - $rootScope.$emit('jobmanager:set-jobs', data.noteJobs) + $rootScope.$emit('jobmanager:set-jobs', data.noteJobs); } else if (op === 'LIST_UPDATE_NOTE_JOBS') { - $rootScope.$emit('jobmanager:update-jobs', data.noteRunningJobs) + $rootScope.$emit('jobmanager:update-jobs', data.noteRunningJobs); } else if (op === 'AUTH_INFO') { - let btn = [] + let btn = []; if ($rootScope.ticket.roles === '[]') { btn = [{ label: 'Close', - action: function (dialog) { - dialog.close() - } - }] + action: function(dialog) { + dialog.close(); + }, + }]; } else { btn = [{ label: 'Login', - action: function (dialog) { - dialog.close() + action: function(dialog) { + dialog.close(); angular.element('#loginModal').modal({ - show: 'true' - }) - } + show: 'true', + }); + }, }, { label: 'Cancel', - action: function (dialog) { - dialog.close() + action: function(dialog) { + dialog.close(); // using $rootScope.apply to trigger angular digest cycle // changing $location.path inside bootstrap modal wont trigger digest - $rootScope.$apply(function () { - $location.path('/') - }) - } - }] + $rootScope.$apply(function() { + $location.path('/'); + }); + }, + }]; } BootstrapDialog.show({ @@ -106,44 +106,44 @@ function WebsocketEventFactory ($rootScope, $websocket, $location, baseUrlSrv) { closeByKeyboard: false, title: 'Insufficient privileges', message: data.info.toString(), - buttons: btn - }) + buttons: btn, + }); } else if (op === 'PARAGRAPH') { - $rootScope.$broadcast('updateParagraph', data) + $rootScope.$broadcast('updateParagraph', data); } else if (op === 'RUN_PARAGRAPH_USING_SPELL') { - $rootScope.$broadcast('runParagraphUsingSpell', data) + $rootScope.$broadcast('runParagraphUsingSpell', data); } else if (op === 'PARAGRAPH_APPEND_OUTPUT') { - $rootScope.$broadcast('appendParagraphOutput', data) + $rootScope.$broadcast('appendParagraphOutput', data); } else if (op === 'PARAGRAPH_UPDATE_OUTPUT') { - $rootScope.$broadcast('updateParagraphOutput', data) + $rootScope.$broadcast('updateParagraphOutput', data); } else if (op === 'PROGRESS') { - $rootScope.$broadcast('updateProgress', data) + $rootScope.$broadcast('updateProgress', data); } else if (op === 'COMPLETION_LIST') { - $rootScope.$broadcast('completionList', data) + $rootScope.$broadcast('completionList', data); } else if (op === 'EDITOR_SETTING') { - $rootScope.$broadcast('editorSetting', data) + $rootScope.$broadcast('editorSetting', data); } else if (op === 'ANGULAR_OBJECT_UPDATE') { - $rootScope.$broadcast('angularObjectUpdate', data) + $rootScope.$broadcast('angularObjectUpdate', data); } else if (op === 'ANGULAR_OBJECT_REMOVE') { - $rootScope.$broadcast('angularObjectRemove', data) + $rootScope.$broadcast('angularObjectRemove', data); } else if (op === 'APP_APPEND_OUTPUT') { - $rootScope.$broadcast('appendAppOutput', data) + $rootScope.$broadcast('appendAppOutput', data); } else if (op === 'APP_UPDATE_OUTPUT') { - $rootScope.$broadcast('updateAppOutput', data) + $rootScope.$broadcast('updateAppOutput', data); } else if (op === 'APP_LOAD') { - $rootScope.$broadcast('appLoad', data) + $rootScope.$broadcast('appLoad', data); } else if (op === 'APP_STATUS_CHANGE') { - $rootScope.$broadcast('appStatusChange', data) + $rootScope.$broadcast('appStatusChange', data); } else if (op === 'LIST_REVISION_HISTORY') { - $rootScope.$broadcast('listRevisionHistory', data) + $rootScope.$broadcast('listRevisionHistory', data); } else if (op === 'NOTE_REVISION') { - $rootScope.$broadcast('noteRevision', data) + $rootScope.$broadcast('noteRevision', data); } else if (op === 'NOTE_REVISION_FOR_COMPARE') { - $rootScope.$broadcast('noteRevisionForCompare', data) + $rootScope.$broadcast('noteRevisionForCompare', data); } else if (op === 'INTERPRETER_BINDINGS') { - $rootScope.$broadcast('interpreterBindings', data) + $rootScope.$broadcast('interpreterBindings', data); } else if (op === 'SAVE_NOTE_FORMS') { - $rootScope.$broadcast('saveNoteForms', data) + $rootScope.$broadcast('saveNoteForms', data); } else if (op === 'ERROR_INFO') { BootstrapDialog.show({ closable: false, @@ -154,47 +154,47 @@ function WebsocketEventFactory ($rootScope, $websocket, $location, baseUrlSrv) { buttons: [{ // close all the dialogs when there are error on running all paragraphs label: 'Close', - action: function () { - BootstrapDialog.closeAll() - } - }] - }) + action: function() { + BootstrapDialog.closeAll(); + }, + }], + }); } else if (op === 'SESSION_LOGOUT') { - $rootScope.$broadcast('session_logout', data) + $rootScope.$broadcast('session_logout', data); } else if (op === 'CONFIGURATIONS_INFO') { - $rootScope.$broadcast('configurationsInfo', data) + $rootScope.$broadcast('configurationsInfo', data); } else if (op === 'INTERPRETER_SETTINGS') { - $rootScope.$broadcast('interpreterSettings', data) + $rootScope.$broadcast('interpreterSettings', data); } else if (op === 'PARAGRAPH_ADDED') { - $rootScope.$broadcast('addParagraph', data.paragraph, data.index) + $rootScope.$broadcast('addParagraph', data.paragraph, data.index); } else if (op === 'PARAGRAPH_REMOVED') { - $rootScope.$broadcast('removeParagraph', data.id) + $rootScope.$broadcast('removeParagraph', data.id); } else if (op === 'PARAGRAPH_MOVED') { - $rootScope.$broadcast('moveParagraph', data.id, data.index) + $rootScope.$broadcast('moveParagraph', data.id, data.index); } else if (op === 'NOTE_UPDATED') { - $rootScope.$broadcast('updateNote', data.name, data.config, data.info) + $rootScope.$broadcast('updateNote', data.name, data.config, data.info); } else if (op === 'SET_NOTE_REVISION') { - $rootScope.$broadcast('setNoteRevisionResult', data) + $rootScope.$broadcast('setNoteRevisionResult', data); } else if (op === 'PARAS_INFO') { - $rootScope.$broadcast('updateParaInfos', data) + $rootScope.$broadcast('updateParaInfos', data); } else { - console.error(`unknown websocket op: ${op}`) + console.error(`unknown websocket op: ${op}`); } - }) + }); - websocketCalls.ws.onError(function (event) { - console.log('error message: ', event) - $rootScope.$broadcast('setConnectedStatus', false) - }) + websocketCalls.ws.onError(function(event) { + console.log('error message: ', event); + $rootScope.$broadcast('setConnectedStatus', false); + }); - websocketCalls.ws.onClose(function (event) { - console.log('close message: ', event) + websocketCalls.ws.onClose(function(event) { + console.log('close message: ', event); if (pingIntervalId !== undefined) { - clearInterval(pingIntervalId) - pingIntervalId = undefined + clearInterval(pingIntervalId); + pingIntervalId = undefined; } - $rootScope.$broadcast('setConnectedStatus', false) - }) + $rootScope.$broadcast('setConnectedStatus', false); + }); - return websocketCalls + return websocketCalls; } diff --git a/zeppelin-web/src/components/websocket/websocket-message.service.js b/zeppelin-web/src/components/websocket/websocket-message.service.js index cd65e1d3194..f0cf92b3787 100644 --- a/zeppelin-web/src/components/websocket/websocket-message.service.js +++ b/zeppelin-web/src/components/websocket/websocket-message.service.js @@ -12,100 +12,100 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').service('websocketMsgSrv', WebsocketMessageService) +angular.module('zeppelinWebApp').service('websocketMsgSrv', WebsocketMessageService); -function WebsocketMessageService ($rootScope, websocketEvents) { - 'ngInject' +function WebsocketMessageService($rootScope, websocketEvents) { + 'ngInject'; return { - getHomeNote: function () { - websocketEvents.sendNewEvent({op: 'GET_HOME_NOTE'}) + getHomeNote: function() { + websocketEvents.sendNewEvent({op: 'GET_HOME_NOTE'}); }, - createNotebook: function (noteName, defaultInterpreterId) { + createNotebook: function(noteName, defaultInterpreterId) { websocketEvents.sendNewEvent({ op: 'NEW_NOTE', data: { name: noteName, - defaultInterpreterId: defaultInterpreterId - } - }) + defaultInterpreterId: defaultInterpreterId, + }, + }); }, - moveNoteToTrash: function (noteId) { - websocketEvents.sendNewEvent({op: 'MOVE_NOTE_TO_TRASH', data: {id: noteId}}) + moveNoteToTrash: function(noteId) { + websocketEvents.sendNewEvent({op: 'MOVE_NOTE_TO_TRASH', data: {id: noteId}}); }, - moveFolderToTrash: function (folderId) { - websocketEvents.sendNewEvent({op: 'MOVE_FOLDER_TO_TRASH', data: {id: folderId}}) + moveFolderToTrash: function(folderId) { + websocketEvents.sendNewEvent({op: 'MOVE_FOLDER_TO_TRASH', data: {id: folderId}}); }, - restoreNote: function (noteId) { - websocketEvents.sendNewEvent({op: 'RESTORE_NOTE', data: {id: noteId}}) + restoreNote: function(noteId) { + websocketEvents.sendNewEvent({op: 'RESTORE_NOTE', data: {id: noteId}}); }, - restoreFolder: function (folderId) { - websocketEvents.sendNewEvent({op: 'RESTORE_FOLDER', data: {id: folderId}}) + restoreFolder: function(folderId) { + websocketEvents.sendNewEvent({op: 'RESTORE_FOLDER', data: {id: folderId}}); }, - restoreAll: function () { - websocketEvents.sendNewEvent({op: 'RESTORE_ALL'}) + restoreAll: function() { + websocketEvents.sendNewEvent({op: 'RESTORE_ALL'}); }, - deleteNote: function (noteId) { - websocketEvents.sendNewEvent({op: 'DEL_NOTE', data: {id: noteId}}) + deleteNote: function(noteId) { + websocketEvents.sendNewEvent({op: 'DEL_NOTE', data: {id: noteId}}); }, - removeFolder: function (folderId) { - websocketEvents.sendNewEvent({op: 'REMOVE_FOLDER', data: {id: folderId}}) + removeFolder: function(folderId) { + websocketEvents.sendNewEvent({op: 'REMOVE_FOLDER', data: {id: folderId}}); }, - emptyTrash: function () { - websocketEvents.sendNewEvent({op: 'EMPTY_TRASH'}) + emptyTrash: function() { + websocketEvents.sendNewEvent({op: 'EMPTY_TRASH'}); }, - cloneNote: function (noteIdToClone, newNoteName) { - websocketEvents.sendNewEvent({op: 'CLONE_NOTE', data: {id: noteIdToClone, name: newNoteName}}) + cloneNote: function(noteIdToClone, newNoteName) { + websocketEvents.sendNewEvent({op: 'CLONE_NOTE', data: {id: noteIdToClone, name: newNoteName}}); }, - getNoteList: function () { - websocketEvents.sendNewEvent({op: 'LIST_NOTES'}) + getNoteList: function() { + websocketEvents.sendNewEvent({op: 'LIST_NOTES'}); }, - reloadAllNotesFromRepo: function () { - websocketEvents.sendNewEvent({op: 'RELOAD_NOTES_FROM_REPO'}) + reloadAllNotesFromRepo: function() { + websocketEvents.sendNewEvent({op: 'RELOAD_NOTES_FROM_REPO'}); }, - getNote: function (noteId) { - websocketEvents.sendNewEvent({op: 'GET_NOTE', data: {id: noteId}}) + getNote: function(noteId) { + websocketEvents.sendNewEvent({op: 'GET_NOTE', data: {id: noteId}}); }, - updateNote: function (noteId, noteName, noteConfig) { - websocketEvents.sendNewEvent({op: 'NOTE_UPDATE', data: {id: noteId, name: noteName, config: noteConfig}}) + updateNote: function(noteId, noteName, noteConfig) { + websocketEvents.sendNewEvent({op: 'NOTE_UPDATE', data: {id: noteId, name: noteName, config: noteConfig}}); }, - updatePersonalizedMode: function (noteId, modeValue) { - websocketEvents.sendNewEvent({op: 'UPDATE_PERSONALIZED_MODE', data: {id: noteId, personalized: modeValue}}) + updatePersonalizedMode: function(noteId, modeValue) { + websocketEvents.sendNewEvent({op: 'UPDATE_PERSONALIZED_MODE', data: {id: noteId, personalized: modeValue}}); }, - renameNote: function (noteId, noteName) { - websocketEvents.sendNewEvent({op: 'NOTE_RENAME', data: {id: noteId, name: noteName}}) + renameNote: function(noteId, noteName) { + websocketEvents.sendNewEvent({op: 'NOTE_RENAME', data: {id: noteId, name: noteName}}); }, - renameFolder: function (folderId, folderName) { - websocketEvents.sendNewEvent({op: 'FOLDER_RENAME', data: {id: folderId, name: folderName}}) + renameFolder: function(folderId, folderName) { + websocketEvents.sendNewEvent({op: 'FOLDER_RENAME', data: {id: folderId, name: folderName}}); }, - moveParagraph: function (paragraphId, newIndex) { - websocketEvents.sendNewEvent({op: 'MOVE_PARAGRAPH', data: {id: paragraphId, index: newIndex}}) + moveParagraph: function(paragraphId, newIndex) { + websocketEvents.sendNewEvent({op: 'MOVE_PARAGRAPH', data: {id: paragraphId, index: newIndex}}); }, - insertParagraph: function (newIndex) { - websocketEvents.sendNewEvent({op: 'INSERT_PARAGRAPH', data: {index: newIndex}}) + insertParagraph: function(newIndex) { + websocketEvents.sendNewEvent({op: 'INSERT_PARAGRAPH', data: {index: newIndex}}); }, - copyParagraph: function (newIndex, paragraphTitle, paragraphData, + copyParagraph: function(newIndex, paragraphTitle, paragraphData, paragraphConfig, paragraphParams) { websocketEvents.sendNewEvent({ op: 'COPY_PARAGRAPH', @@ -114,12 +114,12 @@ function WebsocketMessageService ($rootScope, websocketEvents) { title: paragraphTitle, paragraph: paragraphData, config: paragraphConfig, - params: paragraphParams - } - }) + params: paragraphParams, + }, + }); }, - updateAngularObject: function (noteId, paragraphId, name, value, interpreterGroupId) { + updateAngularObject: function(noteId, paragraphId, name, value, interpreterGroupId) { websocketEvents.sendNewEvent({ op: 'ANGULAR_OBJECT_UPDATED', data: { @@ -127,39 +127,39 @@ function WebsocketMessageService ($rootScope, websocketEvents) { paragraphId: paragraphId, name: name, value: value, - interpreterGroupId: interpreterGroupId - } - }) + interpreterGroupId: interpreterGroupId, + }, + }); }, - clientBindAngularObject: function (noteId, name, value, paragraphId) { + clientBindAngularObject: function(noteId, name, value, paragraphId) { websocketEvents.sendNewEvent({ op: 'ANGULAR_OBJECT_CLIENT_BIND', data: { noteId: noteId, name: name, value: value, - paragraphId: paragraphId - } - }) + paragraphId: paragraphId, + }, + }); }, - clientUnbindAngularObject: function (noteId, name, paragraphId) { + clientUnbindAngularObject: function(noteId, name, paragraphId) { websocketEvents.sendNewEvent({ op: 'ANGULAR_OBJECT_CLIENT_UNBIND', data: { noteId: noteId, name: name, - paragraphId: paragraphId - } - }) + paragraphId: paragraphId, + }, + }); }, - cancelParagraphRun: function (paragraphId) { - websocketEvents.sendNewEvent({op: 'CANCEL_PARAGRAPH', data: {id: paragraphId}}) + cancelParagraphRun: function(paragraphId) { + websocketEvents.sendNewEvent({op: 'CANCEL_PARAGRAPH', data: {id: paragraphId}}); }, - paragraphExecutedBySpell: function (paragraphId, paragraphTitle, + paragraphExecutedBySpell: function(paragraphId, paragraphTitle, paragraphText, paragraphResultsMsg, paragraphStatus, paragraphErrorMessage, paragraphConfig, paragraphParams, @@ -172,10 +172,10 @@ function WebsocketMessageService ($rootScope, websocketEvents) { paragraph: paragraphText, results: { code: paragraphStatus, - msg: paragraphResultsMsg.map(dataWithType => { - let serializedData = dataWithType.data - return { type: dataWithType.type, data: serializedData, } - }) + msg: paragraphResultsMsg.map((dataWithType) => { + let serializedData = dataWithType.data; + return {type: dataWithType.type, data: serializedData}; + }), }, status: paragraphStatus, errorMessage: paragraphErrorMessage, @@ -183,11 +183,11 @@ function WebsocketMessageService ($rootScope, websocketEvents) { params: paragraphParams, dateStarted: paragraphDateStarted, dateFinished: paragraphDateFinished, - } - }) + }, + }); }, - runParagraph: function (paragraphId, paragraphTitle, paragraphData, paragraphConfig, paragraphParams) { + runParagraph: function(paragraphId, paragraphTitle, paragraphData, paragraphConfig, paragraphParams) { websocketEvents.sendNewEvent({ op: 'RUN_PARAGRAPH', data: { @@ -195,45 +195,45 @@ function WebsocketMessageService ($rootScope, websocketEvents) { title: paragraphTitle, paragraph: paragraphData, config: paragraphConfig, - params: paragraphParams - } - }) + params: paragraphParams, + }, + }); }, - runAllParagraphs: function (noteId, paragraphs) { + runAllParagraphs: function(noteId, paragraphs) { websocketEvents.sendNewEvent({ op: 'RUN_ALL_PARAGRAPHS', data: { noteId: noteId, - paragraphs: JSON.stringify(paragraphs) - } - }) + paragraphs: JSON.stringify(paragraphs), + }, + }); }, - removeParagraph: function (paragraphId) { - websocketEvents.sendNewEvent({op: 'PARAGRAPH_REMOVE', data: {id: paragraphId}}) + removeParagraph: function(paragraphId) { + websocketEvents.sendNewEvent({op: 'PARAGRAPH_REMOVE', data: {id: paragraphId}}); }, - clearParagraphOutput: function (paragraphId) { - websocketEvents.sendNewEvent({op: 'PARAGRAPH_CLEAR_OUTPUT', data: {id: paragraphId}}) + clearParagraphOutput: function(paragraphId) { + websocketEvents.sendNewEvent({op: 'PARAGRAPH_CLEAR_OUTPUT', data: {id: paragraphId}}); }, - clearAllParagraphOutput: function (noteId) { - websocketEvents.sendNewEvent({op: 'PARAGRAPH_CLEAR_ALL_OUTPUT', data: {id: noteId}}) + clearAllParagraphOutput: function(noteId) { + websocketEvents.sendNewEvent({op: 'PARAGRAPH_CLEAR_ALL_OUTPUT', data: {id: noteId}}); }, - completion: function (paragraphId, buf, cursor) { + completion: function(paragraphId, buf, cursor) { websocketEvents.sendNewEvent({ op: 'COMPLETION', data: { id: paragraphId, buf: buf, - cursor: cursor - } - }) + cursor: cursor, + }, + }); }, - commitParagraph: function (paragraphId, paragraphTitle, paragraphData, paragraphConfig, paragraphParams, noteId) { + commitParagraph: function(paragraphId, paragraphTitle, paragraphData, paragraphConfig, paragraphParams, noteId) { return websocketEvents.sendNewEvent({ op: 'COMMIT_PARAGRAPH', data: { @@ -242,132 +242,132 @@ function WebsocketMessageService ($rootScope, websocketEvents) { title: paragraphTitle, paragraph: paragraphData, config: paragraphConfig, - params: paragraphParams - } - }) + params: paragraphParams, + }, + }); }, - importNote: function (note) { + importNote: function(note) { websocketEvents.sendNewEvent({ op: 'IMPORT_NOTE', data: { - note: note - } - }) + note: note, + }, + }); }, - checkpointNote: function (noteId, commitMessage) { + checkpointNote: function(noteId, commitMessage) { websocketEvents.sendNewEvent({ op: 'CHECKPOINT_NOTE', data: { noteId: noteId, - commitMessage: commitMessage - } - }) + commitMessage: commitMessage, + }, + }); }, - setNoteRevision: function (noteId, revisionId) { + setNoteRevision: function(noteId, revisionId) { websocketEvents.sendNewEvent({ op: 'SET_NOTE_REVISION', data: { noteId: noteId, - revisionId: revisionId - } - }) + revisionId: revisionId, + }, + }); }, - listRevisionHistory: function (noteId) { + listRevisionHistory: function(noteId) { websocketEvents.sendNewEvent({ op: 'LIST_REVISION_HISTORY', data: { - noteId: noteId - } - }) + noteId: noteId, + }, + }); }, - getNoteByRevision: function (noteId, revisionId) { + getNoteByRevision: function(noteId, revisionId) { websocketEvents.sendNewEvent({ op: 'NOTE_REVISION', data: { noteId: noteId, - revisionId: revisionId - } - }) + revisionId: revisionId, + }, + }); }, - getNoteByRevisionForCompare: function (noteId, revisionId, position) { + getNoteByRevisionForCompare: function(noteId, revisionId, position) { websocketEvents.sendNewEvent({ op: 'NOTE_REVISION_FOR_COMPARE', data: { noteId: noteId, revisionId: revisionId, - position: position - } - }) + position: position, + }, + }); }, - getEditorSetting: function (paragraphId, replName) { + getEditorSetting: function(paragraphId, replName) { websocketEvents.sendNewEvent({ op: 'EDITOR_SETTING', data: { paragraphId: paragraphId, - magic: replName - } - }) + magic: replName, + }, + }); }, - isConnected: function () { - return websocketEvents.isConnected() + isConnected: function() { + return websocketEvents.isConnected(); }, - getJobs: function () { - websocketEvents.sendNewEvent({op: 'LIST_NOTE_JOBS'}) + getJobs: function() { + websocketEvents.sendNewEvent({op: 'LIST_NOTE_JOBS'}); }, - disconnectJobEvent: function () { - websocketEvents.sendNewEvent({op: 'UNSUBSCRIBE_UPDATE_NOTE_JOBS'}) + disconnectJobEvent: function() { + websocketEvents.sendNewEvent({op: 'UNSUBSCRIBE_UPDATE_NOTE_JOBS'}); }, - getUpdateNoteJobsList: function (lastUpdateServerUnixTime) { + getUpdateNoteJobsList: function(lastUpdateServerUnixTime) { websocketEvents.sendNewEvent( {op: 'LIST_UPDATE_NOTE_JOBS', data: {lastUpdateUnixTime: lastUpdateServerUnixTime * 1}} - ) + ); }, - getInterpreterBindings: function (noteId) { - websocketEvents.sendNewEvent({op: 'GET_INTERPRETER_BINDINGS', data: {noteId: noteId}}) + getInterpreterBindings: function(noteId) { + websocketEvents.sendNewEvent({op: 'GET_INTERPRETER_BINDINGS', data: {noteId: noteId}}); }, - saveInterpreterBindings: function (noteId, selectedSettingIds) { + saveInterpreterBindings: function(noteId, selectedSettingIds) { websocketEvents.sendNewEvent({op: 'SAVE_INTERPRETER_BINDINGS', - data: {noteId: noteId, selectedSettingIds: selectedSettingIds}}) + data: {noteId: noteId, selectedSettingIds: selectedSettingIds}}); }, - listConfigurations: function () { - websocketEvents.sendNewEvent({op: 'LIST_CONFIGURATIONS'}) + listConfigurations: function() { + websocketEvents.sendNewEvent({op: 'LIST_CONFIGURATIONS'}); }, - getInterpreterSettings: function () { - websocketEvents.sendNewEvent({op: 'GET_INTERPRETER_SETTINGS'}) + getInterpreterSettings: function() { + websocketEvents.sendNewEvent({op: 'GET_INTERPRETER_SETTINGS'}); }, - saveNoteForms: function (note) { + saveNoteForms: function(note) { websocketEvents.sendNewEvent({op: 'SAVE_NOTE_FORMS', data: { noteId: note.id, - noteParams: note.noteParams - } - }) + noteParams: note.noteParams, + }, + }); }, - removeNoteForms: function (note, formName) { + removeNoteForms: function(note, formName) { websocketEvents.sendNewEvent({op: 'REMOVE_NOTE_FORMS', data: { noteId: note.id, - formName: formName - } - }) - } + formName: formName, + }, + }); + }, - } + }; } diff --git a/zeppelin-web/src/index.js b/zeppelin-web/src/index.js index 4c41336a1f7..55d6155aca0 100644 --- a/zeppelin-web/src/index.js +++ b/zeppelin-web/src/index.js @@ -13,65 +13,65 @@ */ // import globally uses css here -import 'github-markdown-css/github-markdown.css' +import 'github-markdown-css/github-markdown.css'; -import './app/app.js' -import './app/app.controller.js' -import './app/home/home.controller.js' -import './app/notebook/notebook.controller.js' +import './app/app.js'; +import './app/app.controller.js'; +import './app/home/home.controller.js'; +import './app/notebook/notebook.controller.js'; -import './app/tabledata/tabledata.js' -import './app/tabledata/transformation.js' -import './app/tabledata/pivot.js' -import './app/tabledata/passthrough.js' -import './app/tabledata/columnselector.js' -import './app/tabledata/advanced-transformation.js' -import './app/visualization/visualization.js' -import './app/visualization/builtins/visualization-table.js' -import './app/visualization/builtins/visualization-nvd3chart.js' -import './app/visualization/builtins/visualization-barchart.js' -import './app/visualization/builtins/visualization-piechart.js' -import './app/visualization/builtins/visualization-areachart.js' -import './app/visualization/builtins/visualization-linechart.js' -import './app/visualization/builtins/visualization-scatterchart.js' +import './app/tabledata/tabledata.js'; +import './app/tabledata/transformation.js'; +import './app/tabledata/pivot.js'; +import './app/tabledata/passthrough.js'; +import './app/tabledata/columnselector.js'; +import './app/tabledata/advanced-transformation.js'; +import './app/visualization/visualization.js'; +import './app/visualization/builtins/visualization-table.js'; +import './app/visualization/builtins/visualization-nvd3chart.js'; +import './app/visualization/builtins/visualization-barchart.js'; +import './app/visualization/builtins/visualization-piechart.js'; +import './app/visualization/builtins/visualization-areachart.js'; +import './app/visualization/builtins/visualization-linechart.js'; +import './app/visualization/builtins/visualization-scatterchart.js'; -import './app/jobmanager/jobmanager.component.js' -import './app/interpreter/interpreter.controller.js' -import './app/interpreter/interpreter.filter.js' -import './app/interpreter/interpreter-item.directive.js' -import './app/interpreter/widget/number-widget.directive.js' -import './app/credential/credential.controller.js' -import './app/configuration/configuration.controller.js' -import './app/notebook/revisions-comparator/revisions-comparator.component.js' -import './app/notebook/paragraph/paragraph.controller.js' -import './app/notebook/paragraph/clipboard.controller.js' -import './app/notebook/paragraph/resizable.directive.js' -import './app/notebook/paragraph/result/result.controller.js' -import './app/notebook/paragraph/code-editor/code-editor.directive.js' -import './app/notebook/save-as/save-as.service.js' -import './app/notebook/save-as/browser-detect.service.js' -import './app/notebook/elastic-input/elastic-input.controller.js' -import './app/notebook/dropdown-input/dropdown-input.directive.js' -import './app/notebook/note-var-share.service.js' -import './app/notebook-repository/notebook-repository.controller.js' -import './app/search/result-list.controller.js' -import './app/search/search.service.js' -import './app/helium' -import './app/helium/helium.service.js' -import './app/notebook/dynamic-forms/dynamic-forms.directive.js' -import './components/array-ordering/array-ordering.service.js' -import './components/navbar/navbar.controller.js' -import './components/navbar/expand-collapse/expand-collapse.directive.js' -import './components/note-create/note-create.controller.js' -import './components/note-create/visible.directive.js' -import './components/note-import/note-import.controller.js' -import './components/ng-enter/ng-enter.directive.js' -import './components/ng-escape/ng-escape.directive.js' -import './components/websocket/websocket-message.service.js' -import './components/websocket/websocket-event.factory.js' -import './components/note-list/note-list.factory.js' -import './components/base-url/base-url.service.js' -import './components/login/login.controller.js' -import './components/note-action/note-action.service.js' -import './components/note-rename/note-rename.controller.js' -import './components/note-rename/note-rename.service.js' +import './app/jobmanager/jobmanager.component.js'; +import './app/interpreter/interpreter.controller.js'; +import './app/interpreter/interpreter.filter.js'; +import './app/interpreter/interpreter-item.directive.js'; +import './app/interpreter/widget/number-widget.directive.js'; +import './app/credential/credential.controller.js'; +import './app/configuration/configuration.controller.js'; +import './app/notebook/revisions-comparator/revisions-comparator.component.js'; +import './app/notebook/paragraph/paragraph.controller.js'; +import './app/notebook/paragraph/clipboard.controller.js'; +import './app/notebook/paragraph/resizable.directive.js'; +import './app/notebook/paragraph/result/result.controller.js'; +import './app/notebook/paragraph/code-editor/code-editor.directive.js'; +import './app/notebook/save-as/save-as.service.js'; +import './app/notebook/save-as/browser-detect.service.js'; +import './app/notebook/elastic-input/elastic-input.controller.js'; +import './app/notebook/dropdown-input/dropdown-input.directive.js'; +import './app/notebook/note-var-share.service.js'; +import './app/notebook-repository/notebook-repository.controller.js'; +import './app/search/result-list.controller.js'; +import './app/search/search.service.js'; +import './app/helium'; +import './app/helium/helium.service.js'; +import './app/notebook/dynamic-forms/dynamic-forms.directive.js'; +import './components/array-ordering/array-ordering.service.js'; +import './components/navbar/navbar.controller.js'; +import './components/navbar/expand-collapse/expand-collapse.directive.js'; +import './components/note-create/note-create.controller.js'; +import './components/note-create/visible.directive.js'; +import './components/note-import/note-import.controller.js'; +import './components/ng-enter/ng-enter.directive.js'; +import './components/ng-escape/ng-escape.directive.js'; +import './components/websocket/websocket-message.service.js'; +import './components/websocket/websocket-event.factory.js'; +import './components/note-list/note-list.factory.js'; +import './components/base-url/base-url.service.js'; +import './components/login/login.controller.js'; +import './components/note-action/note-action.service.js'; +import './components/note-rename/note-rename.controller.js'; +import './components/note-rename/note-rename.service.js'; From 6fef61588d44da0543d7607caab62505887732b6 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Sat, 24 Feb 2018 14:25:38 +0800 Subject: [PATCH 196/492] [HOTFIX] rename zeppelin.ipython.grpc.framesize to zeppelin.ipython.grpc.message_size ### What is this PR for? trivial change for property name renaming. Followup of #2802 ### What type of PR is it? [Refactoring] ### Todos * [ ] - Task ### How should this be tested? * CI pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2813 from zjffdu/HOTFIX_GRPC and squashes the following commits: d899a29 [Jeff Zhang] [HOTFIX] rename zeppelin.ipython.grpc.framesize to zeppelin.ipython.grpc.message_size (cherry picked from commit 0660164379c12aa48321d0772598f61a56904a17) Signed-off-by: Jeff Zhang --- .../java/org/apache/zeppelin/python/IPythonInterpreter.java | 4 ++-- python/src/main/resources/interpreter-setting.json | 6 +++--- .../org/apache/zeppelin/python/IPythonInterpreterTest.java | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java b/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java index 10bf530b4ae..5c5bfe39500 100644 --- a/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java +++ b/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java @@ -143,10 +143,10 @@ public void open() throws InterpreterException { int jvmGatewayPort = RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces(); LOGGER.info("Launching IPython Kernel at port: " + ipythonPort); LOGGER.info("Launching JVM Gateway at port: " + jvmGatewayPort); - int framesize = Integer.parseInt(getProperty("zeppelin.ipython.grpc.framesize", + int message_size = Integer.parseInt(getProperty("zeppelin.ipython.grpc.message_size", 32 * 1024 * 1024 + "")); ipythonClient = new IPythonClient(ManagedChannelBuilder.forAddress("127.0.0.1", ipythonPort) - .usePlaintext(true).maxInboundMessageSize(framesize)); + .usePlaintext(true).maxInboundMessageSize(message_size)); launchIPythonKernel(ipythonPort); setupJVMGateway(jvmGatewayPort); } catch (Exception e) { diff --git a/python/src/main/resources/interpreter-setting.json b/python/src/main/resources/interpreter-setting.json index 3257e58abfb..f36add32aa3 100644 --- a/python/src/main/resources/interpreter-setting.json +++ b/python/src/main/resources/interpreter-setting.json @@ -41,10 +41,10 @@ "description": "time out for ipython launch", "type": "number" }, - "zeppelin.ipython.grpc.framesize": { - "propertyName": "zeppelin.ipython.grpc.framesize", + "zeppelin.ipython.grpc.message_size": { + "propertyName": "zeppelin.ipython.grpc.message_size", "defaultValue": "33554432", - "description": "grpc framesize, default is 32M", + "description": "grpc message size, default is 32M", "type": "number" } }, diff --git a/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java b/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java index dfc8c36b74a..480cae311cd 100644 --- a/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java @@ -78,7 +78,7 @@ public void testIPython() throws IOException, InterruptedException, InterpreterE @Test public void testGrpcFrameSize() throws InterpreterException, IOException { Properties properties = new Properties(); - properties.setProperty("zeppelin.ipython.grpc.framesize", "4"); + properties.setProperty("zeppelin.ipython.grpc.message_size", "4"); startInterpreter(properties); // to make this test can run under both python2 and python3 @@ -99,7 +99,7 @@ public void testGrpcFrameSize() throws InterpreterException, IOException { close(); // increase framesize to make it work - properties.setProperty("zeppelin.ipython.grpc.framesize", "40"); + properties.setProperty("zeppelin.ipython.grpc.message_size", "40"); startInterpreter(properties); // to make this test can run under both python2 and python3 result = interpreter.interpret("from __future__ import print_function", getInterpreterContext()); From 7b3011d317d97617d97d84adbb66547aa8da6757 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Fri, 23 Feb 2018 10:43:12 +0530 Subject: [PATCH 197/492] [ZEPPELIN-3249] Add support for streaming table ### What is this PR for? Since Zeppelin support streaming from various backends, I think it will be useful if even tables (and later graphs) can also be streamed. ### What type of PR is it? [Improvement | Feature] ### Todos * [x] - At times it fails with `Uncaught TypeError: Cannot read property 'p20180220_113300_1663645286_0_table_gridApi' of undefined`, have to fix this error. ### What is the Jira issue? * [ZEPPELIN-3249](https://issues.apache.org/jira/browse/ZEPPELIN-3249) ### How should this be tested? I have done it using shell interpreter, but this should work for all other backends as well ``` %sh echo "%table" echo "Col1 Col2" echo "1 2" sleep 1 echo "3 4" echo "5 6" sleep 2 echo "7 8" sleep 3 echo "9 10" echo "11 12" sleep 4 echo "12 13" ``` ### Screenshots (if appropriate) ![zeppelin-3249](https://user-images.githubusercontent.com/674497/36419469-fa4c26e0-1657-11e8-8a8a-717b29d91771.gif) ### Questions: * Does the licenses files need update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: Prabhjyot Singh Closes #2809 from prabhjyotsingh/ZEPPELIN-3249 and squashes the following commits: 463599d [Prabhjyot Singh] Merge remote-tracking branch 'origin/master' into ZEPPELIN-3249 568f334 [Prabhjyot Singh] fallback option for persistedTableOption don't commit viz if paragraph is in running or pending state e6bce80 [Prabhjyot Singh] fix undefined 6ce5d18 [Prabhjyot Singh] concat not required all time 21564fc [Prabhjyot Singh] Add support for streaming table (cherry picked from commit 720537aab99229309fb7b00c3f6006f0fea4a779) Signed-off-by: Prabhjyot Singh --- .../InterpreterResultMessageOutput.java | 2 +- .../src/app/notebook/notebook.controller.js | 23 ++-- .../paragraph/result/result.controller.js | 100 +++++++++++++----- .../builtins/visualization-table.js | 40 ++++++- .../builtins/visualization-util.js | 11 +- 5 files changed, 131 insertions(+), 45 deletions(-) diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterResultMessageOutput.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterResultMessageOutput.java index 41e1fd0e184..578bf2c940d 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterResultMessageOutput.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterResultMessageOutput.java @@ -227,7 +227,7 @@ public void flush() throws IOException { } public boolean isAppendSupported() { - return type == InterpreterResult.Type.TEXT; + return type == InterpreterResult.Type.TEXT || type == InterpreterResult.Type.TABLE; } private void copyStream(InputStream in, OutputStream out) throws IOException { diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index 05ab9fb7992..4c9de9cac70 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -278,16 +278,19 @@ function NotebookCtrl($scope, $route, $routeParams, $location, $rootScope, $scope.$on('listRevisionHistory', function(event, data) { console.debug('received list of revisions %o', data); $scope.noteRevisions = data.revisionList; - if ($scope.noteRevisions.length === 0 || $scope.noteRevisions[0].id !== 'Head') { - $scope.noteRevisions.splice(0, 0, { - id: 'Head', - message: 'Head', - }); - } - if ($routeParams.revisionId) { - let index = _.findIndex($scope.noteRevisions, {'id': $routeParams.revisionId}); - if (index > -1) { - $scope.currentRevision = $scope.noteRevisions[index].message; + if ($scope.noteRevisions) { + if ($scope.noteRevisions.length === 0 || $scope.noteRevisions[0].id !== 'Head') { + $scope.noteRevisions.splice(0, 0, { + id: 'Head', + message: 'Head', + }); + } + if ($routeParams.revisionId) { + let index = _.findIndex($scope.noteRevisions, + {'id': $routeParams.revisionId}); + if (index > -1) { + $scope.currentRevision = $scope.noteRevisions[index].message; + } } } }); diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js index 5bf77dcd71e..29465e5bd5c 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -243,10 +243,22 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location if (paragraph.id === data.paragraphId && resultIndex === data.index && (paragraph.status === ParagraphStatus.PENDING || paragraph.status === ParagraphStatus.RUNNING)) { - if (DefaultDisplayType.TEXT !== $scope.type) { + // Check if result type is eiter TEXT or TABLE, if not then treat it like TEXT + if ([DefaultDisplayType.TEXT, DefaultDisplayType.TABLE].indexOf($scope.type) < 0) { $scope.type = DefaultDisplayType.TEXT; } - appendTextOutput(data.data); + if ($scope.type === DefaultDisplayType.TEXT) { + appendTextOutput(data.data); + } else if ($scope.type === DefaultDisplayType.TABLE) { + appendTableOutput(data); + } + } + if (paragraph.id === data.paragraphId && + resultIndex === data.index && + paragraph.status === ParagraphStatus.FINISHED) { + if ($scope.type === DefaultDisplayType.TABLE) { + appendTableOutput(data); + } } }); @@ -531,6 +543,39 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } }; + function appendTableOutput(data) { + if (!$scope.$parent.result.data) { + $scope.$parent.result.data = []; + tableData = undefined; + } + if (!$scope.$parent.result.data[data.index]) { + $scope.$parent.result.data[data.index] = ''; + } + if (!tableData) { + $scope.$parent.result.data[data.index] = $scope.$parent.result.data[data.index].concat(data.data); + $rootScope.$broadcast( + 'updateResult', + {'data': $scope.$parent.result.data[data.index], 'type': 'TABLE'}, + undefined, + paragraph, + data.index); + let elemId = `p${$scope.id}_table`; + renderGraph(elemId, 'table', true); + } else { + let textRows = data.data.split('\n'); + for (let i = 0; i < textRows.length; i++) { + if (textRows[i] !== '') { + let row = textRows[i].split('\t'); + tableData.rows.push(row); + let builtInViz = builtInVisualizations['table']; + if (builtInViz.instance !== undefined) { + builtInViz.instance.append([row], tableData.columns); + } + } + } + } + } + function appendTextOutput(data) { const elemId = getTextResultElemId($scope.id); textResultQueueForAppend.push(data); @@ -744,33 +789,32 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location }; const commitVizConfigChange = function(config, vizId) { - let newConfig = angular.copy($scope.config); - if (!newConfig.graph) { - newConfig.graph = {}; - } - - // copy setting for vizId - if (!newConfig.graph.setting) { - newConfig.graph.setting = {}; - } - newConfig.graph.setting[vizId] = angular.copy(config); - - // copy common setting - if (newConfig.graph.setting[vizId]) { - newConfig.graph.commonSetting = newConfig.graph.setting[vizId].common; - delete newConfig.graph.setting[vizId].common; - } - - // copy pivot setting - if (newConfig.graph.commonSetting && newConfig.graph.commonSetting.pivot) { - newConfig.graph.keys = newConfig.graph.commonSetting.pivot.keys; - newConfig.graph.groups = newConfig.graph.commonSetting.pivot.groups; - newConfig.graph.values = newConfig.graph.commonSetting.pivot.values; - delete newConfig.graph.commonSetting.pivot; + if ([ParagraphStatus.RUNNING, ParagraphStatus.PENDING].indexOf(paragraph.status) < 0) { + let newConfig = angular.copy($scope.config); + if (!newConfig.graph) { + newConfig.graph = {}; + } + // copy setting for vizId + if (!newConfig.graph.setting) { + newConfig.graph.setting = {}; + } + newConfig.graph.setting[vizId] = angular.copy(config); + // copy common setting + if (newConfig.graph.setting[vizId]) { + newConfig.graph.commonSetting = newConfig.graph.setting[vizId].common; + delete newConfig.graph.setting[vizId].common; + } + // copy pivot setting + if (newConfig.graph.commonSetting && newConfig.graph.commonSetting.pivot) { + newConfig.graph.keys = newConfig.graph.commonSetting.pivot.keys; + newConfig.graph.groups = newConfig.graph.commonSetting.pivot.groups; + newConfig.graph.values = newConfig.graph.commonSetting.pivot.values; + delete newConfig.graph.commonSetting.pivot; + } + console.debug('committVizConfig', newConfig); + let newParams = angular.copy(paragraph.settings.params); + commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams); } - console.debug('committVizConfig', newConfig); - let newParams = angular.copy(paragraph.settings.params); - commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams); }; $scope.$on('paragraphResized', function(event, paragraphId) { diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-table.js b/zeppelin-web/src/app/visualization/builtins/visualization-table.js index d77efbc805a..723bb3aca13 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-table.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-table.js @@ -16,12 +16,19 @@ import Visualization from '../visualization'; import PassthroughTransformation from '../../tabledata/passthrough'; import { - Widget, ValueType, - isInputWidget, isOptionWidget, isCheckboxWidget, - isTextareaWidget, isBtnGroupWidget, - initializeTableConfig, resetTableOptionConfig, - DefaultTableColumnType, TableColumnType, updateColumnTypeState, + DefaultTableColumnType, + initializeTableConfig, + isBtnGroupWidget, + isCheckboxWidget, + isInputWidget, + isOptionWidget, + isTextareaWidget, parseTableOption, + resetTableOptionConfig, + TableColumnType, + updateColumnTypeState, + ValueType, + Widget, } from './visualization-util'; const SETTING_TEMPLATE = require('./visualization-table-setting.html'); @@ -247,6 +254,29 @@ export default class TableVisualization extends Visualization { gridOptions.enableSelectionBatchEvent = false; } + append(row, columns) { + const gridOptions = this.getGridOptions(); + this.setDynamicGridOptions(gridOptions, this.config); + // this.refreshGrid() + const gridElemId = this.getGridElemId(); + const gridElem = angular.element(`#${gridElemId}`); + + if (gridElem) { + const scope = this.getScope(); + + const columnNames = columns.map((c) => c.name); + let gridData = row.map((r) => { + return columnNames.reduce((acc, colName, index) => { + acc[colName] = r[index]; + return acc; + }, {}); + }); + gridData.map((data) => { + scope[gridElemId].data.push(data); + }); + } + } + render(tableData) { const gridElemId = this.getGridElemId(); let gridElem = document.getElementById(gridElemId); diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-util.js b/zeppelin-web/src/app/visualization/builtins/visualization-util.js index a82a18ecceb..7feb129bc0e 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-util.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-util.js @@ -100,7 +100,16 @@ export function initializeTableConfig(config, tableOptionSpecs) { export function parseTableOption(specs, persistedTableOption) { /** copy original params */ - const parsed = JSON.parse(JSON.stringify(persistedTableOption)); + let parsed; + try { + parsed = JSON.parse(JSON.stringify(persistedTableOption)); + } catch (e) { + // if not able to parse fall back to default values coming from specs + parsed = {}; + for (let spec of specs) { + parsed[spec['name']] = spec['defaultValue']; + } + } for (let i = 0; i < specs.length; i++) { const s = specs[i]; From 0f05288ad157176e345da958c79e65c51aee27d7 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Mon, 26 Feb 2018 10:18:31 +0800 Subject: [PATCH 198/492] ZEPPELIN-3246. Need option for automatically restart the livy interpreter automatically as zeppelin does not start new Livy session if yarn livy session application is killed ### What is this PR for? Add one new property `zeppelin.livy.restart_dead_session` to allow livy session to be created automatically. By default it is false, because there's many reason that session is dead, it is encouraged to ask user to check why it is dead and restart interpreter by themselves. ### What type of PR is it? [Feature] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3246 ### How should this be tested? * Manually tested. ### Screenshots (if appropriate) ![image](https://user-images.githubusercontent.com/164491/36651529-4a22ab36-1ae4-11e8-88dc-23a814e4c1b1.png) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2818 from zjffdu/ZPPELIN-3246 and squashes the following commits: 89d44f6 [Jeff Zhang] ZEPPELIN-3246. Need option for automatically restart the livy interpreter automatically as zeppelin does not start new Livy session if yarn livy session application is killed (cherry picked from commit 75ada89f4d36a16713f18c6f6fc54dab4f659ef5) Signed-off-by: Jeff Zhang --- .../zeppelin/livy/BaseLivyInterpreter.java | 64 +++++++++++++------ .../apache/zeppelin/livy/LivyException.java | 4 +- .../livy/LivyPySparkBaseInterpreter.java | 4 +- .../zeppelin/livy/LivySharedInterpreter.java | 2 +- .../zeppelin/livy/LivySparkInterpreter.java | 6 +- .../zeppelin/livy/SessionDeadException.java | 22 +++++++ .../main/resources/interpreter-setting.json | 6 ++ .../interpreter/InterpreterException.java | 9 ++- 8 files changed, 90 insertions(+), 27 deletions(-) create mode 100644 livy/src/main/java/org/apache/zeppelin/livy/SessionDeadException.java diff --git a/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java b/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java index 724a4b36c7c..5fe7ce40c9b 100644 --- a/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java +++ b/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java @@ -93,6 +93,7 @@ public abstract class BaseLivyInterpreter extends Interpreter { private int sessionCreationTimeout; private int pullStatusInterval; protected boolean displayAppInfo; + private boolean restartDeadSession; protected LivyVersion livyVersion; private RestTemplate restTemplate; private Map customHeaders = new HashMap<>(); @@ -110,6 +111,8 @@ public BaseLivyInterpreter(Properties property) { this.livyURL = property.getProperty("zeppelin.livy.url"); this.displayAppInfo = Boolean.parseBoolean( property.getProperty("zeppelin.livy.displayAppInfo", "true")); + this.restartDeadSession = Boolean.parseBoolean( + property.getProperty("zeppelin.livy.restart_dead_session", "false")); this.sessionCreationTimeout = Integer.parseInt( property.getProperty("zeppelin.livy.session.create_timeout", 120 + "")); this.pullStatusInterval = Integer.parseInt( @@ -159,7 +162,7 @@ public void open() throws InterpreterException { } catch (LivyException e) { String msg = "Fail to create session, please check livy interpreter log and " + "livy server log"; - throw new RuntimeException(msg, e); + throw new InterpreterException(msg, e); } } @@ -246,7 +249,7 @@ public InterpreterResult interpret(String st, InterpreterContext context) { } try { - return interpret(st, null, context.getParagraphId(), this.displayAppInfo, true); + return interpret(st, null, context.getParagraphId(), this.displayAppInfo, true, true); } catch (LivyException e) { LOGGER.error("Fail to interpret:" + st, e); return new InterpreterResult(InterpreterResult.Code.ERROR, @@ -359,18 +362,21 @@ private SessionInfo getSessionInfo(int sessionId) throws LivyException { public InterpreterResult interpret(String code, String paragraphId, boolean displayAppInfo, - boolean appendSessionExpired) throws LivyException { + boolean appendSessionExpired, + boolean appendSessionDead) throws LivyException { return interpret(code, sharedInterpreter.isSupported() ? getSessionKind() : null, - paragraphId, displayAppInfo, appendSessionExpired); + paragraphId, displayAppInfo, appendSessionExpired, appendSessionDead); } public InterpreterResult interpret(String code, String codeType, String paragraphId, boolean displayAppInfo, - boolean appendSessionExpired) throws LivyException { + boolean appendSessionExpired, + boolean appendSessionDead) throws LivyException { StatementInfo stmtInfo = null; boolean sessionExpired = false; + boolean sessionDead = false; try { try { stmtInfo = executeStatement(new ExecuteRequest(code, codeType)); @@ -386,6 +392,21 @@ public InterpreterResult interpret(String code, } } stmtInfo = executeStatement(new ExecuteRequest(code, codeType)); + } catch (SessionDeadException e) { + sessionDead = true; + if (restartDeadSession) { + LOGGER.warn("Livy session {} is dead, new session will be created.", sessionInfo.id); + close(); + try { + open(); + } catch (InterpreterException ie) { + throw new LivyException("Fail to restart livy session", ie); + } + stmtInfo = executeStatement(new ExecuteRequest(code, codeType)); + } else { + throw new LivyException("%html Livy session is dead somehow, " + + "please check log to see why it is dead, and then restart livy interpreter"); + } } // pull the statement status @@ -405,9 +426,9 @@ public InterpreterResult interpret(String code, paragraphId2StmtProgressMap.put(paragraphId, (int) (stmtInfo.progress * 100)); } } - if (appendSessionExpired) { - return appendSessionExpire(getResultFromStatementInfo(stmtInfo, displayAppInfo), - sessionExpired); + if (appendSessionExpired || appendSessionDead) { + return appendSessionExpireDead(getResultFromStatementInfo(stmtInfo, displayAppInfo), + sessionExpired, sessionDead); } else { return getResultFromStatementInfo(stmtInfo, displayAppInfo); } @@ -451,21 +472,27 @@ private boolean isSessionExpired() throws LivyException { } } - private InterpreterResult appendSessionExpire(InterpreterResult result, boolean sessionExpired) { + private InterpreterResult appendSessionExpireDead(InterpreterResult result, + boolean sessionExpired, + boolean sessionDead) { + InterpreterResult result2 = new InterpreterResult(result.code()); if (sessionExpired) { - InterpreterResult result2 = new InterpreterResult(result.code()); result2.add(InterpreterResult.Type.HTML, "Previous livy session is expired, new livy session is created. " + "Paragraphs that depend on this paragraph need to be re-executed!"); - for (InterpreterResultMessage message : result.message()) { - result2.add(message.getType(), message.getData()); - } - return result2; - } else { - return result; + + } + if (sessionDead) { + result2.add(InterpreterResult.Type.HTML, + "Previous livy session is dead, new livy session is created. " + + "Paragraphs that depend on this paragraph need to be re-executed!"); } - } + for (InterpreterResultMessage message : result.message()) { + result2.add(message.getType(), message.getData()); + } + return result2; + } private InterpreterResult getResultFromStatementInfo(StatementInfo stmtInfo, boolean displayAppInfo) { @@ -684,8 +711,7 @@ private String callRestAPI(String targetURL, String method, String jsonData) HttpServerErrorException errorException = (HttpServerErrorException) e; String errorResponse = errorException.getResponseBodyAsString(); if (errorResponse.contains("Session is in state dead")) { - throw new LivyException("%html Livy session is dead somehow, " + - "please check log to see why it is dead, and then restart livy interpreter"); + throw new SessionDeadException(); } throw new LivyException(errorResponse, e); } diff --git a/livy/src/main/java/org/apache/zeppelin/livy/LivyException.java b/livy/src/main/java/org/apache/zeppelin/livy/LivyException.java index 5adffd4dc1a..e126a0f0bb4 100644 --- a/livy/src/main/java/org/apache/zeppelin/livy/LivyException.java +++ b/livy/src/main/java/org/apache/zeppelin/livy/LivyException.java @@ -17,10 +17,12 @@ package org.apache.zeppelin.livy; +import org.apache.zeppelin.interpreter.InterpreterException; + /** * Livy api related exception */ -public class LivyException extends Exception { +public class LivyException extends InterpreterException { public LivyException() { } diff --git a/livy/src/main/java/org/apache/zeppelin/livy/LivyPySparkBaseInterpreter.java b/livy/src/main/java/org/apache/zeppelin/livy/LivyPySparkBaseInterpreter.java index 17b20e3634f..6d399814a2e 100644 --- a/livy/src/main/java/org/apache/zeppelin/livy/LivyPySparkBaseInterpreter.java +++ b/livy/src/main/java/org/apache/zeppelin/livy/LivyPySparkBaseInterpreter.java @@ -32,7 +32,7 @@ public LivyPySparkBaseInterpreter(Properties property) { @Override protected String extractAppId() throws LivyException { return extractStatementResult( - interpret("sc.applicationId", null, false, false).message() + interpret("sc.applicationId", null, false, false, false).message() .get(0).getData()); } @@ -40,7 +40,7 @@ protected String extractAppId() throws LivyException { protected String extractWebUIAddress() throws LivyException { return extractStatementResult( interpret( - "sc._jsc.sc().ui().get().appUIAddress()", null, false, false) + "sc._jsc.sc().ui().get().appUIAddress()", null, false, false, false) .message().get(0).getData()); } diff --git a/livy/src/main/java/org/apache/zeppelin/livy/LivySharedInterpreter.java b/livy/src/main/java/org/apache/zeppelin/livy/LivySharedInterpreter.java index 77e288bfa65..cef08582a40 100644 --- a/livy/src/main/java/org/apache/zeppelin/livy/LivySharedInterpreter.java +++ b/livy/src/main/java/org/apache/zeppelin/livy/LivySharedInterpreter.java @@ -78,7 +78,7 @@ public InterpreterResult interpret(String st, String codeType, InterpreterContex } try { - return interpret(st, codeType, context.getParagraphId(), this.displayAppInfo, true); + return interpret(st, codeType, context.getParagraphId(), this.displayAppInfo, true, true); } catch (LivyException e) { LOGGER.error("Fail to interpret:" + st, e); return new InterpreterResult(InterpreterResult.Code.ERROR, diff --git a/livy/src/main/java/org/apache/zeppelin/livy/LivySparkInterpreter.java b/livy/src/main/java/org/apache/zeppelin/livy/LivySparkInterpreter.java index 066d0da8cc9..ad62e9b5de5 100644 --- a/livy/src/main/java/org/apache/zeppelin/livy/LivySparkInterpreter.java +++ b/livy/src/main/java/org/apache/zeppelin/livy/LivySparkInterpreter.java @@ -36,7 +36,7 @@ public String getSessionKind() { @Override protected String extractAppId() throws LivyException { return extractStatementResult( - interpret("sc.applicationId", null, false, false).message() + interpret("sc.applicationId", null, false, false, false).message() .get(0).getData()); } @@ -45,10 +45,10 @@ protected String extractWebUIAddress() throws LivyException { interpret( "val webui=sc.getClass.getMethod(\"ui\").invoke(sc).asInstanceOf[Some[_]].get", null, - null, false, false); + null, false, false, false); return extractStatementResult( interpret( - "webui.getClass.getMethod(\"appUIAddress\").invoke(webui)", null, false, false) + "webui.getClass.getMethod(\"appUIAddress\").invoke(webui)", null, false, false, false) .message().get(0).getData()); } diff --git a/livy/src/main/java/org/apache/zeppelin/livy/SessionDeadException.java b/livy/src/main/java/org/apache/zeppelin/livy/SessionDeadException.java new file mode 100644 index 00000000000..58117907628 --- /dev/null +++ b/livy/src/main/java/org/apache/zeppelin/livy/SessionDeadException.java @@ -0,0 +1,22 @@ +/* + * 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.livy; + +public class SessionDeadException extends LivyException { + +} diff --git a/livy/src/main/resources/interpreter-setting.json b/livy/src/main/resources/interpreter-setting.json index cecacac2e48..d096c467ea8 100644 --- a/livy/src/main/resources/interpreter-setting.json +++ b/livy/src/main/resources/interpreter-setting.json @@ -108,6 +108,12 @@ "defaultValue": "true", "description": "Whether display app info", "type": "checkbox" + }, + "zeppelin.livy.restart_dead_session": { + "propertyName": "zeppelin.livy.restart_dead_session", + "defaultValue": "false", + "description": "Whether restart a dead session", + "type": "checkbox" } }, "option": { diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterException.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterException.java index 8b8a2297658..1ce63f3b78f 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterException.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterException.java @@ -19,11 +19,14 @@ /** - * Runtime Exception for interpreters. + * General Exception for interpreters. * */ public class InterpreterException extends Exception { + public InterpreterException() { + } + public InterpreterException(Throwable e) { super(e); } @@ -36,4 +39,8 @@ public InterpreterException(String msg, Throwable t) { super(msg, t); } + public InterpreterException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } } From 7ddfaed10b593aa4356f05643425e0c38cb49f0f Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Mon, 26 Feb 2018 16:58:53 +0800 Subject: [PATCH 199/492] ZEPPELIN-3265. DevInterpreter doesn't work ### What is this PR for? This PR is trying the fix the bug that DevInterpreter doesn't work due the interpreter code refactoring. ### What type of PR is it? [Bug Fix ] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3265 ### How should this be tested? * Manually verify the zeppelin clock example. ### Screenshots (if appropriate) ![jietu20180226-210416-hd](https://user-images.githubusercontent.com/164491/36671998-ae453166-1b38-11e8-92da-3c812061251a.gif) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2819 from zjffdu/ZEPPELIN-3265 and squashes the following commits: 0e85992 [Jeff Zhang] ZEPPELIN-3265. DevInterpreter doesn't work (cherry picked from commit fc44693fe6589d9fbcb8a63a4664369bb7e2a2a3) Signed-off-by: Jeff Zhang --- helium-dev/pom.xml | 2 +- .../interpreter/InterpreterSettingManager.java | 3 ++- .../interpreter/ManagedInterpreterGroup.java | 18 +++++++----------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index 8514946d76b..395f8ffac4f 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -34,7 +34,7 @@ Zeppelin: Helium development interpreter - helium-dev + dev diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java index 04d409289a3..0601c6ff5b4 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java @@ -403,13 +403,14 @@ private void registerInterpreterSetting(List registeredIn .setIntepreterSettingManager(this) .create(); - LOGGER.info("Register InterpreterSettingTemplate & InterpreterSetting: {}", + LOGGER.info("Register InterpreterSettingTemplate & Create InterpreterSetting: {}", interpreterSettingTemplate.getName()); interpreterSettingTemplates.put(interpreterSettingTemplate.getName(), interpreterSettingTemplate); InterpreterSetting interpreterSetting = new InterpreterSetting(interpreterSettingTemplate); initInterpreterSetting(interpreterSetting); + interpreterSettings.put(interpreterSetting.getName(), interpreterSetting); } @VisibleForTesting diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java index d21a34d57be..e19c9caeaa0 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java @@ -62,17 +62,13 @@ public synchronized RemoteInterpreterProcess getOrCreateInterpreterProcess(Strin LOGGER.info("Create InterpreterProcess for InterpreterGroup: " + getId()); remoteInterpreterProcess = interpreterSetting.createInterpreterProcess(id, userName, properties); - synchronized (remoteInterpreterProcess) { - if (!remoteInterpreterProcess.isRunning()) { - remoteInterpreterProcess.start(userName); - remoteInterpreterProcess.getRemoteInterpreterEventPoller() - .setInterpreterProcess(remoteInterpreterProcess); - remoteInterpreterProcess.getRemoteInterpreterEventPoller().setInterpreterGroup(this); - remoteInterpreterProcess.getRemoteInterpreterEventPoller().start(); - getInterpreterSetting().getRecoveryStorage() - .onInterpreterClientStart(remoteInterpreterProcess); - } - } + remoteInterpreterProcess.start(userName); + remoteInterpreterProcess.getRemoteInterpreterEventPoller() + .setInterpreterProcess(remoteInterpreterProcess); + remoteInterpreterProcess.getRemoteInterpreterEventPoller().setInterpreterGroup(this); + remoteInterpreterProcess.getRemoteInterpreterEventPoller().start(); + getInterpreterSetting().getRecoveryStorage() + .onInterpreterClientStart(remoteInterpreterProcess); } return remoteInterpreterProcess; } From fd96855d923daa9cc323979ad8178e514ebba09f Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Sun, 25 Feb 2018 21:24:08 +0800 Subject: [PATCH 200/492] ZEPPELIN-3255. Can not run spark1 and spark2 in one zeppelin instance ### What is this PR for? Although #2750 enable the support of spark 2.3, it breaks the support of spark 1.6. Users have to build zeppelin against spark 1.6 to make zeppelin work with that. But previous one zeppelin instance can work with multiple versions of spark. This PR introduce spark shims module which is to resolve the api incompatible issue between different versions of spark, so that one zeppelin instance can work with multiple versions of spark. ### What type of PR is it? [ Improvement | Refactoring] ### Todos * https://issues.apache.org/jira/browse/ZEPPELIN-3254 Although zeppelin should support to run multiple versions of spark in one instance, but our travis test doesn't cover it, ZEPPELIN-3254 would do that. ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3255 ### How should this be tested? * CI pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2814 from zjffdu/ZEPPELIN-3255 and squashes the following commits: 68fa437 [Jeff Zhang] Remove akka from spark-dependencies 14fef45 [Jeff Zhang] ZEPPELIN-3255. Can not run spark1 and spark2 in one zeppelin instance (cherry picked from commit 64bbba4796fe1ddfd1ca1facde7dcda33ac86ef7) Signed-off-by: Jeff Zhang --- spark/interpreter/figure/null-1.png | Bin 13599 -> 0 bytes spark/interpreter/pom.xml | 12 + .../zeppelin/spark/NewSparkInterpreter.java | 67 +---- .../zeppelin/spark/OldSparkInterpreter.java | 235 +----------------- .../spark/OldSparkInterpreterTest.java | 10 +- spark/pom.xml | 41 +-- spark/spark-dependencies/pom.xml | 41 +-- spark/spark-shims/pom.xml | 70 ++++++ .../org/apache/zeppelin/spark/SparkShims.java | 110 ++++++++ spark/spark1-shims/pom.xml | 89 +++++++ .../apache/zeppelin/spark/Spark1Shims.java | 57 +++++ spark/spark2-shims/pom.xml | 88 +++++++ .../apache/zeppelin/spark/Spark2Shims.java | 36 +++ 13 files changed, 489 insertions(+), 367 deletions(-) delete mode 100644 spark/interpreter/figure/null-1.png create mode 100644 spark/spark-shims/pom.xml create mode 100644 spark/spark-shims/src/main/scala/org/apache/zeppelin/spark/SparkShims.java create mode 100644 spark/spark1-shims/pom.xml create mode 100644 spark/spark1-shims/src/main/scala/org/apache/zeppelin/spark/Spark1Shims.java create mode 100644 spark/spark2-shims/pom.xml create mode 100644 spark/spark2-shims/src/main/scala/org/apache/zeppelin/spark/Spark2Shims.java diff --git a/spark/interpreter/figure/null-1.png b/spark/interpreter/figure/null-1.png deleted file mode 100644 index 8b1ce07ea9e7d0f24bae214f3bda98a7787ee662..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13599 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 diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 5e24933bdf2..1653cd02896 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -81,6 +81,18 @@ ${project.version} + + org.apache.zeppelin + spark1-shims + ${project.version} + + + + org.apache.zeppelin + spark2-shims + ${project.version} + + org.apache.zeppelin zeppelin-python diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java index 1d3ccd65fde..c8efa7a7d9f 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java @@ -69,6 +69,7 @@ public class NewSparkInterpreter extends AbstractSparkInterpreter { private SparkVersion sparkVersion; private boolean enableSupportedVersionCheck; private String sparkUrl; + private SparkShims sparkShims; private static InterpreterHookRegistry hooks; @@ -117,7 +118,8 @@ public void open() throws InterpreterException { sqlContext = this.innerInterpreter.sqlContext(); sparkSession = this.innerInterpreter.sparkSession(); sparkUrl = this.innerInterpreter.sparkUrl(); - setupListeners(); + sparkShims = SparkShims.getInstance(sc.version()); + sparkShims.setupSparkListener(sparkUrl); hooks = getInterpreterGroup().getInterpreterHookRegistry(); z = new SparkZeppelinContext(sc, hooks, @@ -125,7 +127,7 @@ public void open() throws InterpreterException { this.innerInterpreter.bind("z", z.getClass().getCanonicalName(), z, Lists.newArrayList("@transient")); } catch (Exception e) { - LOGGER.error(ExceptionUtils.getStackTrace(e)); + LOGGER.error("Fail to open SparkInterpreter", ExceptionUtils.getStackTrace(e)); throw new InterpreterException("Fail to open SparkInterpreter", e); } } @@ -213,67 +215,6 @@ public int getProgress(InterpreterContext context) { return innerInterpreter.getProgress(Utils.buildJobGroupId(context), context); } - private void setupListeners() { - JobProgressListener pl = new JobProgressListener(sc.getConf()) { - @Override - public synchronized void onJobStart(SparkListenerJobStart jobStart) { - super.onJobStart(jobStart); - int jobId = jobStart.jobId(); - String jobGroupId = jobStart.properties().getProperty("spark.jobGroup.id"); - String uiEnabled = jobStart.properties().getProperty("spark.ui.enabled"); - String jobUrl = getJobUrl(jobId); - String noteId = Utils.getNoteId(jobGroupId); - String paragraphId = Utils.getParagraphId(jobGroupId); - // Button visible if Spark UI property not set, set as invalid boolean or true - java.lang.Boolean showSparkUI = - uiEnabled == null || !uiEnabled.trim().toLowerCase().equals("false"); - if (showSparkUI && jobUrl != null) { - RemoteEventClientWrapper eventClient = BaseZeppelinContext.getEventClient(); - Map infos = new java.util.HashMap<>(); - infos.put("jobUrl", jobUrl); - infos.put("label", "SPARK JOB"); - infos.put("tooltip", "View in Spark web UI"); - if (eventClient != null) { - eventClient.onParaInfosReceived(noteId, paragraphId, infos); - } - } - } - - private String getJobUrl(int jobId) { - String jobUrl = null; - if (sparkUrl != null) { - jobUrl = sparkUrl + "/jobs/job?id=" + jobId; - } - return jobUrl; - } - }; - try { - Object listenerBus = sc.getClass().getMethod("listenerBus").invoke(sc); - Method[] methods = listenerBus.getClass().getMethods(); - Method addListenerMethod = null; - for (Method m : methods) { - if (!m.getName().equals("addListener")) { - continue; - } - Class[] parameterTypes = m.getParameterTypes(); - if (parameterTypes.length != 1) { - continue; - } - if (!parameterTypes[0].isAssignableFrom(JobProgressListener.class)) { - continue; - } - addListenerMethod = m; - break; - } - if (addListenerMethod != null) { - addListenerMethod.invoke(listenerBus, pl); - } - } catch (NoSuchMethodException | SecurityException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException e) { - LOGGER.error(e.toString(), e); - } - } - public SparkZeppelinContext getZeppelinContext() { return this.z; } diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java index ff3a2caa565..1f59d18d339 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java @@ -151,6 +151,8 @@ public class OldSparkInterpreter extends AbstractSparkInterpreter { private JavaSparkContext jsc; private boolean enableSupportedVersionCheck; + private SparkShims sparkShims; + public OldSparkInterpreter(Properties property) { super(property); out = new InterpreterOutputStream(logger); @@ -158,10 +160,10 @@ public OldSparkInterpreter(Properties property) { public OldSparkInterpreter(Properties property, SparkContext sc) { this(property); - this.sc = sc; env = SparkEnv.get(); - sparkListener = setupListeners(this.sc); + sparkShims = SparkShims.getInstance(sc.version()); + sparkShims.setupSparkListener(sparkUrl); } public SparkContext getSparkContext() { @@ -169,7 +171,6 @@ public SparkContext getSparkContext() { if (sc == null) { sc = createSparkContext(); env = SparkEnv.get(); - sparkListener = setupListeners(sc); } return sc; } @@ -190,157 +191,6 @@ public boolean isSparkContextInitialized() { } } - static SparkListener setupListeners(SparkContext context) { - SparkListener pl = new SparkListener() { - @Override - public synchronized void onJobStart(SparkListenerJobStart jobStart) { - int jobId = jobStart.jobId(); - String jobGroupId = jobStart.properties().getProperty("spark.jobGroup.id"); - String uiEnabled = jobStart.properties().getProperty("spark.ui.enabled"); - String jobUrl = getJobUrl(jobId); - String noteId = Utils.getNoteId(jobGroupId); - String paragraphId = Utils.getParagraphId(jobGroupId); - // Button visible if Spark UI property not set, set as invalid boolean or true - java.lang.Boolean showSparkUI = - uiEnabled == null || !uiEnabled.trim().toLowerCase().equals("false"); - if (showSparkUI && jobUrl != null) { - RemoteEventClientWrapper eventClient = BaseZeppelinContext.getEventClient(); - Map infos = new java.util.HashMap<>(); - infos.put("jobUrl", jobUrl); - infos.put("label", "SPARK JOB"); - infos.put("tooltip", "View in Spark web UI"); - if (eventClient != null) { - eventClient.onParaInfosReceived(noteId, paragraphId, infos); - } - } - } - - private String getJobUrl(int jobId) { - String jobUrl = null; - if (sparkUrl != null) { - jobUrl = sparkUrl + "/jobs/job/?id=" + jobId; - } - return jobUrl; - } - - @Override - public void onBlockUpdated(SparkListenerBlockUpdated blockUpdated) { - - } - - @Override - public void onExecutorRemoved(SparkListenerExecutorRemoved executorRemoved) { - - } - - @Override - public void onExecutorAdded(SparkListenerExecutorAdded executorAdded) { - - } - - @Override - public void onExecutorMetricsUpdate( - SparkListenerExecutorMetricsUpdate executorMetricsUpdate) { - - } - - @Override - public void onApplicationEnd(SparkListenerApplicationEnd applicationEnd) { - - } - - @Override - public void onApplicationStart(SparkListenerApplicationStart applicationStart) { - - } - - @Override - public void onUnpersistRDD(SparkListenerUnpersistRDD unpersistRDD) { - - } - - @Override - public void onBlockManagerAdded(SparkListenerBlockManagerAdded blockManagerAdded) { - - } - - @Override - public void onBlockManagerRemoved(SparkListenerBlockManagerRemoved blockManagerRemoved) { - - } - - @Override - public void onEnvironmentUpdate(SparkListenerEnvironmentUpdate environmentUpdate) { - - } - - @Override - public void onJobEnd(SparkListenerJobEnd jobEnd) { - - } - - @Override - public void onStageCompleted(SparkListenerStageCompleted stageCompleted) { - - } - - @Override - public void onStageSubmitted(SparkListenerStageSubmitted stageSubmitted) { - - } - - @Override - public void onTaskEnd(SparkListenerTaskEnd taskEnd) { - - } - - @Override - public void onTaskGettingResult(SparkListenerTaskGettingResult taskGettingResult) { - - } - - @Override - public void onTaskStart(SparkListenerTaskStart taskStart) { - - } - }; - try { - Object listenerBus = context.getClass().getMethod("listenerBus").invoke(context); - - Method[] methods = listenerBus.getClass().getMethods(); - Method addListenerMethod = null; - for (Method m : methods) { - if (!m.getName().equals("addListener")) { - continue; - } - - Class[] parameterTypes = m.getParameterTypes(); - - if (parameterTypes.length != 1) { - continue; - } - - if (!parameterTypes[0].isAssignableFrom(SparkListener.class)) { - continue; - } - - addListenerMethod = m; - break; - } - - if (addListenerMethod != null) { - addListenerMethod.invoke(listenerBus, pl); - } else { - return null; - } - } catch (NoSuchMethodException | SecurityException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException e) { - logger.error(e.toString(), e); - return null; - } - return pl; - } - private boolean useHiveContext() { return java.lang.Boolean.parseBoolean(getProperty("zeppelin.spark.useHiveContext")); } @@ -1020,6 +870,10 @@ public void open() throws InterpreterException { } } + sparkUrl = getSparkUIUrl(); + sparkShims = SparkShims.getInstance(sc.version()); + sparkShims.setupSparkListener(sparkUrl); + numReferenceOfSparkContext.incrementAndGet(); } @@ -1373,75 +1227,6 @@ public int getProgress(InterpreterContext context) { return JobProgressUtil.progress(sc, jobGroup); } - private int[] getProgressFromStage_1_0x(SparkListener sparkListener, Object stage) - throws IllegalAccessException, IllegalArgumentException, - InvocationTargetException, NoSuchMethodException, SecurityException { - int numTasks = (int) stage.getClass().getMethod("numTasks").invoke(stage); - int completedTasks = 0; - - int id = (int) stage.getClass().getMethod("id").invoke(stage); - - Object completedTaskInfo = null; - - completedTaskInfo = JavaConversions.mapAsJavaMap( - (HashMap) sparkListener.getClass() - .getMethod("stageIdToTasksComplete").invoke(sparkListener)).get(id); - - if (completedTaskInfo != null) { - completedTasks += (int) completedTaskInfo; - } - List parents = JavaConversions.seqAsJavaList((Seq) stage.getClass() - .getMethod("parents").invoke(stage)); - if (parents != null) { - for (Object s : parents) { - int[] p = getProgressFromStage_1_0x(sparkListener, s); - numTasks += p[0]; - completedTasks += p[1]; - } - } - - return new int[] {numTasks, completedTasks}; - } - - private int[] getProgressFromStage_1_1x(SparkListener sparkListener, Object stage) - throws IllegalAccessException, IllegalArgumentException, - InvocationTargetException, NoSuchMethodException, SecurityException { - int numTasks = (int) stage.getClass().getMethod("numTasks").invoke(stage); - int completedTasks = 0; - int id = (int) stage.getClass().getMethod("id").invoke(stage); - - try { - Method stageIdToData = sparkListener.getClass().getMethod("stageIdToData"); - HashMap, Object> stageIdData = - (HashMap, Object>) stageIdToData.invoke(sparkListener); - Class stageUIDataClass = - this.getClass().forName("org.apache.spark.ui.jobs.UIData$StageUIData"); - - Method numCompletedTasks = stageUIDataClass.getMethod("numCompleteTasks"); - Set> keys = - JavaConverters.setAsJavaSetConverter(stageIdData.keySet()).asJava(); - for (Tuple2 k : keys) { - if (id == (int) k._1()) { - Object uiData = stageIdData.get(k).get(); - completedTasks += (int) numCompletedTasks.invoke(uiData); - } - } - } catch (Exception e) { - logger.error("Error on getting progress information", e); - } - - List parents = JavaConversions.seqAsJavaList((Seq) stage.getClass() - .getMethod("parents").invoke(stage)); - if (parents != null) { - for (Object s : parents) { - int[] p = getProgressFromStage_1_1x(sparkListener, s); - numTasks += p[0]; - completedTasks += p[1]; - } - } - return new int[] {numTasks, completedTasks}; - } - private Code getResultCode(scala.tools.nsc.interpreter.Results.Result r) { if (r instanceof scala.tools.nsc.interpreter.Results.Success$) { return Code.SUCCESS; @@ -1479,10 +1264,6 @@ public FormType getFormType() { return FormType.NATIVE; } - public SparkListener getJobProgressListener() { - return sparkListener; - } - @Override public Scheduler getScheduler() { return SchedulerFactory.singleton().createOrGetFIFOScheduler( diff --git a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/OldSparkInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/OldSparkInterpreterTest.java index 14214a284f2..068ff50c3d8 100644 --- a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/OldSparkInterpreterTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/OldSparkInterpreterTest.java @@ -192,13 +192,7 @@ public void testNextLineCompanionObject() throws InterpreterException { public void testEndWithComment() throws InterpreterException { assertEquals(InterpreterResult.Code.SUCCESS, repl.interpret("val c=1\n//comment", context).code()); } - - @Test - public void testListener() { - SparkContext sc = repl.getSparkContext(); - assertNotNull(OldSparkInterpreter.setupListeners(sc)); - } - + @Test public void testCreateDataFrame() throws InterpreterException { if (getSparkVersionNumber(repl) >= 13) { @@ -362,7 +356,7 @@ public void testParagraphUrls() throws InterpreterException { } String sparkUIUrl = repl.getSparkUIUrl(); assertNotNull(jobUrl); - assertTrue(jobUrl.startsWith(sparkUIUrl + "/jobs/job/?id=")); + assertTrue(jobUrl.startsWith(sparkUIUrl + "/jobs/job?id=")); } } diff --git a/spark/pom.xml b/spark/pom.xml index 215f4be5c84..186b0501187 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -63,21 +63,26 @@ scala-2.10 scala-2.11 spark-dependencies + spark-shims + spark1-shims + spark2-shims - org.apache.zeppelin - zeppelin-interpreter - ${project.version} + org.slf4j + slf4j-api - org.apache.zeppelin - zeppelin-display - ${project.version} - test + org.slf4j + slf4j-log4j12 + + + + log4j + log4j @@ -92,28 +97,6 @@ junit test - - - org.datanucleus - datanucleus-core - ${datanucleus.core.version} - test - - - - org.datanucleus - datanucleus-api-jdo - ${datanucleus.apijdo.version} - test - - - - org.datanucleus - datanucleus-rdbms - ${datanucleus.rdbms.version} - test - - diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index 1ebc81bafca..3b7048acbc8 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -294,46 +294,7 @@ hadoop-client ${hadoop.version} - - - - com.google.protobuf - protobuf-java - ${protobuf.version} - - - - ${akka.group} - akka-actor_${scala.binary.version} - ${akka.version} - - - ${akka.group} - akka-remote_${scala.binary.version} - ${akka.version} - - - ${akka.group} - akka-slf4j_${scala.binary.version} - ${akka.version} - - - ${akka.group} - akka-testkit_${scala.binary.version} - ${akka.version} - - - ${akka.group} - akka-zeromq_${scala.binary.version} - ${akka.version} - - - ${akka.group} - akka-actor_${scala.binary.version} - - - - + org.apache.spark diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml new file mode 100644 index 00000000000..619c7a42a86 --- /dev/null +++ b/spark/spark-shims/pom.xml @@ -0,0 +1,70 @@ + + + + + + + spark-parent + org.apache.zeppelin + 0.9.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.apache.zeppelin + spark-shims + 0.9.0-SNAPSHOT + jar + Zeppelin: Spark Shims + + + + org.apache.zeppelin + zeppelin-interpreter + ${project.version} + provided + + + + + + + maven-dependency-plugin + + true + + + + + maven-resources-plugin + + + copy-interpreter-setting + none + + true + + + + + + + + \ No newline at end of file diff --git a/spark/spark-shims/src/main/scala/org/apache/zeppelin/spark/SparkShims.java b/spark/spark-shims/src/main/scala/org/apache/zeppelin/spark/SparkShims.java new file mode 100644 index 00000000000..acf717c5ae3 --- /dev/null +++ b/spark/spark-shims/src/main/scala/org/apache/zeppelin/spark/SparkShims.java @@ -0,0 +1,110 @@ +/* + * 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.spark; + + +import org.apache.zeppelin.interpreter.BaseZeppelinContext; +import org.apache.zeppelin.interpreter.remote.RemoteEventClientWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Constructor; +import java.util.Map; +import java.util.Properties; + +/** + * This is abstract class for anything that is api incompatible between spark1 and spark2. + * It will load the correct version of SparkShims based on the version of Spark. + */ +public abstract class SparkShims { + + private static final Logger LOGGER = LoggerFactory.getLogger(SparkShims.class); + + private static SparkShims sparkShims; + + private static SparkShims loadShims(String sparkVersion) throws ReflectiveOperationException { + Class sparkShimsClass; + if ("2".equals(sparkVersion)) { + LOGGER.info("Initializing shims for Spark 2.x"); + sparkShimsClass = Class.forName("org.apache.zeppelin.spark.Spark2Shims"); + } else { + LOGGER.info("Initializing shims for Spark 1.x"); + sparkShimsClass = Class.forName("org.apache.zeppelin.spark.Spark1Shims"); + } + + Constructor c = sparkShimsClass.getConstructor(); + return (SparkShims) c.newInstance(); + } + + public static SparkShims getInstance(String sparkVersion) { + if (sparkShims == null) { + String sparkMajorVersion = getSparkMajorVersion(sparkVersion); + try { + sparkShims = loadShims(sparkMajorVersion); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + return sparkShims; + } + + private static String getSparkMajorVersion(String sparkVersion) { + return sparkVersion.startsWith("2") ? "2" : "1"; + } + + /** + * This is due to SparkListener api change between spark1 and spark2. + * SparkListener is trait in spark1 while it is abstract class in spark2. + */ + public abstract void setupSparkListener(String sparkWebUrl); + + + protected String getNoteId(String jobgroupId) { + int indexOf = jobgroupId.indexOf("-"); + int secondIndex = jobgroupId.indexOf("-", indexOf + 1); + return jobgroupId.substring(indexOf + 1, secondIndex); + } + + protected String getParagraphId(String jobgroupId) { + int indexOf = jobgroupId.indexOf("-"); + int secondIndex = jobgroupId.indexOf("-", indexOf + 1); + return jobgroupId.substring(secondIndex + 1, jobgroupId.length()); + } + + protected void buildSparkJobUrl(String sparkWebUrl, int jobId, Properties jobProperties) { + String jobGroupId = jobProperties.getProperty("spark.jobGroup.id"); + String uiEnabled = jobProperties.getProperty("spark.ui.enabled"); + String jobUrl = sparkWebUrl + "/jobs/job?id=" + jobId; + String noteId = getNoteId(jobGroupId); + String paragraphId = getParagraphId(jobGroupId); + // Button visible if Spark UI property not set, set as invalid boolean or true + boolean showSparkUI = + uiEnabled == null || !uiEnabled.trim().toLowerCase().equals("false"); + if (showSparkUI && jobUrl != null) { + RemoteEventClientWrapper eventClient = BaseZeppelinContext.getEventClient(); + Map infos = new java.util.HashMap(); + infos.put("jobUrl", jobUrl); + infos.put("label", "SPARK JOB"); + infos.put("tooltip", "View in Spark web UI"); + if (eventClient != null) { + eventClient.onParaInfosReceived(noteId, paragraphId, infos); + } + } + } +} diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml new file mode 100644 index 00000000000..93640c6ffe0 --- /dev/null +++ b/spark/spark1-shims/pom.xml @@ -0,0 +1,89 @@ + + + + + + + spark-parent + org.apache.zeppelin + 0.9.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.apache.zeppelin + spark1-shims + 0.9.0-SNAPSHOT + jar + Zeppelin: Spark1 Shims + + + 2.10 + 1.6.3 + + + + + + org.apache.zeppelin + spark-shims + ${project.version} + + + + org.apache.spark + spark-core_${scala.binary.version} + ${spark.version} + provided + + + + org.apache.zeppelin + zeppelin-interpreter + ${project.version} + provided + + + + + + + maven-dependency-plugin + + true + + + + + maven-resources-plugin + + + copy-interpreter-setting + none + + true + + + + + + + + \ No newline at end of file diff --git a/spark/spark1-shims/src/main/scala/org/apache/zeppelin/spark/Spark1Shims.java b/spark/spark1-shims/src/main/scala/org/apache/zeppelin/spark/Spark1Shims.java new file mode 100644 index 00000000000..9f233136799 --- /dev/null +++ b/spark/spark1-shims/src/main/scala/org/apache/zeppelin/spark/Spark1Shims.java @@ -0,0 +1,57 @@ +/* + * 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.spark; + +import org.apache.spark.SparkContext; +import org.apache.spark.scheduler.SparkListener; +import org.apache.spark.scheduler.SparkListenerApplicationEnd; +import org.apache.spark.scheduler.SparkListenerApplicationStart; +import org.apache.spark.scheduler.SparkListenerBlockManagerAdded; +import org.apache.spark.scheduler.SparkListenerBlockManagerRemoved; +import org.apache.spark.scheduler.SparkListenerBlockUpdated; +import org.apache.spark.scheduler.SparkListenerEnvironmentUpdate; +import org.apache.spark.scheduler.SparkListenerExecutorAdded; +import org.apache.spark.scheduler.SparkListenerExecutorMetricsUpdate; +import org.apache.spark.scheduler.SparkListenerExecutorRemoved; +import org.apache.spark.scheduler.SparkListenerJobEnd; +import org.apache.spark.scheduler.SparkListenerJobStart; +import org.apache.spark.scheduler.SparkListenerStageCompleted; +import org.apache.spark.scheduler.SparkListenerStageSubmitted; +import org.apache.spark.scheduler.SparkListenerTaskEnd; +import org.apache.spark.scheduler.SparkListenerTaskGettingResult; +import org.apache.spark.scheduler.SparkListenerTaskStart; +import org.apache.spark.scheduler.SparkListenerUnpersistRDD; +import org.apache.spark.ui.jobs.JobProgressListener; +import org.apache.zeppelin.interpreter.BaseZeppelinContext; +import org.apache.zeppelin.interpreter.remote.RemoteEventClientWrapper; + +import java.util.Map; + +public class Spark1Shims extends SparkShims { + + public void setupSparkListener(final String sparkWebUrl) { + SparkContext sc = SparkContext.getOrCreate(); + sc.addSparkListener(new JobProgressListener(sc.getConf()) { + @Override + public void onJobStart(SparkListenerJobStart jobStart) { + buildSparkJobUrl(sparkWebUrl, jobStart.jobId(), jobStart.properties()); + } + }); + } +} diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml new file mode 100644 index 00000000000..000e3abd864 --- /dev/null +++ b/spark/spark2-shims/pom.xml @@ -0,0 +1,88 @@ + + + + + + spark-parent + org.apache.zeppelin + 0.9.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.apache.zeppelin + spark2-shims + 0.9.0-SNAPSHOT + jar + Zeppelin: Spark2 Shims + + + 2.11 + 2.1.2 + + + + + + org.apache.zeppelin + spark-shims + ${project.version} + + + + org.apache.spark + spark-core_${scala.binary.version} + ${spark.version} + provided + + + + org.apache.zeppelin + zeppelin-interpreter + ${project.version} + provided + + + + + + + maven-dependency-plugin + + true + + + + + maven-resources-plugin + + + copy-interpreter-setting + none + + true + + + + + + + + \ No newline at end of file diff --git a/spark/spark2-shims/src/main/scala/org/apache/zeppelin/spark/Spark2Shims.java b/spark/spark2-shims/src/main/scala/org/apache/zeppelin/spark/Spark2Shims.java new file mode 100644 index 00000000000..4b3961064c5 --- /dev/null +++ b/spark/spark2-shims/src/main/scala/org/apache/zeppelin/spark/Spark2Shims.java @@ -0,0 +1,36 @@ +/* + * 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.spark; + +import org.apache.spark.SparkContext; +import org.apache.spark.scheduler.SparkListener; +import org.apache.spark.scheduler.SparkListenerJobStart; + +public class Spark2Shims extends SparkShims { + + public void setupSparkListener(final String sparkWebUrl) { + SparkContext sc = SparkContext.getOrCreate(); + sc.addSparkListener(new SparkListener() { + @Override + public void onJobStart(SparkListenerJobStart jobStart) { + buildSparkJobUrl(sparkWebUrl, jobStart.jobId(), jobStart.properties()); + } + }); + } +} From 15733f4ec69027d13878f7439199b16bb56afc8e Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 27 Feb 2018 14:44:43 +0800 Subject: [PATCH 201/492] [HOTFIX]. Correct spark shim version ### What is this PR for? Correct the version for spark shims ### What type of PR is it? [Bug Fix ] ### Todos * [ ] - Task ### What is the Jira issue? * No jira created ### How should this be tested? * CI pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2820 from zjffdu/HotFix_Version and squashes the following commits: cadd2cc [Jeff Zhang] HOTFIX. Correct spark shim version --- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 619c7a42a86..ca602f3d0b0 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index 93640c6ffe0..388c4e54231 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index 000e3abd864..d43c35ec0b9 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.9.0-SNAPSHOT + 0.8.0-SNAPSHOT jar Zeppelin: Spark2 Shims From 18f3e547ae8d9b3ad81782b016593512fb8664f2 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 27 Feb 2018 20:53:54 +0800 Subject: [PATCH 202/492] ZEPPELIN-3242. Listener threw an exception java.lang.NPEat o.a.zeppelin.spark.Utils.getNoteId(Utils.java:156) ### What is this PR for? This issue also cause spark url can not be displayed in frontend. The root cause is that PySparkInterpreter/IPySparkInterpreter doesn't set JobGroup correctly. ### What type of PR is it? [Bug Fix] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3242 ### How should this be tested? * CI pass and also manually verified. ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? NO * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2822 from zjffdu/ZEPPELIN-3242 and squashes the following commits: 8254162 [Jeff Zhang] ZEPPELIN-3242. Listener threw an exception java.lang.NPEat o.a.zeppelin.spark.Utils.getNoteId(Utils.java:156) (cherry picked from commit 500b74b196b740c810553c43216a56e23ab9caf0) Signed-off-by: Jeff Zhang --- .../zeppelin/spark/IPySparkInterpreter.java | 11 +++++++ .../zeppelin/spark/PySparkInterpreter.java | 13 +++++--- .../spark/IPySparkInterpreterTest.java | 33 ++++++++++++------- .../spark/NewSparkInterpreterTest.java | 17 ++++++++-- .../zeppelin/spark/SparkRInterpreterTest.java | 19 +++++++++-- .../interpreter/InterpreterContext.java | 4 +++ 6 files changed, 77 insertions(+), 20 deletions(-) diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java index a75fda8c1d9..3691156e3bf 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java @@ -23,6 +23,7 @@ import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.LazyOpenInterpreter; import org.apache.zeppelin.interpreter.WrappedInterpreter; import org.apache.zeppelin.python.IPythonInterpreter; @@ -98,6 +99,16 @@ public BaseZeppelinContext buildZeppelinContext() { return sparkInterpreter.getZeppelinContext(); } + @Override + public InterpreterResult interpret(String st, InterpreterContext context) { + InterpreterContext.set(context); + sparkInterpreter.populateSparkWebUrl(context); + String jobGroupId = Utils.buildJobGroupId(context); + String jobDesc = "Started by: " + Utils.getUserName(context.getAuthenticationInfo()); + String setJobGroupStmt = "sc.setJobGroup('" + jobGroupId + "', '" + jobDesc + "')"; + return super.interpret(setJobGroupStmt +"\n" + st, context); + } + @Override public void cancel(InterpreterContext context) throws InterpreterException { super.cancel(context); diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java index 0703ad79186..f5e4793bb1b 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/PySparkInterpreter.java @@ -406,16 +406,16 @@ public void appendOutput(String message) throws IOException { @Override public InterpreterResult interpret(String st, InterpreterContext context) throws InterpreterException { + if (iPySparkInterpreter != null) { + return iPySparkInterpreter.interpret(st, context); + } + SparkInterpreter sparkInterpreter = getSparkInterpreter(); - sparkInterpreter.populateSparkWebUrl(context); if (sparkInterpreter.isUnsupportedSparkVersion()) { return new InterpreterResult(Code.ERROR, "Spark " + sparkInterpreter.getSparkVersion().toString() + " is not supported"); } - - if (iPySparkInterpreter != null) { - return iPySparkInterpreter.interpret(st, context); - } + sparkInterpreter.populateSparkWebUrl(context); if (!pythonscriptRunning) { return new InterpreterResult(Code.ERROR, "python process not running" @@ -467,10 +467,13 @@ public InterpreterResult interpret(String st, InterpreterContext context) } String jobGroup = Utils.buildJobGroupId(context); String jobDesc = "Started by: " + Utils.getUserName(context.getAuthenticationInfo()); + SparkZeppelinContext __zeppelin__ = sparkInterpreter.getZeppelinContext(); __zeppelin__.setInterpreterContext(context); __zeppelin__.setGui(context.getGui()); __zeppelin__.setNoteGui(context.getNoteGui()); + InterpreterContext.set(context); + pythonInterpretRequest = new PythonInterpretRequest(st, jobGroup, jobDesc); statementOutput = null; diff --git a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java index 5eaa42c4625..46a3a7277fe 100644 --- a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java @@ -27,6 +27,7 @@ import org.apache.zeppelin.interpreter.InterpreterOutput; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResultMessage; +import org.apache.zeppelin.interpreter.remote.RemoteEventClient; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.python.IPythonInterpreterTest; import org.apache.zeppelin.user.AuthenticationInfo; @@ -39,15 +40,21 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Properties; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; public class IPySparkInterpreterTest { private IPySparkInterpreter iPySparkInterpreter; private InterpreterGroup intpGroup; + private RemoteEventClient mockRemoteEventClient = mock(RemoteEventClient.class); @Before public void setup() throws InterpreterException { @@ -69,11 +76,13 @@ public void setup() throws InterpreterException { intpGroup.get("session_1").add(sparkInterpreter); sparkInterpreter.setInterpreterGroup(intpGroup); sparkInterpreter.open(); + sparkInterpreter.getZeppelinContext().setEventClient(mockRemoteEventClient); iPySparkInterpreter = new IPySparkInterpreter(p); intpGroup.get("session_1").add(iPySparkInterpreter); iPySparkInterpreter.setInterpreterGroup(intpGroup); iPySparkInterpreter.open(); + sparkInterpreter.getZeppelinContext().setEventClient(mockRemoteEventClient); } @@ -91,17 +100,21 @@ public void testBasics() throws InterruptedException, IOException, InterpreterEx // rdd InterpreterContext context = getInterpreterContext(); - InterpreterResult result = iPySparkInterpreter.interpret("sc.range(1,10).sum()", context); + InterpreterResult result = iPySparkInterpreter.interpret("sc.version", context); Thread.sleep(100); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - List interpreterResultMessages = context.out.getInterpreterResultMessages(); - assertEquals("45", interpreterResultMessages.get(0).getData()); + // spark url is sent + verify(mockRemoteEventClient).onMetaInfosReceived(any(Map.class)); context = getInterpreterContext(); - result = iPySparkInterpreter.interpret("sc.version", context); + result = iPySparkInterpreter.interpret("sc.range(1,10).sum()", context); Thread.sleep(100); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - interpreterResultMessages = context.out.getInterpreterResultMessages(); + List interpreterResultMessages = context.out.getInterpreterResultMessages(); + assertEquals("45", interpreterResultMessages.get(0).getData()); + // spark job url is sent + verify(mockRemoteEventClient).onParaInfosReceived(any(String.class), any(String.class), any(Map.class)); + // spark sql context = getInterpreterContext(); if (interpreterResultMessages.get(0).getData().startsWith("'1.") || @@ -146,7 +159,6 @@ public void testBasics() throws InterruptedException, IOException, InterpreterEx "1 a\n" + "2 b\n", interpreterResultMessages.get(0).getData()); } - // cancel final InterpreterContext context2 = getInterpreterContext(); @@ -166,6 +178,7 @@ public void run() { }; thread.start(); + // sleep 1 second to wait for the spark job starts Thread.sleep(1000); iPySparkInterpreter.cancel(context); @@ -177,10 +190,6 @@ public void run() { assertEquals("range", completions.get(0).getValue()); // pyspark streaming - - Class klass = py4j.GatewayServer.class; - URL location = klass.getResource('/' + klass.getName().replace('.', '/') + ".class"); - System.out.println("py4j location: " + location); context = getInterpreterContext(); result = iPySparkInterpreter.interpret( "from pyspark.streaming import StreamingContext\n" + @@ -204,7 +213,7 @@ public void run() { } private InterpreterContext getInterpreterContext() { - return new InterpreterContext( + InterpreterContext context = new InterpreterContext( "noteId", "paragraphId", "replName", @@ -218,5 +227,7 @@ private InterpreterContext getInterpreterContext() { null, null, new InterpreterOutput(null)); + context.setClient(mockRemoteEventClient); + return context; } } diff --git a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java index cfcf2a54aaa..3d22af31963 100644 --- a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java @@ -29,6 +29,8 @@ import org.apache.zeppelin.interpreter.InterpreterOutputListener; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResultMessageOutput; +import org.apache.zeppelin.interpreter.remote.RemoteEventClient; +import org.apache.zeppelin.interpreter.remote.RemoteEventClientWrapper; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.user.AuthenticationInfo; import org.junit.After; @@ -42,12 +44,14 @@ import java.nio.channels.ReadableByteChannel; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Properties; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; public class NewSparkInterpreterTest { @@ -59,6 +63,8 @@ public class NewSparkInterpreterTest { // catch the interpreter output in onUpdate private InterpreterResultMessageOutput messageOutput; + private RemoteEventClient mockRemoteEventClient = mock(RemoteEventClient.class); + @Test public void testSparkInterpreter() throws IOException, InterruptedException, InterpreterException { Properties properties = new Properties(); @@ -72,9 +78,12 @@ public void testSparkInterpreter() throws IOException, InterruptedException, Int interpreter.setInterpreterGroup(mock(InterpreterGroup.class)); interpreter.open(); + interpreter.getZeppelinContext().setEventClient(mockRemoteEventClient); InterpreterResult result = interpreter.interpret("val a=\"hello world\"", getInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); assertEquals("a: String = hello world\n", output); + // spark web url is sent + verify(mockRemoteEventClient).onMetaInfosReceived(any(Map.class)); result = interpreter.interpret("print(a)", getInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); @@ -124,6 +133,8 @@ public void testSparkInterpreter() throws IOException, InterruptedException, Int result = interpreter.interpret("sc.range(1, 10).sum", getInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); assertTrue(output.contains("45")); + // spark job url is sent + verify(mockRemoteEventClient).onParaInfosReceived(any(String.class), any(String.class), any(Map.class)); // case class result = interpreter.interpret("val bankText = sc.textFile(\"bank.csv\")", getInterpreterContext()); @@ -349,7 +360,7 @@ public void tearDown() throws InterpreterException { private InterpreterContext getInterpreterContext() { output = ""; - return new InterpreterContext( + InterpreterContext context = new InterpreterContext( "noteId", "paragraphId", "replName", @@ -385,5 +396,7 @@ public void onUpdate(int index, InterpreterResultMessageOutput out) { } }) ); + context.setClient(mockRemoteEventClient); + return context; } } diff --git a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkRInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkRInterpreterTest.java index 2d585f5387c..0bd88d44d77 100644 --- a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkRInterpreterTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkRInterpreterTest.java @@ -24,21 +24,27 @@ import org.apache.zeppelin.interpreter.InterpreterGroup; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.LazyOpenInterpreter; +import org.apache.zeppelin.interpreter.remote.RemoteEventClient; import org.apache.zeppelin.user.AuthenticationInfo; import org.junit.Test; import java.io.IOException; import java.util.HashMap; +import java.util.Map; import java.util.Properties; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; public class SparkRInterpreterTest { private SparkRInterpreter sparkRInterpreter; private SparkInterpreter sparkInterpreter; - + private RemoteEventClient mockRemoteEventClient = mock(RemoteEventClient.class); @Test public void testSparkRInterpreter() throws IOException, InterruptedException, InterpreterException { @@ -60,10 +66,13 @@ public void testSparkRInterpreter() throws IOException, InterruptedException, In sparkInterpreter.setInterpreterGroup(interpreterGroup); sparkRInterpreter.open(); + sparkInterpreter.getZeppelinContext().setEventClient(mockRemoteEventClient); InterpreterResult result = sparkRInterpreter.interpret("1+1", getInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); assertTrue(result.message().get(0).getData().contains("2")); + // spark web url is sent + verify(mockRemoteEventClient).onMetaInfosReceived(any(Map.class)); result = sparkRInterpreter.interpret("sparkR.version()", getInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); @@ -72,16 +81,20 @@ public void testSparkRInterpreter() throws IOException, InterruptedException, In result = sparkRInterpreter.interpret("df <- as.DataFrame(faithful)\nhead(df)", getInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); assertTrue(result.message().get(0).getData().contains("eruptions waiting")); + // spark job url is sent + verify(mockRemoteEventClient, atLeastOnce()).onParaInfosReceived(any(String.class), any(String.class), any(Map.class)); } else { // spark 1.x result = sparkRInterpreter.interpret("df <- createDataFrame(sqlContext, faithful)\nhead(df)", getInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); assertTrue(result.message().get(0).getData().contains("eruptions waiting")); + // spark job url is sent + verify(mockRemoteEventClient, atLeastOnce()).onParaInfosReceived(any(String.class), any(String.class), any(Map.class)); } } private InterpreterContext getInterpreterContext() { - return new InterpreterContext( + InterpreterContext context = new InterpreterContext( "noteId", "paragraphId", "replName", @@ -95,5 +108,7 @@ private InterpreterContext getInterpreterContext() { null, null, null); + context.setClient(mockRemoteEventClient); + return context; } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterContext.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterContext.java index 293f9bfa2bf..8fa09049773 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterContext.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterContext.java @@ -226,6 +226,10 @@ public RemoteEventClientWrapper getClient() { return client; } + public void setClient(RemoteEventClientWrapper client) { + this.client = client; + } + public RemoteWorksController getRemoteWorksController() { return remoteWorksController; } From d958b568c79766e34dd2e4c1f01f076dca4eb1e6 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Thu, 1 Mar 2018 15:28:08 +0530 Subject: [PATCH 203/492] [ZEPPELIN-3271] Option for disabling scheduler ### What is this PR for? Zeppelin server should have an option to enable/disable cron scheduler from Zeppelin config. ### What type of PR is it? [Bug Fix] ### What is the Jira issue? * [ZEPPELIN-3271](https://issues.apache.org/jira/browse/ZEPPELIN-3271) ### How should this be tested? * On adding below mentioned property in `zeppelin-site.xml`, this feature should get disabled from both web-socket and REST-APIs. ``` zeppelin.notebook.cron.enable false Notebook enable cron scheduler feature ``` ### Questions: * Does the licenses files need update? N/A * Is there breaking changes for older versions? Yes * Does this needs documentation? Yes Author: Prabhjyot Singh Closes #2821 from prabhjyotsingh/ZEPPELIN-3271 and squashes the following commits: 93e9dc6 [Prabhjyot Singh] cron disable by default 359e687 [Prabhjyot Singh] add test-case for Cron disabled eaec944 [Prabhjyot Singh] refactor isCronSupported 6421439 [Prabhjyot Singh] setCronSupported property for import, clone and create new 48488cf [Prabhjyot Singh] option for 'zeppelin.notebook.cron.folders' 9491d3d [Prabhjyot Singh] log error before throwing ForbiddenException d25ebcc [Prabhjyot Singh] ZEPPELIN-3271: Option for disabling scheduler (cherry picked from commit 2c322d72b662e703710a30abc4460f9aee2a29bb) Signed-off-by: Prabhjyot Singh --- conf/zeppelin-site.xml.template | 11 ++ docs/usage/other_features/cron_scheduler.md | 8 ++ .../zeppelin/conf/ZeppelinConfiguration.java | 12 +- .../apache/zeppelin/rest/NotebookRestApi.java | 38 ++++-- .../zeppelin/socket/NotebookServer.java | 9 +- .../zeppelin/rest/ZeppelinRestApiTest.java | 63 +++++++-- .../src/app/notebook/notebook-actionBar.html | 2 +- .../org/apache/zeppelin/notebook/Note.java | 33 ++++- .../apache/zeppelin/notebook/Notebook.java | 53 ++++++-- .../zeppelin/notebook/NotebookTest.java | 121 +++++++++++++++--- 10 files changed, 285 insertions(+), 65 deletions(-) diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template index 9774f0d7c5c..d39b19c2669 100755 --- a/conf/zeppelin-site.xml.template +++ b/conf/zeppelin-site.xml.template @@ -540,5 +540,16 @@ origin Git repository remote + + + zeppelin.notebook.cron.enable + false + Notebook enable cron scheduler feature + + + zeppelin.notebook.cron.folders + + Notebook cron folders + --> diff --git a/docs/usage/other_features/cron_scheduler.md b/docs/usage/other_features/cron_scheduler.md index e8a9975a0ed..c7fc2844574 100644 --- a/docs/usage/other_features/cron_scheduler.md +++ b/docs/usage/other_features/cron_scheduler.md @@ -50,3 +50,11 @@ You can set the cron executing user by filling in this form and press the enter When this checkbox is set to "on", the interpreters which are binded to the notebook are stopped automatically after the cron execution. This feature is useful if you want to release the interpreter resources after the cron execution. > **Note**: A cron execution is skipped if one of the paragraphs is in a state of `RUNNING` or `PENDING` no matter whether it is executed automatically (i.e. by the cron scheduler) or manually by a user opening this notebook. + +### Enable cron + +Set property **zeppelin.notebook.cron.enable** to **true** in `$ZEPPELIN_HOME/conf/zeppelin-site.xml` to enable Cron feature. + +### Run cron selectively on folders + +In `$ZEPPELIN_HOME/conf/zeppelin-site.xml` make sure the property **zeppelin.notebook.cron.enable** is set to **true**, and then set property **zeppelin.notebook.cron.folders** to the desired folder as comma-separated values, e.g. `*yst*, Sys?em, System`. This property accepts wildcard and joker. diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java index 3a2b5a25b7e..dd5491d6b82 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java @@ -581,6 +581,14 @@ public String getZeppelinNotebookGitRemoteOrigin() { return getString(ConfVars.ZEPPELIN_NOTEBOOK_GIT_REMOTE_ORIGIN); } + public Boolean isZeppelinNotebookCronEnable() { + return getBoolean(ConfVars.ZEPPELIN_NOTEBOOK_CRON_ENABLE); + } + + public String getZeppelinNotebookCronFolders() { + return getString(ConfVars.ZEPPELIN_NOTEBOOK_CRON_FOLDERS); + } + public Map dumpConfigurations(ZeppelinConfiguration conf, ConfigurationKeyPredicate predicate) { Map configurations = new HashMap<>(); @@ -771,7 +779,9 @@ public static enum ConfVars { ZEPPELIN_NOTEBOOK_GIT_REMOTE_URL("zeppelin.notebook.git.remote.url", ""), ZEPPELIN_NOTEBOOK_GIT_REMOTE_USERNAME("zeppelin.notebook.git.remote.username", "token"), ZEPPELIN_NOTEBOOK_GIT_REMOTE_ACCESS_TOKEN("zeppelin.notebook.git.remote.access-token", ""), - ZEPPELIN_NOTEBOOK_GIT_REMOTE_ORIGIN("zeppelin.notebook.git.remote.origin", "origin"); + ZEPPELIN_NOTEBOOK_GIT_REMOTE_ORIGIN("zeppelin.notebook.git.remote.origin", "origin"), + ZEPPELIN_NOTEBOOK_CRON_ENABLE("zeppelin.notebook.cron.enable", false), + ZEPPELIN_NOTEBOOK_CRON_FOLDERS("zeppelin.notebook.cron.folders", null); private String varName; @SuppressWarnings("rawtypes") diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java index 2042c4c2024..8bfaef57184 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java @@ -33,6 +33,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.zeppelin.annotation.ZeppelinApi; +import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.Notebook; @@ -139,7 +140,7 @@ private void checkIfUserIsOwner(String noteId, String errorMsg) { throw new ForbiddenException(errorMsg); } } - + /** * Check if the current user is either Owner or Writer for the given note. */ @@ -151,7 +152,7 @@ private void checkIfUserCanWrite(String noteId, String errorMsg) { throw new ForbiddenException(errorMsg); } } - + /** * Check if the current user can access (at least he have to be reader) the given note. */ @@ -175,19 +176,26 @@ private void checkIfUserCanRun(String noteId, String errorMsg) { throw new ForbiddenException(errorMsg); } } - + private void checkIfNoteIsNotNull(Note note) { if (note == null) { throw new NotFoundException("note not found"); } } - + + private void checkIfNoteSupportsCron(Note note) { + if (!note.isCronSupported(notebook.getConf())) { + LOG.error("Cron is not enabled from Zeppelin server"); + throw new ForbiddenException("Cron is not enabled from Zeppelin server"); + } + } + private void checkIfParagraphIsNotNull(Paragraph paragraph) { if (paragraph == null) { throw new NotFoundException("paragraph not found"); } } - + /** * set note authorization information */ @@ -205,7 +213,7 @@ public Response putNotePermissions(@PathParam("noteId") String noteId, String re checkIfUserIsAnon(getBlockNotAuthenticatedUserErrorMsg()); checkIfUserIsOwner(noteId, ownerPermissionError(userAndRoles, notebookAuthorization.getOwners(noteId))); - + HashMap> permMap = gson.fromJson(req, new TypeToken>>() {}.getType()); Note note = notebook.getNote(noteId); @@ -380,6 +388,7 @@ public Response createNote(String message) throws IOException { note.setName(noteName); note.persist(subject); + note.setCronSupported(notebook.getConf()); notebookServer.broadcastNote(note); notebookServer.broadcastNoteList(subject, SecurityUtils.getRoles()); return new JsonResponse<>(Status.OK, "", note.getId()).build(); @@ -715,7 +724,7 @@ public Response getNoteJobStatus(@PathParam("noteId") String noteId) @GET @Path("job/{noteId}/{paragraphId}") @ZeppelinApi - public Response getNoteParagraphJobStatus(@PathParam("noteId") String noteId, + public Response getNoteParagraphJobStatus(@PathParam("noteId") String noteId, @PathParam("paragraphId") String paragraphId) throws IOException, IllegalArgumentException { LOG.info("get note paragraph job status."); @@ -853,6 +862,7 @@ public Response registerCronJob(@PathParam("noteId") String noteId, String messa Note note = notebook.getNote(noteId); checkIfNoteIsNotNull(note); checkIfUserCanRun(noteId, "Insufficient privileges you cannot set a cron job for this note"); + checkIfNoteSupportsCron(note); if (!CronExpression.isValidExpression(request.getCronString())) { return new JsonResponse<>(Status.BAD_REQUEST, "wrong cron expressions.").build(); @@ -879,11 +889,12 @@ public Response registerCronJob(@PathParam("noteId") String noteId, String messa public Response removeCronJob(@PathParam("noteId") String noteId) throws IOException, IllegalArgumentException { LOG.info("Remove cron job note {}", noteId); - + Note note = notebook.getNote(noteId); checkIfNoteIsNotNull(note); checkIfUserIsOwner(noteId, "Insufficient privileges you cannot remove this cron job from this note"); + checkIfNoteSupportsCron(note); Map config = note.getConfig(); config.put("cron", null); @@ -910,6 +921,7 @@ public Response getCronJob(@PathParam("noteId") String noteId) Note note = notebook.getNote(noteId); checkIfNoteIsNotNull(note); checkIfUserCanRead(noteId, "Insufficient privileges you cannot get cron information"); + checkIfNoteSupportsCron(note); return new JsonResponse<>(Status.OK, note.getConfig().get("cron")).build(); } @@ -1015,22 +1027,22 @@ private void initParagraph(Paragraph p, NewParagraphRequest request, String user checkIfParagraphIsNotNull(p); p.setTitle(request.getTitle()); p.setText(request.getText()); - Map< String, Object > config = request.getConfig(); - if ( config != null && !config.isEmpty()) { + Map config = request.getConfig(); + if (config != null && !config.isEmpty()) { configureParagraph(p, config, user); } } - private void configureParagraph(Paragraph p, Map< String, Object> newConfig, String user) + private void configureParagraph(Paragraph p, Map newConfig, String user) throws IOException { LOG.info("Configure Paragraph for user {}", user); if (newConfig == null || newConfig.isEmpty()) { LOG.warn("{} is trying to update paragraph {} of note {} with empty config", - user, p.getId(), p.getNote().getId()); + user, p.getId(), p.getNote().getId()); throw new BadRequestException("paragraph config cannot be empty"); } Map origConfig = p.getConfig(); - for ( final Map.Entry entry : newConfig.entrySet()){ + for (final Map.Entry entry : newConfig.entrySet()) { origConfig.put(entry.getKey(), entry.getValue()); } 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 d1cf9e5c081..113dfd60edd 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 @@ -869,7 +869,7 @@ private void sendHomeNote(NotebookSocket conn, HashSet userAndRoles, Not } private void updateNote(NotebookSocket conn, HashSet userAndRoles, Notebook notebook, - Message fromMessage) throws SchedulerException, IOException { + Message fromMessage) throws IOException { String noteId = (String) fromMessage.get("id"); String name = (String) fromMessage.get("name"); Map config = (Map) fromMessage.get("config"); @@ -887,6 +887,11 @@ private void updateNote(NotebookSocket conn, HashSet userAndRoles, Noteb Note note = notebook.getNote(noteId); if (note != null) { + if (!(Boolean) note.getConfig().get("isZeppelinNotebookCronEnable")) { + if (config.get("cron") != null) { + config.remove("cron"); + } + } boolean cronUpdated = isCronUpdated(config, note.getConfig()); note.setName(name); note.setConfig(config); @@ -950,6 +955,7 @@ private void renameNote(NotebookSocket conn, HashSet userAndRoles, Note note = notebook.getNote(noteId); if (note != null) { note.setName(name); + note.setCronSupported(notebook.getConf()); AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal); note.persist(subject); @@ -1040,6 +1046,7 @@ private void createNote(NotebookSocket conn, HashSet userAndRoles, Noteb noteName = "Note " + note.getId(); } note.setName(noteName); + note.setCronSupported(notebook.getConf()); } note.persist(subject); diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java index 7e1a28a3855..da68087c970 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java @@ -29,6 +29,7 @@ import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.PutMethod; import org.apache.commons.lang3.StringUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.Paragraph; import org.apache.zeppelin.server.ZeppelinServer; @@ -375,11 +376,11 @@ public void testNoteJobs() throws IOException, InterruptedException { assertNotNull("can't create new note", note); note.setName("note for run test"); Paragraph paragraph = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - + Map config = paragraph.getConfig(); config.put("enabled", true); paragraph.setConfig(config); - + paragraph.setText("%md This is test paragraph."); note.persist(anonymous); String noteId = note.getId(); @@ -394,7 +395,7 @@ public void testNoteJobs() throws IOException, InterruptedException { break; } } - + // Call Run note jobs REST API PostMethod postNoteJobs = httpPost("/notebook/job/" + noteId, ""); assertThat("test note jobs run:", postNoteJobs, isAllowed()); @@ -403,21 +404,21 @@ public void testNoteJobs() throws IOException, InterruptedException { // Call Stop note jobs REST API DeleteMethod deleteNoteJobs = httpDelete("/notebook/job/" + noteId); assertThat("test note stop:", deleteNoteJobs, isAllowed()); - deleteNoteJobs.releaseConnection(); + deleteNoteJobs.releaseConnection(); Thread.sleep(1000); - + // Call Run paragraph REST API PostMethod postParagraph = httpPost("/notebook/job/" + noteId + "/" + paragraph.getId(), ""); assertThat("test paragraph run:", postParagraph, isAllowed()); - postParagraph.releaseConnection(); + postParagraph.releaseConnection(); Thread.sleep(1000); - + // Call Stop paragraph REST API DeleteMethod deleteParagraph = httpDelete("/notebook/job/" + noteId + "/" + paragraph.getId()); assertThat("test paragraph stop:", deleteParagraph, isAllowed()); - deleteParagraph.releaseConnection(); + deleteParagraph.releaseConnection(); Thread.sleep(1000); - + //cleanup ZeppelinServer.notebook.removeNote(note.getId(), anonymous); } @@ -514,7 +515,7 @@ public void testJobs() throws InterruptedException, IOException{ note.setName("note for run test"); Paragraph paragraph = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); paragraph.setText("%md This is test paragraph."); - + Map config = paragraph.getConfig(); config.put("enabled", true); paragraph.setConfig(config); @@ -526,20 +527,20 @@ public void testJobs() throws InterruptedException, IOException{ PostMethod postCron = httpPost("/notebook/cron/notexistnote", jsonRequest); assertThat("", postCron, isNotFound()); postCron.releaseConnection(); - + // right cron expression. postCron = httpPost("/notebook/cron/" + note.getId(), jsonRequest); assertThat("", postCron, isAllowed()); postCron.releaseConnection(); Thread.sleep(1000); - + // wrong cron expression. jsonRequest = "{\"cron\":\"a * * * * ?\" }"; postCron = httpPost("/notebook/cron/" + note.getId(), jsonRequest); assertThat("", postCron, isBadRequest()); postCron.releaseConnection(); Thread.sleep(1000); - + // remove cron job. DeleteMethod deleteCron = httpDelete("/notebook/cron/" + note.getId()); assertThat("", deleteCron, isAllowed()); @@ -547,6 +548,42 @@ public void testJobs() throws InterruptedException, IOException{ ZeppelinServer.notebook.removeNote(note.getId(), anonymous); } + @Test + public void testCronDisable() throws InterruptedException, IOException{ + // create a note and a paragraph + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_ENABLE.getVarName(), "false"); + Note note = ZeppelinServer.notebook.createNote(anonymous); + + note.setName("note for run test"); + Paragraph paragraph = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); + paragraph.setText("%md This is test paragraph."); + + Map config = paragraph.getConfig(); + config.put("enabled", true); + paragraph.setConfig(config); + + note.runAll(AuthenticationInfo.ANONYMOUS, false); + + String jsonRequest = "{\"cron\":\"* * * * * ?\" }"; + // right cron expression. + PostMethod postCron = httpPost("/notebook/cron/" + note.getId(), jsonRequest); + assertThat("", postCron, isForbidden()); + postCron.releaseConnection(); + + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_ENABLE.getVarName(), "true"); + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_FOLDERS.getVarName(), "System/*"); + + note.setName("System/test2"); + note.runAll(AuthenticationInfo.ANONYMOUS, false); + postCron = httpPost("/notebook/cron/" + note.getId(), jsonRequest); + assertThat("", postCron, isAllowed()); + postCron.releaseConnection(); + Thread.sleep(1000); + + System.clearProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_FOLDERS.getVarName()); + ZeppelinServer.notebook.removeNote(note.getId(), anonymous); + } + @Test public void testRegressionZEPPELIN_527() throws IOException { Note note = ZeppelinServer.notebook.createNote(anonymous); diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index f8ff830846c..b4add9375a5 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -249,7 +249,7 @@

    -
    +
    + diff --git a/docs/index.md b/docs/index.md index f00571e5bed..788a979c2ab 100644 --- a/docs/index.md +++ b/docs/index.md @@ -74,6 +74,7 @@ limitations under the License. * [Personalized Mode](./usage/other_features/personalized_mode.html) * [Customizing Zeppelin Homepage](./usage/other_features/customizing_homepage.html) with one of your notebooks * [Notebook actions](./usage/other_features/notebook_actions.html) + * [Zeppelin-Context](./usage/other_features/zeppelin_context.html) * REST API: available REST API list in Apache Zeppelin * [Interpreter API](./usage/rest_api/interpreter.html) * [Zeppelin Server API](./usage/rest_api/zeppelin_server.html) @@ -160,3 +161,4 @@ limitations under the License. * [Mailing List](https://zeppelin.apache.org/community.html) * [Apache Zeppelin Wiki](https://cwiki.apache.org/confluence/display/ZEPPELIN/Zeppelin+Home) * [Stackoverflow Questions about Zeppelin (tag: `apache-zeppelin`)](http://stackoverflow.com/questions/tagged/apache-zeppelin) + diff --git a/docs/interpreter/shell.md b/docs/interpreter/shell.md index 25349aa5156..9ab4036be4c 100644 --- a/docs/interpreter/shell.md +++ b/docs/interpreter/shell.md @@ -107,5 +107,5 @@ val members = spark.read.parquet(z.get("dataFileName")) Object interpolation is disabled by default, and can be enabled (for the Shell interpreter) by setting the value of the property `zeppelin.shell.interpolation` to `true` (see _Configuration_ above). -More details of this feature can be found in the Spark interpreter documentation under -[Object Interpolation](spark.html#object-interpolation) +More details of this feature can be found in [Zeppelin-Context](../usage/other_features/zeppelin_context.html) + diff --git a/docs/interpreter/spark.md b/docs/interpreter/spark.md index 3999e3a0d78..85b2981fcc0 100644 --- a/docs/interpreter/spark.md +++ b/docs/interpreter/spark.md @@ -323,158 +323,7 @@ z.load("groupId:artifactId:version").local() ## ZeppelinContext Zeppelin automatically injects `ZeppelinContext` as variable `z` in your Scala/Python environment. `ZeppelinContext` provides some additional functions and utilities. - -### Exploring Spark DataFrames -`ZeppelinContext` provides a `show` method, which, using Zeppelin's `table` feature, can be used to nicely display a Spark DataFrame: - -``` -df = spark.read.csv('/path/to/csv') -z.show(df) -``` - -### Object Exchange -`ZeppelinContext` extends map and it's shared between Scala and Python environment. -So you can put some objects from Scala and read it from Python, vice versa. - -
    -
    - -{% highlight scala %} -// Put object from scala -%spark -val myObject = ... -z.put("objName", myObject) - -// Exchanging data frames -myScalaDataFrame = ... -z.put("myScalaDataFrame", myScalaDataFrame) - -val myPythonDataFrame = z.get("myPythonDataFrame").asInstanceOf[DataFrame] -{% endhighlight %} - -
    -
    - -{% highlight python %} -# Get object from python -%spark.pyspark -myObject = z.get("objName") - -# Exchanging data frames -myPythonDataFrame = ... -z.put("myPythonDataFrame", postsDf._jdf) - -myScalaDataFrame = DataFrame(z.get("myScalaDataFrame"), sqlContext) -{% endhighlight %} - -
    -
    - -### Object Interpolation -Some interpreters can interpolate object values from `z` into the paragraph text by using the -`{variable-name}` syntax. The value of any object previously `put` into `z` can be -interpolated into a paragraph text by using such a pattern containing the object's name. -The following example shows one use of this facility: - -####In Scala cell: -``` -z.put("minAge", 35) -``` - -####In later SQL cell: -``` -%sql select * from members where age >= {minAge} -``` - -The interpolation of a `{var-name}` pattern is performed only when `z` contains an object with the specified name. -But the pattern is left unchanged if the named object does not exist in `z`. -Further, all `{var-name}` patterns within the paragraph text must must be translatable for any interpolation to occur -- -translation of only some of the patterns in a paragraph text is never done. - -In some situations, it is necessary to use { and } characters in a paragraph text without invoking the -object interpolation mechanism. For these cases an escaping mechanism is available -- -doubled braces {{ and }} should be used. The following example shows the use of {{ and }} for passing a -regular expression containing just { and } into the paragraph text. - -``` -%sql select * from members where name rlike '[aeiou]{{3}}' -``` - -To summarize, patterns of the form `{var-name}` within the paragraph text will be interpolated only if a predefined -object of the specified name exists. Additionally, all such patterns within the paragraph text should also -be translatable for any interpolation to occur. Patterns of the form `{{any-text}}` are translated into `{any-text}`. -These translations are performed only when all occurrences of `{`, `}`, `{{`, and `}}` in the paragraph text conform -to one of the two forms described above. Paragraph text containing `{` and/or `}` characters used in any other way -(than `{var-name}` and `{{any-text}}`) is used as-is without any changes. -No error is flagged in any case. This behavior is identical to the implementation of a similar feature in -Jupyter's shell invocation using the `!` magic command. - -This feature is disabled by default, and must be explicitly turned on for each interpreter independently -by setting the value of an interpreter-specific property to `true`. -Consult the _Configuration_ section of each interpreter's documentation -to find out if object interpolation is implemented, and the name of the parameter that must be set to `true` to -enable the feature. The name of the parameter used to enable this feature it is different for each interpreter. -For example, the SparkSQL and Shell interpreters use the parameter names `zeppelin.spark.sql.interpolation` and -`zeppelin.shell.interpolation` respectively. - -At present only the SparkSQL and Shell interpreters support object interpolation. - -### Form Creation - -`ZeppelinContext` provides functions for creating forms. -In Scala and Python environments, you can create forms programmatically. -
    -
    - -{% highlight scala %} -%spark -/* Create text input form */ -z.input("formName") - -/* Create text input form with default value */ -z.input("formName", "defaultValue") - -/* Create select form */ -z.select("formName", Seq(("option1", "option1DisplayName"), - ("option2", "option2DisplayName"))) - -/* Create select form with default value*/ -z.select("formName", "option1", Seq(("option1", "option1DisplayName"), - ("option2", "option2DisplayName"))) -{% endhighlight %} - -
    -
    - -{% highlight python %} -%spark.pyspark -# Create text input form -z.input("formName") - -# Create text input form with default value -z.input("formName", "defaultValue") - -# Create select form -z.select("formName", [("option1", "option1DisplayName"), - ("option2", "option2DisplayName")]) - -# Create select form with default value -z.select("formName", [("option1", "option1DisplayName"), - ("option2", "option2DisplayName")], "option1") -{% endhighlight %} - -
    -
    - -In sql environment, you can create form in simple template. - -```sql -%spark.sql -select * from ${table=defaultTableName} where text like '%${search}%' -``` - -To learn more about dynamic form, checkout [Dynamic Form](../usage/dynamic_form/intro.html). - +See [Zeppelin-Context](../usage/other_features/zeppelin_context.html) for more details. ## Matplotlib Integration (pyspark) Both the `python` and `pyspark` interpreters have built-in support for inline visualization using `matplotlib`, @@ -518,3 +367,4 @@ This is to make the server communicate with KDC. > **NOTE:** If you do not have permission to access for the above spark-defaults.conf file, optionally, you can add the above lines to the Spark Interpreter setting through the Interpreter tab in the Zeppelin UI. 4. That's it. Play with Zeppelin! + diff --git a/docs/usage/other_features/zeppelin_context.md b/docs/usage/other_features/zeppelin_context.md new file mode 100644 index 00000000000..5a777278968 --- /dev/null +++ b/docs/usage/other_features/zeppelin_context.md @@ -0,0 +1,233 @@ +--- +layout: page +title: "Zeppelin-Context" +description: "The Zeppelin-Context is a system-wide container for a variety of user-specific settings and parameters that are accessible across notebooks, cells, and interpreters." +group: usage/other_features +--- + +{% include JB/setup %} + +# Zeppelin-Context + +
    + +The zeppelin-context is a system-wide container for common utility functions and +user-specific data. It implements functions for data input, data display, etc. that are +often needed but are not uniformly available in all interpreters. +Its single per-user instance is accessible across all of the user's notebooks and cells, +enabling data exchange between cells - even in different notebooks. +But the way in which the zeppelin-context is used, and the functionality available differs +depending on whether or not the associated interpreter is based on a programming language. +Details of how the zeppelin-context is used for different purposes and in different +environments is described below. + +## Usage in Programming Language Cells + +In many programming-language interpreters (e.g. Apache Spark, Python, R) the zeppelin-context is available +as a predefined variable `z` that can be used by directly invoking its methods. +The methods available on the `z` object are described below. +Other interpreters based on programming languages like spark.dep, Apache Beam, etc. also provide the +predefined variable `z`. + +### Exploring Spark DataFrames +In the Apache Spark interpreter, the zeppelin-context provides a `show` method, which, +using Zeppelin's `table` feature, can be used to nicely display a Spark DataFrame: + +``` +df = spark.read.csv('/path/to/csv') +z.show(df) +``` + +This display functionality using the `show` method is planned to be extended uniformly to +other interpreters that can access the `z` object. + +### Object Exchange +`ZeppelinContext` extends map and it's shared between the Apache Spark and Python environments. +So you can put some objects using Scala (in an Apache Spark cell) and read it from Python, and vice versa. + +
    +
    + +{% highlight scala %} +// Put object from scala +%spark +val myObject = ... +z.put("objName", myObject) + +// Exchanging data frames +myScalaDataFrame = ... +z.put("myScalaDataFrame", myScalaDataFrame) + +val myPythonDataFrame = z.get("myPythonDataFrame").asInstanceOf[DataFrame] +{% endhighlight %} + +
    +
    + +{% highlight python %} +# Get object from python +%spark.pyspark +myObject = z.get("objName") + +# Exchanging data frames +myPythonDataFrame = ... +z.put("myPythonDataFrame", postsDf._jdf) + +myScalaDataFrame = DataFrame(z.get("myScalaDataFrame"), sqlContext) +{% endhighlight %} + +
    +
    + +### Form Creation + +`ZeppelinContext` provides functions for creating forms. +In Scala and Python environments, you can create forms programmatically. +
    +
    + +{% highlight scala %} +%spark +/* Create text input form */ +z.input("formName") + +/* Create text input form with default value */ +z.input("formName", "defaultValue") + +/* Create select form */ +z.select("formName", Seq(("option1", "option1DisplayName"), + ("option2", "option2DisplayName"))) + +/* Create select form with default value*/ +z.select("formName", "option1", Seq(("option1", "option1DisplayName"), + ("option2", "option2DisplayName"))) +{% endhighlight %} + +
    +
    + +{% highlight python %} +%spark.pyspark +# Create text input form +z.input("formName") + +# Create text input form with default value +z.input("formName", "defaultValue") + +# Create select form +z.select("formName", [("option1", "option1DisplayName"), + ("option2", "option2DisplayName")]) + +# Create select form with default value +z.select("formName", [("option1", "option1DisplayName"), + ("option2", "option2DisplayName")], "option1") +{% endhighlight %} + +
    +
    + +In sql environment, you can create form in simple template. + +```sql +%spark.sql +select * from ${table=defaultTableName} where text like '%${search}%' +``` + +To learn more about dynamic form, checkout [Dynamic Form](../usage/dynamic_form/intro.html). + +### Interpreter-Specific Functions + +Some interpreters use a subclass of `BaseZepplinContext` augmented with interpreter-specific functions. +For example functions of the dependency loader (%spark.dep) can be invoked as `z.addRepo()`, `z.load()`, etc. +Such interpreter-specific functions are described within each interpreter's documentation. + +## Usage with Embedded Commands + +In certain interpreters (see table below) zeppelin-context features may be invoked by embedding +command strings into the paragraph text. Such embedded command strings are used to invoke +dynamic-forms and object-interpolation as described below. + +| Interpreters that use Embedded Commands | +|-------------------------------------------------------------------| +|spark.sql (\*), bigquery, cassandra, elasticsearch, file, hbase, ignite, jdbc (\*), kylin, livy, markdown, neo4j, pig, python, shell (\*), zengine | + +Dynamic forms are available in all of the interpreters in the table above, +but object interpolation is only available in a small, but growing, list of interpreters +(marked with an asterisk in the table above). +Both these zeppelin-context features are described below. + +### Dynamic Forms + +Patterns of the form ${ ... } are used to dynamically create additional HTML elements +for requesting user input (that replaces the corresponding pattern in the paragraph text). +Currently only [text](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/text), +[select](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select) with +[options](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option), and +[checkbox](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox) are supported. + +Dynamic forms are described in detail here: [Dynamic Form](../usage/dynamic_form/intro.html). + +### Object Interpolation +Some interpreters can interpolate object values from `z` into the paragraph text by using the +`{variable-name}` syntax. The value of any object previously `put` into `z` can be +interpolated into a paragraph text by using such a pattern containing the object's name. +The following example shows one use of this facility: + +####In Scala cell: +``` +z.put("minAge", 35) +``` + +####In later SQL cell: +``` +%sql select * from members where age >= {minAge} +``` + +The interpolation of a `{var-name}` pattern is performed only when `z` contains an object with the specified name. +But the pattern is left unchanged if the named object does not exist in `z`. +Further, all `{var-name}` patterns within the paragraph text must must be translatable for any interpolation to occur -- +translation of only some of the patterns in a paragraph text is never done. + +In some situations, it is necessary to use { and } characters in a paragraph text without invoking the +object interpolation mechanism. For these cases an escaping mechanism is available -- +doubled braces {{ and }} should be used. The following example shows the use of {{ and }} for passing a +regular expression containing just { and } into the paragraph text. + +``` +%sql select * from members where name rlike '[aeiou]{{3}}' +``` + +To summarize, patterns of the form `{var-name}` within the paragraph text will be interpolated only if a predefined +object of the specified name exists. Additionally, all such patterns within the paragraph text should also +be translatable for any interpolation to occur. Patterns of the form `{{any-text}}` are translated into `{any-text}`. +These translations are performed only when all occurrences of `{`, `}`, `{{`, and `}}` in the paragraph text conform +to one of the two forms described above. Paragraph text containing `{` and/or `}` characters used in any other way +(than `{var-name}` and `{{any-text}}`) is used as-is without any changes. +No error is flagged in any case. This behavior is identical to the implementation of a similar feature in +Jupyter's shell invocation using the `!` magic command. + +This feature is disabled by default, and must be explicitly turned on for each interpreter independently +by setting the value of an interpreter-specific property to `true`. +Consult the _Configuration_ section of each interpreter's documentation +to find out if object interpolation is implemented, and the name of the parameter that must be set to `true` to +enable the feature. The name of the parameter used to enable this feature it is different for each interpreter. +For example, the SparkSQL and Shell interpreters use the parameter names `zeppelin.spark.sql.interpolation` and +`zeppelin.shell.interpolation` respectively. + +At present only the SparkSQL, JDBC, and Shell interpreters support object interpolation. + + + + From bcef737a4df5c4548cf9ee6bbe2daaaac28e5a8c Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Wed, 2 May 2018 15:17:29 +0800 Subject: [PATCH 286/492] [HOTFIX] Build Fix for branch-0.8 ### What is this PR for? HotFix for build issue of branch-0.8 ### What type of PR is it? [Hot Fix] ### How should this be tested? * CI pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? NO Author: Jeff Zhang Closes #2954 from zjffdu/javadoc_branch-0.8 and squashes the following commits: b4f3e91 [Jeff Zhang] [HOTFIX] Build Fix for branch-0.8 --- .../apache/zeppelin/bigquery/BigQueryInterpreterTest.java | 1 - .../interpreter/remote/RemoteInterpreterServer.java | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bigquery/src/test/java/org/apache/zeppelin/bigquery/BigQueryInterpreterTest.java b/bigquery/src/test/java/org/apache/zeppelin/bigquery/BigQueryInterpreterTest.java index c80e3d58d0d..64c6e17a4fe 100644 --- a/bigquery/src/test/java/org/apache/zeppelin/bigquery/BigQueryInterpreterTest.java +++ b/bigquery/src/test/java/org/apache/zeppelin/bigquery/BigQueryInterpreterTest.java @@ -129,5 +129,4 @@ public void testInterpreterOutputData() { assertEquals("col1\tcol2", lines[0]); assertEquals("1\t2", lines[1]); } ->>>>>>> 0c3b446... ZEPPELIN-3412 Enable query prefix syntax in bigquery interpreter } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java index be3cc368530..2376579d92c 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java @@ -517,7 +517,11 @@ public void afterStatusChange(Job job, Status before, Status after) { } } - // TODO(jl): Need to extract this class from RemoteInterpreterServer to test it + + + /** + * TODO(jl): Need to extract this class from RemoteInterpreterServer to test it + */ public static class InterpretJob extends Job { private Interpreter interpreter; From 8b23485e8b3e66c256aa290442fac8dd46f5f96c Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Wed, 2 May 2018 16:41:57 +0800 Subject: [PATCH 287/492] [HOTFIX] Revert version to 0.8.0-SNAPSHOT ### What is this PR for? HotFix for version revert ### What type of PR is it? [ Hot Fix ] ### Todos * [ ] - Task ### How should this be tested? * CI pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? NO Author: Jeff Zhang Closes #2955 from zjffdu/version_revert and squashes the following commits: 35fa826 [Jeff Zhang] [HOTFIX] Revert version to 0.8.0-SNAPSHOT --- alluxio/pom.xml | 4 ++-- angular/pom.xml | 4 ++-- beam/pom.xml | 4 ++-- bigquery/pom.xml | 4 ++-- cassandra/pom.xml | 4 ++-- elasticsearch/pom.xml | 4 ++-- file/pom.xml | 4 ++-- flink/pom.xml | 4 ++-- geode/pom.xml | 4 ++-- groovy/pom.xml | 4 ++-- hbase/pom.xml | 4 ++-- helium-dev/pom.xml | 4 ++-- ignite/pom.xml | 4 ++-- interpreter-parent/pom.xml | 4 ++-- jdbc/pom.xml | 4 ++-- kylin/pom.xml | 4 ++-- lens/pom.xml | 4 ++-- livy/pom.xml | 4 ++-- markdown/pom.xml | 4 ++-- neo4j/pom.xml | 4 ++-- pig/pom.xml | 4 ++-- pom.xml | 2 +- python/pom.xml | 4 ++-- r/pom.xml | 4 ++-- sap/pom.xml | 4 ++-- scalding/pom.xml | 4 ++-- scio/pom.xml | 4 ++-- shell/pom.xml | 4 ++-- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 4 ++-- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-dependencies/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- zeppelin-display/pom.xml | 4 ++-- zeppelin-distribution/pom.xml | 2 +- zeppelin-examples/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-clock/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-horizontalbar/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-echo/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-markdown/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-translator/pom.xml | 4 ++-- zeppelin-integration/pom.xml | 4 ++-- zeppelin-interpreter/pom.xml | 4 ++-- zeppelin-jupyter/pom.xml | 4 ++-- zeppelin-server/pom.xml | 4 ++-- zeppelin-web/pom.xml | 4 ++-- zeppelin-zengine/pom.xml | 4 ++-- 52 files changed, 102 insertions(+), 102 deletions(-) diff --git a/alluxio/pom.xml b/alluxio/pom.xml index acef4942230..f36494e94c5 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Alluxio interpreter diff --git a/angular/pom.xml b/angular/pom.xml index 341d7d5ec54..9ff2acb80e4 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Angular interpreter diff --git a/beam/pom.xml b/beam/pom.xml index 7592e77e010..a1d47615355 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Beam interpreter diff --git a/bigquery/pom.xml b/bigquery/pom.xml index f3877179a9b..db88edafaf6 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: BigQuery interpreter diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 2be0606157a..4b139e9c60a 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-cassandra_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index 3b7c2b54b4d..b317e285813 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent zeppelin-elasticsearch jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Elasticsearch interpreter diff --git a/file/pom.xml b/file/pom.xml index 1ea8eace07f..1a76683215b 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: File System Interpreters diff --git a/flink/pom.xml b/flink/pom.xml index 47bd2877c83..5c0cc3bf906 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-flink_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Flink Zeppelin flink support diff --git a/geode/pom.xml b/geode/pom.xml index 59c16b731b2..4a9dadd234b 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Apache Geode interpreter diff --git a/groovy/pom.xml b/groovy/pom.xml index 7dc4282d6e4..64c51f85612 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Groovy interpreter diff --git a/hbase/pom.xml b/hbase/pom.xml index 6bf1a80905d..8f27631ea2d 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: HBase interpreter diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index 8445b4dcf9e..395f8ffac4f 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -24,13 +24,13 @@ org.apache.zeppelin interpreter-parent - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Helium development interpreter diff --git a/ignite/pom.xml b/ignite/pom.xml index 31df05eac77..5ce6e37296c 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent zeppelin-ignite_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Apache Ignite interpreter diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index 35c8fc0e99f..c40bc1192ad 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin interpreter-parent pom - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Interpreter Parent diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 58de18199ae..d5f1b827a52 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: JDBC interpreter diff --git a/kylin/pom.xml b/kylin/pom.xml index 1690ec2e87f..b70facbcc12 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -23,7 +23,7 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent 4.0.0 @@ -31,7 +31,7 @@ org.apache.zeppelin zeppelin-kylin jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Kylin interpreter diff --git a/lens/pom.xml b/lens/pom.xml index c67828733e0..ecc0a12ed6f 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Lens interpreter diff --git a/livy/pom.xml b/livy/pom.xml index 33c327b979b..d33adf6dc91 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-livy jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Livy interpreter diff --git a/markdown/pom.xml b/markdown/pom.xml index cf8e766c673..ca954498075 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Markdown interpreter diff --git a/neo4j/pom.xml b/neo4j/pom.xml index 6883bdd996f..298726fb82b 100644 --- a/neo4j/pom.xml +++ b/neo4j/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-neo4j jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Neo4j interpreter diff --git a/pig/pom.xml b/pig/pom.xml index 06a58bbc9ee..4a56a342909 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org diff --git a/pom.xml b/pom.xml index e696705aa0b..8fba697eb3e 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin Zeppelin project http://zeppelin.apache.org diff --git a/python/pom.xml b/python/pom.xml index 03da41cfd25..7f8e671e5c0 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Python interpreter diff --git a/r/pom.xml b/r/pom.xml index fc9fa6621f1..e1a02a08dca 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-zrinterpreter_${scala.binary.version} jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: R Interpreter R Interpreter for Zeppelin http://zeppelin.apache.org diff --git a/sap/pom.xml b/sap/pom.xml index e75cc10a38f..861ade91674 100644 --- a/sap/pom.xml +++ b/sap/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin sap jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Sap Zeppelin SAP support diff --git a/scalding/pom.xml b/scalding/pom.xml index 051368fa9a2..763afe0bb41 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-scalding_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Scalding interpreter diff --git a/scio/pom.xml b/scio/pom.xml index 3c9281c627d..27ccb0cc7b8 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-scio_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Scio Zeppelin Scio support diff --git a/shell/pom.xml b/shell/pom.xml index cd5a99b4e5f..56714f5b159 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Shell interpreter diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 2c219a60655..1653cd02896 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index 5505aa7f57b..186b0501187 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../interpreter-parent/pom.xml org.apache.zeppelin spark-parent pom - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Spark Parent Zeppelin Spark Support diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index 9b111da94d5..0f8593f0b7c 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT jar Zeppelin: Spark Interpreter Scala_2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index 190f7101ca0..d523f5e4c8b 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT jar Zeppelin: Spark Interpreter Scala_2.11 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index 6da2ac1a290..3b7048acbc8 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-spark-dependencies jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Spark dependencies Zeppelin spark support diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index c3b03d39e22..7cf0d14cd7d 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT pom Zeppelin: Spark Scala Parent diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 81275e47083..ca602f3d0b0 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index 458f1d2858b..388c4e54231 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index 648ff385121..d43c35ec0b9 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT jar Zeppelin: Spark2 Shims diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index 5db50f76769..ae2292307fc 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-display jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Display system apis diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index d43d8bc759e..ed05c9383be 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -23,7 +23,7 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 85387b707c5..e9f04731b80 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-examples pom - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Examples Zeppelin examples diff --git a/zeppelin-examples/zeppelin-example-clock/pom.xml b/zeppelin-examples/zeppelin-example-clock/pom.xml index 55333aa46ae..d4fed2171fa 100644 --- a/zeppelin-examples/zeppelin-example-clock/pom.xml +++ b/zeppelin-examples/zeppelin-example-clock/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index 29f4e381e72..8e08c4a3af2 100644 --- a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml +++ b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Example application - Horizontal Bar chart diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml index d76c281f1de..348abd20354 100644 --- a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index d84c887f7b0..b3575c99c6a 100644 --- a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index 67604437bcc..b615eadc8a7 100644 --- a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml index 527990924dd..09e6daaad38 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml index a2efef70c32..41f88333431 100644 --- a/zeppelin-integration/pom.xml +++ b/zeppelin-integration/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-integration jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Integration Test diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index e75e1f8029f..8df470c22e5 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Interpreter Zeppelin Interpreter diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index bc9ec5a816e..eef0e367b95 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,13 +24,13 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. zeppelin-jupyter jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index c11bc423de5..d7a4642990b 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-server jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Server diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 2bc22c02d5b..2f36d9f1fcb 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-web war - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: web Application diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index a438242c4a4..0b165277e7a 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,14 +23,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-zengine jar - 0.8.1-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Zengine Zeppelin Zengine From 4c3a010dfa17be15958389a20c9bb12fe524cbce Mon Sep 17 00:00:00 2001 From: Savalek Date: Fri, 6 Apr 2018 11:58:50 +0300 Subject: [PATCH 288/492] [Zeppelin-3224] - Fix "ultimate" visualization ### What is this PR for? This PR fixes problems with incorrect data visualization using "ultimate". With certain aggregations, the data was not displayed. Also PR improves sorting by columns / rows, before PR it compared by Unicode encoding, but now if there are only numeric data on the axis, sorting will be by values. ### What type of PR is it? [Bug Fix & Improvement] ### What is the Jira issue? [ZEPPELIN-3224](https://issues.apache.org/jira/browse/ZEPPELIN-3224) ### Screenshots (if appropriate) **Error:** ![error](https://user-images.githubusercontent.com/30798933/37035027-3ca6022c-215c-11e8-901c-d33e833ad505.PNG) **Now:** ![default](https://user-images.githubusercontent.com/30798933/37038600-48482d8a-2166-11e8-87cd-d0cb861441d2.PNG) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Savalek Author: tinkoff-dwh Closes #2841 from Savalek/ZEPPELIN-3224 and squashes the following commits: f546b457c [Savalek] fix count of arguments in parseFloat function a56ea59d9 [Savalek] Improved number check 5c866b183 [tinkoff-dwh] small fix e7d1cd7cc [tinkoff-dwh] Merge branch 'master' into ZEPPELIN-3224 d4b2a4913 [Savalek] add test 17d3f8d0e [Savalek] add tests for sortWithNumberSupport() dc78f3d2b [Savalek] code style fix ba7a6e3e4 [tinkoff-dwh] Merge branch 'master' into ZEPPELIN-3224 9583f2cce [Savalek] small fix eab5d86d2 [Savalek] Merge branch 'master' into ZEPPELIN-3224 33676b468 [Savalek] fix XY legend sort for numbers 94026be39 [Savalek] fix graph display (cherry picked from commit c1c1aa892dbd7621815c19a9d72b175a330ee51e) Signed-off-by: Jeff Zhang --- .../tabledata/advanced-transformation-util.js | 38 +++++++++++++------ .../advanced-transformation-util.test.js | 37 ++++++++++++++++++ zeppelin-web/src/app/tabledata/tabledata.js | 4 ++ 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/zeppelin-web/src/app/tabledata/advanced-transformation-util.js b/zeppelin-web/src/app/tabledata/advanced-transformation-util.js index 97c1b2c1561..71ed7b86507 100644 --- a/zeppelin-web/src/app/tabledata/advanced-transformation-util.js +++ b/zeppelin-web/src/app/tabledata/advanced-transformation-util.js @@ -771,7 +771,7 @@ export function getKGACube(rows, keyColumns, groupColumns, aggrColumns) { cube = {[mergedGroupColumnName]: cube}; keyNames = [mergedGroupColumnName]; } else { - keyNames = Object.keys(cube).sort(); /** keys should be sorted */ + keyNames = sortWithNumberSupport(Object.keys(cube)); /** keys should be sorted */ } return { @@ -883,7 +883,7 @@ export function getKAGCube(rows, keyColumns, groupColumns, aggrColumns) { cube = {[mergedGroupColumnName]: cube}; keyNames = [mergedGroupColumnName]; } else { - keyNames = Object.keys(cube).sort(); /** keys should be sorted */ + keyNames = sortWithNumberSupport(Object.keys(cube)); /** keys should be sorted */ } return { @@ -1015,8 +1015,8 @@ export function getKKGACube(rows, key1Columns, key2Columns, groupColumns, aggrCo } /** end loop for aggrColumns */ } - let key1Names = Object.keys(key1NameSet).sort(); /** keys should be sorted */ - let key2Names = Object.keys(key2NameSet).sort(); /** keys should be sorted */ + let key1Names = sortWithNumberSupport(Object.keys(key1NameSet)); /** keys should be sorted */ + let key2Names = sortWithNumberSupport(Object.keys(key2NameSet)); /** keys should be sorted */ return { cube: cube, @@ -1072,7 +1072,7 @@ export function getNameWithIndex(names) { export function getArrayRowsFromKKGACube(cube, schema, aggregatorColumns, key1Names, key2Names, groupNameSet, selectorNameWithIndex) { - const sortedSelectors = Object.keys(selectorNameWithIndex).sort(); + const sortedSelectors = sortWithNumberSupport(Object.keys(selectorNameWithIndex)); const sortedSelectorNameWithIndex = getNameWithIndex(sortedSelectors); const selectorRows = new Array(sortedSelectors.length); @@ -1087,7 +1087,7 @@ export function getArrayRowsFromKKGACube(cube, schema, aggregatorColumns, key1NameWithIndex: key1NameWithIndex, key2NameWithIndex: key2NameWithIndex, transformed: selectorRows, - groupNames: Array.from(groupNameSet).sort(), + groupNames: sortWithNumberSupport(Array.from(groupNameSet)), sortedSelectors: sortedSelectors, }; } @@ -1167,7 +1167,7 @@ export function fillSelectorRows(schema, cube, selectorRows, export function getArrayRowsFromKGACube(cube, schema, aggregatorColumns, keyColumnName, keyNames, groupNameSet, selectorNameWithIndex) { - const sortedSelectors = Object.keys(selectorNameWithIndex).sort(); + const sortedSelectors = sortWithNumberSupport(Object.keys(selectorNameWithIndex)); const sortedSelectorNameWithIndex = getNameWithIndex(sortedSelectors); const keyArrowRows = new Array(sortedSelectors.length); @@ -1184,7 +1184,7 @@ export function getArrayRowsFromKGACube(cube, schema, aggregatorColumns, return { transformed: keyArrowRows, - groupNames: Array.from(groupNameSet).sort(), + groupNames: sortWithNumberSupport(Array.from(groupNameSet)), sortedSelectors: sortedSelectors, }; } @@ -1244,8 +1244,8 @@ export function getObjectRowsFromKGACube(cube, schema, aggregatorColumns, return { transformed: rows, - sortedSelectors: Object.keys(selectorNameWithIndex).sort(), - groupNames: Array.from(groupNameSet).sort(), + sortedSelectors: sortWithNumberSupport(Object.keys(selectorNameWithIndex)), + groupNames: sortWithNumberSupport(Array.from(groupNameSet)), }; } @@ -1289,12 +1289,12 @@ export function getObjectRow(schema, aggrColumns, obj, groupNameSet) { export function getDrilldownRowsFromKAGCube(cube, schema, aggregatorColumns, keyColumnName, keyNames, groupNameSet, selectorNameWithIndex) { - const sortedSelectors = Object.keys(selectorNameWithIndex).sort(); + const sortedSelectors = sortWithNumberSupport(Object.keys(selectorNameWithIndex)); const sortedSelectorNameWithIndex = getNameWithIndex(sortedSelectors); const rows = new Array(sortedSelectors.length); - const groupNames = Array.from(groupNameSet).sort(); + const groupNames = sortWithNumberSupport(Array.from(groupNameSet)); keyNames.map((key) => { const obj = cube[key]; @@ -1339,3 +1339,17 @@ export function fillDrillDownRow(schema, obj, rows, key, rows[selectorIndex] = row; } } + +export function sortWithNumberSupport(arr) { + let isNumeric = function(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + }; + + if (arr.every(isNumeric)) { + return arr.sort(function(a, b) { + return parseFloat(a) - parseFloat(b); + }); + } else { + return arr.sort(); + } +} diff --git a/zeppelin-web/src/app/tabledata/advanced-transformation-util.test.js b/zeppelin-web/src/app/tabledata/advanced-transformation-util.test.js index 84ea4419dbc..28ce67d5d7d 100644 --- a/zeppelin-web/src/app/tabledata/advanced-transformation-util.test.js +++ b/zeppelin-web/src/app/tabledata/advanced-transformation-util.test.js @@ -1737,5 +1737,42 @@ describe('advanced-transformation-util', () => { ]); }); }); // end: describe('method: array:2-key') + + describe('sortWithNumberSupport() check', () => { + it('sorting a positive numeric array', () => { + let positive = [5, 4, 9, 8, 3, 1, 7, 2, 6]; + let sortedArray = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + let testArr = Util.sortWithNumberSupport(positive); + expect(testArr).toEqual(sortedArray); + }); + + it('sorting a negative numeric array', () => { + let negative = [-5, -4, -9, -8, -3, -1, -7, -2, -6]; + let sortedArray = [-9, -8, -7, -6, -5, -4, -3, -2, -1]; + let testArr = Util.sortWithNumberSupport(negative); + expect(testArr).toEqual(sortedArray); + }); + + it('sorting a mixed numeric array', () => { + let mixed = [5, -4, 9, -8, 3, 1, 7, -2, -6]; + let sortedArray = [-8, -6, -4, -2, 1, 3, 5, 7, 9]; + let testArr = Util.sortWithNumberSupport(mixed); + expect(testArr).toEqual(sortedArray); + }); + + it('checking sorting by value (not by unicode\'s encoding)', () => { + let long = [2, 3, 1, 4, 9999, 30, 33, 20, 27, 42, 26, 58, 73, 99, 21, 122]; + let sortedArray = [1, 2, 3, 4, 20, 21, 26, 27, 30, 33, 42, 58, 73, 99, 122, 9999]; + let testArr = Util.sortWithNumberSupport(long); + expect(testArr).toEqual(sortedArray); + }); + + it('sorting a string array', () => { + let strings = ['34', '77', '5', '65', '7', '23', '88', '-45']; + let sortedArray = ['-45', '5', '7', '23', '34', '65', '77', '88']; + let testArr = Util.sortWithNumberSupport(strings); + expect(testArr).toEqual(sortedArray); + }); + }); }); // end: describe('getTransformer') }); diff --git a/zeppelin-web/src/app/tabledata/tabledata.js b/zeppelin-web/src/app/tabledata/tabledata.js index 745ab179050..1f01bca67c0 100644 --- a/zeppelin-web/src/app/tabledata/tabledata.js +++ b/zeppelin-web/src/app/tabledata/tabledata.js @@ -59,6 +59,10 @@ export default class TableData extends Dataset { if (i === 0) { columnNames.push({name: col, index: j, aggr: 'sum'}); } else { + let valueOfCol; + if (!isNaN(valueOfCol = parseFloat(col)) && isFinite(col)) { + col = valueOfCol; + } cols.push(col); cols2.push({key: (columnNames[i]) ? columnNames[i].name : undefined, value: col}); } From c76812a790cc79521b63ccc6de1ccfcb3cdaba80 Mon Sep 17 00:00:00 2001 From: Lee moon soo Date: Wed, 2 May 2018 15:25:13 -0400 Subject: [PATCH 289/492] [DOC] Fix broken link to writing visualizations doc ### What is this PR for? This PR fixes broken link to doc. Need to be merged branch-0.8. ### What type of PR is it? Bug Fix ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: Lee moon soo Closes #2956 from Leemoonsoo/fix/doc_link_to_helium and squashes the following commits: 9d8cac7 [Lee moon soo] Fix broken link to writing visualizations doc (cherry picked from commit c4da31b2bb5e71a8e7dd22d0ce8e63cea80d871c) Signed-off-by: Jeff Zhang --- docs/_includes/themes/zeppelin/_navigation.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_includes/themes/zeppelin/_navigation.html b/docs/_includes/themes/zeppelin/_navigation.html index 24b3cde8269..acedc29e217 100644 --- a/docs/_includes/themes/zeppelin/_navigation.html +++ b/docs/_includes/themes/zeppelin/_navigation.html @@ -159,7 +159,7 @@
  • Overview
  • Writing Helium Application
  • Writing Helium Spell
  • -
  • Writing Helium Visualization: Basics
  • +
  • Writing Helium Visualization: Basics
  • Writing Helium Visualization: Transformation
  • Contributing to Zeppelin
  • From 52ff1ca6c1ec68864a00d49cf4e3c5d7f54e3f39 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Thu, 3 May 2018 15:21:37 +0800 Subject: [PATCH 290/492] [HOTFIX] Script and pom file fix for release --- dev/create_release.sh | 4 ++-- dev/publish_release.sh | 2 +- pom.xml | 3 +++ zeppelin-distribution/pom.xml | 21 --------------------- 4 files changed, 6 insertions(+), 24 deletions(-) diff --git a/dev/create_release.sh b/dev/create_release.sh index 9cb61e0351c..6836e02aed5 100755 --- a/dev/create_release.sh +++ b/dev/create_release.sh @@ -106,8 +106,8 @@ function make_binary_release() { git_clone make_source_package -make_binary_release all "-Pspark-2.1 -Phadoop-2.6 -Pscala-${SCALA_VERSION}" -make_binary_release netinst "-Pspark-2.1 -Phadoop-2.6 -Pscala-${SCALA_VERSION} -pl zeppelin-interpreter,zeppelin-zengine,:zeppelin-display_${SCALA_VERSION},:zeppelin-spark-dependencies_${SCALA_VERSION},:zeppelin-spark_${SCALA_VERSION},zeppelin-web,zeppelin-server,zeppelin-distribution -am" +make_binary_release all "-Pspark-2.2 -Pscala-${SCALA_VERSION}" +make_binary_release netinst "-Pspark-2.2 -Pscala-${SCALA_VERSION} -pl zeppelin-interpreter,zeppelin-zengine,spark/interpreter,spark/spark-dependencies,zeppelin-web,zeppelin-server,zeppelin-distribution -am" # remove non release files and dirs rm -rf "${WORKING_DIR}/zeppelin" diff --git a/dev/publish_release.sh b/dev/publish_release.sh index b569ec4ba92..ec3c1aaa60f 100755 --- a/dev/publish_release.sh +++ b/dev/publish_release.sh @@ -46,7 +46,7 @@ if [[ $RELEASE_VERSION == *"SNAPSHOT"* ]]; then DO_SNAPSHOT="yes" fi -PUBLISH_PROFILES="-Ppublish-distr -Pspark-2.1 -Phadoop-2.6 -Pr" +PUBLISH_PROFILES="-Ppublish-distr -Pspark-2.2 -Pr" PROJECT_OPTIONS="-pl !zeppelin-distribution" NEXUS_STAGING="https://repository.apache.org/service/local/staging" NEXUS_PROFILE="153446d1ac37c4" diff --git a/pom.xml b/pom.xml index 8fba697eb3e..16fe86c1192 100644 --- a/pom.xml +++ b/pom.xml @@ -888,6 +888,9 @@ jar + + -Xdoclint:none + diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index ed05c9383be..2238bc9d7af 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -171,27 +171,6 @@ - - com.bazaarvoice.maven.plugins - s3-upload-maven-plugin - 1.2 - - zeppel.in - s3-ap-northeast-1.amazonaws.com - true - zeppelin-distribution/target/zeppelin-${project.version}.tar.gz - zeppelin-${project.version}.tar.gz - - - - publish-distr-to-s3 - package - - s3-upload - - - - From 2745afe734f06558155e8e0af876cfc1bbb62109 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Thu, 3 May 2018 15:27:13 +0800 Subject: [PATCH 291/492] Preparing Apache Zeppelin release 0.8.0 --- alluxio/pom.xml | 4 ++-- angular/pom.xml | 4 ++-- beam/pom.xml | 4 ++-- bigquery/pom.xml | 4 ++-- cassandra/pom.xml | 4 ++-- elasticsearch/pom.xml | 4 ++-- file/pom.xml | 4 ++-- flink/pom.xml | 4 ++-- geode/pom.xml | 4 ++-- groovy/pom.xml | 4 ++-- hbase/pom.xml | 4 ++-- helium-dev/pom.xml | 4 ++-- ignite/pom.xml | 4 ++-- interpreter-parent/pom.xml | 4 ++-- jdbc/pom.xml | 4 ++-- kylin/pom.xml | 4 ++-- lens/pom.xml | 4 ++-- livy/pom.xml | 4 ++-- markdown/pom.xml | 4 ++-- neo4j/pom.xml | 4 ++-- pig/pom.xml | 4 ++-- pom.xml | 2 +- python/pom.xml | 4 ++-- r/pom.xml | 4 ++-- sap/pom.xml | 4 ++-- scalding/pom.xml | 4 ++-- scio/pom.xml | 4 ++-- shell/pom.xml | 4 ++-- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 4 ++-- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-dependencies/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- zeppelin-display/pom.xml | 4 ++-- zeppelin-distribution/pom.xml | 2 +- zeppelin-examples/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-clock/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-horizontalbar/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-echo/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-markdown/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-translator/pom.xml | 4 ++-- zeppelin-integration/pom.xml | 4 ++-- zeppelin-interpreter/pom.xml | 4 ++-- zeppelin-jupyter/pom.xml | 4 ++-- zeppelin-server/pom.xml | 4 ++-- zeppelin-web/pom.xml | 4 ++-- zeppelin-zengine/pom.xml | 4 ++-- 52 files changed, 102 insertions(+), 102 deletions(-) diff --git a/alluxio/pom.xml b/alluxio/pom.xml index f36494e94c5..b6a4876cd8e 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Alluxio interpreter diff --git a/angular/pom.xml b/angular/pom.xml index 9ff2acb80e4..a44f227d53a 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Angular interpreter diff --git a/beam/pom.xml b/beam/pom.xml index a1d47615355..83b8e0d1480 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Beam interpreter diff --git a/bigquery/pom.xml b/bigquery/pom.xml index db88edafaf6..abe94d28665 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: BigQuery interpreter diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 4b139e9c60a..5e19e0abad4 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-cassandra_2.10 jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index b317e285813..b7bdbf159ae 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent zeppelin-elasticsearch jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Elasticsearch interpreter diff --git a/file/pom.xml b/file/pom.xml index 1a76683215b..d2fbde9c89d 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: File System Interpreters diff --git a/flink/pom.xml b/flink/pom.xml index 5c0cc3bf906..6824b8da75c 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-flink_2.10 jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Flink Zeppelin flink support diff --git a/geode/pom.xml b/geode/pom.xml index 4a9dadd234b..3af4456c967 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Apache Geode interpreter diff --git a/groovy/pom.xml b/groovy/pom.xml index 64c51f85612..8bb49227e72 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Groovy interpreter diff --git a/hbase/pom.xml b/hbase/pom.xml index 8f27631ea2d..d2c4d0962d2 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: HBase interpreter diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index 395f8ffac4f..dba601d341c 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -24,13 +24,13 @@ org.apache.zeppelin interpreter-parent - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Helium development interpreter diff --git a/ignite/pom.xml b/ignite/pom.xml index 5ce6e37296c..49f8ae2ea19 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent zeppelin-ignite_2.10 jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Apache Ignite interpreter diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index c40bc1192ad..1b46aef94ae 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin interpreter-parent pom - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Interpreter Parent diff --git a/jdbc/pom.xml b/jdbc/pom.xml index d5f1b827a52..9a1d82b3d35 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: JDBC interpreter diff --git a/kylin/pom.xml b/kylin/pom.xml index b70facbcc12..0a0acfb4ca9 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -23,7 +23,7 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent 4.0.0 @@ -31,7 +31,7 @@ org.apache.zeppelin zeppelin-kylin jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Kylin interpreter diff --git a/lens/pom.xml b/lens/pom.xml index ecc0a12ed6f..57a96e22933 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Lens interpreter diff --git a/livy/pom.xml b/livy/pom.xml index d33adf6dc91..5295fd1d909 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-livy jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Livy interpreter diff --git a/markdown/pom.xml b/markdown/pom.xml index ca954498075..d8685e7a4f6 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Markdown interpreter diff --git a/neo4j/pom.xml b/neo4j/pom.xml index 298726fb82b..6aa768e2f02 100644 --- a/neo4j/pom.xml +++ b/neo4j/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-neo4j jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Neo4j interpreter diff --git a/pig/pom.xml b/pig/pom.xml index 4a56a342909..a7b19420f37 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org diff --git a/pom.xml b/pom.xml index 16fe86c1192..bbdbd0b3c7a 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin Zeppelin project http://zeppelin.apache.org diff --git a/python/pom.xml b/python/pom.xml index 7f8e671e5c0..88c1d8a875b 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Python interpreter diff --git a/r/pom.xml b/r/pom.xml index e1a02a08dca..1f8bd81a954 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-zrinterpreter_${scala.binary.version} jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: R Interpreter R Interpreter for Zeppelin http://zeppelin.apache.org diff --git a/sap/pom.xml b/sap/pom.xml index 861ade91674..e0f3a90de2a 100644 --- a/sap/pom.xml +++ b/sap/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin sap jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Sap Zeppelin SAP support diff --git a/scalding/pom.xml b/scalding/pom.xml index 763afe0bb41..fa4679ad3ef 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-scalding_2.10 jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Scalding interpreter diff --git a/scio/pom.xml b/scio/pom.xml index 27ccb0cc7b8..a2857ffd005 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-scio_2.10 jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Scio Zeppelin Scio support diff --git a/shell/pom.xml b/shell/pom.xml index 56714f5b159..390f685e8c5 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Shell interpreter diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 1653cd02896..6d258b2892a 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index 186b0501187..d248271be43 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../interpreter-parent/pom.xml org.apache.zeppelin spark-parent pom - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Spark Parent Zeppelin Spark Support diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index 0f8593f0b7c..21abb637e24 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.0-SNAPSHOT + 0.8.0 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.8.0-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Interpreter Scala_2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index d523f5e4c8b..301622d2658 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.0-SNAPSHOT + 0.8.0 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.8.0-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Interpreter Scala_2.11 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index 3b7048acbc8..0b018cef686 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-spark-dependencies jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Spark dependencies Zeppelin spark support diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index 7cf0d14cd7d..30dcd1cceb7 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.8.0-SNAPSHOT + 0.8.0 pom Zeppelin: Spark Scala Parent diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index ca602f3d0b0..17760db77ed 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.8.0-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index 388c4e54231..1b2c0d64360 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.8.0-SNAPSHOT + 0.8.0 jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index d43c35ec0b9..3cbceaea707 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.8.0-SNAPSHOT + 0.8.0 jar Zeppelin: Spark2 Shims diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index ae2292307fc..d553128d13a 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-display jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Display system apis diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index 2238bc9d7af..9fbb6ae04ac 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -23,7 +23,7 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index e9f04731b80..6c0c15d4408 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-examples pom - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Examples Zeppelin examples diff --git a/zeppelin-examples/zeppelin-example-clock/pom.xml b/zeppelin-examples/zeppelin-example-clock/pom.xml index d4fed2171fa..12f7aa8cf96 100644 --- a/zeppelin-examples/zeppelin-example-clock/pom.xml +++ b/zeppelin-examples/zeppelin-example-clock/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index 8e08c4a3af2..60837386383 100644 --- a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml +++ b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Example application - Horizontal Bar chart diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml index 348abd20354..2b04ce9f148 100644 --- a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index b3575c99c6a..83621a87dc8 100644 --- a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index b615eadc8a7..2f8dc8d39a6 100644 --- a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml index 09e6daaad38..04c030be582 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml index 41f88333431..a8fcc7ce33d 100644 --- a/zeppelin-integration/pom.xml +++ b/zeppelin-integration/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-integration jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Integration Test diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index 8df470c22e5..4a491f23f2c 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Interpreter Zeppelin Interpreter diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index eef0e367b95..8c1f52b3cb0 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,13 +24,13 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. zeppelin-jupyter jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index d7a4642990b..35d0129b74d 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-server jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Server diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 2f36d9f1fcb..902c9b80e8b 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-web war - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: web Application diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index 0b165277e7a..cfaf66abd4b 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,14 +23,14 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-zengine jar - 0.8.0-SNAPSHOT + 0.8.0 Zeppelin: Zengine Zeppelin Zengine From 4cc6ff279de12442499acff524e4d7080bacc47d Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Thu, 3 May 2018 15:27:18 +0800 Subject: [PATCH 292/492] Preparing development version 0.8.1-SNAPSHOT --- alluxio/pom.xml | 4 ++-- angular/pom.xml | 4 ++-- beam/pom.xml | 4 ++-- bigquery/pom.xml | 4 ++-- cassandra/pom.xml | 4 ++-- elasticsearch/pom.xml | 4 ++-- file/pom.xml | 4 ++-- flink/pom.xml | 4 ++-- geode/pom.xml | 4 ++-- groovy/pom.xml | 4 ++-- hbase/pom.xml | 4 ++-- helium-dev/pom.xml | 4 ++-- ignite/pom.xml | 4 ++-- interpreter-parent/pom.xml | 4 ++-- jdbc/pom.xml | 4 ++-- kylin/pom.xml | 4 ++-- lens/pom.xml | 4 ++-- livy/pom.xml | 4 ++-- markdown/pom.xml | 4 ++-- neo4j/pom.xml | 4 ++-- pig/pom.xml | 4 ++-- pom.xml | 2 +- python/pom.xml | 4 ++-- r/pom.xml | 4 ++-- sap/pom.xml | 4 ++-- scalding/pom.xml | 4 ++-- scio/pom.xml | 4 ++-- shell/pom.xml | 4 ++-- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 4 ++-- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-dependencies/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- zeppelin-display/pom.xml | 4 ++-- zeppelin-distribution/pom.xml | 2 +- zeppelin-examples/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-clock/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-horizontalbar/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-echo/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-markdown/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-translator/pom.xml | 4 ++-- zeppelin-integration/pom.xml | 4 ++-- zeppelin-interpreter/pom.xml | 4 ++-- zeppelin-jupyter/pom.xml | 4 ++-- zeppelin-server/pom.xml | 4 ++-- zeppelin-web/pom.xml | 4 ++-- zeppelin-zengine/pom.xml | 4 ++-- 52 files changed, 102 insertions(+), 102 deletions(-) diff --git a/alluxio/pom.xml b/alluxio/pom.xml index b6a4876cd8e..acef4942230 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Alluxio interpreter diff --git a/angular/pom.xml b/angular/pom.xml index a44f227d53a..341d7d5ec54 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Angular interpreter diff --git a/beam/pom.xml b/beam/pom.xml index 83b8e0d1480..7592e77e010 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Beam interpreter diff --git a/bigquery/pom.xml b/bigquery/pom.xml index abe94d28665..f3877179a9b 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: BigQuery interpreter diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 5e19e0abad4..2be0606157a 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-cassandra_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index b7bdbf159ae..3b7c2b54b4d 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent zeppelin-elasticsearch jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Elasticsearch interpreter diff --git a/file/pom.xml b/file/pom.xml index d2fbde9c89d..1ea8eace07f 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: File System Interpreters diff --git a/flink/pom.xml b/flink/pom.xml index 6824b8da75c..47bd2877c83 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-flink_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Flink Zeppelin flink support diff --git a/geode/pom.xml b/geode/pom.xml index 3af4456c967..59c16b731b2 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Geode interpreter diff --git a/groovy/pom.xml b/groovy/pom.xml index 8bb49227e72..7dc4282d6e4 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Groovy interpreter diff --git a/hbase/pom.xml b/hbase/pom.xml index d2c4d0962d2..6bf1a80905d 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: HBase interpreter diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index dba601d341c..8445b4dcf9e 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -24,13 +24,13 @@ org.apache.zeppelin interpreter-parent - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Helium development interpreter diff --git a/ignite/pom.xml b/ignite/pom.xml index 49f8ae2ea19..31df05eac77 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent zeppelin-ignite_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Ignite interpreter diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index 1b46aef94ae..35c8fc0e99f 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin interpreter-parent pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Interpreter Parent diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 9a1d82b3d35..58de18199ae 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: JDBC interpreter diff --git a/kylin/pom.xml b/kylin/pom.xml index 0a0acfb4ca9..1690ec2e87f 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -23,7 +23,7 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent 4.0.0 @@ -31,7 +31,7 @@ org.apache.zeppelin zeppelin-kylin jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Kylin interpreter diff --git a/lens/pom.xml b/lens/pom.xml index 57a96e22933..c67828733e0 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Lens interpreter diff --git a/livy/pom.xml b/livy/pom.xml index 5295fd1d909..33c327b979b 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-livy jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Livy interpreter diff --git a/markdown/pom.xml b/markdown/pom.xml index d8685e7a4f6..cf8e766c673 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Markdown interpreter diff --git a/neo4j/pom.xml b/neo4j/pom.xml index 6aa768e2f02..6883bdd996f 100644 --- a/neo4j/pom.xml +++ b/neo4j/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-neo4j jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Neo4j interpreter diff --git a/pig/pom.xml b/pig/pom.xml index a7b19420f37..06a58bbc9ee 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org diff --git a/pom.xml b/pom.xml index bbdbd0b3c7a..d05d16d58b0 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin Zeppelin project http://zeppelin.apache.org diff --git a/python/pom.xml b/python/pom.xml index 88c1d8a875b..03da41cfd25 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Python interpreter diff --git a/r/pom.xml b/r/pom.xml index 1f8bd81a954..fc9fa6621f1 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-zrinterpreter_${scala.binary.version} jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: R Interpreter R Interpreter for Zeppelin http://zeppelin.apache.org diff --git a/sap/pom.xml b/sap/pom.xml index e0f3a90de2a..e75cc10a38f 100644 --- a/sap/pom.xml +++ b/sap/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin sap jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Sap Zeppelin SAP support diff --git a/scalding/pom.xml b/scalding/pom.xml index fa4679ad3ef..051368fa9a2 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-scalding_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Scalding interpreter diff --git a/scio/pom.xml b/scio/pom.xml index a2857ffd005..3c9281c627d 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-scio_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Scio Zeppelin Scio support diff --git a/shell/pom.xml b/shell/pom.xml index 390f685e8c5..cd5a99b4e5f 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Shell interpreter diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 6d258b2892a..2c219a60655 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index d248271be43..5505aa7f57b 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent/pom.xml org.apache.zeppelin spark-parent pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark Parent Zeppelin Spark Support diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index 21abb637e24..9b111da94d5 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Interpreter Scala_2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index 301622d2658..190f7101ca0 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Interpreter Scala_2.11 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index 0b018cef686..6da2ac1a290 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-spark-dependencies jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark dependencies Zeppelin spark support diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index 30dcd1cceb7..c3b03d39e22 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT pom Zeppelin: Spark Scala Parent diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 17760db77ed..81275e47083 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index 1b2c0d64360..458f1d2858b 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index 3cbceaea707..648ff385121 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark2 Shims diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index d553128d13a..5db50f76769 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-display jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Display system apis diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index 9fbb6ae04ac..9545082256f 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -23,7 +23,7 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 6c0c15d4408..85387b707c5 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-examples pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Examples Zeppelin examples diff --git a/zeppelin-examples/zeppelin-example-clock/pom.xml b/zeppelin-examples/zeppelin-example-clock/pom.xml index 12f7aa8cf96..55333aa46ae 100644 --- a/zeppelin-examples/zeppelin-example-clock/pom.xml +++ b/zeppelin-examples/zeppelin-example-clock/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index 60837386383..29f4e381e72 100644 --- a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml +++ b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example application - Horizontal Bar chart diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml index 2b04ce9f148..d76c281f1de 100644 --- a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index 83621a87dc8..d84c887f7b0 100644 --- a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index 2f8dc8d39a6..67604437bcc 100644 --- a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml index 04c030be582..527990924dd 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml index a8fcc7ce33d..a2efef70c32 100644 --- a/zeppelin-integration/pom.xml +++ b/zeppelin-integration/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-integration jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Integration Test diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index 4a491f23f2c..e75e1f8029f 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Interpreter Zeppelin Interpreter diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index 8c1f52b3cb0..bc9ec5a816e 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,13 +24,13 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. zeppelin-jupyter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 35d0129b74d..c11bc423de5 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-server jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Server diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 902c9b80e8b..2bc22c02d5b 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-web war - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: web Application diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index cfaf66abd4b..a438242c4a4 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,14 +23,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-zengine jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Zengine Zeppelin Zengine From cb51be888c8f0b9e2302a478996fcba3319cd6a4 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Fri, 4 May 2018 21:58:11 +0530 Subject: [PATCH 293/492] [ZEPPELIN-3442] bump up commons-collections to 3.2.2 ### What is this PR for? This is to upgrade Apache commons-collections to 3.2.2 ### What type of PR is it? [Improvement] ### What is the Jira issue? * [ZEPPELIN-3442](https://issues.apache.org/jira/browse/ZEPPELIN-3442) ### Questions: * Does the licenses files need update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: Prabhjyot Singh Closes #2958 from prabhjyotsingh/ZEPPELIN-3442 and squashes the following commits: 9cc2e810d [Prabhjyot Singh] [ZEPPELIN-3442] bump up commons-collections to 3.2.2 (cherry picked from commit b9222381bfe4a9e8a055076a05b50fa6e13a0e05) Signed-off-by: Prabhjyot Singh --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d05d16d58b0..74888a05851 100644 --- a/pom.xml +++ b/pom.xml @@ -111,7 +111,7 @@ 1.9 1.5 2.4 - 3.2.1 + 3.2.2 1.1.1 1.3.1 1.3.2 From d41c6343b44592fd473269e29a2f2c4735bf179c Mon Sep 17 00:00:00 2001 From: Mina Lee Date: Fri, 4 May 2018 23:29:29 +0900 Subject: [PATCH 294/492] [ZEPPELIN-3441] Fix license check failure in r rat plugin for r interpreter fail, seems to be related with #2089 It doesn't need to be included in 0.8.0 release, but want it to be cherry-picked into `branch-0.8` for 0.8.x releases, so release manager doesn't have issue with running `dev/publish_release.sh` Hot Fix [ZEPPELIN-3441](https://issues.apache.org/jira/browse/ZEPPELIN-3441) Run `mvn verify -Pr` should pass In CI, first matrix should pass. (https://travis-ci.org/minahlee/zeppelin/builds/374917837) * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Mina Lee Closes #2957 from minahlee/hotfix/ratR and squashes the following commits: 1d4ae6c91 [Mina Lee] Add rat check for r in travis c323c4a8b [Mina Lee] Fix rat check exclude directory for r (cherry picked from commit 3712ce697e5dd5df217280134968155c5d0b1276) Signed-off-by: Jeff Zhang --- .travis.yml | 2 +- pom.xml | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7561a48cc19..0a04458af41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,7 +44,7 @@ matrix: # Test License compliance using RAT tool - jdk: "openjdk7" dist: trusty - env: SCALA_VER="2.11" PROFILE="-Prat" BUILD_FLAG="clean" TEST_FLAG="org.apache.rat:apache-rat-plugin:check" TEST_PROJECTS="" + env: SCALA_VER="2.11" PROFILE="-Prat -Pr" BUILD_FLAG="clean" TEST_FLAG="org.apache.rat:apache-rat-plugin:check" TEST_PROJECTS="" # Run e2e tests (in zeppelin-web) # chrome dropped the support for precise (ubuntu 12.04), so need to use trusty diff --git a/pom.xml b/pom.xml index 74888a05851..e580b59b1b2 100644 --- a/pom.xml +++ b/pom.xml @@ -1071,16 +1071,11 @@ **/R/lib/** - **/r/lib/** + **/lib/rzeppelin/** - + - **/R/rzeppelin/R/globals.R - **/R/rzeppelin/R/common.R - **/R/rzeppelin/R/protocol.R - **/R/rzeppelin/R/rServer.R - **/R/rzeppelin/R/scalaInterpreter.R - **/R/rzeppelin/R/zzz.R + **/R/rzeppelin/R/*.R **/src/main/scala/scala/Console.scala **/src/main/scala/org/apache/zeppelin/rinterpreter/rscala/Package.scala **/src/main/scala/org/apache/zeppelin/rinterpreter/rscala/RClient.scala From df06f4542d5aef31b9a2c3af1d3bf5a4bc213adc Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Wed, 9 May 2018 16:42:48 +0800 Subject: [PATCH 295/492] [HOTFIX] Script update for release --- dev/common_release.sh | 1 - dev/publish_release.sh | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dev/common_release.sh b/dev/common_release.sh index 6b7e901b130..70afd1dfa09 100644 --- a/dev/common_release.sh +++ b/dev/common_release.sh @@ -58,7 +58,6 @@ function git_clone() { # remove unnecessary files rm "${WORKING_DIR}/zeppelin/.gitignore" - rm -rf "${WORKING_DIR}/zeppelin/.git" rm -rf "${WORKING_DIR}/zeppelin/.github" rm -rf "${WORKING_DIR}/zeppelin/docs" } diff --git a/dev/publish_release.sh b/dev/publish_release.sh index ec3c1aaa60f..8fcd6d35d7c 100755 --- a/dev/publish_release.sh +++ b/dev/publish_release.sh @@ -46,7 +46,7 @@ if [[ $RELEASE_VERSION == *"SNAPSHOT"* ]]; then DO_SNAPSHOT="yes" fi -PUBLISH_PROFILES="-Ppublish-distr -Pspark-2.2 -Pr" +PUBLISH_PROFILES="-Pspark-2.2 -Pr" PROJECT_OPTIONS="-pl !zeppelin-distribution" NEXUS_STAGING="https://repository.apache.org/service/local/staging" NEXUS_PROFILE="153446d1ac37c4" @@ -126,7 +126,8 @@ function publish_to_maven() { echo "Created Nexus staging repository: ${staged_repo_id}" - tmp_repo="$(mktemp -d /tmp/zeppelin-repo-XXXXX)" + mkdir /tmp/zeppelin-repo-${RELEASE_VERSION} + tmp_repo=/tmp/zeppelin-repo-${RELEASE_VERSION} # build with scala-2.10 echo "mvn clean install -DskipTests \ From e4a89ba38733569586cde1552ecb32908d6564ec Mon Sep 17 00:00:00 2001 From: iijima_satoshi Date: Sat, 7 Apr 2018 05:32:52 +0900 Subject: [PATCH 296/492] ZEPPELIN-3115 Fix bigquery interpreter to remove TAB at end-of-line Fix bigquery interpreter layout. The current layout is broken due to a tab character at end-of-line. Bug Fix https://issues.apache.org/jira/browse/ZEPPELIN-3115 * Run bigquery interpreter and check the result (Please see below screenshots). Before (from JIRA issues): ![before](https://issues.apache.org/jira/secure/attachment/12903316/Screen%20Shot%202017-12-21%20at%204.58.58%20PM.png) After: ![after](https://raw.githubusercontent.com/iijima-satoshi/okhttp/master/fixed_bigquery_layout.png) * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: iijima_satoshi Closes #2912 from iijima-satoshi/fix-bigquery-layout and squashes the following commits: f193f6a [iijima_satoshi] ZEPPELIN-3115 Add test for bigquery interpreter output dd2a198 [iijima_satoshi] ZEPPELIN-3115 BigQuery interpreter does not render the output of a sql query (only shows column names) (cherry picked from commit 36a5a917371a47eb257e8cfdc44930c0eb65560e) Signed-off-by: Jeff Zhang --- .../zeppelin/bigquery/BigQueryInterpreter.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/bigquery/src/main/java/org/apache/zeppelin/bigquery/BigQueryInterpreter.java b/bigquery/src/main/java/org/apache/zeppelin/bigquery/BigQueryInterpreter.java index 4aff74232a4..a1768996810 100644 --- a/bigquery/src/main/java/org/apache/zeppelin/bigquery/BigQueryInterpreter.java +++ b/bigquery/src/main/java/org/apache/zeppelin/bigquery/BigQueryInterpreter.java @@ -24,7 +24,7 @@ import com.google.api.client.json.JsonFactory; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.json.jackson2.JacksonFactory; - +import com.google.api.client.util.Joiner; import com.google.api.services.bigquery.Bigquery; import com.google.api.services.bigquery.BigqueryScopes; import com.google.api.client.json.GenericJson; @@ -166,19 +166,20 @@ private static Bigquery createAuthorizedClient() throws IOException { //Function that generates and returns the schema and the rows as string public static String printRows(final GetQueryResultsResponse response) { - StringBuilder msg = null; - msg = new StringBuilder(); + StringBuilder msg = new StringBuilder(); try { + List schemNames = new ArrayList(); for (TableFieldSchema schem: response.getSchema().getFields()) { - msg.append(schem.getName()); - msg.append(TAB); - } + schemNames.add(schem.getName()); + } + msg.append(Joiner.on(TAB).join(schemNames)); msg.append(NEWLINE); for (TableRow row : response.getRows()) { + List fieldValues = new ArrayList(); for (TableCell field : row.getF()) { - msg.append(field.getV().toString()); - msg.append(TAB); + fieldValues.add(field.getV().toString()); } + msg.append(Joiner.on(TAB).join(fieldValues)); msg.append(NEWLINE); } return msg.toString(); From de1a25c73b35a3873638e80cb35bafd96e5b19a0 Mon Sep 17 00:00:00 2001 From: iijima_satoshi Date: Fri, 9 Mar 2018 19:15:24 +0900 Subject: [PATCH 297/492] ZEPPELIN-3310. Scio interpreter layout is broken ### What is this PR for? Fix scio interpreter layout. The current scio interpreter layout is broken because there is not a newline between %table part and %text part in this interpreter output. ### What type of PR is it? [Bug Fix] ### What is the Jira issue? [ZEPPELIN-3310](https://issues.apache.org/jira/browse/ZEPPELIN-3310) ### How should this be tested? * CI should pass * View the snapshot ### Screenshots (if appropriate) [Before applied this change] ![Interceptors Diagram](https://raw.githubusercontent.com/iijima-satoshi/okhttp/master/scio_interpreter_layout_is_broken.png) [Ater applied this change] ![Interceptors Diagram](https://raw.githubusercontent.com/iijima-satoshi/okhttp/master/screenshot_applied_patch.png) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: iijima_satoshi Closes #2854 from iijima-satoshi/fix-scio-interpreter-layout and squashes the following commits: 2b4fd68 [iijima_satoshi] ZEPPELIN-3310. Scio interpreter layout is broken (cherry picked from commit 2a5960bd50ea29ce7b925a6ac3288c0fd5ae7ddd) Signed-off-by: Jeff Zhang --- .../apache/zeppelin/scio/DisplayHelpers.scala | 2 +- .../zeppelin/scio/DisplayHelpersTest.scala | 103 ++++++++---------- 2 files changed, 47 insertions(+), 58 deletions(-) diff --git a/scio/src/main/scala/org/apache/zeppelin/scio/DisplayHelpers.scala b/scio/src/main/scala/org/apache/zeppelin/scio/DisplayHelpers.scala index 8dee3abfe92..bfb4f9c73a8 100644 --- a/scio/src/main/scala/org/apache/zeppelin/scio/DisplayHelpers.scala +++ b/scio/src/main/scala/org/apache/zeppelin/scio/DisplayHelpers.scala @@ -35,7 +35,7 @@ private[scio] object DisplayHelpers { private[scio] val tab = "\t" private[scio] val newline = "\n" private[scio] val table = "%table" - private[scio] val endTable = "%text" + private[scio] val endTable = "\n%text" private[scio] val rowLimitReachedMsg = s"$newlineResults are limited to " + maxResults + s" rows.$newline" private[scio] val bQSchemaIncomplete = diff --git a/scio/src/test/scala/org/apache/zeppelin/scio/DisplayHelpersTest.scala b/scio/src/test/scala/org/apache/zeppelin/scio/DisplayHelpersTest.scala index 6dd05ab6868..a197fafc2d1 100644 --- a/scio/src/test/scala/org/apache/zeppelin/scio/DisplayHelpersTest.scala +++ b/scio/src/test/scala/org/apache/zeppelin/scio/DisplayHelpersTest.scala @@ -48,7 +48,8 @@ class DisplayHelpersTest extends FlatSpec with Matchers { // ----------------------------------------------------------------------------------------------- private val anyValHeader = s"$table value" - private val endTable = DisplayHelpers.endTable + private val endTableFooter = DisplayHelpers.endTable.split("\\n").last + private val endTableSeq = Seq("", endTableFooter) "DisplayHelpers" should "support Integer SCollection via AnyVal" in { import org.apache.zeppelin.scio.DisplaySCollectionImplicits.ZeppelinSCollection @@ -60,10 +61,9 @@ class DisplayHelpersTest extends FlatSpec with Matchers { o should contain theSameElementsAs Seq(anyValHeader, "1", "2", - "3", - endTable) + "3") ++ endTableSeq o.head should be(anyValHeader) - o.last should be(endTable) + o.last should be(endTableFooter) } it should "support Long SCollection via AnyVal" in { @@ -76,10 +76,9 @@ class DisplayHelpersTest extends FlatSpec with Matchers { o should contain theSameElementsAs Seq(anyValHeader, "1", "2", - "3", - endTable) + "3") ++ endTableSeq o.head should be(anyValHeader) - o.last should be(endTable) + o.last should be(endTableFooter) } it should "support Double SCollection via AnyVal" in { @@ -92,10 +91,9 @@ class DisplayHelpersTest extends FlatSpec with Matchers { o should contain theSameElementsAs Seq(anyValHeader, "1.0", "2.0", - "3.0", - endTable) + "3.0") ++ endTableSeq o.head should be(anyValHeader) - o.last should be(endTable) + o.last should be(endTableFooter) } it should "support Float SCollection via AnyVal" in { @@ -108,10 +106,9 @@ class DisplayHelpersTest extends FlatSpec with Matchers { o should contain theSameElementsAs Seq(anyValHeader, "1.0", "2.0", - "3.0", - endTable) + "3.0") ++ endTableSeq o.head should be(anyValHeader) - o.last should be(endTable) + o.last should be(endTableFooter) } it should "support Short SCollection via AnyVal" in { @@ -124,10 +121,9 @@ class DisplayHelpersTest extends FlatSpec with Matchers { o should contain theSameElementsAs Seq(anyValHeader, "1", "2", - "3", - endTable) + "3") ++ endTableSeq o.head should be(anyValHeader) - o.last should be(endTable) + o.last should be(endTableFooter) } it should "support Byte SCollection via AnyVal" in { @@ -140,10 +136,9 @@ class DisplayHelpersTest extends FlatSpec with Matchers { o should contain theSameElementsAs Seq(anyValHeader, "1", "2", - "3", - endTable) + "3") ++ endTableSeq o.head should be(anyValHeader) - o.last should be(endTable) + o.last should be(endTableFooter) } it should "support Boolean SCollection via AnyVal" in { @@ -156,10 +151,9 @@ class DisplayHelpersTest extends FlatSpec with Matchers { o should contain theSameElementsAs Seq(anyValHeader, "true", "false", - "true", - endTable) + "true") ++ endTableSeq o.head should be(anyValHeader) - o.last should be(endTable) + o.last should be(endTableFooter) } it should "support Char SCollection via AnyVal" in { @@ -172,10 +166,9 @@ class DisplayHelpersTest extends FlatSpec with Matchers { o should contain theSameElementsAs Seq(anyValHeader, "a", "b", - "c", - endTable) + "c") ++ endTableSeq o.head should be(anyValHeader) - o.last should be(endTable) + o.last should be(endTableFooter) } it should "support SCollection of AnyVal over row limit" in { @@ -216,10 +209,9 @@ class DisplayHelpersTest extends FlatSpec with Matchers { o should contain theSameElementsAs Seq(stringHeader, "a", "b", - "c", - endTable) + "c") ++ endTableSeq o.head should be (stringHeader) - o.last should be (endTable) + o.last should be (endTableFooter) } it should "support empty SCollection of String" in { @@ -259,10 +251,9 @@ class DisplayHelpersTest extends FlatSpec with Matchers { } o should contain theSameElementsAs Seq(kvHeader, s"3${tab}4", - s"1${tab}2", - endTable) + s"1${tab}2") ++ endTableSeq o.head should be (kvHeader) - o.last should be (endTable) + o.last should be (endTableFooter) } it should "support KV (str keys) SCollection" in { @@ -274,10 +265,9 @@ class DisplayHelpersTest extends FlatSpec with Matchers { } o should contain theSameElementsAs Seq(kvHeader, s"foo${tab}2", - s"bar${tab}4", - endTable) + s"bar${tab}4") ++ endTableSeq o.head should be (kvHeader) - o.last should be (endTable) + o.last should be (endTableFooter) } it should "support KV (str values) SCollection" in { @@ -289,10 +279,9 @@ class DisplayHelpersTest extends FlatSpec with Matchers { } o should contain theSameElementsAs Seq(kvHeader, s"2${tab}foo", - s"4${tab}bar", - endTable) + s"4${tab}bar") ++ endTableSeq o.head should be (kvHeader) - o.last should be (endTable) + o.last should be (endTableFooter) } it should "support empty KV SCollection" in { @@ -331,9 +320,9 @@ class DisplayHelpersTest extends FlatSpec with Matchers { } } o should contain theSameElementsAs - (Seq(tupleHeader, endTable) ++ Seq.fill(3)(s"1${tab}2${tab}3")) + (Seq(tupleHeader) ++ Seq.fill(3)(s"1${tab}2${tab}3") ++ endTableSeq) o.head should be(tupleHeader) - o.last should be (endTable) + o.last should be (endTableFooter) } it should "support SCollection of Tuple of 22" in { @@ -345,10 +334,10 @@ class DisplayHelpersTest extends FlatSpec with Matchers { in.closeAndDisplay() } } - o should contain theSameElementsAs (Seq(tupleHeader, endTable) ++ - Seq.fill(3)((1 to 21).map(i => s"$i$tab").mkString + "22")) + o should contain theSameElementsAs (Seq(tupleHeader) ++ + Seq.fill(3)((1 to 21).map(i => s"$i$tab").mkString + "22") ++ endTableSeq) o.head should be(tupleHeader) - o.last should be (endTable) + o.last should be (endTableFooter) } it should "support SCollection of Case Class of 22" in { @@ -360,10 +349,10 @@ class DisplayHelpersTest extends FlatSpec with Matchers { in.closeAndDisplay() } } - o should contain theSameElementsAs (Seq(tupleHeader, endTable) ++ - Seq.fill(3)((1 to 21).map(i => s"$i$tab").mkString + "22")) + o should contain theSameElementsAs (Seq(tupleHeader) ++ + Seq.fill(3)((1 to 21).map(i => s"$i$tab").mkString + "22") ++ endTableSeq) o.head should be(tupleHeader) - o.last should be (endTable) + o.last should be (endTableFooter) } it should "support SCollection of Case Class" in { @@ -373,10 +362,10 @@ class DisplayHelpersTest extends FlatSpec with Matchers { in.closeAndDisplay() } } - o should contain theSameElementsAs (Seq(testCaseClassHeader, endTable) ++ - Seq.fill(3)(s"1${tab}foo${tab}2.0")) + o should contain theSameElementsAs (Seq(testCaseClassHeader) ++ + Seq.fill(3)(s"1${tab}foo${tab}2.0") ++ endTableSeq) o.head should be(testCaseClassHeader) - o.last should be (endTable) + o.last should be (endTableFooter) } it should "support empty SCollection of Product" in { @@ -453,10 +442,10 @@ class DisplayHelpersTest extends FlatSpec with Matchers { in.closeAndDisplay() } } - o should contain theSameElementsAs (Seq(avroGenericRecordHeader, endTable) ++ - Seq.fill(3)(s"1${tab}1.0${tab}user1${tab}checking")) + o should contain theSameElementsAs (Seq(avroGenericRecordHeader) ++ + Seq.fill(3)(s"1${tab}1.0${tab}user1${tab}checking") ++ endTableSeq) o.head should be(avroGenericRecordHeader) - o.last should be (endTable) + o.last should be (endTableFooter) } it should "support SCollection of SpecificRecord Avro" in { @@ -467,10 +456,10 @@ class DisplayHelpersTest extends FlatSpec with Matchers { in.closeAndDisplay() } } - o should contain theSameElementsAs (Seq(avroAccountHeader, endTable) ++ - Seq.fill(3)(s"2${tab}checking${tab}user2${tab}2.0")) + o should contain theSameElementsAs (Seq(avroAccountHeader) ++ + Seq.fill(3)(s"2${tab}checking${tab}user2${tab}2.0") ++ endTableSeq) o.head should be(avroAccountHeader) - o.last should be (endTable) + o.last should be (endTableFooter) } it should "support empty SCollection of SpecificRecord Avro" in { @@ -541,10 +530,10 @@ class DisplayHelpersTest extends FlatSpec with Matchers { in.closeAndDisplay(bQSchema) } } - o should contain theSameElementsAs (Seq(bQHeader, endTable) ++ - Seq.fill(3)(s"3${tab}3.0${tab}checking${tab}user3")) + o should contain theSameElementsAs (Seq(bQHeader) ++ + Seq.fill(3)(s"3${tab}3.0${tab}checking${tab}user3") ++ endTableSeq) o.head should be(bQHeader) - o.last should be (endTable) + o.last should be (endTableFooter) } it should "print error on empty BQ schema" in { From 1b4d376394b7a2d3345394c1fcf2f776c1bab5d7 Mon Sep 17 00:00:00 2001 From: Sanjay Dasgupta Date: Sat, 5 May 2018 08:31:14 +0530 Subject: [PATCH 298/492] [Zeppelin 3388] Correcting documentation link to zeppelin-context documentation ### What is this PR for? A small change was needed in the documentation of the JDBC interpreter also, but had been missed. The change required is the same as the change made in the documentation of the Shell interpreter (as seen [here](https://github.com/apache/zeppelin/commit/0c3260e91f4c1ec58e67856d466ea9cba89d0085#diff-5a017ebfb7e890a2b475f9c8c7844fb0)) ### What type of PR is it? [Documentation] ### Todos * [ ] - Task ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-3388 ### How should this be tested? Documentation only. ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No, this _is_ documentation. Author: Sanjay Dasgupta Closes #2960 from sanjaydasgupta/z-3388-jdbc and squashes the following commits: b43ff21 [Sanjay Dasgupta] Correcting link to zeppelin-context documentation (cherry picked from commit 941ccbbbdb6c2ca11f09f54bf0ddd31d6ba25d1c) Signed-off-by: Felix Cheung --- docs/interpreter/jdbc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/interpreter/jdbc.md b/docs/interpreter/jdbc.md index 2f64da97963..aee9c4ec1f7 100644 --- a/docs/interpreter/jdbc.md +++ b/docs/interpreter/jdbc.md @@ -753,7 +753,7 @@ z.put("country_code", "KR") Object interpolation is disabled by default, and can be enabled for all instances of the JDBC interpreter by setting the value of the property `zeppelin.jdbc.interpolation` to `true` (see _More Properties_ above). More details of this feature can be found in the Spark interpreter documentation under -[Object Interpolation](spark.html#object-interpolation) +[Zeppelin-Context](../usage/other_features/zeppelin_context.html) ## Bug reporting If you find a bug using JDBC interpreter, please create a [JIRA](https://issues.apache.org/jira/browse/ZEPPELIN) ticket. From 15090d3bef8540af41184b0a1a8f0fd0a4d94c97 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Tue, 8 May 2018 14:18:35 +0530 Subject: [PATCH 299/492] [ZEPPELIN-3447] Bump up version of jackson-databind This is to upgrade the package jackson-databind libary to 2.27 [Improvement] * [ZEPPELIN-3447](https://issues.apache.org/jira/projects/ZEPPELIN/issues/ZEPPELIN-3447) * Does the licenses files need an update? N?A * Is there breaking changes for older versions? N?A * Does this needs documentation? N?A Author: Prabhjyot Singh Closes #2962 from prabhjyotsingh/ZEPPELIN-3447 and squashes the following commits: a9d2abcdf [Prabhjyot Singh] ZEPPELIN-3447: Bump up version of jackson-databind Change-Id: I5f8ff956ba147d24e43af81076e3e869de1bbab0 (cherry picked from commit c1aeb620bb0030638624daf19d711a2bec8eab69) Signed-off-by: Prabhjyot Singh --- zeppelin-server/pom.xml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index c11bc423de5..165123c9d80 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -36,10 +36,10 @@ 4.3.6 - 2.22.2 + 2.27 2.2.1 1.13 - 2.0.1 + 2.1 1.8 4.1.0 @@ -143,8 +143,22 @@ com.fasterxml.jackson.core jackson-annotations + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-databind + 2.8.11.1 + + + org.glassfish.jersey.inject + jersey-hk2 + ${jersey.version} + org.glassfish.jersey.core jersey-server @@ -205,7 +219,7 @@ com.fasterxml.jackson.core jackson-annotations - 2.5.4 + 2.8.0 From b396df0f360001bad62be2180e36ada531d96e07 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Sat, 12 May 2018 22:07:49 +0800 Subject: [PATCH 300/492] ZEPPELIN-3419. Potential dependency conflict when the version of a dependency is changed on zeppelin interpreters ### What is this PR for? Straightforward fix for this issue. Credit to Jhon Anderson Cardenas Diaz ### What type of PR is it? [Bug Fix] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3419 ### How should this be tested? * CI pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2966 from zjffdu/ZEPPELIN-3419 and squashes the following commits: baeff3a4d [Jeff Zhang] ZEPPELIN-3419. Potential dependency conflict when the version of a dependency is changed on zeppelin interpreters (cherry picked from commit 2eb084a221377ac905aafda51471708314b26a0f) Signed-off-by: Jeff Zhang --- .../org/apache/zeppelin/interpreter/InterpreterSetting.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java index 816499c2a16..2eb634b7db7 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java @@ -808,7 +808,7 @@ private void loadInterpreterDependencies() { public void run() { try { // dependencies to prevent library conflict - File localRepoDir = new File(conf.getInterpreterLocalRepoPath() + "/" + getId()); + File localRepoDir = new File(conf.getInterpreterLocalRepoPath() + "/" + id); if (localRepoDir.exists()) { try { FileUtils.forceDelete(localRepoDir); From a88e4679a2f28a914fa181ad2df55e3744a8ff6b Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Mon, 14 May 2018 10:38:25 +0800 Subject: [PATCH 301/492] Preparing Apache Zeppelin release 0.8.0 --- alluxio/pom.xml | 4 ++-- angular/pom.xml | 4 ++-- beam/pom.xml | 4 ++-- bigquery/pom.xml | 4 ++-- cassandra/pom.xml | 4 ++-- elasticsearch/pom.xml | 4 ++-- file/pom.xml | 4 ++-- flink/pom.xml | 4 ++-- geode/pom.xml | 4 ++-- groovy/pom.xml | 4 ++-- hbase/pom.xml | 4 ++-- helium-dev/pom.xml | 4 ++-- ignite/pom.xml | 4 ++-- interpreter-parent/pom.xml | 4 ++-- jdbc/pom.xml | 4 ++-- kylin/pom.xml | 4 ++-- lens/pom.xml | 4 ++-- livy/pom.xml | 4 ++-- markdown/pom.xml | 4 ++-- neo4j/pom.xml | 4 ++-- pig/pom.xml | 4 ++-- pom.xml | 2 +- python/pom.xml | 4 ++-- r/pom.xml | 4 ++-- sap/pom.xml | 4 ++-- scalding/pom.xml | 4 ++-- scio/pom.xml | 4 ++-- shell/pom.xml | 4 ++-- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 4 ++-- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-dependencies/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- zeppelin-display/pom.xml | 4 ++-- zeppelin-distribution/pom.xml | 2 +- zeppelin-examples/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-clock/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-horizontalbar/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-echo/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-markdown/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-translator/pom.xml | 4 ++-- zeppelin-integration/pom.xml | 4 ++-- zeppelin-interpreter/pom.xml | 4 ++-- zeppelin-jupyter/pom.xml | 4 ++-- zeppelin-server/pom.xml | 4 ++-- zeppelin-web/pom.xml | 4 ++-- zeppelin-zengine/pom.xml | 4 ++-- 52 files changed, 102 insertions(+), 102 deletions(-) diff --git a/alluxio/pom.xml b/alluxio/pom.xml index acef4942230..b6a4876cd8e 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Alluxio interpreter diff --git a/angular/pom.xml b/angular/pom.xml index 341d7d5ec54..a44f227d53a 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Angular interpreter diff --git a/beam/pom.xml b/beam/pom.xml index 7592e77e010..83b8e0d1480 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Beam interpreter diff --git a/bigquery/pom.xml b/bigquery/pom.xml index f3877179a9b..abe94d28665 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: BigQuery interpreter diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 2be0606157a..5e19e0abad4 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-cassandra_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index 3b7c2b54b4d..b7bdbf159ae 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent zeppelin-elasticsearch jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Elasticsearch interpreter diff --git a/file/pom.xml b/file/pom.xml index 1ea8eace07f..d2fbde9c89d 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: File System Interpreters diff --git a/flink/pom.xml b/flink/pom.xml index 47bd2877c83..6824b8da75c 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-flink_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Flink Zeppelin flink support diff --git a/geode/pom.xml b/geode/pom.xml index 59c16b731b2..3af4456c967 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Geode interpreter diff --git a/groovy/pom.xml b/groovy/pom.xml index 7dc4282d6e4..8bb49227e72 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Groovy interpreter diff --git a/hbase/pom.xml b/hbase/pom.xml index 6bf1a80905d..d2c4d0962d2 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: HBase interpreter diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index 8445b4dcf9e..dba601d341c 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -24,13 +24,13 @@ org.apache.zeppelin interpreter-parent - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Helium development interpreter diff --git a/ignite/pom.xml b/ignite/pom.xml index 31df05eac77..49f8ae2ea19 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent zeppelin-ignite_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Ignite interpreter diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index 35c8fc0e99f..1b46aef94ae 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin interpreter-parent pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Interpreter Parent diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 58de18199ae..9a1d82b3d35 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: JDBC interpreter diff --git a/kylin/pom.xml b/kylin/pom.xml index 1690ec2e87f..0a0acfb4ca9 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -23,7 +23,7 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent 4.0.0 @@ -31,7 +31,7 @@ org.apache.zeppelin zeppelin-kylin jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Kylin interpreter diff --git a/lens/pom.xml b/lens/pom.xml index c67828733e0..57a96e22933 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Lens interpreter diff --git a/livy/pom.xml b/livy/pom.xml index 33c327b979b..5295fd1d909 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-livy jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Livy interpreter diff --git a/markdown/pom.xml b/markdown/pom.xml index cf8e766c673..d8685e7a4f6 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Markdown interpreter diff --git a/neo4j/pom.xml b/neo4j/pom.xml index 6883bdd996f..6aa768e2f02 100644 --- a/neo4j/pom.xml +++ b/neo4j/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-neo4j jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Neo4j interpreter diff --git a/pig/pom.xml b/pig/pom.xml index 06a58bbc9ee..a7b19420f37 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org diff --git a/pom.xml b/pom.xml index e580b59b1b2..8a41d843007 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin Zeppelin project http://zeppelin.apache.org diff --git a/python/pom.xml b/python/pom.xml index 03da41cfd25..88c1d8a875b 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Python interpreter diff --git a/r/pom.xml b/r/pom.xml index fc9fa6621f1..1f8bd81a954 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-zrinterpreter_${scala.binary.version} jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: R Interpreter R Interpreter for Zeppelin http://zeppelin.apache.org diff --git a/sap/pom.xml b/sap/pom.xml index e75cc10a38f..e0f3a90de2a 100644 --- a/sap/pom.xml +++ b/sap/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin sap jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Sap Zeppelin SAP support diff --git a/scalding/pom.xml b/scalding/pom.xml index 051368fa9a2..fa4679ad3ef 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-scalding_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Scalding interpreter diff --git a/scio/pom.xml b/scio/pom.xml index 3c9281c627d..a2857ffd005 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-scio_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Scio Zeppelin Scio support diff --git a/shell/pom.xml b/shell/pom.xml index cd5a99b4e5f..390f685e8c5 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Shell interpreter diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 2c219a60655..6d258b2892a 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index 5505aa7f57b..d248271be43 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent/pom.xml org.apache.zeppelin spark-parent pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Spark Parent Zeppelin Spark Support diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index 9b111da94d5..21abb637e24 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Interpreter Scala_2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index 190f7101ca0..301622d2658 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Interpreter Scala_2.11 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index 6da2ac1a290..0b018cef686 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-spark-dependencies jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Spark dependencies Zeppelin spark support diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index c3b03d39e22..30dcd1cceb7 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0 pom Zeppelin: Spark Scala Parent diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 81275e47083..17760db77ed 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index 458f1d2858b..1b2c0d64360 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index 648ff385121..3cbceaea707 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark2 Shims diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index 5db50f76769..d553128d13a 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-display jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Display system apis diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index 9545082256f..9fbb6ae04ac 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -23,7 +23,7 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 85387b707c5..6c0c15d4408 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-examples pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Examples Zeppelin examples diff --git a/zeppelin-examples/zeppelin-example-clock/pom.xml b/zeppelin-examples/zeppelin-example-clock/pom.xml index 55333aa46ae..12f7aa8cf96 100644 --- a/zeppelin-examples/zeppelin-example-clock/pom.xml +++ b/zeppelin-examples/zeppelin-example-clock/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index 29f4e381e72..60837386383 100644 --- a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml +++ b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example application - Horizontal Bar chart diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml index d76c281f1de..2b04ce9f148 100644 --- a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index d84c887f7b0..83621a87dc8 100644 --- a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index 67604437bcc..2f8dc8d39a6 100644 --- a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml index 527990924dd..04c030be582 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml index a2efef70c32..a8fcc7ce33d 100644 --- a/zeppelin-integration/pom.xml +++ b/zeppelin-integration/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-integration jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Integration Test diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index e75e1f8029f..4a491f23f2c 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Interpreter Zeppelin Interpreter diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index bc9ec5a816e..8c1f52b3cb0 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,13 +24,13 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. zeppelin-jupyter jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 165123c9d80..248aa90cd33 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-server jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Server diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 2bc22c02d5b..902c9b80e8b 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-web war - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: web Application diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index a438242c4a4..cfaf66abd4b 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,14 +23,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-zengine jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Zengine Zeppelin Zengine From 1f1b99b696787f990c48a63f7be45f1c8d77c3cf Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Mon, 14 May 2018 10:38:30 +0800 Subject: [PATCH 302/492] Preparing development version 0.8.1-SNAPSHOT --- alluxio/pom.xml | 4 ++-- angular/pom.xml | 4 ++-- beam/pom.xml | 4 ++-- bigquery/pom.xml | 4 ++-- cassandra/pom.xml | 4 ++-- elasticsearch/pom.xml | 4 ++-- file/pom.xml | 4 ++-- flink/pom.xml | 4 ++-- geode/pom.xml | 4 ++-- groovy/pom.xml | 4 ++-- hbase/pom.xml | 4 ++-- helium-dev/pom.xml | 4 ++-- ignite/pom.xml | 4 ++-- interpreter-parent/pom.xml | 4 ++-- jdbc/pom.xml | 4 ++-- kylin/pom.xml | 4 ++-- lens/pom.xml | 4 ++-- livy/pom.xml | 4 ++-- markdown/pom.xml | 4 ++-- neo4j/pom.xml | 4 ++-- pig/pom.xml | 4 ++-- pom.xml | 2 +- python/pom.xml | 4 ++-- r/pom.xml | 4 ++-- sap/pom.xml | 4 ++-- scalding/pom.xml | 4 ++-- scio/pom.xml | 4 ++-- shell/pom.xml | 4 ++-- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 4 ++-- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-dependencies/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- zeppelin-display/pom.xml | 4 ++-- zeppelin-distribution/pom.xml | 2 +- zeppelin-examples/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-clock/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-horizontalbar/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-echo/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-markdown/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-translator/pom.xml | 4 ++-- zeppelin-integration/pom.xml | 4 ++-- zeppelin-interpreter/pom.xml | 4 ++-- zeppelin-jupyter/pom.xml | 4 ++-- zeppelin-server/pom.xml | 4 ++-- zeppelin-web/pom.xml | 4 ++-- zeppelin-zengine/pom.xml | 4 ++-- 52 files changed, 102 insertions(+), 102 deletions(-) diff --git a/alluxio/pom.xml b/alluxio/pom.xml index b6a4876cd8e..acef4942230 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Alluxio interpreter diff --git a/angular/pom.xml b/angular/pom.xml index a44f227d53a..341d7d5ec54 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Angular interpreter diff --git a/beam/pom.xml b/beam/pom.xml index 83b8e0d1480..7592e77e010 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Beam interpreter diff --git a/bigquery/pom.xml b/bigquery/pom.xml index abe94d28665..f3877179a9b 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: BigQuery interpreter diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 5e19e0abad4..2be0606157a 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-cassandra_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index b7bdbf159ae..3b7c2b54b4d 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent zeppelin-elasticsearch jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Elasticsearch interpreter diff --git a/file/pom.xml b/file/pom.xml index d2fbde9c89d..1ea8eace07f 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: File System Interpreters diff --git a/flink/pom.xml b/flink/pom.xml index 6824b8da75c..47bd2877c83 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-flink_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Flink Zeppelin flink support diff --git a/geode/pom.xml b/geode/pom.xml index 3af4456c967..59c16b731b2 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Geode interpreter diff --git a/groovy/pom.xml b/groovy/pom.xml index 8bb49227e72..7dc4282d6e4 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Groovy interpreter diff --git a/hbase/pom.xml b/hbase/pom.xml index d2c4d0962d2..6bf1a80905d 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: HBase interpreter diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index dba601d341c..8445b4dcf9e 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -24,13 +24,13 @@ org.apache.zeppelin interpreter-parent - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Helium development interpreter diff --git a/ignite/pom.xml b/ignite/pom.xml index 49f8ae2ea19..31df05eac77 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent zeppelin-ignite_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Ignite interpreter diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index 1b46aef94ae..35c8fc0e99f 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin interpreter-parent pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Interpreter Parent diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 9a1d82b3d35..58de18199ae 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: JDBC interpreter diff --git a/kylin/pom.xml b/kylin/pom.xml index 0a0acfb4ca9..1690ec2e87f 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -23,7 +23,7 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent 4.0.0 @@ -31,7 +31,7 @@ org.apache.zeppelin zeppelin-kylin jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Kylin interpreter diff --git a/lens/pom.xml b/lens/pom.xml index 57a96e22933..c67828733e0 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Lens interpreter diff --git a/livy/pom.xml b/livy/pom.xml index 5295fd1d909..33c327b979b 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-livy jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Livy interpreter diff --git a/markdown/pom.xml b/markdown/pom.xml index d8685e7a4f6..cf8e766c673 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Markdown interpreter diff --git a/neo4j/pom.xml b/neo4j/pom.xml index 6aa768e2f02..6883bdd996f 100644 --- a/neo4j/pom.xml +++ b/neo4j/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-neo4j jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Neo4j interpreter diff --git a/pig/pom.xml b/pig/pom.xml index a7b19420f37..06a58bbc9ee 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org diff --git a/pom.xml b/pom.xml index 8a41d843007..e580b59b1b2 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin Zeppelin project http://zeppelin.apache.org diff --git a/python/pom.xml b/python/pom.xml index 88c1d8a875b..03da41cfd25 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Python interpreter diff --git a/r/pom.xml b/r/pom.xml index 1f8bd81a954..fc9fa6621f1 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-zrinterpreter_${scala.binary.version} jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: R Interpreter R Interpreter for Zeppelin http://zeppelin.apache.org diff --git a/sap/pom.xml b/sap/pom.xml index e0f3a90de2a..e75cc10a38f 100644 --- a/sap/pom.xml +++ b/sap/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin sap jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Sap Zeppelin SAP support diff --git a/scalding/pom.xml b/scalding/pom.xml index fa4679ad3ef..051368fa9a2 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-scalding_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Scalding interpreter diff --git a/scio/pom.xml b/scio/pom.xml index a2857ffd005..3c9281c627d 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-scio_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Scio Zeppelin Scio support diff --git a/shell/pom.xml b/shell/pom.xml index 390f685e8c5..cd5a99b4e5f 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Shell interpreter diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 6d258b2892a..2c219a60655 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index d248271be43..5505aa7f57b 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent/pom.xml org.apache.zeppelin spark-parent pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark Parent Zeppelin Spark Support diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index 21abb637e24..9b111da94d5 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Interpreter Scala_2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index 301622d2658..190f7101ca0 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Interpreter Scala_2.11 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index 0b018cef686..6da2ac1a290 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-spark-dependencies jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark dependencies Zeppelin spark support diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index 30dcd1cceb7..c3b03d39e22 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT pom Zeppelin: Spark Scala Parent diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 17760db77ed..81275e47083 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index 1b2c0d64360..458f1d2858b 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index 3cbceaea707..648ff385121 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark2 Shims diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index d553128d13a..5db50f76769 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-display jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Display system apis diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index 9fbb6ae04ac..9545082256f 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -23,7 +23,7 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 6c0c15d4408..85387b707c5 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-examples pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Examples Zeppelin examples diff --git a/zeppelin-examples/zeppelin-example-clock/pom.xml b/zeppelin-examples/zeppelin-example-clock/pom.xml index 12f7aa8cf96..55333aa46ae 100644 --- a/zeppelin-examples/zeppelin-example-clock/pom.xml +++ b/zeppelin-examples/zeppelin-example-clock/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index 60837386383..29f4e381e72 100644 --- a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml +++ b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example application - Horizontal Bar chart diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml index 2b04ce9f148..d76c281f1de 100644 --- a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index 83621a87dc8..d84c887f7b0 100644 --- a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index 2f8dc8d39a6..67604437bcc 100644 --- a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml index 04c030be582..527990924dd 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml index a8fcc7ce33d..a2efef70c32 100644 --- a/zeppelin-integration/pom.xml +++ b/zeppelin-integration/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-integration jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Integration Test diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index 4a491f23f2c..e75e1f8029f 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Interpreter Zeppelin Interpreter diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index 8c1f52b3cb0..bc9ec5a816e 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,13 +24,13 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. zeppelin-jupyter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 248aa90cd33..165123c9d80 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-server jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Server diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 902c9b80e8b..2bc22c02d5b 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-web war - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: web Application diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index cfaf66abd4b..a438242c4a4 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,14 +23,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-zengine jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Zengine Zeppelin Zengine From 7b599817bb20cde83deea1701b692982d10de22c Mon Sep 17 00:00:00 2001 From: Vi On Date: Tue, 15 May 2018 16:09:17 -0700 Subject: [PATCH 303/492] ZEPPELIN-3463 pip freeze statsmodel 0.8.0 ### What is this PR for? pip freeze statsmodel 0.8.0. The new version statsmodel 0.9.0 fails to install using install_external_dependencies.sh ### What type of PR is it? Bug Fix ### What is the Jira issue? * [ZEPPELIN-3463] * https://issues.apache.org/jira/browse/ZEPPELIN-3463 ### How should this be tested? * sh testing/install_external_dependencies.sh ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Vi On Closes #2969 from vion1/ZEPPELIN-3463 and squashes the following commits: 084cda8c2 [Vi On] ZEPPELIN-3463 pip freeze statsmodel 0.8.0 (cherry picked from commit c037378a06f326aeb83afae0bac2964eb6c73106) Signed-off-by: Jeff Zhang --- testing/install_external_dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/install_external_dependencies.sh b/testing/install_external_dependencies.sh index d0b0f638bb2..d59f9c1c371 100755 --- a/testing/install_external_dependencies.sh +++ b/testing/install_external_dependencies.sh @@ -46,5 +46,5 @@ if [[ -n "$PYTHON" ]] ; then conda info -a conda config --add channels conda-forge conda install -q matplotlib=2.1.2 pandasql ipython=5.4.1 jupyter_client ipykernel matplotlib bokeh=0.12.10 - pip install -q grpcio ggplot bkzep==0.4.0 + pip install -q grpcio ggplot bkzep==0.4.0 statsmodels==0.8.0 fi From 8756ea796a8d56a41afc6ad9f5c6964f3e92cfbb Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Wed, 16 May 2018 23:08:47 +0530 Subject: [PATCH 304/492] [ZEPPELIN-3466] Table export to excel is not working due to missing dependencies Table export to excel is not working due to missing dependencies. angular-ui-grid 4.4.7 onwards comes pre-packaged with jszip and excel-builder, but have few noticeable bugs hence not upgrading to latest. [Improvement] * [ZEPPELIN-3466](https://issues.apache.org/jira/browse/ZEPPELIN-3466) * Export to excel should work. * Does the licenses files need update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: Prabhjyot Singh Closes #2971 from prabhjyotsingh/ZEPPELIN-3466 and squashes the following commits: 8ce42c7c4 [Prabhjyot Singh] ZEPPELIN-3466: Table export to excel is not working due to missing dependencies Change-Id: I5daf83f49defbbf909e54d43924da206031f5120 (cherry picked from commit 0f6d94f60aa566559aed7870da4b7efdbf481607) Signed-off-by: Prabhjyot Singh # Conflicts: # zeppelin-web/package.json --- zeppelin-web/bower.json | 4 +++- zeppelin-web/karma.conf.js | 2 ++ zeppelin-web/package.json | 2 +- .../src/app/visualization/builtins/visualization-table.js | 1 + zeppelin-web/src/index.html | 2 ++ 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/zeppelin-web/bower.json b/zeppelin-web/bower.json index 4d0d0285aa3..33194adeccb 100644 --- a/zeppelin-web/bower.json +++ b/zeppelin-web/bower.json @@ -34,7 +34,9 @@ "MathJax": "2.7.0", "ngclipboard": "^1.1.1", "jsdiff": "3.3.0", - "ngInfiniteScroll": "^1.3.4" + "ngInfiniteScroll": "^1.3.4", + "jszip": "2.6.1", + "excel-builder-js": "excelbuilder#2.0.0" }, "devDependencies": { "angular-mocks": "1.5.7" diff --git a/zeppelin-web/karma.conf.js b/zeppelin-web/karma.conf.js index 3e573a98f17..5daceb91fff 100644 --- a/zeppelin-web/karma.conf.js +++ b/zeppelin-web/karma.conf.js @@ -88,6 +88,8 @@ module.exports = function(config) { 'bower_components/ngclipboard/dist/ngclipboard.js', 'bower_components/jsdiff/diff.js', 'bower_components/ngInfiniteScroll/build/ng-infinite-scroll.js', + 'bower_components/jszip/dist/jszip.js', + 'bower_components/excel-builder-js/dist/excel-builder.dist.js', 'bower_components/angular-mocks/angular-mocks.js', // endbower diff --git a/zeppelin-web/package.json b/zeppelin-web/package.json index 71c691b2c41..0c54eeac0d3 100644 --- a/zeppelin-web/package.json +++ b/zeppelin-web/package.json @@ -25,7 +25,7 @@ "test": "karma start karma.conf.js" }, "dependencies": { - "angular-ui-grid": "^4.2.4", + "angular-ui-grid": "4.4.6", "angular-viewport-watch": "github:shahata/angular-viewport-watch", "ansi_up": "^2.0.2", "github-markdown-css": "2.6.0", diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-table.js b/zeppelin-web/src/app/visualization/builtins/visualization-table.js index 3eb08868a7b..ebb68faa1f6 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-table.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-table.js @@ -102,6 +102,7 @@ export default class TableVisualization extends Visualization { flatEntityAccess: true, fastWatch: false, treeRowHeaderAlwaysVisible: false, + exporterExcelFilename: 'myFile.xlsx', columnDefs: columnNames.map((colName) => { return { diff --git a/zeppelin-web/src/index.html b/zeppelin-web/src/index.html index 15a5085f635..6e9cabb5ed8 100644 --- a/zeppelin-web/src/index.html +++ b/zeppelin-web/src/index.html @@ -167,6 +167,8 @@ + + From 39e67e078086cc704f41899407dfc5995ed27e91 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Fri, 18 May 2018 09:07:13 +0800 Subject: [PATCH 305/492] [DOC] Fix interpreter version in doc ### What is this PR for? Change the version from 0.7.0 to 0.8.0 ### What type of PR is it? [Documentation] ### Todos * [ ] - Task ### What is the Jira issue? * No jira created ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2973 from zjffdu/version_doc and squashes the following commits: 48d6d72ad [Jeff Zhang] [DOC] Fix interpreter version in doc --- docs/usage/interpreter/installation.md | 77 +++++++++++++------------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/docs/usage/interpreter/installation.md b/docs/usage/interpreter/installation.md index 8bc3b3d8d18..7e1cb80cd52 100644 --- a/docs/usage/interpreter/installation.md +++ b/docs/usage/interpreter/installation.md @@ -58,46 +58,42 @@ Zeppelin support both Scala 2.10 and 2.11 for several interpreters as below: cassandra - org.apache.zeppelin:zeppelin-cassandra_2.10:0.7.0 - org.apache.zeppelin:zeppelin-cassandra_2.11:0.7.0 + org.apache.zeppelin:zeppelin-cassandra_2.10:0.8.0 + org.apache.zeppelin:zeppelin-cassandra_2.11:0.8.0 flink - org.apache.zeppelin:zeppelin-flink_2.10:0.7.0 - org.apache.zeppelin:zeppelin-flink_2.11:0.7.0 + org.apache.zeppelin:zeppelin-flink_2.10:0.8.0 + org.apache.zeppelin:zeppelin-flink_2.11:0.8.0 ignite - org.apache.zeppelin:zeppelin-ignite_2.10:0.7.0 - org.apache.zeppelin:zeppelin-ignite_2.11:0.7.0 + org.apache.zeppelin:zeppelin-ignite_2.10:0.8.0 + org.apache.zeppelin:zeppelin-ignite_2.11:0.8.0 scio - org.apache.zeppelin:zeppelin-scio_2.10:0.7.0 - org.apache.zeppelin:zeppelin-scio_2.11:0.7.0 - - - spark - org.apache.zeppelin:zeppelin-spark_2.10:0.7.0 - org.apache.zeppelin:zeppelin-spark_2.11:0.7.0 + org.apache.zeppelin:zeppelin-scio_2.10:0.8.0 + org.apache.zeppelin:zeppelin-scio_2.11:0.8.0 If you install one of these interpreters only with `--name` option, installer will download interpreter built with Scala 2.11 by default. If you want to specify Scala version, you will need to add `--artifact` option. Here is the example of installing flink interpreter built with Scala 2.10. ``` -./bin/install-interpreter.sh --name flink --artifact org.apache.zeppelin:zeppelin-flink_2.10:0.7.0 +./bin/install-interpreter.sh --name flink --artifact org.apache.zeppelin:zeppelin-flink_2.10:0.8.0 ``` -#### Install Spark interpreter built with Scala 2.10 -Spark distribution package has been built with Scala 2.10 until 1.6.2. If you have `SPARK_HOME` set pointing to Spark version earlier than 2.0.0, you need to download Spark interpreter packaged with Scala 2.10. To do so, use follow command: +#### Spark interpreter for scala 2.10 and 2.11 +Starting from Zeppelin 0.8.0, Zeppelin support both scala 2.10 and 2.11 in one zeppelin instance. You don't need to install different scala specific interpreter for spark. +It would pick up the right scala specific interpreter for the spark distribution where `SPARK_HOME` point to. + +Here's the command to install spark interpreter ``` -rm -rf ./interpreter/spark -./bin/install-interpreter.sh --name spark --artifact org.apache.zeppelin:zeppelin-spark_2.10:0.7.0 +./bin/install-interpreter.sh --name spark --artifact org.apache.zeppelin:spark-interpreter:0.8.0 ``` -
    Once you have installed interpreters, you need to restart Zeppelin. And then [create interpreter setting](./overview.html#what-is-zeppelin-interpreter) and [bind it with your notebook](./overview.html#what-is-zeppelin-interpreter-setting). @@ -134,97 +130,102 @@ You can also find the below community managed interpreter list in `conf/interpre alluxio - org.apache.zeppelin:zeppelin-alluxio:0.7.0 + org.apache.zeppelin:zeppelin-alluxio:0.8.0 Alluxio interpreter angular - org.apache.zeppelin:zeppelin-angular:0.7.0 + org.apache.zeppelin:zeppelin-angular:0.8.0 HTML and AngularJS view rendering beam - org.apache.zeppelin:zeppelin-beam:0.7.0 + org.apache.zeppelin:zeppelin-beam:0.8.0 Beam interpreter bigquery - org.apache.zeppelin:zeppelin-bigquery:0.7.0 + org.apache.zeppelin:zeppelin-bigquery:0.8.0 BigQuery interpreter cassandra - org.apache.zeppelin:zeppelin-cassandra\_2.11:0.7.0 + org.apache.zeppelin:zeppelin-cassandra\_2.11:0.8.0 Cassandra interpreter built with Scala 2.11 elasticsearch - org.apache.zeppelin:zeppelin-elasticsearch:0.7.0 + org.apache.zeppelin:zeppelin-elasticsearch:0.8.0 Elasticsearch interpreter file - org.apache.zeppelin:zeppelin-file:0.7.0 + org.apache.zeppelin:zeppelin-file:0.8.0 HDFS file interpreter flink - org.apache.zeppelin:zeppelin-flink\_2.11:0.7.0 + org.apache.zeppelin:zeppelin-flink\_2.11:0.8.0 Flink interpreter built with Scala 2.11 hbase - org.apache.zeppelin:zeppelin-hbase:0.7.0 + org.apache.zeppelin:zeppelin-hbase:0.8.0 Hbase interpreter ignite - org.apache.zeppelin:zeppelin-ignite\_2.11:0.7.0 + org.apache.zeppelin:zeppelin-ignite\_2.11:0.8.0 Ignite interpreter built with Scala 2.11 jdbc - org.apache.zeppelin:zeppelin-jdbc:0.7.0 + org.apache.zeppelin:zeppelin-jdbc:0.8.0 Jdbc interpreter kylin - org.apache.zeppelin:zeppelin-kylin:0.7.0 + org.apache.zeppelin:zeppelin-kylin:0.8.0 Kylin interpreter lens - org.apache.zeppelin:zeppelin-lens:0.7.0 + org.apache.zeppelin:zeppelin-lens:0.8.0 Lens interpreter livy - org.apache.zeppelin:zeppelin-livy:0.7.0 + org.apache.zeppelin:zeppelin-livy:0.8.0 Livy interpreter md - org.apache.zeppelin:zeppelin-markdown:0.7.0 + org.apache.zeppelin:zeppelin-markdown:0.8.0 Markdown support pig - org.apache.zeppelin:zeppelin-pig:0.7.0 + org.apache.zeppelin:zeppelin-pig:0.8.0 Pig interpreter python - org.apache.zeppelin:zeppelin-python:0.7.0 + org.apache.zeppelin:zeppelin-python:0.8.0 Python interpreter scio - org.apache.zeppelin:zeppelin-scio\_2.11:0.7.0 + org.apache.zeppelin:zeppelin-scio\_2.11:0.8.0 Scio interpreter built with Scala 2.11 shell - org.apache.zeppelin:zeppelin-shell:0.7.0 + org.apache.zeppelin:zeppelin-shell:0.8.0 Shell command + + sap + org.apache.zeppelin:zeppelin-sap:0.8.0 + sap interpreter + From cd3edca62c8c5c2f6e2eba06d0cb81bcfc66c662 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Tue, 22 May 2018 13:28:00 +0530 Subject: [PATCH 306/492] [ZEPPELIN-3478] Download Data as CSV downloads data as a single line All data is in one single line - lines separated with backslash and "n" sequence and not actual newline characters "\n". [Bug Fix] * [ZEPPELIN-3478](https://issues.apache.org/jira/browse/ZEPPELIN-3478) All the exports should work in all browsers * Export this notebook * Export as CVS * Export as TSV * Does the licenses files need update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: Prabhjyot Singh Closes #2976 from prabhjyotsingh/ZEPPELIN-3478 and squashes the following commits: 62ea2e57e [Prabhjyot Singh] ZEPPELIN-3478: Download Data as CSV downloads data as a single line Change-Id: Ia46a53347d8b8d8961caba6b89182c4ff5724269 (cherry picked from commit fbe42d2d39774dc3b26874c3fbc9c23f9ade8fd7) Signed-off-by: Prabhjyot Singh --- zeppelin-web/src/app/notebook/save-as/save-as.service.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/zeppelin-web/src/app/notebook/save-as/save-as.service.js b/zeppelin-web/src/app/notebook/save-as/save-as.service.js index 46530980ec7..9330d711d79 100644 --- a/zeppelin-web/src/app/notebook/save-as/save-as.service.js +++ b/zeppelin-web/src/app/notebook/save-as/save-as.service.js @@ -39,8 +39,10 @@ function SaveAsService(browserDetectService) { angular.element('body > iframe#SaveAsId').remove(); } else { const fileName = filename + '.' + extension; - const json = JSON.stringify(content); - const blob = new Blob([json], {type: 'octet/stream'}); + let binaryData = []; + binaryData.push(BOM); + binaryData.push(content); + let blob = new Blob(binaryData, {type: 'octet/stream'}); const url = window.URL.createObjectURL(blob); let a = document.createElement('a'); document.body.appendChild(a); From 53e3153975ea906fcdd498d12b134c1a413a884b Mon Sep 17 00:00:00 2001 From: sameer79 Date: Wed, 9 May 2018 11:02:04 +0530 Subject: [PATCH 307/492] [ZEPPELIN-3450] Number sorting issue ### What is this PR for? Sorting value with types [NUMBER,STRING,DATE]. ### What type of PR is it? [Bug Fix] ### What is the Jira issue? * [ZEPPELIN-3450](https://issues.apache.org/jira/browse/ZEPPELIN-3450) ### How should this be tested? First select the type of sorting, then click the sort asc/desc. * Does the licenses files need update? * Is there breaking changes for older versions? * Does this needs documentation? Author: sameer79 Closes #2963 from sameer79/ZEPPELIN-3450 and squashes the following commits: b83bda925 [sameer79] [ZEPPELIN-3450] Number sorting issue Change-Id: I3346102ca868ac6538b5491cc294aec0e1d80479 (cherry picked from commit 3ca7d723704bb58577830cd0c6390453e04d7f10) Signed-off-by: Prabhjyot Singh --- .../builtins/visualization-table.js | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-table.js b/zeppelin-web/src/app/visualization/builtins/visualization-table.js index ebb68faa1f6..831bf95ccd6 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-table.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-table.js @@ -82,6 +82,10 @@ export default class TableVisualization extends Visualization { return width; } + getSortedValue(a, b) { + return a > b ? 1 : a === b ? 0 : -1; + } + createGridOptions(tableData, onRegisterApiCallback, config) { const rows = tableData.rows; const columnNames = tableData.columns.map((c) => c.name); @@ -105,6 +109,7 @@ export default class TableVisualization extends Visualization { exporterExcelFilename: 'myFile.xlsx', columnDefs: columnNames.map((colName) => { + const self = this; return { displayName: colName, name: colName, @@ -121,6 +126,18 @@ export default class TableVisualization extends Visualization { `, minWidth: this.getColumnMinWidth(colName), width: '*', + sortingAlgorithm: function(a, b, row1, row2, sortType, gridCol) { + const colType = gridCol.colDef.type.toLowerCase(); + if (colType === TableColumnType.NUMBER) { + return self.getSortedValue(a, b); + } else if (colType === TableColumnType.STRING) { + return self.getSortedValue(a.toString(), b.toString()); + } else if (colType === TableColumnType.DATE) { + return self.getSortedValue(new Date(a), new Date(b)); + } else { + return self.getSortedValue(a, b); + } + }, }; }), rowEditWaitInterval: -1, /** disable saveRow event */ @@ -339,12 +356,12 @@ export default class TableVisualization extends Visualization { // create, compile and append grid elem gridElem = angular.element( `
    Date: Wed, 23 May 2018 11:55:34 +0800 Subject: [PATCH 308/492] ZEPPELIN-3489. Yarn cluster mode doesn't work for multiple node cluster ### What is this PR for? Change the scope of zeppelin-interpreter to compile, so that it will be packaged into the spark interpreter jar ### What type of PR is it? [Bug Fix] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3489 ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2980 from zjffdu/ZEPPELIN-3489 and squashes the following commits: aee482142 [Jeff Zhang] ZEPPELIN-3489. Yarn cluster mode doesn't work for multiple node cluster (cherry picked from commit 965f71e8557a95c845699097cd35936d4e33d491) Signed-off-by: Jeff Zhang --- interpreter-parent/pom.xml | 1 - testing/install_external_dependencies.sh | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index 35c8fc0e99f..33837913182 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -38,7 +38,6 @@ ${project.groupId} zeppelin-interpreter ${project.version} - provided diff --git a/testing/install_external_dependencies.sh b/testing/install_external_dependencies.sh index d59f9c1c371..2847a07803b 100755 --- a/testing/install_external_dependencies.sh +++ b/testing/install_external_dependencies.sh @@ -45,6 +45,6 @@ if [[ -n "$PYTHON" ]] ; then conda update -q conda conda info -a conda config --add channels conda-forge - conda install -q matplotlib=2.1.2 pandasql ipython=5.4.1 jupyter_client ipykernel matplotlib bokeh=0.12.10 - pip install -q grpcio ggplot bkzep==0.4.0 statsmodels==0.8.0 + conda install -q pandas=0.21.1 matplotlib=2.1.1 pandasql=0.7.3 ipython=5.4.1 jupyter_client=5.1.0 ipykernel=4.7.0 bokeh=0.12.10 + pip install -q ggplot==0.11.5 grpcio==1.8.2 bkzep==0.4.0 fi From 23ee74ce052e073b6f141e9b7ea2830dce0db775 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Wed, 23 May 2018 08:53:55 +0800 Subject: [PATCH 309/492] ZEPPELIN-3484. sc.setJobGroup() shows up in error stack and shifts line numbering This PR would call sc.setJobGroup and the user code separately. [Bug Fix] * [ ] - Task * https://issues.apache.org/jira/browse/ZEPPELIN-3483 * CI pass ![screen shot 2018-05-23 at 8 56 40 am](https://user-images.githubusercontent.com/164491/40397801-3c364d18-5e67-11e8-8fb5-4369d1d78e62.png) * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2979 from zjffdu/ZEPPELIN-3484 and squashes the following commits: 774db4c00 [Jeff Zhang] ZEPPELIN-3484. sc.setJobGroup() shows up in error stack and shifts line numbering (cherry picked from commit 3b041f46962ad8f3393f38946bd304b485c87c2a) Signed-off-by: Jeff Zhang --- .../java/org/apache/zeppelin/spark/IPySparkInterpreter.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java index da7e7e2eece..35c5729b917 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java @@ -106,7 +106,11 @@ public InterpreterResult interpret(String st, InterpreterContext context) { String jobGroupId = Utils.buildJobGroupId(context); String jobDesc = "Started by: " + Utils.getUserName(context.getAuthenticationInfo()); String setJobGroupStmt = "sc.setJobGroup('" + jobGroupId + "', '" + jobDesc + "')"; - return super.interpret(setJobGroupStmt + "\n" + st, context); + InterpreterResult result = super.interpret(setJobGroupStmt, context); + if (result.code().equals(InterpreterResult.Code.ERROR)) { + return new InterpreterResult(InterpreterResult.Code.ERROR, "Fail to setJobGroup"); + } + return super.interpret(st, context); } @Override From ccccf0ecb60455f55f28068a6db3f886adae8c71 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Thu, 24 May 2018 16:10:47 +0800 Subject: [PATCH 310/492] ZEPPELIN-3497. Improve error message when livy session is failed to create ### What is this PR for? The current behavior we will only return 10 lines of log to frontend which is not enough for user. This PR would invoke another livy api to get more logs (by default it is 1000 lines) ### What type of PR is it? [Improvement ] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3497 ### How should this be tested? * Manually tested ### Screenshots (if appropriate) Before ![screen shot 2018-05-24 at 2 09 09 pm](https://user-images.githubusercontent.com/164491/40473592-2c7cccfe-5f6f-11e8-8212-9bc7b5404bcc.png) After ![screen shot 2018-05-24 at 4 25 51 pm](https://user-images.githubusercontent.com/164491/40473602-34452170-5f6f-11e8-967e-4956042aceec.png) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2984 from zjffdu/ZEPPELIN-3497 and squashes the following commits: 2d1bbbb08 [Jeff Zhang] ZEPPELIN-3497. Improve error message when livy session is failed to create (cherry picked from commit a6075017d5c8d1503c4cc392eef22bfb53db6258) Signed-off-by: Jeff Zhang --- .../zeppelin/livy/BaseLivyInterpreter.java | 26 +++++++++++++++++-- .../main/resources/interpreter-setting.json | 6 +++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java b/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java index 858145c634d..079a4271784 100644 --- a/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java +++ b/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java @@ -95,6 +95,7 @@ public abstract class BaseLivyInterpreter extends Interpreter { private String livyURL; private int sessionCreationTimeout; private int pullStatusInterval; + private int maxLogLines; protected boolean displayAppInfo; private boolean restartDeadSession; protected LivyVersion livyVersion; @@ -120,6 +121,8 @@ public BaseLivyInterpreter(Properties property) { property.getProperty("zeppelin.livy.session.create_timeout", 120 + "")); this.pullStatusInterval = Integer.parseInt( property.getProperty("zeppelin.livy.pull_status.interval.millis", 1000 + "")); + this.maxLogLines = Integer.parseInt(property.getProperty("zeppelin.livy.maxLogLines", + "1000")); this.restTemplate = createRestTemplate(); if (!StringUtils.isBlank(property.getProperty("zeppelin.livy.http.headers"))) { String[] headers = property.getProperty("zeppelin.livy.http.headers").split(";"); @@ -338,7 +341,7 @@ private SessionInfo createSession(String user, String kind) if ((System.currentTimeMillis() - start) / 1000 > sessionCreationTimeout) { String msg = "The creation of session " + sessionInfo.id + " is timeout within " + sessionCreationTimeout + " seconds, appId: " + sessionInfo.appId - + ", log: " + sessionInfo.log; + + ", log:\n" + StringUtils.join(getSessionLog(sessionInfo.id).log, "\n"); throw new LivyException(msg); } Thread.sleep(pullStatusInterval); @@ -347,7 +350,7 @@ private SessionInfo createSession(String user, String kind) sessionInfo.appId); if (sessionInfo.isFinished()) { String msg = "Session " + sessionInfo.id + " is finished, appId: " + sessionInfo.appId - + ", log: " + sessionInfo.log; + + ", log:\n" + StringUtils.join(getSessionLog(sessionInfo.id).log, "\n"); throw new LivyException(msg); } } @@ -362,6 +365,11 @@ private SessionInfo getSessionInfo(int sessionId) throws LivyException { return SessionInfo.fromJson(callRestAPI("/sessions/" + sessionId, "GET")); } + private SessionLog getSessionLog(int sessionId) throws LivyException { + return SessionLog.fromJson(callRestAPI("/sessions/" + sessionId + "/log?size=" + maxLogLines, + "GET")); + } + public InterpreterResult interpret(String code, String paragraphId, boolean displayAppInfo, @@ -821,6 +829,20 @@ public static SessionInfo fromJson(String json) { } } + private static class SessionLog { + public int id; + public int from; + public int size; + public List log; + + SessionLog() { + } + + public static SessionLog fromJson(String json) { + return gson.fromJson(json, SessionLog.class); + } + } + static class ExecuteRequest { public final String code; public final String kind; diff --git a/livy/src/main/resources/interpreter-setting.json b/livy/src/main/resources/interpreter-setting.json index 8b9b90613a2..a7ce2b38454 100644 --- a/livy/src/main/resources/interpreter-setting.json +++ b/livy/src/main/resources/interpreter-setting.json @@ -97,6 +97,12 @@ "description": "The interval for checking paragraph execution status", "type": "number" }, + "zeppelin.livy.maxLogLines": { + "propertyName": "zeppelin.livy.maxLogLines", + "defaultValue": "1000", + "description": "Max number of lines of logs", + "type": "number" + }, "livy.spark.jars.packages": { "propertyName": "livy.spark.jars.packages", "defaultValue": "", From 1d930368e781acaedf68501da47b81ddb9a8b9b7 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Fri, 25 May 2018 12:14:32 +0800 Subject: [PATCH 311/492] Preparing Apache Zeppelin release 0.8.0 --- alluxio/pom.xml | 4 ++-- angular/pom.xml | 4 ++-- beam/pom.xml | 4 ++-- bigquery/pom.xml | 4 ++-- cassandra/pom.xml | 4 ++-- elasticsearch/pom.xml | 4 ++-- file/pom.xml | 4 ++-- flink/pom.xml | 4 ++-- geode/pom.xml | 4 ++-- groovy/pom.xml | 4 ++-- hbase/pom.xml | 4 ++-- helium-dev/pom.xml | 4 ++-- ignite/pom.xml | 4 ++-- interpreter-parent/pom.xml | 4 ++-- jdbc/pom.xml | 4 ++-- kylin/pom.xml | 4 ++-- lens/pom.xml | 4 ++-- livy/pom.xml | 4 ++-- markdown/pom.xml | 4 ++-- neo4j/pom.xml | 4 ++-- pig/pom.xml | 4 ++-- pom.xml | 2 +- python/pom.xml | 4 ++-- r/pom.xml | 4 ++-- sap/pom.xml | 4 ++-- scalding/pom.xml | 4 ++-- scio/pom.xml | 4 ++-- shell/pom.xml | 4 ++-- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 4 ++-- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-dependencies/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- zeppelin-display/pom.xml | 4 ++-- zeppelin-distribution/pom.xml | 2 +- zeppelin-examples/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-clock/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-horizontalbar/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-echo/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-markdown/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-translator/pom.xml | 4 ++-- zeppelin-integration/pom.xml | 4 ++-- zeppelin-interpreter/pom.xml | 4 ++-- zeppelin-jupyter/pom.xml | 4 ++-- zeppelin-server/pom.xml | 4 ++-- zeppelin-web/pom.xml | 4 ++-- zeppelin-zengine/pom.xml | 4 ++-- 52 files changed, 102 insertions(+), 102 deletions(-) diff --git a/alluxio/pom.xml b/alluxio/pom.xml index acef4942230..b6a4876cd8e 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Alluxio interpreter diff --git a/angular/pom.xml b/angular/pom.xml index 341d7d5ec54..a44f227d53a 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Angular interpreter diff --git a/beam/pom.xml b/beam/pom.xml index 7592e77e010..83b8e0d1480 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Beam interpreter diff --git a/bigquery/pom.xml b/bigquery/pom.xml index f3877179a9b..abe94d28665 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: BigQuery interpreter diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 2be0606157a..5e19e0abad4 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-cassandra_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index 3b7c2b54b4d..b7bdbf159ae 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent zeppelin-elasticsearch jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Elasticsearch interpreter diff --git a/file/pom.xml b/file/pom.xml index 1ea8eace07f..d2fbde9c89d 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: File System Interpreters diff --git a/flink/pom.xml b/flink/pom.xml index 47bd2877c83..6824b8da75c 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-flink_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Flink Zeppelin flink support diff --git a/geode/pom.xml b/geode/pom.xml index 59c16b731b2..3af4456c967 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Geode interpreter diff --git a/groovy/pom.xml b/groovy/pom.xml index 7dc4282d6e4..8bb49227e72 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Groovy interpreter diff --git a/hbase/pom.xml b/hbase/pom.xml index 6bf1a80905d..d2c4d0962d2 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: HBase interpreter diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index 8445b4dcf9e..dba601d341c 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -24,13 +24,13 @@ org.apache.zeppelin interpreter-parent - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Helium development interpreter diff --git a/ignite/pom.xml b/ignite/pom.xml index 31df05eac77..49f8ae2ea19 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent zeppelin-ignite_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Ignite interpreter diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index 33837913182..06e9b8c6834 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin interpreter-parent pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Interpreter Parent diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 58de18199ae..9a1d82b3d35 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: JDBC interpreter diff --git a/kylin/pom.xml b/kylin/pom.xml index 1690ec2e87f..0a0acfb4ca9 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -23,7 +23,7 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent 4.0.0 @@ -31,7 +31,7 @@ org.apache.zeppelin zeppelin-kylin jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Kylin interpreter diff --git a/lens/pom.xml b/lens/pom.xml index c67828733e0..57a96e22933 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Lens interpreter diff --git a/livy/pom.xml b/livy/pom.xml index 33c327b979b..5295fd1d909 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-livy jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Livy interpreter diff --git a/markdown/pom.xml b/markdown/pom.xml index cf8e766c673..d8685e7a4f6 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Markdown interpreter diff --git a/neo4j/pom.xml b/neo4j/pom.xml index 6883bdd996f..6aa768e2f02 100644 --- a/neo4j/pom.xml +++ b/neo4j/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-neo4j jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Neo4j interpreter diff --git a/pig/pom.xml b/pig/pom.xml index 06a58bbc9ee..a7b19420f37 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org diff --git a/pom.xml b/pom.xml index e580b59b1b2..8a41d843007 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin Zeppelin project http://zeppelin.apache.org diff --git a/python/pom.xml b/python/pom.xml index 03da41cfd25..88c1d8a875b 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Python interpreter diff --git a/r/pom.xml b/r/pom.xml index fc9fa6621f1..1f8bd81a954 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-zrinterpreter_${scala.binary.version} jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: R Interpreter R Interpreter for Zeppelin http://zeppelin.apache.org diff --git a/sap/pom.xml b/sap/pom.xml index e75cc10a38f..e0f3a90de2a 100644 --- a/sap/pom.xml +++ b/sap/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin sap jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Sap Zeppelin SAP support diff --git a/scalding/pom.xml b/scalding/pom.xml index 051368fa9a2..fa4679ad3ef 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-scalding_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Scalding interpreter diff --git a/scio/pom.xml b/scio/pom.xml index 3c9281c627d..a2857ffd005 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-scio_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Scio Zeppelin Scio support diff --git a/shell/pom.xml b/shell/pom.xml index cd5a99b4e5f..390f685e8c5 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Shell interpreter diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 2c219a60655..6d258b2892a 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index 5505aa7f57b..d248271be43 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent/pom.xml org.apache.zeppelin spark-parent pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Spark Parent Zeppelin Spark Support diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index 9b111da94d5..21abb637e24 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Interpreter Scala_2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index 190f7101ca0..301622d2658 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Interpreter Scala_2.11 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index 6da2ac1a290..0b018cef686 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-spark-dependencies jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Spark dependencies Zeppelin spark support diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index c3b03d39e22..30dcd1cceb7 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0 pom Zeppelin: Spark Scala Parent diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 81275e47083..17760db77ed 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index 458f1d2858b..1b2c0d64360 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index 648ff385121..3cbceaea707 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark2 Shims diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index 5db50f76769..d553128d13a 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-display jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Display system apis diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index 9545082256f..9fbb6ae04ac 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -23,7 +23,7 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 85387b707c5..6c0c15d4408 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-examples pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Examples Zeppelin examples diff --git a/zeppelin-examples/zeppelin-example-clock/pom.xml b/zeppelin-examples/zeppelin-example-clock/pom.xml index 55333aa46ae..12f7aa8cf96 100644 --- a/zeppelin-examples/zeppelin-example-clock/pom.xml +++ b/zeppelin-examples/zeppelin-example-clock/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index 29f4e381e72..60837386383 100644 --- a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml +++ b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example application - Horizontal Bar chart diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml index d76c281f1de..2b04ce9f148 100644 --- a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index d84c887f7b0..83621a87dc8 100644 --- a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index 67604437bcc..2f8dc8d39a6 100644 --- a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml index 527990924dd..04c030be582 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml index a2efef70c32..a8fcc7ce33d 100644 --- a/zeppelin-integration/pom.xml +++ b/zeppelin-integration/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-integration jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Integration Test diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index e75e1f8029f..4a491f23f2c 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Interpreter Zeppelin Interpreter diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index bc9ec5a816e..8c1f52b3cb0 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,13 +24,13 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. zeppelin-jupyter jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 165123c9d80..248aa90cd33 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-server jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Server diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 2bc22c02d5b..902c9b80e8b 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-web war - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: web Application diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index a438242c4a4..cfaf66abd4b 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,14 +23,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-zengine jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Zengine Zeppelin Zengine From 664e2880ec9cc619fe727480322ee6ff7354022e Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Fri, 25 May 2018 12:14:42 +0800 Subject: [PATCH 312/492] Preparing development version 0.8.1-SNAPSHOT --- alluxio/pom.xml | 4 ++-- angular/pom.xml | 4 ++-- beam/pom.xml | 4 ++-- bigquery/pom.xml | 4 ++-- cassandra/pom.xml | 4 ++-- elasticsearch/pom.xml | 4 ++-- file/pom.xml | 4 ++-- flink/pom.xml | 4 ++-- geode/pom.xml | 4 ++-- groovy/pom.xml | 4 ++-- hbase/pom.xml | 4 ++-- helium-dev/pom.xml | 4 ++-- ignite/pom.xml | 4 ++-- interpreter-parent/pom.xml | 4 ++-- jdbc/pom.xml | 4 ++-- kylin/pom.xml | 4 ++-- lens/pom.xml | 4 ++-- livy/pom.xml | 4 ++-- markdown/pom.xml | 4 ++-- neo4j/pom.xml | 4 ++-- pig/pom.xml | 4 ++-- pom.xml | 2 +- python/pom.xml | 4 ++-- r/pom.xml | 4 ++-- sap/pom.xml | 4 ++-- scalding/pom.xml | 4 ++-- scio/pom.xml | 4 ++-- shell/pom.xml | 4 ++-- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 4 ++-- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-dependencies/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- zeppelin-display/pom.xml | 4 ++-- zeppelin-distribution/pom.xml | 2 +- zeppelin-examples/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-clock/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-horizontalbar/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-echo/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-markdown/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-translator/pom.xml | 4 ++-- zeppelin-integration/pom.xml | 4 ++-- zeppelin-interpreter/pom.xml | 4 ++-- zeppelin-jupyter/pom.xml | 4 ++-- zeppelin-server/pom.xml | 4 ++-- zeppelin-web/pom.xml | 4 ++-- zeppelin-zengine/pom.xml | 4 ++-- 52 files changed, 102 insertions(+), 102 deletions(-) diff --git a/alluxio/pom.xml b/alluxio/pom.xml index b6a4876cd8e..acef4942230 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Alluxio interpreter diff --git a/angular/pom.xml b/angular/pom.xml index a44f227d53a..341d7d5ec54 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Angular interpreter diff --git a/beam/pom.xml b/beam/pom.xml index 83b8e0d1480..7592e77e010 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Beam interpreter diff --git a/bigquery/pom.xml b/bigquery/pom.xml index abe94d28665..f3877179a9b 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: BigQuery interpreter diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 5e19e0abad4..2be0606157a 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-cassandra_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index b7bdbf159ae..3b7c2b54b4d 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent zeppelin-elasticsearch jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Elasticsearch interpreter diff --git a/file/pom.xml b/file/pom.xml index d2fbde9c89d..1ea8eace07f 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: File System Interpreters diff --git a/flink/pom.xml b/flink/pom.xml index 6824b8da75c..47bd2877c83 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-flink_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Flink Zeppelin flink support diff --git a/geode/pom.xml b/geode/pom.xml index 3af4456c967..59c16b731b2 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Geode interpreter diff --git a/groovy/pom.xml b/groovy/pom.xml index 8bb49227e72..7dc4282d6e4 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Groovy interpreter diff --git a/hbase/pom.xml b/hbase/pom.xml index d2c4d0962d2..6bf1a80905d 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: HBase interpreter diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index dba601d341c..8445b4dcf9e 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -24,13 +24,13 @@ org.apache.zeppelin interpreter-parent - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Helium development interpreter diff --git a/ignite/pom.xml b/ignite/pom.xml index 49f8ae2ea19..31df05eac77 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent zeppelin-ignite_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Ignite interpreter diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index 06e9b8c6834..33837913182 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin interpreter-parent pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Interpreter Parent diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 9a1d82b3d35..58de18199ae 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: JDBC interpreter diff --git a/kylin/pom.xml b/kylin/pom.xml index 0a0acfb4ca9..1690ec2e87f 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -23,7 +23,7 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent 4.0.0 @@ -31,7 +31,7 @@ org.apache.zeppelin zeppelin-kylin jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Kylin interpreter diff --git a/lens/pom.xml b/lens/pom.xml index 57a96e22933..c67828733e0 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Lens interpreter diff --git a/livy/pom.xml b/livy/pom.xml index 5295fd1d909..33c327b979b 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-livy jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Livy interpreter diff --git a/markdown/pom.xml b/markdown/pom.xml index d8685e7a4f6..cf8e766c673 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Markdown interpreter diff --git a/neo4j/pom.xml b/neo4j/pom.xml index 6aa768e2f02..6883bdd996f 100644 --- a/neo4j/pom.xml +++ b/neo4j/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-neo4j jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Neo4j interpreter diff --git a/pig/pom.xml b/pig/pom.xml index a7b19420f37..06a58bbc9ee 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org diff --git a/pom.xml b/pom.xml index 8a41d843007..e580b59b1b2 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin Zeppelin project http://zeppelin.apache.org diff --git a/python/pom.xml b/python/pom.xml index 88c1d8a875b..03da41cfd25 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Python interpreter diff --git a/r/pom.xml b/r/pom.xml index 1f8bd81a954..fc9fa6621f1 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-zrinterpreter_${scala.binary.version} jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: R Interpreter R Interpreter for Zeppelin http://zeppelin.apache.org diff --git a/sap/pom.xml b/sap/pom.xml index e0f3a90de2a..e75cc10a38f 100644 --- a/sap/pom.xml +++ b/sap/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin sap jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Sap Zeppelin SAP support diff --git a/scalding/pom.xml b/scalding/pom.xml index fa4679ad3ef..051368fa9a2 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-scalding_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Scalding interpreter diff --git a/scio/pom.xml b/scio/pom.xml index a2857ffd005..3c9281c627d 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-scio_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Scio Zeppelin Scio support diff --git a/shell/pom.xml b/shell/pom.xml index 390f685e8c5..cd5a99b4e5f 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Shell interpreter diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 6d258b2892a..2c219a60655 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index d248271be43..5505aa7f57b 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent/pom.xml org.apache.zeppelin spark-parent pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark Parent Zeppelin Spark Support diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index 21abb637e24..9b111da94d5 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Interpreter Scala_2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index 301622d2658..190f7101ca0 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Interpreter Scala_2.11 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index 0b018cef686..6da2ac1a290 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-spark-dependencies jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark dependencies Zeppelin spark support diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index 30dcd1cceb7..c3b03d39e22 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT pom Zeppelin: Spark Scala Parent diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 17760db77ed..81275e47083 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index 1b2c0d64360..458f1d2858b 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index 3cbceaea707..648ff385121 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark2 Shims diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index d553128d13a..5db50f76769 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-display jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Display system apis diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index 9fbb6ae04ac..9545082256f 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -23,7 +23,7 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 6c0c15d4408..85387b707c5 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-examples pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Examples Zeppelin examples diff --git a/zeppelin-examples/zeppelin-example-clock/pom.xml b/zeppelin-examples/zeppelin-example-clock/pom.xml index 12f7aa8cf96..55333aa46ae 100644 --- a/zeppelin-examples/zeppelin-example-clock/pom.xml +++ b/zeppelin-examples/zeppelin-example-clock/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index 60837386383..29f4e381e72 100644 --- a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml +++ b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example application - Horizontal Bar chart diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml index 2b04ce9f148..d76c281f1de 100644 --- a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index 83621a87dc8..d84c887f7b0 100644 --- a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index 2f8dc8d39a6..67604437bcc 100644 --- a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml index 04c030be582..527990924dd 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml index a8fcc7ce33d..a2efef70c32 100644 --- a/zeppelin-integration/pom.xml +++ b/zeppelin-integration/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-integration jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Integration Test diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index 4a491f23f2c..e75e1f8029f 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Interpreter Zeppelin Interpreter diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index 8c1f52b3cb0..bc9ec5a816e 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,13 +24,13 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. zeppelin-jupyter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 248aa90cd33..165123c9d80 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-server jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Server diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 902c9b80e8b..2bc22c02d5b 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-web war - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: web Application diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index cfaf66abd4b..a438242c4a4 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,14 +23,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-zengine jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Zengine Zeppelin Zengine From 613d90dd514eda5170a04a43a29b3fe539124dfc Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Mon, 28 May 2018 10:19:42 +0800 Subject: [PATCH 313/492] ZEPPELIN-3506. DepInterpreter is broken ### What is this PR for? The bug is due to getInterpreterInTheSameSessionByClassName doesn't find the correct DepInterpreter. This PR fix this issue. The unit test fails due to classpath issue, will enable it later. ### What type of PR is it? [Bug Fix] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3506 ### How should this be tested? * CI pass and manually tested ### Screenshots (if appropriate) ![screen shot 2018-05-28 at 11 49 33 am](https://user-images.githubusercontent.com/164491/40596424-36e407e2-626d-11e8-8965-05a5833af54c.png) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2988 from zjffdu/ZEPPELIN-3506 and squashes the following commits: dd77d5c28 [Jeff Zhang] ZEPPELIN-3506. DepInterpreter is broken (cherry picked from commit e9dedab46df9dfe3ff6902e453db92cf0e712e82) Signed-off-by: Jeff Zhang --- .../spark/AbstractSparkInterpreter.java | 10 +++++ .../zeppelin/spark/NewSparkInterpreter.java | 3 +- .../zeppelin/spark/OldSparkInterpreter.java | 3 +- .../zeppelin/spark/SparkInterpreter.java | 1 + .../spark/NewSparkInterpreterTest.java | 39 ++++++++++++++++++- 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/AbstractSparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/AbstractSparkInterpreter.java index 9968dc6e5f1..aa1343aae50 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/AbstractSparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/AbstractSparkInterpreter.java @@ -31,6 +31,8 @@ */ public abstract class AbstractSparkInterpreter extends Interpreter { + private SparkInterpreter parentSparkInterpreter; + public AbstractSparkInterpreter(Properties properties) { super(properties); } @@ -54,4 +56,12 @@ public AbstractSparkInterpreter(Properties properties) { public abstract String getSparkUIUrl(); public abstract boolean isUnsupportedSparkVersion(); + + public void setParentSparkInterpreter(SparkInterpreter parentSparkInterpreter) { + this.parentSparkInterpreter = parentSparkInterpreter; + } + + public SparkInterpreter getParentSparkInterpreter() { + return parentSparkInterpreter; + } } diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java index c8efa7a7d9f..591ef96867e 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java @@ -241,7 +241,8 @@ public SparkVersion getSparkVersion() { } private DepInterpreter getDepInterpreter() { - Interpreter p = getInterpreterInTheSameSessionByClassName(DepInterpreter.class.getName()); + Interpreter p = getParentSparkInterpreter() + .getInterpreterInTheSameSessionByClassName(DepInterpreter.class.getName()); if (p == null) { return null; } diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java index 1f59d18d339..0dfe3cb9380 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java @@ -281,7 +281,8 @@ public SparkDependencyResolver getDependencyResolver() { } private DepInterpreter getDepInterpreter() { - Interpreter p = getInterpreterInTheSameSessionByClassName(DepInterpreter.class.getName()); + Interpreter p = getParentSparkInterpreter() + .getInterpreterInTheSameSessionByClassName(DepInterpreter.class.getName()); if (p == null) { return null; } diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java index d9be57363f2..7df1bc95aa6 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java @@ -50,6 +50,7 @@ public SparkInterpreter(Properties properties) { } else { delegation = new OldSparkInterpreter(properties); } + delegation.setParentSparkInterpreter(this); } @Override diff --git a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java index 3d22af31963..f6cb9a9e3f4 100644 --- a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java @@ -17,11 +17,13 @@ package org.apache.zeppelin.spark; +import com.google.common.io.Files; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.GUI; import org.apache.zeppelin.display.ui.CheckBox; import org.apache.zeppelin.display.ui.Select; import org.apache.zeppelin.display.ui.TextBox; +import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterException; import org.apache.zeppelin.interpreter.InterpreterGroup; @@ -30,10 +32,10 @@ import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResultMessageOutput; import org.apache.zeppelin.interpreter.remote.RemoteEventClient; -import org.apache.zeppelin.interpreter.remote.RemoteEventClientWrapper; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.user.AuthenticationInfo; import org.junit.After; +import org.junit.Ignore; import org.junit.Test; import java.io.File; @@ -42,6 +44,7 @@ import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -57,6 +60,7 @@ public class NewSparkInterpreterTest { private SparkInterpreter interpreter; + private DepInterpreter depInterpreter; // catch the streaming output in onAppend private volatile String output = ""; @@ -351,11 +355,44 @@ public void testDependencies() throws IOException, InterpreterException { assertEquals(InterpreterResult.Code.SUCCESS, result.code()); } + //TODO(zjffdu) This unit test will fail due to classpath issue, should enable it after the classpath issue is fixed. + @Ignore + public void testDepInterpreter() throws InterpreterException { + Properties properties = new Properties(); + properties.setProperty("spark.master", "local"); + properties.setProperty("spark.app.name", "test"); + properties.setProperty("zeppelin.spark.maxResult", "100"); + properties.setProperty("zeppelin.spark.test", "true"); + properties.setProperty("zeppelin.spark.useNew", "true"); + properties.setProperty("zeppelin.dep.localrepo", Files.createTempDir().getAbsolutePath()); + + InterpreterGroup intpGroup = new InterpreterGroup(); + interpreter = new SparkInterpreter(properties); + depInterpreter = new DepInterpreter(properties); + interpreter.setInterpreterGroup(intpGroup); + depInterpreter.setInterpreterGroup(intpGroup); + intpGroup.put("session_1", new ArrayList()); + intpGroup.get("session_1").add(interpreter); + intpGroup.get("session_1").add(depInterpreter); + + depInterpreter.open(); + InterpreterResult result = + depInterpreter.interpret("z.load(\"com.databricks:spark-avro_2.11:3.2.0\")", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + interpreter.open(); + result = interpreter.interpret("import com.databricks.spark.avro._", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + } + @After public void tearDown() throws InterpreterException { if (this.interpreter != null) { this.interpreter.close(); } + if (this.depInterpreter != null) { + this.depInterpreter.close(); + } } private InterpreterContext getInterpreterContext() { From b025caac587140e6ee1a6be2cf1261d5c638c8bf Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Mon, 28 May 2018 12:54:11 +0800 Subject: [PATCH 314/492] Preparing Apache Zeppelin release 0.8.0 --- alluxio/pom.xml | 4 ++-- angular/pom.xml | 4 ++-- beam/pom.xml | 4 ++-- bigquery/pom.xml | 4 ++-- cassandra/pom.xml | 4 ++-- elasticsearch/pom.xml | 4 ++-- file/pom.xml | 4 ++-- flink/pom.xml | 4 ++-- geode/pom.xml | 4 ++-- groovy/pom.xml | 4 ++-- hbase/pom.xml | 4 ++-- helium-dev/pom.xml | 4 ++-- ignite/pom.xml | 4 ++-- interpreter-parent/pom.xml | 4 ++-- jdbc/pom.xml | 4 ++-- kylin/pom.xml | 4 ++-- lens/pom.xml | 4 ++-- livy/pom.xml | 4 ++-- markdown/pom.xml | 4 ++-- neo4j/pom.xml | 4 ++-- pig/pom.xml | 4 ++-- pom.xml | 2 +- python/pom.xml | 4 ++-- r/pom.xml | 4 ++-- sap/pom.xml | 4 ++-- scalding/pom.xml | 4 ++-- scio/pom.xml | 4 ++-- shell/pom.xml | 4 ++-- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 4 ++-- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-dependencies/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- zeppelin-display/pom.xml | 4 ++-- zeppelin-distribution/pom.xml | 2 +- zeppelin-examples/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-clock/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-horizontalbar/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-echo/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-markdown/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-translator/pom.xml | 4 ++-- zeppelin-integration/pom.xml | 4 ++-- zeppelin-interpreter/pom.xml | 4 ++-- zeppelin-jupyter/pom.xml | 4 ++-- zeppelin-server/pom.xml | 4 ++-- zeppelin-web/pom.xml | 4 ++-- zeppelin-zengine/pom.xml | 4 ++-- 52 files changed, 102 insertions(+), 102 deletions(-) diff --git a/alluxio/pom.xml b/alluxio/pom.xml index acef4942230..b6a4876cd8e 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Alluxio interpreter diff --git a/angular/pom.xml b/angular/pom.xml index 341d7d5ec54..a44f227d53a 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Angular interpreter diff --git a/beam/pom.xml b/beam/pom.xml index 7592e77e010..83b8e0d1480 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Beam interpreter diff --git a/bigquery/pom.xml b/bigquery/pom.xml index f3877179a9b..abe94d28665 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: BigQuery interpreter diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 2be0606157a..5e19e0abad4 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-cassandra_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index 3b7c2b54b4d..b7bdbf159ae 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent zeppelin-elasticsearch jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Elasticsearch interpreter diff --git a/file/pom.xml b/file/pom.xml index 1ea8eace07f..d2fbde9c89d 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: File System Interpreters diff --git a/flink/pom.xml b/flink/pom.xml index 47bd2877c83..6824b8da75c 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-flink_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Flink Zeppelin flink support diff --git a/geode/pom.xml b/geode/pom.xml index 59c16b731b2..3af4456c967 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Geode interpreter diff --git a/groovy/pom.xml b/groovy/pom.xml index 7dc4282d6e4..8bb49227e72 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Groovy interpreter diff --git a/hbase/pom.xml b/hbase/pom.xml index 6bf1a80905d..d2c4d0962d2 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: HBase interpreter diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index 8445b4dcf9e..dba601d341c 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -24,13 +24,13 @@ org.apache.zeppelin interpreter-parent - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Helium development interpreter diff --git a/ignite/pom.xml b/ignite/pom.xml index 31df05eac77..49f8ae2ea19 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent zeppelin-ignite_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Ignite interpreter diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index 33837913182..06e9b8c6834 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin interpreter-parent pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Interpreter Parent diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 58de18199ae..9a1d82b3d35 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: JDBC interpreter diff --git a/kylin/pom.xml b/kylin/pom.xml index 1690ec2e87f..0a0acfb4ca9 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -23,7 +23,7 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent 4.0.0 @@ -31,7 +31,7 @@ org.apache.zeppelin zeppelin-kylin jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Kylin interpreter diff --git a/lens/pom.xml b/lens/pom.xml index c67828733e0..57a96e22933 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Lens interpreter diff --git a/livy/pom.xml b/livy/pom.xml index 33c327b979b..5295fd1d909 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-livy jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Livy interpreter diff --git a/markdown/pom.xml b/markdown/pom.xml index cf8e766c673..d8685e7a4f6 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Markdown interpreter diff --git a/neo4j/pom.xml b/neo4j/pom.xml index 6883bdd996f..6aa768e2f02 100644 --- a/neo4j/pom.xml +++ b/neo4j/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-neo4j jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Neo4j interpreter diff --git a/pig/pom.xml b/pig/pom.xml index 06a58bbc9ee..a7b19420f37 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org diff --git a/pom.xml b/pom.xml index e580b59b1b2..8a41d843007 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin Zeppelin project http://zeppelin.apache.org diff --git a/python/pom.xml b/python/pom.xml index 03da41cfd25..88c1d8a875b 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Python interpreter diff --git a/r/pom.xml b/r/pom.xml index fc9fa6621f1..1f8bd81a954 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-zrinterpreter_${scala.binary.version} jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: R Interpreter R Interpreter for Zeppelin http://zeppelin.apache.org diff --git a/sap/pom.xml b/sap/pom.xml index e75cc10a38f..e0f3a90de2a 100644 --- a/sap/pom.xml +++ b/sap/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin sap jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Sap Zeppelin SAP support diff --git a/scalding/pom.xml b/scalding/pom.xml index 051368fa9a2..fa4679ad3ef 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-scalding_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Scalding interpreter diff --git a/scio/pom.xml b/scio/pom.xml index 3c9281c627d..a2857ffd005 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-scio_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Scio Zeppelin Scio support diff --git a/shell/pom.xml b/shell/pom.xml index cd5a99b4e5f..390f685e8c5 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Shell interpreter diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 2c219a60655..6d258b2892a 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index 5505aa7f57b..d248271be43 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent/pom.xml org.apache.zeppelin spark-parent pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Spark Parent Zeppelin Spark Support diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index 9b111da94d5..21abb637e24 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Interpreter Scala_2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index 190f7101ca0..301622d2658 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Interpreter Scala_2.11 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index 6da2ac1a290..0b018cef686 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-spark-dependencies jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Spark dependencies Zeppelin spark support diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index c3b03d39e22..30dcd1cceb7 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0 pom Zeppelin: Spark Scala Parent diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 81275e47083..17760db77ed 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index 458f1d2858b..1b2c0d64360 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index 648ff385121..3cbceaea707 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark2 Shims diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index 5db50f76769..d553128d13a 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-display jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Display system apis diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index 9545082256f..9fbb6ae04ac 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -23,7 +23,7 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 85387b707c5..6c0c15d4408 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-examples pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Examples Zeppelin examples diff --git a/zeppelin-examples/zeppelin-example-clock/pom.xml b/zeppelin-examples/zeppelin-example-clock/pom.xml index 55333aa46ae..12f7aa8cf96 100644 --- a/zeppelin-examples/zeppelin-example-clock/pom.xml +++ b/zeppelin-examples/zeppelin-example-clock/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index 29f4e381e72..60837386383 100644 --- a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml +++ b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example application - Horizontal Bar chart diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml index d76c281f1de..2b04ce9f148 100644 --- a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index d84c887f7b0..83621a87dc8 100644 --- a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index 67604437bcc..2f8dc8d39a6 100644 --- a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml index 527990924dd..04c030be582 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml index a2efef70c32..a8fcc7ce33d 100644 --- a/zeppelin-integration/pom.xml +++ b/zeppelin-integration/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-integration jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Integration Test diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index e75e1f8029f..4a491f23f2c 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Interpreter Zeppelin Interpreter diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index bc9ec5a816e..8c1f52b3cb0 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,13 +24,13 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. zeppelin-jupyter jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 165123c9d80..248aa90cd33 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-server jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Server diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 2bc22c02d5b..902c9b80e8b 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-web war - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: web Application diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index a438242c4a4..cfaf66abd4b 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,14 +23,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-zengine jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Zengine Zeppelin Zengine From 877bcd7011b14c2213e9cd2de94f0e6299bfe789 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Mon, 28 May 2018 12:54:18 +0800 Subject: [PATCH 315/492] Preparing development version 0.8.1-SNAPSHOT --- alluxio/pom.xml | 4 ++-- angular/pom.xml | 4 ++-- beam/pom.xml | 4 ++-- bigquery/pom.xml | 4 ++-- cassandra/pom.xml | 4 ++-- elasticsearch/pom.xml | 4 ++-- file/pom.xml | 4 ++-- flink/pom.xml | 4 ++-- geode/pom.xml | 4 ++-- groovy/pom.xml | 4 ++-- hbase/pom.xml | 4 ++-- helium-dev/pom.xml | 4 ++-- ignite/pom.xml | 4 ++-- interpreter-parent/pom.xml | 4 ++-- jdbc/pom.xml | 4 ++-- kylin/pom.xml | 4 ++-- lens/pom.xml | 4 ++-- livy/pom.xml | 4 ++-- markdown/pom.xml | 4 ++-- neo4j/pom.xml | 4 ++-- pig/pom.xml | 4 ++-- pom.xml | 2 +- python/pom.xml | 4 ++-- r/pom.xml | 4 ++-- sap/pom.xml | 4 ++-- scalding/pom.xml | 4 ++-- scio/pom.xml | 4 ++-- shell/pom.xml | 4 ++-- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 4 ++-- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-dependencies/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- zeppelin-display/pom.xml | 4 ++-- zeppelin-distribution/pom.xml | 2 +- zeppelin-examples/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-clock/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-horizontalbar/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-echo/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-markdown/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-translator/pom.xml | 4 ++-- zeppelin-integration/pom.xml | 4 ++-- zeppelin-interpreter/pom.xml | 4 ++-- zeppelin-jupyter/pom.xml | 4 ++-- zeppelin-server/pom.xml | 4 ++-- zeppelin-web/pom.xml | 4 ++-- zeppelin-zengine/pom.xml | 4 ++-- 52 files changed, 102 insertions(+), 102 deletions(-) diff --git a/alluxio/pom.xml b/alluxio/pom.xml index b6a4876cd8e..acef4942230 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Alluxio interpreter diff --git a/angular/pom.xml b/angular/pom.xml index a44f227d53a..341d7d5ec54 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Angular interpreter diff --git a/beam/pom.xml b/beam/pom.xml index 83b8e0d1480..7592e77e010 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Beam interpreter diff --git a/bigquery/pom.xml b/bigquery/pom.xml index abe94d28665..f3877179a9b 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: BigQuery interpreter diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 5e19e0abad4..2be0606157a 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-cassandra_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index b7bdbf159ae..3b7c2b54b4d 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent zeppelin-elasticsearch jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Elasticsearch interpreter diff --git a/file/pom.xml b/file/pom.xml index d2fbde9c89d..1ea8eace07f 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: File System Interpreters diff --git a/flink/pom.xml b/flink/pom.xml index 6824b8da75c..47bd2877c83 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-flink_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Flink Zeppelin flink support diff --git a/geode/pom.xml b/geode/pom.xml index 3af4456c967..59c16b731b2 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Geode interpreter diff --git a/groovy/pom.xml b/groovy/pom.xml index 8bb49227e72..7dc4282d6e4 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Groovy interpreter diff --git a/hbase/pom.xml b/hbase/pom.xml index d2c4d0962d2..6bf1a80905d 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: HBase interpreter diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index dba601d341c..8445b4dcf9e 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -24,13 +24,13 @@ org.apache.zeppelin interpreter-parent - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Helium development interpreter diff --git a/ignite/pom.xml b/ignite/pom.xml index 49f8ae2ea19..31df05eac77 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent zeppelin-ignite_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Ignite interpreter diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index 06e9b8c6834..33837913182 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin interpreter-parent pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Interpreter Parent diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 9a1d82b3d35..58de18199ae 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: JDBC interpreter diff --git a/kylin/pom.xml b/kylin/pom.xml index 0a0acfb4ca9..1690ec2e87f 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -23,7 +23,7 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent 4.0.0 @@ -31,7 +31,7 @@ org.apache.zeppelin zeppelin-kylin jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Kylin interpreter diff --git a/lens/pom.xml b/lens/pom.xml index 57a96e22933..c67828733e0 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Lens interpreter diff --git a/livy/pom.xml b/livy/pom.xml index 5295fd1d909..33c327b979b 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-livy jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Livy interpreter diff --git a/markdown/pom.xml b/markdown/pom.xml index d8685e7a4f6..cf8e766c673 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Markdown interpreter diff --git a/neo4j/pom.xml b/neo4j/pom.xml index 6aa768e2f02..6883bdd996f 100644 --- a/neo4j/pom.xml +++ b/neo4j/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-neo4j jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Neo4j interpreter diff --git a/pig/pom.xml b/pig/pom.xml index a7b19420f37..06a58bbc9ee 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org diff --git a/pom.xml b/pom.xml index 8a41d843007..e580b59b1b2 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin Zeppelin project http://zeppelin.apache.org diff --git a/python/pom.xml b/python/pom.xml index 88c1d8a875b..03da41cfd25 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Python interpreter diff --git a/r/pom.xml b/r/pom.xml index 1f8bd81a954..fc9fa6621f1 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-zrinterpreter_${scala.binary.version} jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: R Interpreter R Interpreter for Zeppelin http://zeppelin.apache.org diff --git a/sap/pom.xml b/sap/pom.xml index e0f3a90de2a..e75cc10a38f 100644 --- a/sap/pom.xml +++ b/sap/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin sap jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Sap Zeppelin SAP support diff --git a/scalding/pom.xml b/scalding/pom.xml index fa4679ad3ef..051368fa9a2 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-scalding_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Scalding interpreter diff --git a/scio/pom.xml b/scio/pom.xml index a2857ffd005..3c9281c627d 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-scio_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Scio Zeppelin Scio support diff --git a/shell/pom.xml b/shell/pom.xml index 390f685e8c5..cd5a99b4e5f 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Shell interpreter diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 6d258b2892a..2c219a60655 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index d248271be43..5505aa7f57b 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent/pom.xml org.apache.zeppelin spark-parent pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark Parent Zeppelin Spark Support diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index 21abb637e24..9b111da94d5 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Interpreter Scala_2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index 301622d2658..190f7101ca0 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Interpreter Scala_2.11 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index 0b018cef686..6da2ac1a290 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-spark-dependencies jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark dependencies Zeppelin spark support diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index 30dcd1cceb7..c3b03d39e22 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT pom Zeppelin: Spark Scala Parent diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 17760db77ed..81275e47083 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index 1b2c0d64360..458f1d2858b 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index 3cbceaea707..648ff385121 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark2 Shims diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index d553128d13a..5db50f76769 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-display jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Display system apis diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index 9fbb6ae04ac..9545082256f 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -23,7 +23,7 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 6c0c15d4408..85387b707c5 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-examples pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Examples Zeppelin examples diff --git a/zeppelin-examples/zeppelin-example-clock/pom.xml b/zeppelin-examples/zeppelin-example-clock/pom.xml index 12f7aa8cf96..55333aa46ae 100644 --- a/zeppelin-examples/zeppelin-example-clock/pom.xml +++ b/zeppelin-examples/zeppelin-example-clock/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index 60837386383..29f4e381e72 100644 --- a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml +++ b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example application - Horizontal Bar chart diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml index 2b04ce9f148..d76c281f1de 100644 --- a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index 83621a87dc8..d84c887f7b0 100644 --- a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index 2f8dc8d39a6..67604437bcc 100644 --- a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml index 04c030be582..527990924dd 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml index a8fcc7ce33d..a2efef70c32 100644 --- a/zeppelin-integration/pom.xml +++ b/zeppelin-integration/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-integration jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Integration Test diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index 4a491f23f2c..e75e1f8029f 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Interpreter Zeppelin Interpreter diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index 8c1f52b3cb0..bc9ec5a816e 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,13 +24,13 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. zeppelin-jupyter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 248aa90cd33..165123c9d80 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-server jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Server diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 902c9b80e8b..2bc22c02d5b 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-web war - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: web Application diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index cfaf66abd4b..a438242c4a4 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,14 +23,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-zengine jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Zengine Zeppelin Zengine From ad355b515e9d8ce65bf1eb0527de586f0a1c81f8 Mon Sep 17 00:00:00 2001 From: Renjith Kamath Date: Wed, 23 May 2018 11:28:19 +0530 Subject: [PATCH 316/492] ZEPPELIN-3482 Incorrect user is picked up by Zeppelin during relogin after Knox SSO token expiry ### What is this PR for? Incorrect user is picked up by Zeppelin during relogin after Knox SSO token expiry ### What type of PR is it? Bug Fix ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-3482 ### How should this be tested? * steps in JIRA description. ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: Renjith Kamath Closes #2977 from r-kamath/ZEPPELIN-3482 and squashes the following commits: 1b84467 [Renjith Kamath] ZEPPELIN-3482 Incorrect user is picked up by Zeppelin during relogin after Knox SSO token expiry (cherry picked from commit c41ba77ff9e189daf499cc0d33f54daba0add850) Signed-off-by: Prabhjyot Singh # Conflicts: # zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java --- .../zeppelin/realm/jwt/KnoxJwtRealm.java | 10 +++++++- .../apache/zeppelin/rest/LoginRestApi.java | 25 +++++++++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java index d3a27590d2a..0ba403b22d2 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java @@ -120,7 +120,7 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) return null; } - private String getName(JWTAuthenticationToken upToken) throws ParseException { + public String getName(JWTAuthenticationToken upToken) throws ParseException { SignedJWT signed = SignedJWT.parse(upToken.getToken()); String userName = signed.getJWTClaimsSet().getSubject(); return userName; @@ -139,6 +139,14 @@ protected boolean validateToken(String token) { LOGGER.warn("Expiration time validation of JWT token failed."); return false; } + String currentUser = (String) org.apache.shiro.SecurityUtils.getSubject().getPrincipal(); + if (currentUser == null) { + return true; + } + String cookieUser = signed.getJWTClaimsSet().getSubject(); + if (!cookieUser.equals(currentUser)) { + return false; + } return true; } catch (ParseException ex) { LOGGER.info("ParseException in validateToken", ex); diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java index b05c295e659..8d96188a0c4 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java @@ -18,6 +18,7 @@ import com.google.gson.Gson; +import java.text.ParseException; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -79,9 +80,14 @@ public Response getLogin(@Context HttpHeaders headers) { Cookie cookie = headers.getCookies().get(knoxJwtRealm.getCookieName()); if (cookie != null && cookie.getValue() != null) { Subject currentUser = org.apache.shiro.SecurityUtils.getSubject(); - if (!currentUser.isAuthenticated()) { - JWTAuthenticationToken token = new JWTAuthenticationToken(null, cookie.getValue()); - response = proceedToLogin(currentUser, token); + JWTAuthenticationToken token = new JWTAuthenticationToken(null, cookie.getValue()); + try { + String name = knoxJwtRealm.getName(token); + if (!currentUser.isAuthenticated() || !currentUser.getPrincipal().equals(name)) { + response = proceedToLogin(currentUser, token); + } + } catch (ParseException e) { + LOG.error("ParseException in LoginRestApi: ", e); } } if (response == null) { @@ -129,7 +135,7 @@ private boolean isKnoxSSOEnabled() { private JsonResponse proceedToLogin(Subject currentUser, AuthenticationToken token) { JsonResponse response = null; try { - currentUser.getSession().stop(); + logoutCurrentUser(); currentUser.getSession(true); currentUser.login(token); @@ -206,10 +212,7 @@ public Response postLogin(@FormParam("userName") String userName, @ZeppelinApi public Response logout() { JsonResponse response; - Subject currentUser = org.apache.shiro.SecurityUtils.getSubject(); - TicketContainer.instance.removeTicket(SecurityUtils.getPrincipal()); - currentUser.getSession().stop(); - currentUser.logout(); + logoutCurrentUser(); if (isKnoxSSOEnabled()) { KnoxJwtRealm knoxJwtRealm = getJTWRealm(); Map data = new HashMap<>(); @@ -233,4 +236,10 @@ private String constructKnoxUrl(KnoxJwtRealm knoxJwtRealm, String path) { return redirectURL.toString(); } + private void logoutCurrentUser() { + Subject currentUser = org.apache.shiro.SecurityUtils.getSubject(); + TicketContainer.instance.removeTicket(SecurityUtils.getPrincipal()); + currentUser.getSession().stop(); + currentUser.logout(); + } } From 2b19514954768cb3f73580ded00d32c323888a71 Mon Sep 17 00:00:00 2001 From: Renjith Kamath Date: Thu, 24 May 2018 12:31:18 +0530 Subject: [PATCH 317/492] ZEPPELIN-3496 Notebook title not visible in simple and report mode Notebook title not visible in simple and report mode - fix title visibility - fix title alignment Bug Fix https://issues.apache.org/jira/browse/ZEPPELIN-3496 manually switch views between default, simple and report **Simple view before** simple-view-before **Simple view hover before** (left alignment is off) simple-view-hover-before **Simple view after** simple-view-after **Simple view hover after** simple-view-hover-after * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: Renjith Kamath Closes #2983 from r-kamath/ZEPPELIN-3496 and squashes the following commits: 4dd6286b2 [Renjith Kamath] ZEPPELIN-3496 Notebook title not visible in simple and report mode Change-Id: I101f116ae2d1bcdc1820d32eeb05c9cfdd7864a0 (cherry picked from commit 2a966360026e7b4bb8c19168bbd0a85b0a7a9752) Signed-off-by: Renjith Kamath --- zeppelin-web/src/app/notebook/notebook-actionBar.html | 5 +++-- zeppelin-web/src/assets/styles/looknfeel/report.css | 4 ++++ zeppelin-web/src/assets/styles/looknfeel/simple.css | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index 78ca77efb17..22292239738 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -17,10 +17,11 @@

    -

    span { + visibility: visible; +} + .noteAction:hover span, .noteAction:hover button, .noteAction:hover form { diff --git a/zeppelin-web/src/assets/styles/looknfeel/simple.css b/zeppelin-web/src/assets/styles/looknfeel/simple.css index 0078306185e..55a64843119 100644 --- a/zeppelin-web/src/assets/styles/looknfeel/simple.css +++ b/zeppelin-web/src/assets/styles/looknfeel/simple.css @@ -89,6 +89,10 @@ body { visibility: hidden; } +.noteAction .form-control-static2 > span { + visibility: visible; +} + .noteAction:hover span, .noteAction:hover button, .noteAction:hover form { From 306b0f23dee217ce4d6f09abc63847e34974d844 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 29 May 2018 12:55:02 +0800 Subject: [PATCH 318/492] ZEPPELIN-3508. Enable new spark interpreter in 0.8.0 ### What is this PR for? I'd like to enable it in 0.8 so that we can get more feedback from users. And user can still use the old implementing via setting `zeppelin.spark.useNew` to `false` ### What type of PR is it? [Improvement] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3508 ### How should this be tested? * CI pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2989 from zjffdu/ZEPPELIN-3508 and squashes the following commits: 25c900b9f [Jeff Zhang] ZEPPELIN-3508. Enable new spark interpreter in 0.8.0 (cherry picked from commit ffa4ee31a2d45a0f8e4256cd3c285357c42410e1) Signed-off-by: Jeff Zhang --- docs/interpreter/spark.md | 3 +-- spark/interpreter/src/main/resources/interpreter-setting.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/interpreter/spark.md b/docs/interpreter/spark.md index 85b2981fcc0..51b7c9e5324 100644 --- a/docs/interpreter/spark.md +++ b/docs/interpreter/spark.md @@ -205,8 +205,7 @@ You can either specify them in `zeppelin-env.sh`, or in interpreter setting page in interpreter setting page means you can use multiple versions of `spark` & `hadoop` in one zeppelin instance. ### 4. New Version of SparkInterpreter -There's one new version of SparkInterpreter starting with better spark support and code completion from Zeppelin 0.8.0, by default we still use the old version of SparkInterpreter. -If you want to use the new one, you can configure `zeppelin.spark.useNew` as `true` in its interpreter setting. +There's one new version of SparkInterpreter with better spark support and code completion starting from Zeppelin 0.8.0. We enable it by default, but user can still use the old version of SparkInterpreter by setting `zeppelin.spark.useNew` as `false` in its interpreter setting. ## SparkContext, SQLContext, SparkSession, ZeppelinContext SparkContext, SQLContext and ZeppelinContext are automatically created and exposed as variable names `sc`, `sqlContext` and `z`, respectively, in Scala, Python and R environments. diff --git a/spark/interpreter/src/main/resources/interpreter-setting.json b/spark/interpreter/src/main/resources/interpreter-setting.json index 30ae7379229..c4bd4428ada 100644 --- a/spark/interpreter/src/main/resources/interpreter-setting.json +++ b/spark/interpreter/src/main/resources/interpreter-setting.json @@ -78,7 +78,7 @@ "zeppelin.spark.useNew": { "envName": null, "propertyName": "zeppelin.spark.useNew", - "defaultValue": "false", + "defaultValue": "true", "description": "Whether use new spark interpreter implementation", "type": "checkbox" } From 74ffc213402f3b6a94fa91a55951a251c79f61d3 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 29 May 2018 21:33:56 +0800 Subject: [PATCH 319/492] [HOTFIX] change string true to boolean true ### What is this PR for? Trivial fix ### What type of PR is it? [Hot Fix] ### Todos * [ ] - Task ### What is the Jira issue? * ### How should this be tested? * CI pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? NO Author: Jeff Zhang Closes #2992 from zjffdu/hotfix_true and squashes the following commits: cb0f956f1 [Jeff Zhang] [HOTFIX] change string true to boolean true (cherry picked from commit 0c29a95bb653181feabec24659c05d91a027fcfe) Signed-off-by: Jeff Zhang --- spark/interpreter/src/main/resources/interpreter-setting.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spark/interpreter/src/main/resources/interpreter-setting.json b/spark/interpreter/src/main/resources/interpreter-setting.json index c4bd4428ada..8cd82bb7e2b 100644 --- a/spark/interpreter/src/main/resources/interpreter-setting.json +++ b/spark/interpreter/src/main/resources/interpreter-setting.json @@ -78,7 +78,7 @@ "zeppelin.spark.useNew": { "envName": null, "propertyName": "zeppelin.spark.useNew", - "defaultValue": "true", + "defaultValue": true, "description": "Whether use new spark interpreter implementation", "type": "checkbox" } From 41d9e0ac3f908da2eb4167984271aced78a8bfc6 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 29 May 2018 21:36:55 +0800 Subject: [PATCH 320/492] Preparing Apache Zeppelin release 0.8.0 --- alluxio/pom.xml | 4 ++-- angular/pom.xml | 4 ++-- beam/pom.xml | 4 ++-- bigquery/pom.xml | 4 ++-- cassandra/pom.xml | 4 ++-- elasticsearch/pom.xml | 4 ++-- file/pom.xml | 4 ++-- flink/pom.xml | 4 ++-- geode/pom.xml | 4 ++-- groovy/pom.xml | 4 ++-- hbase/pom.xml | 4 ++-- helium-dev/pom.xml | 4 ++-- ignite/pom.xml | 4 ++-- interpreter-parent/pom.xml | 4 ++-- jdbc/pom.xml | 4 ++-- kylin/pom.xml | 4 ++-- lens/pom.xml | 4 ++-- livy/pom.xml | 4 ++-- markdown/pom.xml | 4 ++-- neo4j/pom.xml | 4 ++-- pig/pom.xml | 4 ++-- pom.xml | 2 +- python/pom.xml | 4 ++-- r/pom.xml | 4 ++-- sap/pom.xml | 4 ++-- scalding/pom.xml | 4 ++-- scio/pom.xml | 4 ++-- shell/pom.xml | 4 ++-- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 4 ++-- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-dependencies/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- zeppelin-display/pom.xml | 4 ++-- zeppelin-distribution/pom.xml | 2 +- zeppelin-examples/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-clock/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-horizontalbar/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-echo/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-markdown/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-translator/pom.xml | 4 ++-- zeppelin-integration/pom.xml | 4 ++-- zeppelin-interpreter/pom.xml | 4 ++-- zeppelin-jupyter/pom.xml | 4 ++-- zeppelin-server/pom.xml | 4 ++-- zeppelin-web/pom.xml | 4 ++-- zeppelin-zengine/pom.xml | 4 ++-- 52 files changed, 102 insertions(+), 102 deletions(-) diff --git a/alluxio/pom.xml b/alluxio/pom.xml index acef4942230..b6a4876cd8e 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Alluxio interpreter diff --git a/angular/pom.xml b/angular/pom.xml index 341d7d5ec54..a44f227d53a 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Angular interpreter diff --git a/beam/pom.xml b/beam/pom.xml index 7592e77e010..83b8e0d1480 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Beam interpreter diff --git a/bigquery/pom.xml b/bigquery/pom.xml index f3877179a9b..abe94d28665 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: BigQuery interpreter diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 2be0606157a..5e19e0abad4 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-cassandra_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index 3b7c2b54b4d..b7bdbf159ae 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent zeppelin-elasticsearch jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Elasticsearch interpreter diff --git a/file/pom.xml b/file/pom.xml index 1ea8eace07f..d2fbde9c89d 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: File System Interpreters diff --git a/flink/pom.xml b/flink/pom.xml index 47bd2877c83..6824b8da75c 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-flink_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Flink Zeppelin flink support diff --git a/geode/pom.xml b/geode/pom.xml index 59c16b731b2..3af4456c967 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Geode interpreter diff --git a/groovy/pom.xml b/groovy/pom.xml index 7dc4282d6e4..8bb49227e72 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Groovy interpreter diff --git a/hbase/pom.xml b/hbase/pom.xml index 6bf1a80905d..d2c4d0962d2 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: HBase interpreter diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index 8445b4dcf9e..dba601d341c 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -24,13 +24,13 @@ org.apache.zeppelin interpreter-parent - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Helium development interpreter diff --git a/ignite/pom.xml b/ignite/pom.xml index 31df05eac77..49f8ae2ea19 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent zeppelin-ignite_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Ignite interpreter diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index 33837913182..06e9b8c6834 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin interpreter-parent pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Interpreter Parent diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 58de18199ae..9a1d82b3d35 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: JDBC interpreter diff --git a/kylin/pom.xml b/kylin/pom.xml index 1690ec2e87f..0a0acfb4ca9 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -23,7 +23,7 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent 4.0.0 @@ -31,7 +31,7 @@ org.apache.zeppelin zeppelin-kylin jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Kylin interpreter diff --git a/lens/pom.xml b/lens/pom.xml index c67828733e0..57a96e22933 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Lens interpreter diff --git a/livy/pom.xml b/livy/pom.xml index 33c327b979b..5295fd1d909 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-livy jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Livy interpreter diff --git a/markdown/pom.xml b/markdown/pom.xml index cf8e766c673..d8685e7a4f6 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Markdown interpreter diff --git a/neo4j/pom.xml b/neo4j/pom.xml index 6883bdd996f..6aa768e2f02 100644 --- a/neo4j/pom.xml +++ b/neo4j/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-neo4j jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Neo4j interpreter diff --git a/pig/pom.xml b/pig/pom.xml index 06a58bbc9ee..a7b19420f37 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org diff --git a/pom.xml b/pom.xml index e580b59b1b2..8a41d843007 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin Zeppelin project http://zeppelin.apache.org diff --git a/python/pom.xml b/python/pom.xml index 03da41cfd25..88c1d8a875b 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Python interpreter diff --git a/r/pom.xml b/r/pom.xml index fc9fa6621f1..1f8bd81a954 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-zrinterpreter_${scala.binary.version} jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: R Interpreter R Interpreter for Zeppelin http://zeppelin.apache.org diff --git a/sap/pom.xml b/sap/pom.xml index e75cc10a38f..e0f3a90de2a 100644 --- a/sap/pom.xml +++ b/sap/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin sap jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Sap Zeppelin SAP support diff --git a/scalding/pom.xml b/scalding/pom.xml index 051368fa9a2..fa4679ad3ef 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-scalding_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Scalding interpreter diff --git a/scio/pom.xml b/scio/pom.xml index 3c9281c627d..a2857ffd005 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-scio_2.10 jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Scio Zeppelin Scio support diff --git a/shell/pom.xml b/shell/pom.xml index cd5a99b4e5f..390f685e8c5 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Shell interpreter diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 2c219a60655..6d258b2892a 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index 5505aa7f57b..d248271be43 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../interpreter-parent/pom.xml org.apache.zeppelin spark-parent pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Spark Parent Zeppelin Spark Support diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index 9b111da94d5..21abb637e24 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Interpreter Scala_2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index 190f7101ca0..301622d2658 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Interpreter Scala_2.11 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index 6da2ac1a290..0b018cef686 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-spark-dependencies jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Spark dependencies Zeppelin spark support diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index c3b03d39e22..30dcd1cceb7 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.8.1-SNAPSHOT + 0.8.0 pom Zeppelin: Spark Scala Parent diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 81275e47083..17760db77ed 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index 458f1d2858b..1b2c0d64360 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index 648ff385121..3cbceaea707 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.8.1-SNAPSHOT + 0.8.0 jar Zeppelin: Spark2 Shims diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index 5db50f76769..d553128d13a 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-display jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Display system apis diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index 9545082256f..9fbb6ae04ac 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -23,7 +23,7 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 85387b707c5..6c0c15d4408 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-examples pom - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Examples Zeppelin examples diff --git a/zeppelin-examples/zeppelin-example-clock/pom.xml b/zeppelin-examples/zeppelin-example-clock/pom.xml index 55333aa46ae..12f7aa8cf96 100644 --- a/zeppelin-examples/zeppelin-example-clock/pom.xml +++ b/zeppelin-examples/zeppelin-example-clock/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index 29f4e381e72..60837386383 100644 --- a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml +++ b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example application - Horizontal Bar chart diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml index d76c281f1de..2b04ce9f148 100644 --- a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index d84c887f7b0..83621a87dc8 100644 --- a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index 67604437bcc..2f8dc8d39a6 100644 --- a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml index 527990924dd..04c030be582 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml index a2efef70c32..a8fcc7ce33d 100644 --- a/zeppelin-integration/pom.xml +++ b/zeppelin-integration/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-integration jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Integration Test diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index e75e1f8029f..4a491f23f2c 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Interpreter Zeppelin Interpreter diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index bc9ec5a816e..8c1f52b3cb0 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,13 +24,13 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. zeppelin-jupyter jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 165123c9d80..248aa90cd33 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-server jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Server diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 2bc22c02d5b..902c9b80e8b 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-web war - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: web Application diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index a438242c4a4..cfaf66abd4b 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,14 +23,14 @@ zeppelin org.apache.zeppelin - 0.8.1-SNAPSHOT + 0.8.0 .. org.apache.zeppelin zeppelin-zengine jar - 0.8.1-SNAPSHOT + 0.8.0 Zeppelin: Zengine Zeppelin Zengine From d7918ab8d15a062fae51107ad954c9b7c6bf13c2 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 29 May 2018 21:37:00 +0800 Subject: [PATCH 321/492] Preparing development version 0.8.1-SNAPSHOT --- alluxio/pom.xml | 4 ++-- angular/pom.xml | 4 ++-- beam/pom.xml | 4 ++-- bigquery/pom.xml | 4 ++-- cassandra/pom.xml | 4 ++-- elasticsearch/pom.xml | 4 ++-- file/pom.xml | 4 ++-- flink/pom.xml | 4 ++-- geode/pom.xml | 4 ++-- groovy/pom.xml | 4 ++-- hbase/pom.xml | 4 ++-- helium-dev/pom.xml | 4 ++-- ignite/pom.xml | 4 ++-- interpreter-parent/pom.xml | 4 ++-- jdbc/pom.xml | 4 ++-- kylin/pom.xml | 4 ++-- lens/pom.xml | 4 ++-- livy/pom.xml | 4 ++-- markdown/pom.xml | 4 ++-- neo4j/pom.xml | 4 ++-- pig/pom.xml | 4 ++-- pom.xml | 2 +- python/pom.xml | 4 ++-- r/pom.xml | 4 ++-- sap/pom.xml | 4 ++-- scalding/pom.xml | 4 ++-- scio/pom.xml | 4 ++-- shell/pom.xml | 4 ++-- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 4 ++-- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-dependencies/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- zeppelin-display/pom.xml | 4 ++-- zeppelin-distribution/pom.xml | 2 +- zeppelin-examples/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-clock/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-horizontalbar/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-echo/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-markdown/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-translator/pom.xml | 4 ++-- zeppelin-integration/pom.xml | 4 ++-- zeppelin-interpreter/pom.xml | 4 ++-- zeppelin-jupyter/pom.xml | 4 ++-- zeppelin-server/pom.xml | 4 ++-- zeppelin-web/pom.xml | 4 ++-- zeppelin-zengine/pom.xml | 4 ++-- 52 files changed, 102 insertions(+), 102 deletions(-) diff --git a/alluxio/pom.xml b/alluxio/pom.xml index b6a4876cd8e..acef4942230 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Alluxio interpreter diff --git a/angular/pom.xml b/angular/pom.xml index a44f227d53a..341d7d5ec54 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Angular interpreter diff --git a/beam/pom.xml b/beam/pom.xml index 83b8e0d1480..7592e77e010 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Beam interpreter diff --git a/bigquery/pom.xml b/bigquery/pom.xml index abe94d28665..f3877179a9b 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: BigQuery interpreter diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 5e19e0abad4..2be0606157a 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-cassandra_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index b7bdbf159ae..3b7c2b54b4d 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent zeppelin-elasticsearch jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Elasticsearch interpreter diff --git a/file/pom.xml b/file/pom.xml index d2fbde9c89d..1ea8eace07f 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: File System Interpreters diff --git a/flink/pom.xml b/flink/pom.xml index 6824b8da75c..47bd2877c83 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-flink_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Flink Zeppelin flink support diff --git a/geode/pom.xml b/geode/pom.xml index 3af4456c967..59c16b731b2 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Geode interpreter diff --git a/groovy/pom.xml b/groovy/pom.xml index 8bb49227e72..7dc4282d6e4 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Groovy interpreter diff --git a/hbase/pom.xml b/hbase/pom.xml index d2c4d0962d2..6bf1a80905d 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: HBase interpreter diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index dba601d341c..8445b4dcf9e 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -24,13 +24,13 @@ org.apache.zeppelin interpreter-parent - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Helium development interpreter diff --git a/ignite/pom.xml b/ignite/pom.xml index 49f8ae2ea19..31df05eac77 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent zeppelin-ignite_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Ignite interpreter diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index 06e9b8c6834..33837913182 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin interpreter-parent pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Interpreter Parent diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 9a1d82b3d35..58de18199ae 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: JDBC interpreter diff --git a/kylin/pom.xml b/kylin/pom.xml index 0a0acfb4ca9..1690ec2e87f 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -23,7 +23,7 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent 4.0.0 @@ -31,7 +31,7 @@ org.apache.zeppelin zeppelin-kylin jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Kylin interpreter diff --git a/lens/pom.xml b/lens/pom.xml index 57a96e22933..c67828733e0 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Lens interpreter diff --git a/livy/pom.xml b/livy/pom.xml index 5295fd1d909..33c327b979b 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-livy jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Livy interpreter diff --git a/markdown/pom.xml b/markdown/pom.xml index d8685e7a4f6..cf8e766c673 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Markdown interpreter diff --git a/neo4j/pom.xml b/neo4j/pom.xml index 6aa768e2f02..6883bdd996f 100644 --- a/neo4j/pom.xml +++ b/neo4j/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-neo4j jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Neo4j interpreter diff --git a/pig/pom.xml b/pig/pom.xml index a7b19420f37..06a58bbc9ee 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org diff --git a/pom.xml b/pom.xml index 8a41d843007..e580b59b1b2 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin Zeppelin project http://zeppelin.apache.org diff --git a/python/pom.xml b/python/pom.xml index 88c1d8a875b..03da41cfd25 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Python interpreter diff --git a/r/pom.xml b/r/pom.xml index 1f8bd81a954..fc9fa6621f1 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-zrinterpreter_${scala.binary.version} jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: R Interpreter R Interpreter for Zeppelin http://zeppelin.apache.org diff --git a/sap/pom.xml b/sap/pom.xml index e0f3a90de2a..e75cc10a38f 100644 --- a/sap/pom.xml +++ b/sap/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin sap jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Sap Zeppelin SAP support diff --git a/scalding/pom.xml b/scalding/pom.xml index fa4679ad3ef..051368fa9a2 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-scalding_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Scalding interpreter diff --git a/scio/pom.xml b/scio/pom.xml index a2857ffd005..3c9281c627d 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-scio_2.10 jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Scio Zeppelin Scio support diff --git a/shell/pom.xml b/shell/pom.xml index 390f685e8c5..cd5a99b4e5f 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Shell interpreter diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 6d258b2892a..2c219a60655 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index d248271be43..5505aa7f57b 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../interpreter-parent/pom.xml org.apache.zeppelin spark-parent pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark Parent Zeppelin Spark Support diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index 21abb637e24..9b111da94d5 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Interpreter Scala_2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index 301622d2658..190f7101ca0 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Interpreter Scala_2.11 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index 0b018cef686..6da2ac1a290 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-spark-dependencies jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Spark dependencies Zeppelin spark support diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index 30dcd1cceb7..c3b03d39e22 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.8.0 + 0.8.1-SNAPSHOT pom Zeppelin: Spark Scala Parent diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 17760db77ed..81275e47083 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index 1b2c0d64360..458f1d2858b 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index 3cbceaea707..648ff385121 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.8.0 + 0.8.1-SNAPSHOT jar Zeppelin: Spark2 Shims diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index d553128d13a..5db50f76769 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-display jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Display system apis diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index 9fbb6ae04ac..9545082256f 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -23,7 +23,7 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 6c0c15d4408..85387b707c5 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-examples pom - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Examples Zeppelin examples diff --git a/zeppelin-examples/zeppelin-example-clock/pom.xml b/zeppelin-examples/zeppelin-example-clock/pom.xml index 12f7aa8cf96..55333aa46ae 100644 --- a/zeppelin-examples/zeppelin-example-clock/pom.xml +++ b/zeppelin-examples/zeppelin-example-clock/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index 60837386383..29f4e381e72 100644 --- a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml +++ b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example application - Horizontal Bar chart diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml index 2b04ce9f148..d76c281f1de 100644 --- a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index 83621a87dc8..d84c887f7b0 100644 --- a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index 2f8dc8d39a6..67604437bcc 100644 --- a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml index 04c030be582..527990924dd 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml index a8fcc7ce33d..a2efef70c32 100644 --- a/zeppelin-integration/pom.xml +++ b/zeppelin-integration/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-integration jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Integration Test diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index 4a491f23f2c..e75e1f8029f 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Interpreter Zeppelin Interpreter diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index 8c1f52b3cb0..bc9ec5a816e 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,13 +24,13 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. zeppelin-jupyter jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 248aa90cd33..165123c9d80 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-server jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Server diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 902c9b80e8b..2bc22c02d5b 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-web war - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: web Application diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index cfaf66abd4b..a438242c4a4 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,14 +23,14 @@ zeppelin org.apache.zeppelin - 0.8.0 + 0.8.1-SNAPSHOT .. org.apache.zeppelin zeppelin-zengine jar - 0.8.0 + 0.8.1-SNAPSHOT Zeppelin: Zengine Zeppelin Zengine From d17535bca7db8f41a037e8f8026bec4b221632e7 Mon Sep 17 00:00:00 2001 From: Jongyoul Lee Date: Mon, 21 May 2018 15:15:48 +0900 Subject: [PATCH 322/492] ZEPPELIN-2221 Show `Jobs` page when `jobId` is missing ### What is this PR for? Because of yarn's bug, Spark's `jobId` is not passed. This causes some Spark UI link looks broken. In this kind of case, showing `Jobs` page looks reasonable. Yarn's bug is already fixed with the latest version only, so we need to handle it in Zeppelin side. ### What type of PR is it? [Bug Fix] ### Todos * [x] - Return `Jobs` page when `Job` page is not available ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-2221 ### How should this be tested? 1. Install outdated yarn 2. Run script 3. Click `Spark UI` to show `Jobs` page instead of 404 ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jongyoul Lee Closes #2964 from jongyoul/ZEPPELIN-2221 and squashes the following commits: d595028b5 [Jongyoul Lee] Remove unused packages from import statements Add spark's master value as a parameter of setupSparkListener Add parameterized test for various yarn versions Add a test checking jobUrl Add a logic to check a hadoop version and yarn version to support passing `get` parameters Add hadoop dependency as a `provided` scope 8b6f9076d [Jongyoul Lee] Fix to cover spark1 and spark2 8fdc46763 [Jongyoul Lee] Fix to cover spark1 and spark2 7ffcc55ca [Jongyoul Lee] Consume httpEntity 431457308 [Jongyoul Lee] Add httpClient to test if `jobUrl` is valid or not Add test cases for that feature (cherry picked from commit b6beda64e7d25fa93958737fc8d2dad4eae309cc) Signed-off-by: Jongyoul Lee --- .../zeppelin/spark/NewSparkInterpreter.java | 8 +- .../zeppelin/spark/OldSparkInterpreter.java | 4 +- .../apache/zeppelin/spark/SparkShimsTest.java | 153 ++++++++++++++++++ spark/spark-shims/pom.xml | 11 ++ .../org/apache/zeppelin/spark/SparkShims.java | 59 +++++-- .../apache/zeppelin/spark/Spark1Shims.java | 25 +-- .../apache/zeppelin/spark/Spark2Shims.java | 4 +- 7 files changed, 217 insertions(+), 47 deletions(-) create mode 100644 spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkShimsTest.java diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java index 591ef96867e..2fa10933b1e 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java @@ -23,10 +23,7 @@ import org.apache.spark.SparkConf; import org.apache.spark.SparkContext; import org.apache.spark.api.java.JavaSparkContext; -import org.apache.spark.scheduler.SparkListenerJobStart; import org.apache.spark.sql.SQLContext; -import org.apache.spark.ui.jobs.JobProgressListener; -import org.apache.zeppelin.interpreter.BaseZeppelinContext; import org.apache.zeppelin.interpreter.DefaultInterpreterProperty; import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; @@ -34,7 +31,6 @@ import org.apache.zeppelin.interpreter.InterpreterHookRegistry; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.WrappedInterpreter; -import org.apache.zeppelin.interpreter.remote.RemoteEventClientWrapper; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.spark.dep.SparkDependencyContext; import org.slf4j.Logger; @@ -42,8 +38,6 @@ import java.io.File; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -119,7 +113,7 @@ public void open() throws InterpreterException { sparkSession = this.innerInterpreter.sparkSession(); sparkUrl = this.innerInterpreter.sparkUrl(); sparkShims = SparkShims.getInstance(sc.version()); - sparkShims.setupSparkListener(sparkUrl); + sparkShims.setupSparkListener(sc.master(), sparkUrl); hooks = getInterpreterGroup().getInterpreterHookRegistry(); z = new SparkZeppelinContext(sc, hooks, diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java index 0dfe3cb9380..83d3d6a0a7c 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java @@ -163,7 +163,7 @@ public OldSparkInterpreter(Properties property, SparkContext sc) { this.sc = sc; env = SparkEnv.get(); sparkShims = SparkShims.getInstance(sc.version()); - sparkShims.setupSparkListener(sparkUrl); + sparkShims.setupSparkListener(sc.master(), sparkUrl); } public SparkContext getSparkContext() { @@ -873,7 +873,7 @@ public void open() throws InterpreterException { sparkUrl = getSparkUIUrl(); sparkShims = SparkShims.getInstance(sc.version()); - sparkShims.setupSparkListener(sparkUrl); + sparkShims.setupSparkListener(sc.master(), sparkUrl); numReferenceOfSparkContext.incrementAndGet(); } diff --git a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkShimsTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkShimsTest.java new file mode 100644 index 00000000000..25afd4e1169 --- /dev/null +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkShimsTest.java @@ -0,0 +1,153 @@ +/* + * 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.spark; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Properties; +import org.apache.hadoop.util.VersionInfo; +import org.apache.zeppelin.interpreter.BaseZeppelinContext; +import org.apache.zeppelin.interpreter.remote.RemoteEventClientWrapper; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(Enclosed.class) +public class SparkShimsTest { + + @RunWith(Parameterized.class) + public static class ParamTests { + @Parameters(name = "Hadoop {0} supports jobUrl: {1}") + public static Collection data() { + return Arrays.asList( + new Object[][] { + {"2.6.0", false}, + {"2.6.1", false}, + {"2.6.2", false}, + {"2.6.3", false}, + {"2.6.4", false}, + {"2.6.5", false}, + {"2.6.6", true}, // The latest fixed version + {"2.6.7", true}, // Future version + {"2.7.0", false}, + {"2.7.1", false}, + {"2.7.2", false}, + {"2.7.3", false}, + {"2.7.4", true}, // The latest fixed version + {"2.7.5", true}, // Future versions + {"2.8.0", false}, + {"2.8.1", false}, + {"2.8.2", true}, // The latest fixed version + {"2.8.3", true}, // Future versions + {"2.9.0", true}, // The latest fixed version + {"2.9.1", true}, // Future versions + {"3.0.0", true}, // The latest fixed version + {"3.0.0-alpha4", true}, // The latest fixed version + {"3.0.1", true}, // Future versions + }); + } + + @Parameter public String version; + + @Parameter(1) + public boolean expected; + + @Test + public void checkYarnVersionTest() { + SparkShims sparkShims = + new SparkShims() { + @Override + public void setupSparkListener(String master, String sparkWebUrl) {} + }; + assertEquals(expected, sparkShims.supportYarn6615(version)); + } + } + + @RunWith(PowerMockRunner.class) + @PrepareForTest({BaseZeppelinContext.class, VersionInfo.class}) + @PowerMockIgnore({"javax.net.*", "javax.security.*"}) + public static class SingleTests { + @Mock Properties mockProperties; + @Captor ArgumentCaptor> argumentCaptor; + + SparkShims sparkShims; + + @Before + public void setUp() { + PowerMockito.mockStatic(BaseZeppelinContext.class); + RemoteEventClientWrapper mockRemoteEventClientWrapper = mock(RemoteEventClientWrapper.class); + + when(BaseZeppelinContext.getEventClient()).thenReturn(mockRemoteEventClientWrapper); + doNothing() + .when(mockRemoteEventClientWrapper) + .onParaInfosReceived(anyString(), anyString(), argumentCaptor.capture()); + + when(mockProperties.getProperty("spark.jobGroup.id")).thenReturn("job-note-paragraph"); + + try { + sparkShims = SparkShims.getInstance(SparkVersion.SPARK_2_0_0.toString()); + } catch (Throwable ignore) { + sparkShims = SparkShims.getInstance(SparkVersion.SPARK_1_6_0.toString()); + } + } + + @Test + public void runUnerLocalTest() { + sparkShims.buildSparkJobUrl("local", "http://sparkurl", 0, mockProperties); + + Map mapValue = argumentCaptor.getValue(); + assertTrue(mapValue.keySet().contains("jobUrl")); + assertTrue(mapValue.get("jobUrl").contains("/jobs/job?id=")); + } + + @Test + public void runUnerYarnTest() { + + sparkShims.buildSparkJobUrl("yarn", "http://sparkurl", 0, mockProperties); + + Map mapValue = argumentCaptor.getValue(); + assertTrue(mapValue.keySet().contains("jobUrl")); + + if (sparkShims.supportYarn6615(VersionInfo.getVersion())) { + assertTrue(mapValue.get("jobUrl").contains("/jobs/job?id=")); + } else { + assertFalse(mapValue.get("jobUrl").contains("/jobs/job?id=")); + } + } + } +} diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 81275e47083..03b6bf982b2 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -41,6 +41,17 @@ ${project.version} provided + + + + org.apache.hadoop + hadoop-common + 2.2.0 + provided + diff --git a/spark/spark-shims/src/main/scala/org/apache/zeppelin/spark/SparkShims.java b/spark/spark-shims/src/main/scala/org/apache/zeppelin/spark/SparkShims.java index acf717c5ae3..1d7323b0b33 100644 --- a/spark/spark-shims/src/main/scala/org/apache/zeppelin/spark/SparkShims.java +++ b/spark/spark-shims/src/main/scala/org/apache/zeppelin/spark/SparkShims.java @@ -15,10 +15,10 @@ * limitations under the License. */ - package org.apache.zeppelin.spark; - +import org.apache.hadoop.util.VersionInfo; +import org.apache.hadoop.util.VersionUtil; import org.apache.zeppelin.interpreter.BaseZeppelinContext; import org.apache.zeppelin.interpreter.remote.RemoteEventClientWrapper; import org.slf4j.Logger; @@ -29,11 +29,21 @@ import java.util.Properties; /** - * This is abstract class for anything that is api incompatible between spark1 and spark2. - * It will load the correct version of SparkShims based on the version of Spark. + * This is abstract class for anything that is api incompatible between spark1 and spark2. It will + * load the correct version of SparkShims based on the version of Spark. */ public abstract class SparkShims { + // the following lines for checking specific versions + private static final String HADOOP_VERSION_2_6_6 = "2.6.6"; + private static final String HADOOP_VERSION_2_7_0 = "2.7.0"; + private static final String HADOOP_VERSION_2_7_4 = "2.7.4"; + private static final String HADOOP_VERSION_2_8_0 = "2.8.0"; + private static final String HADOOP_VERSION_2_8_2 = "2.8.2"; + private static final String HADOOP_VERSION_2_9_0 = "2.9.0"; + private static final String HADOOP_VERSION_3_0_0 = "3.0.0"; + private static final String HADOOP_VERSION_3_0_0_ALPHA4 = "3.0.0-alpha4"; + private static final Logger LOGGER = LoggerFactory.getLogger(SparkShims.class); private static SparkShims sparkShims; @@ -69,11 +79,10 @@ private static String getSparkMajorVersion(String sparkVersion) { } /** - * This is due to SparkListener api change between spark1 and spark2. - * SparkListener is trait in spark1 while it is abstract class in spark2. + * This is due to SparkListener api change between spark1 and spark2. SparkListener is trait in + * spark1 while it is abstract class in spark2. */ - public abstract void setupSparkListener(String sparkWebUrl); - + public abstract void setupSparkListener(String master, String sparkWebUrl); protected String getNoteId(String jobgroupId) { int indexOf = jobgroupId.indexOf("-"); @@ -87,18 +96,24 @@ protected String getParagraphId(String jobgroupId) { return jobgroupId.substring(secondIndex + 1, jobgroupId.length()); } - protected void buildSparkJobUrl(String sparkWebUrl, int jobId, Properties jobProperties) { + protected void buildSparkJobUrl( + String master, String sparkWebUrl, int jobId, Properties jobProperties) { String jobGroupId = jobProperties.getProperty("spark.jobGroup.id"); String uiEnabled = jobProperties.getProperty("spark.ui.enabled"); String jobUrl = sparkWebUrl + "/jobs/job?id=" + jobId; + + String version = VersionInfo.getVersion(); + if (master.toLowerCase().contains("yarn") && !supportYarn6615(version)) { + jobUrl = sparkWebUrl + "/jobs"; + } + String noteId = getNoteId(jobGroupId); String paragraphId = getParagraphId(jobGroupId); // Button visible if Spark UI property not set, set as invalid boolean or true - boolean showSparkUI = - uiEnabled == null || !uiEnabled.trim().toLowerCase().equals("false"); - if (showSparkUI && jobUrl != null) { + boolean showSparkUI = uiEnabled == null || !uiEnabled.trim().toLowerCase().equals("false"); + if (showSparkUI) { RemoteEventClientWrapper eventClient = BaseZeppelinContext.getEventClient(); - Map infos = new java.util.HashMap(); + Map infos = new java.util.HashMap<>(); infos.put("jobUrl", jobUrl); infos.put("label", "SPARK JOB"); infos.put("tooltip", "View in Spark web UI"); @@ -107,4 +122,22 @@ protected void buildSparkJobUrl(String sparkWebUrl, int jobId, Properties jobPro } } } + + /** + * This is temporal patch for support old versions of Yarn which is not adopted YARN-6615 + * + * @return true if YARN-6615 is patched, false otherwise + */ + protected boolean supportYarn6615(String version) { + return (VersionUtil.compareVersions(HADOOP_VERSION_2_6_6, version) <= 0 + && VersionUtil.compareVersions(HADOOP_VERSION_2_7_0, version) > 0) + || (VersionUtil.compareVersions(HADOOP_VERSION_2_7_4, version) <= 0 + && VersionUtil.compareVersions(HADOOP_VERSION_2_8_0, version) > 0) + || (VersionUtil.compareVersions(HADOOP_VERSION_2_8_2, version) <= 0 + && VersionUtil.compareVersions(HADOOP_VERSION_2_9_0, version) > 0) + || (VersionUtil.compareVersions(HADOOP_VERSION_2_9_0, version) <= 0 + && VersionUtil.compareVersions(HADOOP_VERSION_3_0_0, version) > 0) + || (VersionUtil.compareVersions(HADOOP_VERSION_3_0_0_ALPHA4, version) <= 0) + || (VersionUtil.compareVersions(HADOOP_VERSION_3_0_0, version) <= 0); + } } diff --git a/spark/spark1-shims/src/main/scala/org/apache/zeppelin/spark/Spark1Shims.java b/spark/spark1-shims/src/main/scala/org/apache/zeppelin/spark/Spark1Shims.java index 8934c1e7e72..c84218801ac 100644 --- a/spark/spark1-shims/src/main/scala/org/apache/zeppelin/spark/Spark1Shims.java +++ b/spark/spark1-shims/src/main/scala/org/apache/zeppelin/spark/Spark1Shims.java @@ -19,41 +19,20 @@ package org.apache.zeppelin.spark; import org.apache.spark.SparkContext; -import org.apache.spark.scheduler.SparkListener; -import org.apache.spark.scheduler.SparkListenerApplicationEnd; -import org.apache.spark.scheduler.SparkListenerApplicationStart; -import org.apache.spark.scheduler.SparkListenerBlockManagerAdded; -import org.apache.spark.scheduler.SparkListenerBlockManagerRemoved; -import org.apache.spark.scheduler.SparkListenerBlockUpdated; -import org.apache.spark.scheduler.SparkListenerEnvironmentUpdate; -import org.apache.spark.scheduler.SparkListenerExecutorAdded; -import org.apache.spark.scheduler.SparkListenerExecutorMetricsUpdate; -import org.apache.spark.scheduler.SparkListenerExecutorRemoved; -import org.apache.spark.scheduler.SparkListenerJobEnd; import org.apache.spark.scheduler.SparkListenerJobStart; -import org.apache.spark.scheduler.SparkListenerStageCompleted; -import org.apache.spark.scheduler.SparkListenerStageSubmitted; -import org.apache.spark.scheduler.SparkListenerTaskEnd; -import org.apache.spark.scheduler.SparkListenerTaskGettingResult; -import org.apache.spark.scheduler.SparkListenerTaskStart; -import org.apache.spark.scheduler.SparkListenerUnpersistRDD; import org.apache.spark.ui.jobs.JobProgressListener; -import org.apache.zeppelin.interpreter.BaseZeppelinContext; -import org.apache.zeppelin.interpreter.remote.RemoteEventClientWrapper; - -import java.util.Map; /** * Shims for Spark 1.x */ public class Spark1Shims extends SparkShims { - public void setupSparkListener(final String sparkWebUrl) { + public void setupSparkListener(final String master, final String sparkWebUrl) { SparkContext sc = SparkContext.getOrCreate(); sc.addSparkListener(new JobProgressListener(sc.getConf()) { @Override public void onJobStart(SparkListenerJobStart jobStart) { - buildSparkJobUrl(sparkWebUrl, jobStart.jobId(), jobStart.properties()); + buildSparkJobUrl(master, sparkWebUrl, jobStart.jobId(), jobStart.properties()); } }); } diff --git a/spark/spark2-shims/src/main/scala/org/apache/zeppelin/spark/Spark2Shims.java b/spark/spark2-shims/src/main/scala/org/apache/zeppelin/spark/Spark2Shims.java index e4aea5cfb1f..5a6c5b20fd6 100644 --- a/spark/spark2-shims/src/main/scala/org/apache/zeppelin/spark/Spark2Shims.java +++ b/spark/spark2-shims/src/main/scala/org/apache/zeppelin/spark/Spark2Shims.java @@ -27,12 +27,12 @@ */ public class Spark2Shims extends SparkShims { - public void setupSparkListener(final String sparkWebUrl) { + public void setupSparkListener(final String master, final String sparkWebUrl) { SparkContext sc = SparkContext.getOrCreate(); sc.addSparkListener(new SparkListener() { @Override public void onJobStart(SparkListenerJobStart jobStart) { - buildSparkJobUrl(sparkWebUrl, jobStart.jobId(), jobStart.properties()); + buildSparkJobUrl(master, sparkWebUrl, jobStart.jobId(), jobStart.properties()); } }); } From c2a4d76fca621a2417201ff96efc1efa0b74ce34 Mon Sep 17 00:00:00 2001 From: Renjith Kamath Date: Fri, 25 May 2018 16:01:51 +0530 Subject: [PATCH 323/492] ZEPPELIN-3502 Make notebook dynamic forms title editable Make notebook dynamic forms title editable Improvement https://issues.apache.org/jira/browse/ZEPPELIN-3502 Click to edit the title **Before** screen shot 2018-05-25 at 4 03 20 pm **After** screen shot 2018-05-25 at 4 03 37 pm screen shot 2018-05-25 at 4 03 43 pm * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: Renjith Kamath Closes #2987 from r-kamath/ZEPPELIN-3502 and squashes the following commits: cf6f1b2c1 [Renjith Kamath] ZEPPELIN-3502 Make notebook dynamic forms title editable Change-Id: I9b7e78c8a05d852feba5cc7eaeb61f6ac99a3915 (cherry picked from commit 663918cd4ab852111b9ce99e522103c8674b84ae) Signed-off-by: Renjith Kamath --- .../src/app/notebook/notebook.controller.js | 6 ++++++ zeppelin-web/src/app/notebook/notebook.css | 4 ++++ zeppelin-web/src/app/notebook/notebook.html | 21 ++++++++++++++++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index 448df6ef522..6082a4419bb 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -36,6 +36,7 @@ function NotebookCtrl($scope, $route, $routeParams, $location, $rootScope, $scope.showSetting = false; $scope.showRevisionsComparator = false; $scope.looknfeelOption = ['default', 'simple', 'report']; + $scope.noteFormTitle = null; $scope.cronOption = [ {name: 'None', value: undefined}, {name: '1m', value: '0 0/1 * * * ?'}, @@ -441,6 +442,11 @@ function NotebookCtrl($scope, $route, $routeParams, $location, $rootScope, } }; + $scope.setNoteFormTitle = function(noteFormTitle) { + $scope.note.config.noteFormTitle = noteFormTitle; + $scope.setConfig(); + }; + /** Set cron expression for this note **/ $scope.setCronScheduler = function(cronExpr) { if (cronExpr) { diff --git a/zeppelin-web/src/app/notebook/notebook.css b/zeppelin-web/src/app/notebook/notebook.css index fe5da5c1e24..47a7b865c68 100644 --- a/zeppelin-web/src/app/notebook/notebook.css +++ b/zeppelin-web/src/app/notebook/notebook.css @@ -469,3 +469,7 @@ position: relative; top: -16px; } + +.notebook-form-title { + padding: 3px; +} diff --git a/zeppelin-web/src/app/notebook/notebook.html b/zeppelin-web/src/app/notebook/notebook.html index f004e9b325f..179192bf30e 100644 --- a/zeppelin-web/src/app/notebook/notebook.html +++ b/zeppelin-web/src/app/notebook/notebook.html @@ -120,9 +120,24 @@

    Note Permissions (Only note owners can change)

    -
    -

    Note forms

    -
    +

    +
    + +
    +
    +
    +


    Date: Fri, 1 Jun 2018 15:24:59 +0530 Subject: [PATCH 324/492] ZEPPELIN-3521 Dynamic note form overlaps with paragraph content in iframe page ("Link this paragraph") Dynamic note form overlaps with paragraph content in iframe page ("Link this paragraph") Bug Fix https://issues.apache.org/jira/browse/ZEPPELIN-3521 Click on "Link this paragraph" from the paragraph menu to get paragraph in iframe mode ***Before*** before ***After*** after * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: Renjith Kamath Closes #2996 from r-kamath/ZEPPELIN-3521 and squashes the following commits: 207a0d363 [Renjith Kamath] ZEPPELIN-3521 Dynamic note form overlaps with paragraph content in iframe page ("Link this paragraph") Change-Id: I3a4989dba9a19016ff5ef4806ec229ca428834db (cherry picked from commit b8c6b5d57e1b6b594ddddfca092c90a120c3daad) Signed-off-by: Prabhjyot Singh --- zeppelin-web/src/app/notebook/notebook.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index 6082a4419bb..5135e1bb1f5 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -1504,7 +1504,7 @@ function NotebookCtrl($scope, $route, $routeParams, $location, $rootScope, }); $scope.isShowNoteForms = function() { - if ($scope.note && !angular.equals({}, $scope.note.noteForms)) { + if ($scope.note && !_.isEmpty($scope.note.noteForms) && !$scope.paragraphUrl) { return true; } return false; From 81579da8754ce70bcb32877b662828ed60b61ac1 Mon Sep 17 00:00:00 2001 From: Jongyoul Lee Date: Thu, 7 Jun 2018 12:51:33 +0900 Subject: [PATCH 325/492] [HOTFIX] ZEPPELIN-2221's dependency issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What is this PR for? Fixing some CI by changing `hadoop-client` version of ₩spark_core` and `guava` version of `zeppelin-python`. We basically remove `zeppelin-python` dependency from `zeppelin-spark` interpreter in a near future. ### What type of PR is it? [Hot Fix] ### Todos * [x] - Fix dependency problem between hadoop-client and guava ### What is the Jira issue? N/A ### How should this be tested? * It should pass CI ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jongyoul Lee Closes #3010 from jongyoul/hotfix/zeppelin-2221-dependency-issue and squashes the following commits: d1b89dd5e [Jongyoul Lee] Change hadoop-client version to avoid guava conflict 16fa70e40 [Jongyoul Lee] Exclude guava from zeppelin-python dependency from spark/interpreter ede117b22 [Jongyoul Lee] Add a guava version of `19.0` to avoid guava version mismatch b0873e369 [Jongyoul Lee] Change the version of the dependency of `hadoop-common` to `2.6.5` 542b18820 [Jongyoul Lee] Revert a scope of hadoop-comoon to `provided` 8bc67e6c5 [Jongyoul Lee] Add a dependency of `hadoop-common` as `test` scope (cherry picked from commit 48647e5977fcd5ae8aaa5e3f750ddfe9934761b5) Signed-off-by: Jeff Zhang --- spark/interpreter/pom.xml | 12 ++++++++++++ spark/spark-shims/pom.xml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 2c219a60655..f242a8ef894 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -131,6 +131,18 @@ spark-core_${scala.binary.version} ${spark.version} provided + + + org.apache.hadoop + hadoop-client + + + + + org.apache.hadoop + hadoop-client + 2.6.0 + provided diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 03b6bf982b2..c4dda5599a4 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -49,7 +49,7 @@ org.apache.hadoop hadoop-common - 2.2.0 + 2.6.0 provided From 32698dc19582a46926456d954c3df58b592f7f0e Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Fri, 1 Jun 2018 21:58:40 +0800 Subject: [PATCH 326/492] [MINOR] Verify appId is not null in LivyInterpreterIT ### What is this PR for? Someone complain that they could not get appId, this PR just try to verify appId returned by livy rest api is not null. ### What type of PR is it? [Improvement] ### Todos * [ ] - Task ### What is the Jira issue? * No jira created ### How should this be tested? * CI pass ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2999 from zjffdu/minor_livy and squashes the following commits: eae0cf58c [Jeff Zhang] [MINOR] Verify appId is null in LivyInterpreterIT (cherry picked from commit 6fc3057d529691e697361ef9a67e21cac75e21ed) Signed-off-by: Jeff Zhang --- .../test/java/org/apache/zeppelin/livy/LivyInterpreterIT.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/livy/src/test/java/org/apache/zeppelin/livy/LivyInterpreterIT.java b/livy/src/test/java/org/apache/zeppelin/livy/LivyInterpreterIT.java index c7fbc8d4896..fd3c2532b57 100644 --- a/livy/src/test/java/org/apache/zeppelin/livy/LivyInterpreterIT.java +++ b/livy/src/test/java/org/apache/zeppelin/livy/LivyInterpreterIT.java @@ -491,7 +491,8 @@ public void testSparkInterpreterWithDisplayAppInfo_StringWithoutTruncation() thr InterpreterResult result = sparkInterpreter.interpret("sc.version", context); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); assertEquals(2, result.message().size()); - assertTrue(result.message().get(1).getData().contains("Spark Application Id")); + // check yarn appId and ensure it is not null + assertTrue(result.message().get(1).getData().contains("Spark Application Id: application_")); // html output String htmlCode = "println(\"%html

    hello

    \")"; From ab704a1e27d23a737155185ac274fc710f892f26 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 5 Jun 2018 21:38:17 +0800 Subject: [PATCH 327/492] ZEPPELIN-3531. Don't look for py4j in NewSparkInterpreter ### What is this PR for? Just remove setupConfForPySpark in NewSparkInterpreter as it is not necessary and will cause NPE when the node launch spark interpreter doesn't have spark installed in yarn cluster mode. ### What type of PR is it? [Bug Fix] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-3531 ### How should this be tested? * CI pass & Manually tested in a 3 node cluster ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #3008 from zjffdu/ZEPPELIN-3531 and squashes the following commits: e8b2969e9 [Jeff Zhang] ZEPPELIN-3531. Don't look for py4j in NewSparkInterpreter (cherry picked from commit 767a50b4164ef323ef18160485d226c013c7c00d) Signed-off-by: Jeff Zhang --- .../zeppelin/spark/NewSparkInterpreter.java | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java index 2fa10933b1e..2ea79f0e8c4 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java @@ -81,7 +81,6 @@ public void open() throws InterpreterException { try { String scalaVersion = extractScalaVersion(); LOGGER.info("Using Scala Version: " + scalaVersion); - setupConfForPySpark(); SparkConf conf = new SparkConf(); for (Map.Entry entry : getProperties().entrySet()) { if (!StringUtils.isBlank(entry.getValue().toString())) { @@ -126,48 +125,6 @@ public void open() throws InterpreterException { } } - private void setupConfForPySpark() { - String sparkHome = getProperty("SPARK_HOME"); - File pysparkFolder = null; - if (sparkHome == null) { - String zeppelinHome = - new DefaultInterpreterProperty("ZEPPELIN_HOME", "zeppelin.home", "../../") - .getValue().toString(); - pysparkFolder = new File(zeppelinHome, - "interpreter" + File.separator + "spark" + File.separator + "pyspark"); - } else { - pysparkFolder = new File(sparkHome, "python" + File.separator + "lib"); - } - - ArrayList pysparkPackages = new ArrayList<>(); - for (File file : pysparkFolder.listFiles()) { - if (file.getName().equals("pyspark.zip")) { - pysparkPackages.add(file.getAbsolutePath()); - } - if (file.getName().startsWith("py4j-")) { - pysparkPackages.add(file.getAbsolutePath()); - } - } - - if (pysparkPackages.size() != 2) { - throw new RuntimeException("Not correct number of pyspark packages: " + - StringUtils.join(pysparkPackages, ",")); - } - // Distribute two libraries(pyspark.zip and py4j-*.zip) to workers - System.setProperty("spark.files", mergeProperty(System.getProperty("spark.files", ""), - StringUtils.join(pysparkPackages, ","))); - System.setProperty("spark.submit.pyFiles", mergeProperty( - System.getProperty("spark.submit.pyFiles", ""), StringUtils.join(pysparkPackages, ","))); - - } - - private String mergeProperty(String originalValue, String appendedValue) { - if (StringUtils.isBlank(originalValue)) { - return appendedValue; - } - return originalValue + "," + appendedValue; - } - @Override public void close() { LOGGER.info("Close SparkInterpreter"); From 47c7f4ffad10326d3d37af146f77ddd0cd5e1531 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Wed, 6 Jun 2018 09:11:52 +0530 Subject: [PATCH 328/492] [ZEPPELIN-2913] Support for both user and role for AuthorizationFilter Currently, Zeppelin only supports roles for AuthorizationFilter, but there can be a condition as described in https://issues.apache.org/jira/browse/ZEPPELIN-2913 where Zeppelin's user does not belong to a group/role, and the administrator wants to have control using user only. [Feature] * [x] - Add documentation * https://issues.apache.org/jira/browse/ZEPPELIN-2913 add the following in shiro.ini: ``` [main] ... anyofroles = org.apache.zeppelin.utils.AnyOfRolesUserAuthorizationFilter [urls] ... /api/interpreter/** = authc, anyofroles[admin, user1] /api/configurations/** = authc, roles[admin] /api/credential/** = authc, roles[admin] ``` With the above config both user (user1) and users the belong to role admin will have access to interpreter setting page. Author: Prabhjyot Singh Closes #3004 from prabhjyotsingh/ZEPPELIN-2913 and squashes the following commits: e05d72a9f [Prabhjyot Singh] rename AnyOfRolesAuthorizationFilter to AnyOfRolesUserAuthorizationFilter 724192ff4 [Prabhjyot Singh] add doc 53c0c034a [Prabhjyot Singh] [ZEPPELIN-2913] support for both user and role Change-Id: I63cdebf66d76a67cfca0054283c7d1c65a9b5805 (cherry picked from commit d45d878a162d1012900f2de90314603a33323c5d) Signed-off-by: Prabhjyot Singh # Conflicts: # zeppelin-server/src/main/java/org/apache/zeppelin/utils/AnyOfRolesUserAuthorizationFilter.java --- docs/setup/security/shiro_authentication.md | 8 +++++--- .../org/apache/zeppelin/integration/AuthenticationIT.java | 8 ++++---- ...Filter.java => AnyOfRolesUserAuthorizationFilter.java} | 8 ++++---- 3 files changed, 13 insertions(+), 11 deletions(-) rename zeppelin-server/src/main/java/org/apache/zeppelin/utils/{AnyOfRolesAuthorizationFilter.java => AnyOfRolesUserAuthorizationFilter.java} (89%) diff --git a/docs/setup/security/shiro_authentication.md b/docs/setup/security/shiro_authentication.md index a51f77e7098..49b06c196aa 100644 --- a/docs/setup/security/shiro_authentication.md +++ b/docs/setup/security/shiro_authentication.md @@ -273,14 +273,16 @@ By default, Shiro will allow access to a URL if only user is part of "**all the /api/interpreter/** = authc, roles[admin, role1] ``` -If there is a need that user with "**any of the defined roles**" should be allowed, then following Shiro configuration can be used: +### Apply multiple roles or user in Shiro configuration +If there is a need that user with "**any of the defined roles or user itself**" should be allowed, then following Shiro configuration can be used: + ``` [main] -anyofroles = org.apache.zeppelin.utils.AnyOfRolesAuthorizationFilter +anyofrolesuser = org.apache.zeppelin.utils.AnyOfRolesUserAuthorizationFilter [urls] -/api/interpreter/** = authc, anyofroles[admin, role1] +/api/interpreter/** = authc, anyofrolesuser[admin, user1] /api/configurations/** = authc, roles[admin] /api/credential/** = authc, roles[admin] ``` diff --git a/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java index 4c194f34354..ea6ad692044 100644 --- a/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java +++ b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java @@ -63,14 +63,14 @@ public class AuthenticationIT extends AbstractZeppelinIT { "securityManager.sessionManager = $sessionManager\n" + "securityManager.sessionManager.globalSessionTimeout = 86400000\n" + "shiro.loginUrl = /api/login\n" + - "anyofroles = org.apache.zeppelin.utils.AnyOfRolesAuthorizationFilter\n" + + "anyofrolesuser = org.apache.zeppelin.utils.AnyOfRolesUserAuthorizationFilter\n" + "[roles]\n" + "admin = *\n" + "hr = *\n" + "finance = *\n" + "[urls]\n" + "/api/version = anon\n" + - "/api/interpreter/** = authc, anyofroles[admin, finance]\n" + + "/api/interpreter/** = authc, anyofrolesuser[admin, finance]\n" + "/** = authc"; static String originalShiro = ""; @@ -172,7 +172,7 @@ public void testSimpleAuthentication() throws Exception { } @Test - public void testAnyOfRoles() throws Exception { + public void testAnyOfRolesUser() throws Exception { try { AuthenticationIT authenticationIT = new AuthenticationIT(); authenticationIT.authenticationUser("admin", "password1"); @@ -220,7 +220,7 @@ public void testAnyOfRoles() throws Exception { authenticationIT.logoutUser("hr1"); } catch (Exception e) { - handleException("Exception in AuthenticationIT while testAnyOfRoles ", e); + handleException("Exception in AuthenticationIT while testAnyOfRolesUser ", e); } } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/AnyOfRolesAuthorizationFilter.java b/zeppelin-server/src/main/java/org/apache/zeppelin/utils/AnyOfRolesUserAuthorizationFilter.java similarity index 89% rename from zeppelin-server/src/main/java/org/apache/zeppelin/utils/AnyOfRolesAuthorizationFilter.java rename to zeppelin-server/src/main/java/org/apache/zeppelin/utils/AnyOfRolesUserAuthorizationFilter.java index 37c91466b3d..778d05257a6 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/AnyOfRolesAuthorizationFilter.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/utils/AnyOfRolesUserAuthorizationFilter.java @@ -27,9 +27,9 @@ * Allows access if current user has at least one role of the specified list. *

    * Basically, it's the same as {@link RolesAuthorizationFilter} but using {@literal OR} instead - * of {@literal AND} on the specified roles. + * of {@literal AND} on the specified roles or user. */ -public class AnyOfRolesAuthorizationFilter extends RolesAuthorizationFilter { +public class AnyOfRolesUserAuthorizationFilter extends RolesAuthorizationFilter { @Override public boolean isAccessAllowed(ServletRequest request, ServletResponse response, @@ -44,10 +44,10 @@ public boolean isAccessAllowed(ServletRequest request, ServletResponse response, } for (String roleName : rolesArray) { - if (subject.hasRole(roleName)) { + if (subject.hasRole(roleName) || subject.getPrincipal().equals(roleName)) { return true; } } return false; } -} +} \ No newline at end of file From c19b69d2b51b626f943ecbc4004f0d40a84e3919 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Thu, 7 Jun 2018 15:20:24 +0530 Subject: [PATCH 329/492] [ZEPPELIN-3526] Zeppelin auth mechanisms (LDAP or password based) should be mutually exclusive Problem: When any external authentication (like LDAP/AD) is enabled for Zeppelin, the default password-based authentication could still be configured in addition to that. This makes space for backdoor in Zeppelin where the user can still get in using the local username/password. Proposed Solution: Zeppelin shouldn't allow specifying [users] section in shiro.ini when it is configured to authenticate with LDAP/AD. [Bug Fix | Feature ] * [x] - Add documentation * [ZEPPELIN-3526](https://issues.apache.org/jira/browse/ZEPPELIN-3526) If both [users] and [main] for example activeDirectoryRealm section enabled in shiro, Zeppelin server should not start. Author: Prabhjyot Singh Author: Prabhjyot Closes #3003 from prabhjyotsingh/ZEPPELIN-3526 and squashes the following commits: edc4323d0 [Prabhjyot] Merge branch 'master' into ZEPPELIN-3526 05c9e14ec [Prabhjyot Singh] add doc 529ab3e0e [Prabhjyot Singh] ZEPPELIN-3526: Zeppelin auth mechanisms (LDAP or password based) should be mutually exclusive Change-Id: I0608cdc64ae7952eeec22bfe939810a6b24f357a (cherry picked from commit bbf5ef511601ee58f4acaf3040a5fbba76d37502) Signed-off-by: Prabhjyot Singh # Conflicts: # zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java --- docs/setup/security/shiro_authentication.md | 4 ++++ .../zeppelin/server/ZeppelinServer.java | 20 ++++++++++++++++++- .../AnyOfRolesUserAuthorizationFilter.java | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/setup/security/shiro_authentication.md b/docs/setup/security/shiro_authentication.md index 49b06c196aa..e1bf650e219 100644 --- a/docs/setup/security/shiro_authentication.md +++ b/docs/setup/security/shiro_authentication.md @@ -104,6 +104,9 @@ To learn more about Apache Shiro Realm, please check [this documentation](http:/ We also provide community custom Realms. +**Note**: When using any of the below realms the default + password-based (IniRealm) authentication needs to be disabled. + ### Active Directory ``` @@ -267,6 +270,7 @@ If you want to grant this permission to other users, you can change **roles[ ]** ### Apply multiple roles in Shiro configuration By default, Shiro will allow access to a URL if only user is part of "**all the roles**" defined like this: + ``` [urls] 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 9f3f6074165..539e66eb574 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 @@ -19,6 +19,7 @@ import java.io.File; import java.io.IOException; +import java.util.Collection; import java.util.EnumSet; import java.util.HashSet; import java.util.Set; @@ -27,7 +28,10 @@ import javax.ws.rs.core.Application; import org.apache.commons.lang.StringUtils; +import org.apache.shiro.realm.Realm; +import org.apache.shiro.realm.text.IniRealm; import org.apache.shiro.web.env.EnvironmentLoaderListener; +import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.ShiroFilter; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; @@ -54,7 +58,6 @@ import org.apache.zeppelin.search.SearchService; import org.apache.zeppelin.socket.NotebookServer; import org.apache.zeppelin.storage.ConfigStorage; -import org.apache.zeppelin.storage.FileSystemConfigStorage; import org.apache.zeppelin.user.Credentials; import org.apache.zeppelin.utils.SecurityUtils; import org.eclipse.jetty.http.HttpVersion; @@ -92,6 +95,21 @@ public class ZeppelinServer extends Application { public ZeppelinServer() throws Exception { ZeppelinConfiguration conf = ZeppelinConfiguration.create(); + Collection realms = ((DefaultWebSecurityManager) org.apache.shiro.SecurityUtils + .getSecurityManager()).getRealms(); + if (realms.size() > 1) { + Boolean isIniRealmEnabled = false; + for (Object realm : realms) { + if (realm instanceof IniRealm && ((IniRealm) realm).getIni().get("users") != null) { + isIniRealmEnabled = true; + break; + } + } + if (isIniRealmEnabled) { + throw new Exception("IniRealm/password based auth mechanisms should be exclusive. " + + "Consider removing [users] block from shiro.ini"); + } + } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/AnyOfRolesUserAuthorizationFilter.java b/zeppelin-server/src/main/java/org/apache/zeppelin/utils/AnyOfRolesUserAuthorizationFilter.java index 778d05257a6..ed63d8991ff 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/AnyOfRolesUserAuthorizationFilter.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/utils/AnyOfRolesUserAuthorizationFilter.java @@ -50,4 +50,4 @@ public boolean isAccessAllowed(ServletRequest request, ServletResponse response, } return false; } -} \ No newline at end of file +} From 9ae5de3f00ce2546ca31b4fa12b2f39de2cba2f2 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Wed, 6 Jun 2018 18:34:25 +0530 Subject: [PATCH 330/492] [ZEPPELIN-3513] Zeppelin Note Creation Failure is not shown on UI When creating a note from Zeppelin UI which fails due to permission denied exception, there is no failure/error message shown to a user on UI which is causing confusion. This happens only in the case when HDFS is configured as the default storage for storing notebook/config. [Bug Fix] * [ZEPPELIN-3513](https://issues.apache.org/jira/browse/ZEPPELIN-3513) * Does the licenses files need update? N/A * Is there breaking changes for older versions? N/A * Does this needs documentation? N/A Author: Prabhjyot Singh Closes #3009 from prabhjyotsingh/ZEPPELIN-3513 and squashes the following commits: db0673cc1 [Prabhjyot Singh] [ZEPPELIN-3513]change FileSystemException to IOException as HDFS throws AccessControlException when extends IOException Change-Id: Iec5f556239160a67a55ccfba509c14ad3651c1bb --- .../main/java/org/apache/zeppelin/socket/NotebookServer.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 1e1e9979af4..b6981b256a3 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 @@ -30,7 +30,6 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.StringUtils; -import org.apache.commons.vfs2.FileSystemException; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; import org.apache.zeppelin.display.*; @@ -1052,7 +1051,7 @@ private void createNote(NotebookSocket conn, HashSet userAndRoles, Noteb note.persist(subject); addConnectionToNote(note.getId(), (NotebookSocket) conn); conn.send(serializeMessage(new Message(OP.NEW_NOTE).put("note", note))); - } catch (FileSystemException e) { + } catch (IOException e) { LOG.error("Exception from createNote", e); conn.send(serializeMessage(new Message(OP.ERROR_INFO).put("info", "Oops! There is something wrong with the notebook file system. " @@ -1846,7 +1845,7 @@ private boolean persistNoteWithAuthInfo(NotebookSocket conn, try { note.persist(p.getAuthenticationInfo()); return true; - } catch (FileSystemException ex) { + } catch (IOException ex) { LOG.error("Exception from run", ex); conn.send(serializeMessage(new Message(OP.ERROR_INFO).put("info", "Oops! There is something wrong with the notebook file system. " From 3cd94d0018dab54df3e73e175b83f06a18f8eced Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Fri, 8 Jun 2018 09:53:49 +0530 Subject: [PATCH 331/492] [HOTFIX][ZEPPELIN-3526] fix when no shiro.ini exists This is a side effect of ZEPPELIN-3526, occurs when there's no shiro.ini in the classpath. [Hot Fix] * CI should be green * Does the licenses files need update? n/a * Is there breaking changes for older versions? n/a * Does this needs documentation? n/a Author: Prabhjyot Singh Closes #3011 from prabhjyotsingh/hotfix/ZEPPELIN-3526 and squashes the following commits: ac6565cd3 [Prabhjyot Singh] fix ZEPPELIN-3526 when no shiro.ini exists Change-Id: I5016e293eeec17e44be29dbf7f2668ec542a8dfa (cherry picked from commit 9fbcacf5c528292754ce17d5b1eaab9581770583) Signed-off-by: Prabhjyot Singh # Conflicts: # zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java --- .../zeppelin/server/ZeppelinServer.java | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) 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 539e66eb574..c0467c7951a 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 @@ -28,6 +28,7 @@ import javax.ws.rs.core.Application; import org.apache.commons.lang.StringUtils; +import org.apache.shiro.UnavailableSecurityManagerException; import org.apache.shiro.realm.Realm; import org.apache.shiro.realm.text.IniRealm; import org.apache.shiro.web.env.EnvironmentLoaderListener; @@ -95,24 +96,29 @@ public class ZeppelinServer extends Application { public ZeppelinServer() throws Exception { ZeppelinConfiguration conf = ZeppelinConfiguration.create(); - Collection realms = ((DefaultWebSecurityManager) org.apache.shiro.SecurityUtils - .getSecurityManager()).getRealms(); - if (realms.size() > 1) { - Boolean isIniRealmEnabled = false; - for (Object realm : realms) { - if (realm instanceof IniRealm && ((IniRealm) realm).getIni().get("users") != null) { - isIniRealmEnabled = true; - break; + if (conf.getShiroPath().length() > 0) { + try { + Collection realms = ((DefaultWebSecurityManager) org.apache.shiro.SecurityUtils + .getSecurityManager()).getRealms(); + if (realms.size() > 1) { + Boolean isIniRealmEnabled = false; + for (Object realm : realms) { + if (realm instanceof IniRealm && ((IniRealm) realm).getIni().get("users") != null) { + isIniRealmEnabled = true; + break; + } + } + if (isIniRealmEnabled) { + throw new Exception("IniRealm/password based auth mechanisms should be exclusive. " + + "Consider removing [users] block from shiro.ini"); + } } - } - if (isIniRealmEnabled) { - throw new Exception("IniRealm/password based auth mechanisms should be exclusive. " - + "Consider removing [users] block from shiro.ini"); + } catch (UnavailableSecurityManagerException e) { + LOG.error("Failed to initialise shiro configuraion", e); } } - InterpreterOutput.limit = conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_OUTPUT_LIMIT); HeliumApplicationFactory heliumApplicationFactory = new HeliumApplicationFactory(); From fd54540f7b1cb72fff924aa46fef676f7759e834 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Mon, 4 Jun 2018 15:29:45 +0800 Subject: [PATCH 332/492] [SECURITY] Secure connection between R process and JVM process --- .../zeppelin/spark/SparkRInterpreter.java | 16 ++++++------ .../apache/zeppelin/spark/SparkVersion.java | 4 +++ .../org/apache/zeppelin/spark/ZeppelinR.java | 5 +++- .../src/main/resources/R/zeppelin_sparkr.R | 12 +++++++-- .../org/apache/spark/SparkRBackend.scala | 25 ++++++++++++------- 5 files changed, 42 insertions(+), 20 deletions(-) diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkRInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkRInterpreter.java index 896f3a1e3ab..6d21450e13c 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkRInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkRInterpreter.java @@ -53,6 +53,7 @@ public class SparkRInterpreter extends Interpreter { private AtomicBoolean rbackendDead = new AtomicBoolean(false); private SparkContext sc; private JavaSparkContext jsc; + private String secret; public SparkRInterpreter(Properties property) { super(property); @@ -76,19 +77,18 @@ public void open() throws InterpreterException { sparkRLibPath = "sparkr"; } + this.sparkInterpreter = getSparkInterpreter(); + this.sc = sparkInterpreter.getSparkContext(); + this.jsc = sparkInterpreter.getJavaSparkContext(); + // Share the same SparkRBackend across sessions + SparkVersion sparkVersion = new SparkVersion(sc.version()); synchronized (SparkRBackend.backend()) { if (!SparkRBackend.isStarted()) { - SparkRBackend.init(); + SparkRBackend.init(sparkVersion); SparkRBackend.start(); } } - - int port = SparkRBackend.port(); - this.sparkInterpreter = getSparkInterpreter(); - this.sc = sparkInterpreter.getSparkContext(); - this.jsc = sparkInterpreter.getJavaSparkContext(); - SparkVersion sparkVersion = new SparkVersion(sc.version()); this.isSpark2 = sparkVersion.newerThanEquals(SparkVersion.SPARK_2_0_0); int timeout = this.sc.getConf().getInt("spark.r.backendConnectionTimeout", 6000); @@ -100,7 +100,7 @@ public void open() throws InterpreterException { ZeppelinRContext.setSqlContext(sparkInterpreter.getSQLContext()); ZeppelinRContext.setZeppelinContext(sparkInterpreter.getZeppelinContext()); - zeppelinR = new ZeppelinR(rCmdPath, sparkRLibPath, port, sparkVersion, timeout, this); + zeppelinR = new ZeppelinR(rCmdPath, sparkRLibPath, SparkRBackend.port(), sparkVersion, timeout, this); try { zeppelinR.open(); } catch (IOException e) { diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkVersion.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkVersion.java index 09ea3325ac5..5e412ebb37a 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkVersion.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkVersion.java @@ -34,6 +34,7 @@ public class SparkVersion { public static final SparkVersion SPARK_1_6_0 = SparkVersion.fromVersionString("1.6.0"); public static final SparkVersion SPARK_2_0_0 = SparkVersion.fromVersionString("2.0.0"); + public static final SparkVersion SPARK_2_3_1 = SparkVersion.fromVersionString("2.3.1"); public static final SparkVersion SPARK_2_4_0 = SparkVersion.fromVersionString("2.4.0"); public static final SparkVersion MIN_SUPPORTED_VERSION = SPARK_1_0_0; @@ -108,6 +109,9 @@ public boolean oldSqlContextImplicits() { return this.olderThan(SPARK_1_3_0); } + public boolean isSecretSocketSupported() { + return this.newerThanEquals(SPARK_2_3_1); + } public boolean equals(Object versionToCompare) { return version == ((SparkVersion) versionToCompare).version; } diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/ZeppelinR.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/ZeppelinR.java index edd323329c7..addddc8a0eb 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/ZeppelinR.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/ZeppelinR.java @@ -19,6 +19,7 @@ import org.apache.commons.exec.*; import org.apache.commons.exec.environment.EnvironmentUtils; import org.apache.commons.io.IOUtils; +import org.apache.spark.SparkRBackend; import org.apache.zeppelin.interpreter.InterpreterException; import org.apache.zeppelin.interpreter.InterpreterOutput; import org.apache.zeppelin.interpreter.InterpreterOutputListener; @@ -146,7 +147,9 @@ public void open() throws IOException, InterpreterException { cmd.addArgument(libPath); cmd.addArgument(Integer.toString(sparkVersion.toNumber())); cmd.addArgument(Integer.toString(timeout)); - + if (sparkVersion.isSecretSocketSupported()) { + cmd.addArgument(SparkRBackend.socketSecret()); + } // dump out the R command to facilitate manually running it, e.g. for fault diagnosis purposes logger.debug(cmd.toString()); diff --git a/spark/interpreter/src/main/resources/R/zeppelin_sparkr.R b/spark/interpreter/src/main/resources/R/zeppelin_sparkr.R index 16b8415f659..5f64dfe148f 100644 --- a/spark/interpreter/src/main/resources/R/zeppelin_sparkr.R +++ b/spark/interpreter/src/main/resources/R/zeppelin_sparkr.R @@ -23,6 +23,11 @@ port <- as.integer(args[2]) libPath <- args[3] version <- as.integer(args[4]) timeout <- as.integer(args[5]) +authSecret <- NULL +if (length(args) >= 6) { + authSecret <- args[6] +} + rm(args) print(paste("Port ", toString(port))) @@ -31,8 +36,11 @@ print(paste("LibPath ", libPath)) .libPaths(c(file.path(libPath), .libPaths())) library(SparkR) - -SparkR:::connectBackend("localhost", port, timeout) +if (is.null(authSecret)) { + SparkR:::connectBackend("localhost", port, timeout) +} else { + SparkR:::connectBackend("localhost", port, timeout, authSecret) +} # scStartTime is needed by R/pkg/R/sparkR.R assign(".scStartTime", as.integer(Sys.time()), envir = SparkR:::.sparkREnv) diff --git a/spark/interpreter/src/main/scala/org/apache/spark/SparkRBackend.scala b/spark/interpreter/src/main/scala/org/apache/spark/SparkRBackend.scala index 05f1ac0e3e2..2dc3371eb57 100644 --- a/spark/interpreter/src/main/scala/org/apache/spark/SparkRBackend.scala +++ b/spark/interpreter/src/main/scala/org/apache/spark/SparkRBackend.scala @@ -17,11 +17,13 @@ package org.apache.spark import org.apache.spark.api.r.RBackend +import org.apache.zeppelin.spark.SparkVersion object SparkRBackend { val backend : RBackend = new RBackend() private var started = false; private var portNumber = 0; + private var secret: String = ""; val backendThread : Thread = new Thread("SparkRBackend") { override def run() { @@ -29,9 +31,16 @@ object SparkRBackend { } } - def init() : Int = { - portNumber = backend.init() - portNumber + def init(version: SparkVersion) : Unit = { + val rBackendClass = classOf[RBackend] + if (version.isSecretSocketSupported) { + val result = rBackendClass.getMethod("init").invoke(backend).asInstanceOf[Tuple2[Int, Object]] + portNumber = result._1 + val rAuthHelper = result._2 + secret = rAuthHelper.getClass.getMethod("secret").invoke(rAuthHelper).asInstanceOf[String] + } else { + portNumber = rBackendClass.getMethod("init").invoke(backend).asInstanceOf[Int] + } } def start() : Unit = { @@ -44,11 +53,9 @@ object SparkRBackend { backendThread.join() } - def isStarted() : Boolean = { - started - } + def isStarted() : Boolean = started - def port(): Int = { - return portNumber - } + def port(): Int = portNumber + + def socketSecret(): String = secret; } From 7dfbd060a47980904b59b8d638fa3b9b6efde9ac Mon Sep 17 00:00:00 2001 From: Savalek Date: Wed, 23 May 2018 12:58:30 +0300 Subject: [PATCH 333/492] [ZEPPELIN-3492] The paragraph's table does not scroll if there is a large cell When scrolling (with the mouse wheel), the contents of the table jump and do not scroll down. The scroll bar is working fine. Often this happens if the table has a cell with a large height. [Bug Fix] [ZEPPELIN-3492](https://issues.apache.org/jira/browse/ZEPPELIN-3492) [Text](https://github.com/apache/zeppelin/files/2030555/paragraphText.txt) of the paragraph (python interpreter) ![peek_before](https://user-images.githubusercontent.com/30798933/40417963-c3d158a4-5e89-11e8-8c68-5a0397281c75.gif) ![peek_after](https://user-images.githubusercontent.com/30798933/40417977-ccf60f1a-5e89-11e8-88b3-c7855a198e2d.gif) * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? no Author: Savalek Closes #2982 from Savalek/ZEPPELIN-3492 and squashes the following commits: 06ef2e616 [Savalek] [ZEPPELIN-3492] fix paragraph's table scroll Change-Id: I22218a65345f7e84d65bd50b0d110ceb86b8ecbf (cherry picked from commit f22bbde1ce83b10f0821e26168cf6cd058b94486) Signed-off-by: Renjith Kamath --- .../src/app/visualization/builtins/visualization-table.js | 1 + 1 file changed, 1 insertion(+) diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-table.js b/zeppelin-web/src/app/visualization/builtins/visualization-table.js index 831bf95ccd6..30d32918b6a 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-table.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-table.js @@ -153,6 +153,7 @@ export default class TableVisualization extends Visualization { saveTreeView: true, saveFilter: true, saveSelection: false, + customScroller: (uiGrid) => uiGrid.on('wheel', (event) => event.stopPropagation()), }; return gridOptions; From b7dfa330e20a7420074b548a5fbce06d98a1c176 Mon Sep 17 00:00:00 2001 From: Savalek Date: Wed, 30 May 2018 10:40:43 +0300 Subject: [PATCH 334/492] [ZEPPELIN-3476] change description "auto-restart interpreter on cron execution" Description "auto-restart interpreter on cron execution" is does not quite fit in the sense. Documentation [ZEPPELIN-3476](https://issues.apache.org/jira/browse/ZEPPELIN-3476) ![cron_scheduler_dialog_box](https://user-images.githubusercontent.com/30798933/40646934-4d110526-6333-11e8-90d2-c4feaa1f2c3d.png) Author: Savalek Closes #2991 from Savalek/ZEPPELIN-3476 and squashes the following commits: 4916b0da9 [Savalek] [ZEPPELIN-3476] replace "close" on "stop" d2e9c78ee [Savalek] [ZEPPELIN-3476] change description Change-Id: Ic8a0d283268f843f0ec440b2203c027fea0ffb2b (cherry picked from commit ee06cf030dd9ff83c971dfb9f47f24fb4aae55d4) Signed-off-by: Renjith Kamath --- .../docs-img/cron_scheduler_dialog_box.png | Bin 135264 -> 51362 bytes docs/usage/other_features/cron_scheduler.md | 2 +- .../src/app/notebook/notebook-actionBar.html | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/assets/themes/zeppelin/img/docs-img/cron_scheduler_dialog_box.png b/docs/assets/themes/zeppelin/img/docs-img/cron_scheduler_dialog_box.png index 50633983b2eed3f44d31d0ef51f3c76872c53a02..e68af7b219ee3862241583eca3d0e6d68376539b 100644 GIT binary patch literal 51362 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úrK<=o6FcT0xVeTp8j?zwZY_pi*WWI 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 diff --git a/docs/usage/other_features/cron_scheduler.md b/docs/usage/other_features/cron_scheduler.md index 7223045bff8..2a835a3df6a 100644 --- a/docs/usage/other_features/cron_scheduler.md +++ b/docs/usage/other_features/cron_scheduler.md @@ -45,7 +45,7 @@ You can set the cron schedule by filling in this form. Please see [Cron Trigger You can set the cron executing user by filling in this form and press the enter key. -### auto-restart interpreter on cron execution +### After execution stop the interpreter When this checkbox is set to "on", the interpreters which are binded to the notebook are stopped automatically after the cron execution. This feature is useful if you want to release the interpreter resources after the cron execution. diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index 22292239738..b4c608c0136 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -282,7 +282,7 @@

    - - auto-restart interpreter on cron execution + - After execution stop the interpreter From 9631a7d9ac2221b9a62e86f30c41bb4e11e2169a Mon Sep 17 00:00:00 2001 From: Jay Jin Date: Thu, 31 May 2018 21:08:43 +0900 Subject: [PATCH 335/492] [ZEPPELIN-3285] Refine the style of Notebook title Refine the style of Notebook title (Ellipsis by character, 100% width under medium width browser, align delete button) (Before) ![before](https://user-images.githubusercontent.com/3839771/40781776-1c420044-6518-11e8-94d6-fc7d5103771b.gif) (After) ![after](https://user-images.githubusercontent.com/3839771/40781830-5bd25dee-6518-11e8-871f-23bbf31507e5.gif) Improvement https://issues.apache.org/jira/browse/ZEPPELIN-3285 **1. Run webapp** ```vi cd zeppelin-web yarn run dev ``` **2. Check the title have ellipsis by character, not looks cut.** ![image](https://user-images.githubusercontent.com/3839771/40782010-f1771e98-6518-11e8-9373-00fbeed75f2d.png) (before / after) **3. Check 'delete button' is aligned** ![image](https://user-images.githubusercontent.com/3839771/40782056-166634aa-6519-11e8-8cbe-8aa7f33f665b.png) ![image](https://user-images.githubusercontent.com/3839771/40782062-1b548f52-6519-11e8-985f-6e79084ce927.png) (before / after) **4. Check the title with is 100% under the window width 650px** ![image](https://user-images.githubusercontent.com/3839771/40782107-40b122e2-6519-11e8-9c01-887df47e7f26.png) (before / after) * First time? Setup Travis CI as described on https://zeppelin.apache.org/contribution/contributions.html#continuous-integration * Strongly recommended: add automated unit tests for any new or changed behavior * Outline any manual steps to test the PR here. * Does the licenses files need update? N * Is there breaking changes for older versions? N * Does this needs documentation? N Author: Jay Jin Closes #2994 from milooy/master and squashes the following commits: 6049ee118 [Jay Jin] ZEPPELIN-3285 Delete left margin of title, ellipsis by word, RWD dba654884 [Jay Jin] ZEPPELIN-3285 Align delete button in action bar Change-Id: I09a2ac9afc4eb2f126f6fcc9a11bb0f972561b69 (cherry picked from commit b7c3fec95daacef66089468ca86832602275a716) Signed-off-by: Renjith Kamath --- .../src/app/notebook/notebook-actionBar.html | 11 ++- zeppelin-web/src/app/notebook/notebook.css | 83 ++++++------------- .../src/assets/styles/looknfeel/report.css | 2 +- .../src/assets/styles/looknfeel/simple.css | 2 +- 4 files changed, 31 insertions(+), 67 deletions(-) diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index b4c608c0136..a9358086859 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -14,13 +14,12 @@

    -
    - + -

    - + From 749679f19655a5b19383766b48352cf2b9519d67 Mon Sep 17 00:00:00 2001 From: Vitaliy Levitski Date: Fri, 7 Sep 2018 19:23:32 +0300 Subject: [PATCH 469/492] DSR-67: Empty Helium page with no plugins available when Config Storage is enabled (cherry picked from commit ea656bc) #Squashed MZEP-167 Support only verified and working versions of Helium plugins --- conf/helium-repo.json | 590 ++++++++++++++++++ .../org/apache/zeppelin/helium/Helium.java | 3 + .../zeppelin/helium/HeliumFileRegistry.java | 43 ++ .../zeppelin/helium/HeliumJsonUtils.java | 46 ++ .../zeppelin/helium/HeliumOnlineRegistry.java | 20 +- 5 files changed, 684 insertions(+), 18 deletions(-) create mode 100644 conf/helium-repo.json create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumFileRegistry.java create mode 100644 zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumJsonUtils.java diff --git a/conf/helium-repo.json b/conf/helium-repo.json new file mode 100644 index 00000000000..6a65f61feb8 --- /dev/null +++ b/conf/helium-repo.json @@ -0,0 +1,590 @@ +[{ +"ultimate-area-chart": { + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-area-chart", + "version": "0.0.1", + "published": "2017-04-12T09:58:18.693Z", + "artifact": "ultimate-area-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Area Chart for Apache Zeppelin using amcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-column-chart-negative-values": { + "0.0.2": { + "type": "VISUALIZATION", + "name": "ultimate-column-chart-negative-values", + "version": "0.0.2", + "published": "2017-06-27T19:04:41.438Z", + "artifact": "ultimate-column-chart-negative-values@0.0.2", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-column-chart-negative-values", + "version": "0.0.3", + "published": "2017-07-06T18:58:27.137Z", + "artifact": "ultimate-column-chart-negative-values@0.0.3", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-column-chart": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "ultimate-column-chart", + "version": "0.0.1", + "published": "2017-04-12T10:00:25.424Z", + "artifact": "ultimate-column-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-column-chart", + "version": "0.0.2", + "published": "2018-01-23T03:11:59.022Z", + "artifact": "ultimate-column-chart@0.0.2", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-dual-column-chart": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "ultimate-dual-column-chart", + "version": "0.0.1", + "published": "2017-06-09T16:15:22.329Z", + "artifact": "ultimate-dual-column-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "0.0.2": { + "type": "VISUALIZATION", + "name": "ultimate-dual-column-chart", + "version": "0.0.2", + "published": "2017-06-09T16:52:33.969Z", + "artifact": "ultimate-dual-column-chart@0.0.2", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "0.0.3": { + "type": "VISUALIZATION", + "name": "ultimate-dual-column-chart", + "version": "0.0.3", + "published": "2017-06-16T14:17:57.853Z", + "artifact": "ultimate-dual-column-chart@0.0.3", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "0.0.4": { + "type": "VISUALIZATION", + "name": "ultimate-dual-column-chart", + "version": "0.0.4", + "published": "2017-06-27T19:04:04.764Z", + "artifact": "ultimate-dual-column-chart@0.0.4", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-dual-column-chart", + "version": "0.0.5", + "published": "2017-07-06T18:57:35.252Z", + "artifact": "ultimate-dual-column-chart@0.0.5", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-line-chart": { + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-line-chart", + "version": "0.0.1", + "published": "2017-04-12T09:57:45.479Z", + "artifact": "ultimate-line-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Line Chart for Apache Zeppelin using amcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-pie-chart": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "ultimate-pie-chart", + "version": "0.0.1", + "published": "2017-04-12T09:59:02.580Z", + "artifact": "ultimate-pie-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Pie Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-pie-chart", + "version": "0.0.2", + "published": "2018-01-23T02:47:47.769Z", + "artifact": "ultimate-pie-chart@0.0.2", + "author": "ZEPL", + "description": "The Ultimate Pie Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-scatter-chart": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "ultimate-scatter-chart", + "version": "0.0.1", + "published": "2017-04-12T10:01:36.918Z", + "artifact": "ultimate-scatter-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Scatter Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-scatter-chart", + "version": "0.0.2", + "published": "2018-01-23T03:43:29.010Z", + "artifact": "ultimate-scatter-chart@0.0.2", + "author": "ZEPL", + "description": "The Ultimate Scatter Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"volume-leaflet": { + "1.0.1": { + "type": "VISUALIZATION", + "name": "volume-leaflet", + "version": "1.0.1", + "published": "2017-05-02T18:50:05.500Z", + "artifact": "volume-leaflet@1.0.1", + "author": "Tom Grant", + "description": "Geospatial visualization using the Leaflet map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.2": { + "type": "VISUALIZATION", + "name": "volume-leaflet", + "version": "1.0.2", + "published": "2017-11-03T13:54:18.512Z", + "artifact": "volume-leaflet@1.0.2", + "author": "Tom Grant", + "description": "Geospatial visualization using the Leaflet map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "volume-leaflet", + "version": "1.0.3", + "published": "2017-12-11T20:16:56.719Z", + "artifact": "volume-leaflet@1.0.3", + "author": "Tom Grant", + "description": "Geospatial visualization using the Leaflet map library.", + "license": "BSD-2-Clause", + "icon": "" + } + }, +"zeppelin-bubblechart": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "zeppelin-bubblechart", + "version": "0.0.1", + "published": "2017-01-08T09:56:42.707Z", + "artifact": "zeppelin-bubblechart@0.0.1", + "author": "leemoonsoo", + "description": "Animated bubble chart", + "license": "BSD-2-Clause", + "icon": "" + }, + "0.0.2": { + "type": "VISUALIZATION", + "name": "zeppelin-bubblechart", + "version": "0.0.2", + "published": "2017-01-08T09:58:44.775Z", + "artifact": "zeppelin-bubblechart@0.0.2", + "author": "leemoonsoo", + "description": "Animated bubble chart", + "license": "BSD-2-Clause", + "icon": "" + }, + "0.0.3": { + "type": "VISUALIZATION", + "name": "zeppelin-bubblechart", + "version": "0.0.3", + "published": "2017-01-08T10:41:15.275Z", + "artifact": "zeppelin-bubblechart@0.0.3", + "author": "leemoonsoo", + "description": "Animated bubble chart", + "license": "BSD-2-Clause", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-bubblechart", + "version": "0.0.4", + "published": "2017-01-23T20:42:34.373Z", + "artifact": "zeppelin-bubblechart@0.0.4", + "author": "leemoonsoo", + "description": "Animated bubble chart", + "license": "BSD-2-Clause", + "icon": "" + } + }, +"zeppelin-csv-spell": { + "latest": { + "type": "SPELL", + "name": "zeppelin-csv-spell", + "version": "0.0.1", + "published": "2017-02-28T04:57:05.463Z", + "artifact": "zeppelin-csv-spell@0.0.1", + "author": "1ambda", + "description": "Parse CSV to table for Apache Zeppelin", + "license": "MIT", + "icon": "", + "spell": {"magic": "%csv", "usage": "%csv "} + } + }, +"zeppelin-highcharts-bubble": { + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-bubble", + "version": "0.0.2", + "published": "2017-02-14T12:30:44.199Z", + "artifact": "zeppelin-highcharts-bubble@0.0.2", + "author": "1ambda", + "description": "Bubble Chart for Apache Zeppelin using highcharts.js", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"zeppelin-highcharts-heatmap": { + "0.0.4": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-heatmap", + "version": "0.0.4", + "published": "2017-02-11T07:47:56.338Z", + "artifact": "zeppelin-highcharts-heatmap@0.0.4", + "author": "1ambda", + "description": "Heatmap Charts for Apache Zeppelin using highcharts.js", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-heatmap", + "version": "0.0.5", + "published": "2017-02-14T12:46:02.732Z", + "artifact": "zeppelin-highcharts-heatmap@0.0.5", + "author": "1ambda", + "description": "Heatmap Charts for Apache Zeppelin using highcharts.js", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"zeppelin-highcharts-scatterplot": { + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-scatterplot", + "version": "0.0.2", + "published": "2017-02-14T12:17:22.411Z", + "artifact": "zeppelin-highcharts-scatterplot@0.0.2", + "author": "1ambda", + "description": "Scatter plot for Apache Zeppelin using highcharts.js", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"zeppelin-highmaps": { + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-highmaps", + "version": "1.0.0", + "published": "2018-01-30T15:46:40.265Z", + "artifact": "zeppelin-highmaps@1.0.0", + "author": "odnoklassniki", + "description": "Zeppelin plugin to visualize data using Highmaps", + "license": "Apache-2.0", + "icon": "" + } + }, +"zeppelin-json-spell": { + "latest": { + "type": "SPELL", + "name": "zeppelin-json-spell", + "version": "0.0.3", + "published": "2017-02-28T04:49:27.897Z", + "artifact": "zeppelin-json-spell@0.0.3", + "author": "1ambda", + "description": "Use JSON editor in paragraphs", + "license": "Apache-2.0", + "icon": "", + "spell": {"magic": "%json", "usage": "%json "} + } + }, +"zeppelin-leaflet": { + "1.0.2": { + "type": "VISUALIZATION", + "name": "zeppelin-leaflet", + "version": "1.0.2", + "published": "2017-10-31T09:31:51.660Z", + "artifact": "zeppelin-leaflet@1.0.2", + "author": "Mitchell Yuwono", + "description": "Geospatial visualization using the Leaflet map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.3": { + "type": "VISUALIZATION", + "name": "zeppelin-leaflet", + "version": "1.0.3", + "published": "2017-11-01T12:47:14.830Z", + "artifact": "zeppelin-leaflet@1.0.3", + "author": "Mitchell Yuwono", + "description": "Geospatial visualization using the Leaflet map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-leaflet", + "version": "1.0.4", + "published": "2017-11-05T08:24:52.404Z", + "artifact": "zeppelin-leaflet@1.0.4", + "author": "Mitchell Yuwono", + "description": "Geospatial visualization using the Leaflet map library.", + "license": "BSD-2-Clause", + "icon": "" + } + }, +"zeppelin-markdown-spell": { + "latest": { + "type": "SPELL", + "name": "zeppelin-markdown-spell", + "version": "0.0.3", + "published": "2017-03-07T19:17:35.975Z", + "artifact": "zeppelin-markdown-spell@0.0.3", + "author": "1ambda", + "description": "Parse markdown using https://github.com/evilstreak/markdown-js", + "license": "MIT", + "icon": "", + "spell": {"magic": "%markdown", "usage": "%markdown "} + } + }, +"zeppelin-mathjax-spell": { + "latest": { + "type": "SPELL", + "name": "zeppelin-mathjax-spell", + "version": "0.0.1", + "published": "2017-03-08T10:04:18.787Z", + "artifact": "zeppelin-mathjax-spell@0.0.1", + "author": "1ambda", + "description": "MathJax Spell For Apache Zeppelin", + "license": "Apache-2.0", + "icon": "", + "spell": {"magic": "%mathjax", "usage": "%mathjax "} + } + }, +"zeppelin-number": { + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-number", + "version": "1.0.0", + "published": "2018-07-22T18:55:33.637Z", + "artifact": "zeppelin-number@1.0.0", + "author": "Saravanan Elumalai", + "description": "Zeppelin plugin to visualize number", + "license": "Apache-2.0", + "icon": "" + } + }, +"zeppelin-sigma-spell": { + "0.0.1": { + "type": "SPELL", + "name": "zeppelin-sigma-spell", + "version": "0.0.1", + "published": "2017-03-08T10:09:23.660Z", + "artifact": "zeppelin-sigma-spell@0.0.1", + "author": "datalayer", + "description": "Sigma.js Network Visualization", + "license": "Apache-2.0", + "icon": "" + }, + "latest": { + "type": "SPELL", + "name": "zeppelin-sigma-spell", + "version": "0.0.2", + "published": "2017-03-10T09:31:56.802Z", + "artifact": "zeppelin-sigma-spell@0.0.2", + "author": "datalayer", + "description": "Sigma.js Network Visualization", + "license": "Apache-2.0", + "icon": "" + } + }, +"zeppelin-toc-spell": { + "0.0.1": { + "type": "SPELL", + "name": "zeppelin-toc-spell", + "version": "0.0.1", + "published": "2017-10-14T01:09:22.968Z", + "artifact": "zeppelin-toc-spell@0.0.1", + "author": "Ryan Munro", + "description": "Table of Contents for Zeppelin Notebooks", + "license": "ISC", + "icon": "", + "spell": {"magic": "%toc", "usage": "%toc"} + }, + "latest": { + "type": "SPELL", + "name": "zeppelin-toc-spell", + "version": "0.0.2", + "published": "2017-10-14T01:22:54.639Z", + "artifact": "zeppelin-toc-spell@0.0.2", + "author": "Ryan Munro", + "description": "Table of Contents for Zeppelin Notebooks", + "license": "ISC", + "icon": "", + "spell": {"magic": "%toc", "usage": "%toc"} + } + }, +"zeppelin-translator-spell": { + "latest": { + "type": "SPELL", + "name": "zeppelin-translator-spell", + "version": "0.0.1", + "published": "2017-03-05T07:52:58.069Z", + "artifact": "zeppelin-translator-spell@0.0.1", + "author": "1ambda", + "description": "Translate text using Google Translator API", + "license": "Apache-2.0", + "icon": "", + "config": { + "access-token": { + "type": "string", + "description": "access token for Google Translation API", + "defaultValue": "EXAMPLE-TOKEN" + } + }, + "spell": { + "magic": "%translator", + "usage": "%translator source= target= " + } + } + }, +"zeppelin-echo-spell": { + "1.0.4": { + "type": "SPELL", + "name": "zeppelin-echo-spell", + "version": "1.0.4", + "published": "2017-02-01T02:38:02.497Z", + "artifact": "zeppelin-echo-spell@1.0.4", + "author": "1ambda", + "description": "Zeppelin Echo Spell (example)", + "license": "Apache-2.0", + "icon": "", + "config": { + "repeat": { + "type": "number", + "description": "How many times to repeat", + "defaultValue": 1 + } + }, + "spell": {"magic": "%echo", "usage": "%echo "} + }, + "1.0.5": { + "type": "SPELL", + "name": "zeppelin-echo-spell", + "version": "1.0.5", + "published": "2017-03-05T03:37:00.185Z", + "artifact": "zeppelin-echo-spell@1.0.5", + "author": "1ambda", + "description": "Zeppelin Echo Spell (example)", + "license": "Apache-2.0", + "icon": "", + "config": { + "repeat": { + "type": "number", + "description": "How many times to repeat", + "defaultValue": 1 + } + }, + "spell": {"magic": "%echo", "usage": "%echo "} + }, + "latest": { + "type": "SPELL", + "name": "zeppelin-echo-spell", + "version": "1.0.7", + "published": "2017-03-30T20:33:47.324Z", + "artifact": "zeppelin-echo-spell@1.0.7", + "author": "1ambda", + "description": "Zeppelin Echo Spell (example)", + "license": "Apache-2.0", + "icon": "", + "config": { + "repeat": { + "type": "number", + "description": "How many times to repeat", + "defaultValue": 1 + }, + "delay": { + "type": "number", + "description": "Time to wait", + "defaultValue": 1000 + } + }, + "spell": {"magic": "%echo", "usage": "%echo "} + } + }, +"ultimate-heatmap-chart": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "ultimate-heatmap-chart", + "version": "0.0.1", + "published": "2017-04-12T10:05:54.251Z", + "artifact": "ultimate-heatmap-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Heatmap Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-heatmap-chart", + "version": "0.0.2", + "published": "2018-01-23T03:46:12.597Z", + "artifact": "ultimate-heatmap-chart@0.0.2", + "author": "ZEPL", + "description": "The Ultimate Heatmap Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }}] 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 index 399aea879a7..ca05a1920de 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java @@ -99,6 +99,9 @@ private synchronized HeliumConf loadConf(String path) throws IOException { if (uri.startsWith("http://") || uri.startsWith("https://")) { logger.info("Add helium online registry {}", uri); registry.add(new HeliumOnlineRegistry(uri, uri, registryCacheDir)); + } else if (uri.startsWith("file://")) { + logger.info("Add helium file registry {}", uri); + registry.add(new HeliumFileRegistry(uri, uri)); } else { logger.info("Add helium local registry {}", uri); registry.add(new HeliumLocalRegistry(uri, uri)); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumFileRegistry.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumFileRegistry.java new file mode 100644 index 00000000000..01e712bfe5d --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumFileRegistry.java @@ -0,0 +1,43 @@ +package org.apache.zeppelin.helium; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import com.google.gson.Gson; +import com.sun.jndi.toolkit.url.Uri; + + +/** + * This registry reads helium package json data + * from specified local file url. + * + * File should be look like + * [ + * "packageName": { + * "0.0.1": json serialized HeliumPackage class, + * "0.0.2": json serialized HeliumPackage class, + * ... + * }, + * ... + * ] + */ +public class HeliumFileRegistry extends HeliumRegistry { + private Gson gson; + + public HeliumFileRegistry(String name, String uri) { + super(name, uri); + this.gson = new Gson(); + } + + @Override + public List getAll() throws IOException { + Uri fileUri = new Uri(uri()); + File path = new File(fileUri.getPath()); + InputStream fileInputStream = new FileInputStream(path); + + return HeliumJsonUtils.parsePackages(fileInputStream, gson); + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumJsonUtils.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumJsonUtils.java new file mode 100644 index 00000000000..d418e6e8344 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumJsonUtils.java @@ -0,0 +1,46 @@ +package org.apache.zeppelin.helium; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +/** + * Utils for Helium + */ +public class HeliumJsonUtils { + private static final Gson gson = new Gson(); + + public static List parsePackages(InputStream in, Gson gson) throws IOException { + List packageList = new LinkedList<>(); + + BufferedReader reader; + reader = new BufferedReader( + new InputStreamReader(in)); + + List>> packages = gson.fromJson( + reader, + new TypeToken>>>() {}.getType()); + reader.close(); + + for (Map> pkg : packages) { + for (Map versions : pkg.values()) { + packageList.addAll(versions.values()); + } + } + return packageList; + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumOnlineRegistry.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumOnlineRegistry.java index 3e511b27592..6dcc275b8ad 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumOnlineRegistry.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumOnlineRegistry.java @@ -33,7 +33,6 @@ import java.net.URI; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.UUID; /** @@ -86,23 +85,8 @@ public synchronized List getAll() throws IOException { logger.error(uri() + " returned " + response.getStatusLine().toString()); return readFromCache(); } else { - List packageList = new LinkedList<>(); - - BufferedReader reader; - reader = new BufferedReader( - new InputStreamReader(response.getEntity().getContent())); - - List>> packages = gson.fromJson( - reader, - new TypeToken>>>() { - }.getType()); - reader.close(); - - for (Map> pkg : packages) { - for (Map versions : pkg.values()) { - packageList.addAll(versions.values()); - } - } + List packageList = + HeliumJsonUtils.parsePackages(response.getEntity().getContent(), gson); writeToCache(packageList); return packageList; From 1cf19ea4d093352910148f70fb576208263795bb Mon Sep 17 00:00:00 2001 From: Vitaliy Levitski Date: Tue, 21 Aug 2018 18:22:01 +0300 Subject: [PATCH 470/492] MZEP-164: Disable Credential page in Zeppelin (cherry picked from commit 3a42a50) --- zeppelin-web/src/components/navbar/navbar.html | 1 - 1 file changed, 1 deletion(-) diff --git a/zeppelin-web/src/components/navbar/navbar.html b/zeppelin-web/src/components/navbar/navbar.html index 59d65c9936b..8f6b603dba6 100644 --- a/zeppelin-web/src/components/navbar/navbar.html +++ b/zeppelin-web/src/components/navbar/navbar.html @@ -99,7 +99,6 @@

  • Interpreter
  • Notebook Repos
  • -
  • Credential
  • Helium
  • Configuration
  • From 860829b0acb4d3c247dae3652c8c0d954cebc9bb Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Fri, 22 Mar 2019 20:54:47 +0200 Subject: [PATCH 471/492] Add zookeeper dependency (cherry picked from commit 4528c7b) --- pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pom.xml b/pom.xml index 4ca1ccaeda2..c9ec7b1f5f3 100644 --- a/pom.xml +++ b/pom.xml @@ -324,6 +324,12 @@ test + + org.apache.zookeeper + zookeeper + 3.4.11-mapr-SNAPSHOT + provided + From 6103404c721f6ea8c47480c292d044b76fb683d4 Mon Sep 17 00:00:00 2001 From: Vitaliy Levitski Date: Thu, 30 Aug 2018 12:11:43 +0300 Subject: [PATCH 472/492] MZEP-140: Zeppelin's Config Storage (cherry picked from commit b5f7563) --- .../zeppelin/notebook/FileSystemStorage.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FileSystemStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FileSystemStorage.java index ebec11829f7..b8cdbabf75f 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FileSystemStorage.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FileSystemStorage.java @@ -8,6 +8,7 @@ import org.apache.hadoop.fs.RawLocalFileSystem; import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,7 +23,6 @@ import java.util.ArrayList; import java.util.List; - /** * Hadoop FileSystem wrapper. Support both secure and no-secure mode */ @@ -33,7 +33,7 @@ public class FileSystemStorage { // only do UserGroupInformation.loginUserFromKeytab one time, otherwise you will still get // your ticket expired. static { - if (UserGroupInformation.isSecurityEnabled()) { + if (isKerberosSecurityEnabled()) { ZeppelinConfiguration zConf = ZeppelinConfiguration.create(); String keytab = zConf.getString( ZeppelinConfiguration.ConfVars.ZEPPELIN_SERVER_KERBEROS_KEYTAB); @@ -182,4 +182,20 @@ public T run() throws Exception { } } + private static boolean isKerberosSecurityEnabled() { + return UserGroupInformation.isSecurityEnabled() && isCurrentUserAuthenticatedWithKerberos(); + } + + private static boolean isCurrentUserAuthenticatedWithKerberos() { + return AuthenticationMethod.KERBEROS.equals(getCurrentUserAuthMethod()); + } + + private static AuthenticationMethod getCurrentUserAuthMethod() { + try { + return UserGroupInformation.getCurrentUser().getAuthenticationMethod(); + } catch (IOException e) { + LOGGER.warn("Couldn't get user authentication method for current user: " + e.getMessage()); + return null; + } + } } From 088b6d0fb5953e85e3390114b1716cbd0fb9ff19 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Mon, 21 Oct 2019 17:10:43 +0300 Subject: [PATCH 473/492] Add DSR entrypoint.sh and Dockerfiles --- bin/entrypoint.sh | 535 ++++++++++++++++++ scripts/mapr-dsr/build.sh | 137 +++++ scripts/mapr-dsr/centos7/Dockerfile | 65 +++ scripts/mapr-dsr/kubeflow/Dockerfile | 77 +++ scripts/mapr-dsr/misc/profile.d/mapr.sh | 17 + scripts/mapr-dsr/spark/Dockerfile | 34 ++ scripts/mapr-dsr/ubuntu16/Dockerfile | 71 +++ .../src/assemble/distribution.xml | 6 + 8 files changed, 942 insertions(+) create mode 100755 bin/entrypoint.sh create mode 100755 scripts/mapr-dsr/build.sh create mode 100644 scripts/mapr-dsr/centos7/Dockerfile create mode 100644 scripts/mapr-dsr/kubeflow/Dockerfile create mode 100644 scripts/mapr-dsr/misc/profile.d/mapr.sh create mode 100644 scripts/mapr-dsr/spark/Dockerfile create mode 100644 scripts/mapr-dsr/ubuntu16/Dockerfile diff --git a/bin/entrypoint.sh b/bin/entrypoint.sh new file mode 100755 index 00000000000..c24af4e792f --- /dev/null +++ b/bin/entrypoint.sh @@ -0,0 +1,535 @@ +#!/bin/bash + +if [ ! -e "/.entrypoint_stage_one" ]; then + # + # Setup envinronment for Kubernetes deployment. + # + + if [ -e /mnt/mapr-cluster-cm ]; then + for file in /mnt/mapr-cluster-cm/*; do + name=$(basename "$file") + value=$(cat "$file") + export "$name"="$value" + done + fi + + if [ -z "$MAPT_TZ" ]; then + export MAPR_TZ="UTC" + fi + + if [ -e /mnt/mapr-cluster-secret ]; then + MAPR_CONTAINER_USER=${MAPR_CONTAINER_USER:-$MAPR_USER} + MAPR_CONTAINER_USER=${MAPR_CONTAINER_USER:-$(cat "/mnt/mapr-cluster-secret/MAPR_CONTAINER_USER" 2>/dev/null)} + MAPR_CONTAINER_USER=${MAPR_CONTAINER_USER:-$(cat "/mnt/mapr-cluster-secret/MAPR_USER" 2>/dev/null)} + MAPR_CONTAINER_USER=${MAPR_CONTAINER_USER:-"mapr"} + export MAPR_CONTAINER_USER + MAPR_USER=${MAPR_USER:-$(cat "/mnt/mapr-cluster-secret/MAPR_USER" 2>/dev/null)} + [ -n "$MAPR_USER" ] && export MAPR_USER + + MAPR_CONTAINER_UID=${MAPR_CONTAINER_UID:-$MAPR_UID} + MAPR_CONTAINER_UID=${MAPR_CONTAINER_UID:-$(cat "/mnt/mapr-cluster-secret/MAPR_CONTAINER_UID" 2>/dev/null)} + MAPR_CONTAINER_UID=${MAPR_CONTAINER_UID:-$(cat "/mnt/mapr-cluster-secret/MAPR_UID" 2>/dev/null)} + MAPR_CONTAINER_UID=${MAPR_CONTAINER_UID:-"5000"} + export MAPR_CONTAINER_UID + MAPR_UID=${MAPR_UID:-$(cat "/mnt/mapr-cluster-secret/MAPR_UID" 2>/dev/null)} + [ -n "$MAPR_UID" ] && export MAPR_UID + + MAPR_CONTAINER_GROUP=${MAPR_CONTAINER_GROUP:-$MAPR_GROUP} + MAPR_CONTAINER_GROUP=${MAPR_CONTAINER_GROUP:-$(cat "/mnt/mapr-cluster-secret/MAPR_CONTAINER_GROUP" 2>/dev/null)} + MAPR_CONTAINER_GROUP=${MAPR_CONTAINER_GROUP:-$(cat "/mnt/mapr-cluster-secret/MAPR_GROUP" 2>/dev/null)} + MAPR_CONTAINER_GROUP=${MAPR_CONTAINER_GROUP:-"$MAPR_CONTAINER_USER"} + export MAPR_CONTAINER_GROUP + MAPR_GROUP=${MAPR_GROUP:-$(cat "/mnt/mapr-cluster-secret/MAPR_GROUP" 2>/dev/null)} + [ -n "$MAPR_GROUP" ] && export MAPR_GROUP + + MAPR_CONTAINER_GID=${MAPR_CONTAINER_GID:-$MAPR_GID} + MAPR_CONTAINER_GID=${MAPR_CONTAINER_GID:-$(cat "/mnt/mapr-cluster-secret/MAPR_CONTAINER_GID" 2>/dev/null)} + MAPR_CONTAINER_GID=${MAPR_CONTAINER_GID:-$(cat "/mnt/mapr-cluster-secret/MAPR_GID" 2>/dev/null)} + MAPR_CONTAINER_GID=${MAPR_CONTAINER_GID:-"$MAPR_CONTAINER_UID"} + export MAPR_CONTAINER_GID + MAPR_GID=${MAPR_GID:-$(cat "/mnt/mapr-cluster-secret/MAPR_GID" 2>/dev/null)} + [ -n "$MAPR_GID" ] && export MAPR_GID + + MAPR_CONTAINER_PASSWORD=${MAPR_CONTAINER_PASSWORD:-$MAPR_PASSWORD} + MAPR_CONTAINER_PASSWORD=${MAPR_CONTAINER_PASSWORD:-$(cat "/mnt/mapr-cluster-secret/MAPR_CONTAINER_PASSWORD" 2>/dev/null)} + MAPR_CONTAINER_PASSWORD=${MAPR_CONTAINER_PASSWORD:-$(cat "/mnt/mapr-cluster-secret/MAPR_PASSWORD" 2>/dev/null)} + MAPR_CONTAINER_PASSWORD=${MAPR_CONTAINER_PASSWORD:-"mapr"} + export MAPR_CONTAINER_PASSWORD + MAPR_PASSWORD=${MAPR_PASSWORD:-$(cat "/mnt/mapr-cluster-secret/MAPR_PASSWORD" 2>/dev/null)} + [ -n "$MAPR_PASSWORD" ] && export MAPR_PASSWORD + + mapr_tickefile="/mnt/mapr-cluster-secret/CONTAINER_TICKET" + MAPR_TICKETFILE_LOCATION=${MAPR_TICKETFILE_LOCATION:-$([ -e "$mapr_tickefile" ] && echo "$mapr_tickefile")} + [ -n "$MAPR_TICKETFILE_LOCATION" ] && export MAPR_TICKETFILE_LOCATION + fi + + + if echo "$ZEPPELIN_PORT" | grep -q "^tcp"; then + export ZEPPELIN_PORT=$(echo "$ZEPPELIN_PORT" | cut -d : -f 3) + fi + + + + # + # Run initial mapr-setup.sh for PACC. + # + + # Hack that allows to run "mapr-setup.sh container" not as init script (with PID=1). + # To details take a look at "/opt/mapr/initscripts/mapr-fuse" and "/etc/init.d/functions". + # TODO: Check this on Ubuntu. + if [ -e "/etc/redhat-release" ]; then + export SYSTEMCTL_SKIP_REDIRECT=1 + fi + + /opt/mapr/installer/docker/mapr-setup.sh "container" "/bin/true" + + unset SYSTEMCTL_SKIP_REDIRECT + + + + # + # Continue execution of this entrypoint as MAPR_CONTAINER_USER + # + touch /.entrypoint_stage_one + # Following piece copied from "container_post_client" function of "mapr-setup.sh" + exec sudo -E -H -n -u $MAPR_CONTAINER_USER \ + -g ${MAPR_CONTAINER_GROUP:-$MAPR_GROUP} "$0" "$@" +fi + + + +# +# Fix for Ubuntu issues with environment variables for non-root user in Docker. +# +MAPR_ENV_FILE="/etc/profile.d/mapr.sh" +if [ -e "$MAPR_ENV_FILE" ]; then + . "$MAPR_ENV_FILE" +fi + + + +# +# Common functions +# +log_warn() { + echo "WARN: $@" +} +log_msg() { + echo "MSG: $@" +} +log_err() { + echo "ERR: $@" +} + +# Sielent "hadoop fs" calls +hadoop_fs_mkdir_p() { + hadoop fs -mkdir -p "$1" >/dev/null 2>&1 +} +hadoop_fs_get() { + hadoop fs -get "$1" "$2" >/dev/null 2>&1 +} +hadoop_fs_put() { + hadoop fs -put "$1" "$2" >/dev/null 2>&1 +} +hadoop_fs_test_e() { + hadoop fs -test -e "$1" >/dev/null 2>&1 +} + +# Create files from tuples like "file_source file_destination" +copy_src_dst() { + local src dst + echo "$1" | while read src dst; do + cp --no-clobber "$src" "$dst" + done +} + + + +# +# Common environment variables +# +export MAPR_HOME=${MAPR_HOME:-/opt/mapr} +export MAPR_CLUSTER=${MAPR_CLUSTER:-my.cluster.com} + +SPARK_VERSION_FILE="${MAPR_HOME}/spark/sparkversion" +if [ -e "$SPARK_VERSION_FILE" ]; then + SPARK_VERSION=$(cat "$SPARK_VERSION_FILE") + export SPARK_HOME="${MAPR_HOME}/spark/spark-${SPARK_VERSION}" +fi + +LIVY_VERSION_FILE="${MAPR_HOME}/livy/livyversion" +if [ -e "$LIVY_VERSION_FILE" ]; then + LIVY_VERSION=$(cat "${MAPR_HOME}/livy/livyversion") + export LIVY_HOME="${MAPR_HOME}/livy/livy-${LIVY_VERSION}" +fi + +ZEPPELIN_VERSION=$(cat "${MAPR_HOME}/zeppelin/zeppelinversion") +export ZEPPELIN_HOME="${MAPR_HOME}/zeppelin/zeppelin-${ZEPPELIN_VERSION}" + + + +# +# Local environment variables +# +LIVY_CONF_TUPLES="${LIVY_HOME}/conf/livy-client.conf.container_template ${LIVY_HOME}/conf/livy-client.conf +${LIVY_HOME}/conf/livy.conf.container_template ${LIVY_HOME}/conf/livy.conf +${LIVY_HOME}/conf/livy-env.sh.template ${LIVY_HOME}/conf/livy-env.sh +${LIVY_HOME}/conf/log4j.properties.template ${LIVY_HOME}/conf/log4j.properties +${LIVY_HOME}/conf/spark-blacklist.conf.template ${LIVY_HOME}/conf/spark-blacklist.conf" + +ZEPPELIN_CONF_TUPLES="${ZEPPELIN_HOME}/conf/zeppelin-site.xml.template ${ZEPPELIN_HOME}/conf/zeppelin-site.xml +${ZEPPELIN_HOME}/conf/zeppelin-env.sh.template ${ZEPPELIN_HOME}/conf/zeppelin-env.sh +${ZEPPELIN_HOME}/conf/shiro.ini.template ${ZEPPELIN_HOME}/conf/shiro.ini" + +SPARK_CONF_TUPLES="${SPARK_HOME}/conf/spark-defaults.conf.template ${SPARK_HOME}/conf/spark-defaults.conf +${SPARK_HOME}/conf/log4j.properties.template ${SPARK_HOME}/conf/log4j.properties" + +LIVY_RSC_PORT_RANGE=${LIVY_RSC_PORT_RANGE:-"10000~10010"} +LIVY_RSC_PORT_RANGE=$(echo $LIVY_RSC_PORT_RANGE | sed "s/-/~/") + +# Implicitly increase LIVY_RSC_PORT_RANGE because of LIVY-451 +livy_rsc_port_min=$(echo "$LIVY_RSC_PORT_RANGE" | cut -d '~' -f 1) +livy_rsc_port_max=$(echo "$LIVY_RSC_PORT_RANGE" | cut -d '~' -f 2) +livy_rsc_port_max_new=$(expr "$livy_rsc_port_max" + 10) +LIVY_RSC_PORT_RANGE_NEW="${livy_rsc_port_min}~${livy_rsc_port_max_new}" + +ZEPPELIN_ENV_SH="${ZEPPELIN_HOME}/conf/zeppelin-env.sh" +ZEPPELIN_ENV_DSR="${ZEPPELIN_HOME}/conf/zeppelin-env-dsr.sh" + +SPARK_PORT_RANGE="${SPARK_PORT_RANGE:-11000~11010}" +SPARK_PORT_RANGE=$(echo $SPARK_PORT_RANGE | sed "s/-/~/") + +REMOTE_ARCHIVES_DIR="/user/${MAPR_CONTAINER_USER}/zeppelin/archives" + +LOCAL_ARCHIVES_DIR="$(getent passwd $MAPR_CONTAINER_USER | cut -d':' -f6)/zeppelin/archives" +LOCAL_ARCHIVES_ZIPDIR="${LOCAL_ARCHIVES_DIR}/zip" + + + +# +# Functions to configure Livy +# +livy_conf_subs() { + local livy_conf="$1" + local sub="$2" + local val="$3" + if [ -n "${val}" ]; then + sed -i -r "s|# (.*) ${sub}|\1 ${val}|" "${livy_conf}" + fi +} + +livy_setup() { + copy_src_dst "$LIVY_CONF_TUPLES" + if [ -n "$HOST_IP" ]; then + livy_conf_subs "${LIVY_HOME}/conf/livy-client.conf" "__LIVY_HOST_IP__" "$HOST_IP" + fi + livy_conf_subs "${LIVY_HOME}/conf/livy-client.conf" "__LIVY_RSC_PORT_RANGE__" "$LIVY_RSC_PORT_RANGE_NEW" + + # TODO: refactor setup of livy.conf. + # MZEP-162: + sed -i 's/^.*livy\.ui\.enabled.*$/livy.ui.enabled=false/g' "${LIVY_HOME}/conf/livy.conf" +} + + + +# +# Functions to configure Zeppelin +# +zeppelin_create_certificates() { + if [ "$JAVA_HOME"x = "x" ]; then + KEYTOOL=`which keytool` + else + KEYTOOL=$JAVA_HOME/bin/keytool + fi + + DOMAINNAME=`hostname -d` + if [ "$DOMAINNAME"x = "x" ]; then + CERTNAME=`hostname` + else + CERTNAME="*."$DOMAINNAME + fi + + if [ ! -e "$ZEPPELIN_SSL_KEYSTORE_PATH" ]; then + echo "Creating 10 year self signed certificate for Zeppelin with subjectDN='CN=$CERTNAME'" + mkdir -p $(dirname "$ZEPPELIN_SSL_KEYSTORE_PATH") + $KEYTOOL -genkeypair -sigalg SHA512withRSA -keyalg RSA -alias "$MAPR_CLUSTER" -dname "CN=$CERTNAME" -validity 3650 \ + -storepass "$ZEPPELIN_SSL_KEYSTORE_PASSWORD" -keypass "$ZEPPELIN_SSL_KEYSTORE_PASSWORD" \ + -keystore "$ZEPPELIN_SSL_KEYSTORE_PATH" -storetype "$ZEPPELIN_SSL_KEYSTORE_TYPE" + if [ $? -ne 0 ]; then + echo "Keytool command to generate key store failed" + fi + else + echo "Creating of Zeppelin keystore was skipped as it already exists: ${ZEPPELIN_SSL_KEYSTORE_PATH}." + fi +} + +zeppelin_setup_callback_port_range() { + ZEPPELIN_INTERPRETER_CALLBACK_PORTRANGE=${ZEPPELIN_INTERPRETER_CALLBACK_PORTRANGE:-$(echo "$SPARK_PORT_RANGE" | sed 's/~/:/')} + cat >> "$ZEPPELIN_ENV_DSR" < "$ZEPPELIN_ENV_DSR" + + # Read zeppelin-env.sh, as certificate-related variables definde there + . "$ZEPPELIN_ENV_SH" + zeppelin_create_certificates + + zeppelin_setup_callback_port_range +} + + + +# +# Functions to configure Spark +# +spark_get_property() { + local spark_conf="${SPARK_HOME}/conf/spark-defaults.conf" + local property_name="$1" + grep "^\s*${property_name}" "${spark_conf}" | sed "s|^\s*${property_name}\s*||" +} + +spark_set_property() { + local spark_conf="${SPARK_HOME}/conf/spark-defaults.conf" + local property_name="$1" + local property_value="$2" + if grep -q "^\s*${property_name}\s*" "${spark_conf}"; then + # modify property + sed -i -r "s|^\s*${property_name}.*$|${property_name} ${property_value}|" "${spark_conf}" + else + # add property + echo "${property_name} ${property_value}" >> "${spark_conf}" + fi +} + +spark_append_property() { + local spark_conf="${SPARK_HOME}/conf/spark-defaults.conf" + local property_name="$1" + local property_value="$2" + local old_value=$(spark_get_property "${property_name}") + local new_value="" + if [ -z "${old_value}" ]; then + # new value + new_value="${property_value}" + elif echo "${old_value}" | grep -q -F "${property_value}"; then + # nothing to do + new_value="${old_value}" + else + # modify value + new_value="${old_value},${property_value}" + fi + spark_set_property "${property_name}" "${new_value}" +} + +setup_spark_jars() { + HBASE_VERSION=$(cat "${MAPR_HOME}/hbase/hbaseversion") + HBASE_HOME="${MAPR_HOME}/hbase/hbase-${HBASE_VERSION}" + # Copy MapR-DB and Streaming jars into Spark + JAR_WHILDCARDS=" + ${MAPR_HOME}/lib/kafka-clients-*-mapr-*.jar + ${MAPR_HOME}/lib/mapr-hbase-*-mapr-*.jar + ${HBASE_HOME}/lib/hbase-*-mapr-*.jar + ${ZEPPELIN_HOME}/interpreter/spark/spark-interpreter*.jar + " + for jar_path in $JAR_WHILDCARDS; do + jar_name=$(basename "${jar_path}") + if [ -e "${jar_path}" ] && [ ! -e "${SPARK_HOME}/jars/${jar_name}" ]; then + ln -s "${jar_path}" "${SPARK_HOME}/jars" + fi + done +} + +spark_fix_log4j() { #DSR-20 + # Copied from Spark configure.sh + # + # Improved default logging level (WARN instead of INFO) + # + sed -i 's/rootCategory=INFO/rootCategory=WARN/' "${SPARK_HOME}/conf/log4j.properties" +} + +spark_configure_hive_site() { + local spark_conf="${SPARK_HOME}/conf/spark-defaults.conf" + local spark_hive_site="${SPARK_HOME}/conf/hive-site.xml" + if [ ! -e "${spark_hive_site}" ]; then + cat > "${spark_hive_site}" <<'EOF' + + + + + +EOF + fi + local spark_yarn_dist_files=$(spark_get_property "spark.yarn.dist.files") + # Check if no "hive-site.xml" in "spark.yarn.dist.files" + if ! spark_get_property "spark.yarn.dist.files" | grep -q "hive-site.xml"; then + spark_append_property "spark.yarn.dist.files" "${spark_hive_site}" + fi +} + +out_archive_local="" +out_archive_extracted="" +out_archive_remote="" +out_archive_filename="" +setup_archive() { + local archive_path="$1" + local archive_filename=$(basename "$archive_path") + local archive_local="" + local archive_remote="" + if hadoop_fs_test_e "$archive_path"; then + archive_remote="$archive_path" + archive_local="${LOCAL_ARCHIVES_ZIPDIR}/${archive_filename}" + if [ ! -e "$archive_local" ]; then + log_msg "Copying archive from MapR-FS: ${archive_remote} -> ${archive_local}" + hadoop_fs_get "$archive_remote" "$archive_local" + else + log_msg "Skip copying archive from MapR-FS as it already exists" + fi + elif [ -e "$archive_path" ]; then + archive_local="$archive_path" + archive_remote="${REMOTE_ARCHIVES_DIR}/${archive_filename}" + # Copy archive to MapR-FS + if ! hadoop_fs_test_e "$archive_remote"; then + log_msg "Copying archive to MapR-FS: ${archive_local} -> ${archive_remote}" + hadoop_fs_put "$archive_local" "$archive_remote" + else + log_msg "Skip copying archive to MapR-FS as it already exists" + fi + else + log_err "Archive '${archive_path}' not found" + return 1 + fi + local archive_extracted="${LOCAL_ARCHIVES_DIR}/${archive_filename}" + if [ ! -e "$archive_extracted" ]; then + log_msg "Extracting archive locally" + mkdir -p "$archive_extracted" + unzip -qq "$archive_local" -d "$archive_extracted" || return 1 + else + log_msg "Skip extracting archive locally as it already exists" + fi + + out_archive_local="$archive_local" + out_archive_extracted="$archive_extracted" + out_archive_remote=$(echo "$archive_remote" | sed "s|maprfs://||") + out_archive_filename="$archive_filename" + return 0 +} + +spark_configure_python() { + log_msg "Setting up Python archive" + setup_archive "$ZEPPELIN_ARCHIVE_PYTHON" || return 1 + log_msg "Configuring Spark to use custom Python" + spark_append_property "spark.yarn.dist.archives" "maprfs://${out_archive_remote}" + spark_set_property "spark.yarn.appMasterEnv.PYSPARK_PYTHON" "./${out_archive_filename}/bin/python" + log_msg "Configuring Zeppelin to use custom Python with Spark interpreter" + cat >> "$ZEPPELIN_ENV_DSR" << EOF +export ZEPPELIN_SPARK_YARN_DIST_ARCHIVES="maprfs://${out_archive_remote}" +export PYSPARK_PYTHON='./${out_archive_filename}/bin/python' + +EOF + return 0 +} + +spark_configure_custom_envs() { + if ! hadoop_fs_test_e "/user/${MAPR_CONTAINER_USER}/"; then + log_warn "/user/${MAPR_CONTAINER_USER} does not exist in MapR-FS" + return 1 + fi + + hadoop_fs_mkdir_p "$REMOTE_ARCHIVES_DIR" + mkdir -p "$LOCAL_ARCHIVES_DIR" "$LOCAL_ARCHIVES_ZIPDIR" + + if [ -n "$ZEPPELIN_ARCHIVE_PYTHON" ]; then + spark_configure_python || log_msg "Using default Python" + else + log_msg "Using default Python" + fi + + if [ -n "$ZEPPELIN_ARCHIVE_PYTHON3" ]; then + log_warn "Property 'ZEPPELIN_ARCHIVE_PYTHON3' is deprecated. Ignoring." + fi +} + +spark_setup() { + copy_src_dst "$SPARK_CONF_TUPLES" + + setup_spark_jars + spark_fix_log4j + spark_configure_hive_site + spark_configure_custom_envs + + if [ -n "$HOST_IP" ]; then + spark_ports=$(echo "$SPARK_PORT_RANGE" | sed 's/~/\n/') + read -a ports <<< $(seq $spark_ports) + spark_set_property "spark.driver.bindAddress" "0.0.0.0" + spark_set_property "spark.driver.host" "${HOST_IP}" + spark_set_property "spark.driver.port" "${ports[0]}" + spark_set_property "spark.blockManager.port" "${ports[1]}" + spark_set_property "spark.ui.port" "${ports[2]}" + else + log_err "Can't configure Spark networking because \$HOST_IP is not set" + fi +} + + + +# +# Configure Livy, Zeppelin, and Spark +# +if [ -e "$LIVY_HOME" ]; then + livy_setup +else + log_warn '$LIVY_HOME not found' +fi + +zeppelin_setup + +if [ -e "$SPARK_HOME" ]; then + spark_setup +else + log_warn '$SPARK_HOME not found' +fi + + + +# +# Start Livy and Zeppelin +# +if [ -e "$LIVY_HOME" ]; then + cd "$LIVY_HOME" + "${LIVY_HOME}/bin/livy-server" start & +fi + +# Explicitly set Zeppelin working directory +# To prevent issues when Zeppelin started in / and its subprocesses cannot write to CWD +cd "${ZEPPELIN_HOME}" + +# DSR-42 +# Ensure that DEPLOY_MODE variable is not set as it affects Spark behaviour. +if [ -n "$DEPLOY_MODE" ]; then + log_warn "'DEPLOY_MODE' parameter is obsolete. Use 'ZEPPELIN_DEPLOY_MODE' instead." + + # Backward compatibility with DEPLOY_MODE parameter. + if [ -z "$ZEPPELIN_DEPLOY_MODE" ]; then + ZEPPELIN_DEPLOY_MODE="$DEPLOY_MODE" + fi + + unset DEPLOY_MODE +fi + + +if [ "$ZEPPELIN_DEPLOY_MODE" = "kubernetes" ]; then + exec "${ZEPPELIN_HOME}/bin/zeppelin.sh" start +else + "${ZEPPELIN_HOME}/bin/zeppelin-daemon.sh" start + if [ $# -eq 0 ]; then + exec bash + else + exec "$@" + fi +fi diff --git a/scripts/mapr-dsr/build.sh b/scripts/mapr-dsr/build.sh new file mode 100755 index 00000000000..06e92620074 --- /dev/null +++ b/scripts/mapr-dsr/build.sh @@ -0,0 +1,137 @@ +#!/bin/sh + +MAPR_VERSION_DSR=${MAPR_VERSION_DSR:-"v1.4.0"} +MAPR_VERSION_CORE=${MAPR_VERSION_CORE:-"6.1.0"} +MAPR_VERSION_MEP=${MAPR_VERSION_MEP:-"6.3.0"} + +PUSH_IMAGES=false +RELEASE=false +BUILD_ALL=true +BUILD_SUCC=true +for arg in "$@"; do + case "$arg" in + -r|--release) + RELEASE=true + ;; + -p|--push) + PUSH_IMAGES=true + ;; + centos7) + BUILD_CENTOS7=true + BUILD_ALL=false + ;; + ubuntu16) + BUILD_UBUNTU16=true + BUILD_ALL=false + ;; + kubeflow) + BUILD_KUBEFLOW=true + BUILD_ALL=false + ;; + spark) + BUILD_SPARK=true + BUILD_ALL=false + ;; + *) + echo "Wrong argument: '$arg'" + exit 1 + esac +done + +if [ "$PUSH_IMAGES" = true ] && [ "$RELEASE" = true ]; then + echo "It's bad idea to build release images and push it without testing" + exit 1 +fi + +if [ "$RELEASE" = true ]; then + DOCKER_REPO=${DOCKER_REPO:-"maprtech/data-science-refinery"} + IMAGE_VERSION=${IMAGE_VERSION:-"${MAPR_VERSION_DSR}_${MAPR_VERSION_CORE}_${MAPR_VERSION_MEP}"} + ZEPPELIN_GIT_REPO=${ZEPPELIN_GIT_REPO:-"git@github.com:mapr/zeppelin.git"} + ZEPPELIN_GIT_TAG=${ZEPPELIN_GIT_TAG:-"0.8.2-mapr-1911"} + MAPR_REPO_ROOT=${MAPR_REPO_ROOT:-"https://package.mapr.com/releases"} + MAPR_MAVEN_REPO=${MAPR_MAVEN_REPO:-"http://repository.mapr.com/maven/"} +else + DOCKER_REPO=${DOCKER_REPO:-"maprtech/testzepplinpacc"} + IMAGE_VERSION=${IMAGE_VERSION:-$(date -u "+%Y%m%d%H%M")} + ZEPPELIN_GIT_REPO=${ZEPPELIN_GIT_REPO:-"git@github.com:mapr/private-zeppelin.git"} + ZEPPELIN_GIT_TAG=${ZEPPELIN_GIT_TAG:-"branch-0.8.2-mapr"} + MAPR_REPO_ROOT=${MAPR_REPO_ROOT:-"http://artifactory.devops.lab/artifactory/prestage/releases-dev"} + MAPR_MAVEN_REPO=${MAPR_MAVEN_REPO:-"http://maven.corp.maprtech.com/nexus/content/groups/public/"} +fi +KUBEFLOW_REPO=${KUBEFLOW_REPO:-"us.gcr.io/mapreng-1/maprtech/zeppelin"} +KUBEFLOW_IMAGE_VERSION=${KUBEFLOW_IMAGE_VERSION:-"$IMAGE_VERSION"} +SPARK_REPO="us.gcr.io/mapreng-1/maprtech/spark-zeppelin-2.4.4" +SPARK_IMAGE_VERSION=${SPARK_IMAGE_VERSION:-"$IMAGE_VERSION"} + +if [ "$BUILD_ALL" = true ]; then + BUILD_UBUNTU16=true + BUILD_CENTOS7=true + BUILD_KUBEFLOW=true + BUILD_SPARK=true +fi + +docker_build() { + export DOCKER_BUILDKIT=1 + docker build . \ + --no-cache \ + --ssh default="${HOME}/.ssh/id_rsa" \ + --build-arg MAPR_VERSION_CORE="$MAPR_VERSION_CORE" \ + --build-arg MAPR_VERSION_MEP="$MAPR_VERSION_MEP" \ + --build-arg MAPR_REPO_ROOT="$MAPR_REPO_ROOT" \ + --build-arg ZEPPELIN_GIT_REPO="$ZEPPELIN_GIT_REPO" \ + --build-arg ZEPPELIN_GIT_TAG="$ZEPPELIN_GIT_TAG" \ + --build-arg MAPR_MAVEN_REPO="$MAPR_MAVEN_REPO" \ + --file "$1" \ + --tag "$2" + res="$?" + + return "$res" +} + +if [ "$BUILD_UBUNTU16" = true ]; then + echo "Building ubuntu16" + docker_build "ubuntu16/Dockerfile" "${DOCKER_REPO}:${IMAGE_VERSION}_ubuntu16" + [ "$?" -ne 0 ] && BUILD_SUCC=false +fi + +if [ "$BUILD_CENTOS7" = true ]; then + echo "Building centos7" + docker_build "centos7/Dockerfile" "${DOCKER_REPO}:${IMAGE_VERSION}_centos7" + [ "$?" -ne 0 ] && BUILD_SUCC=false +fi + +if [ "$BUILD_KUBEFLOW" = true ]; then + echo "Building kubeflow" + docker_build "kubeflow/Dockerfile" "${KUBEFLOW_REPO}:${KUBEFLOW_IMAGE_VERSION}" + [ "$?" -ne 0 ] && BUILD_SUCC=false +fi + +if [ "$BUILD_SPARK" = true ]; then + echo "Building Spark images with Zeppelin" + docker_build "spark/Dockerfile" "${SPARK_REPO}:${SPARK_IMAGE_VERSION}" + [ "$?" -ne 0 ] && BUILD_SUCC=false +fi + +if [ "$PUSH_IMAGES" = true ] && [ "$BUILD_SUCC" = true ]; then + if [ "$BUILD_UBUNTU16" = true ]; then + docker push "${DOCKER_REPO}:${IMAGE_VERSION}_ubuntu16" + fi + + if [ "$BUILD_CENTOS7" = true ]; then + docker push "${DOCKER_REPO}:${IMAGE_VERSION}_centos7" + docker tag "${DOCKER_REPO}:${IMAGE_VERSION}_centos7" "${DOCKER_REPO}:latest" + docker push "${DOCKER_REPO}:latest" + fi + + if [ "$BUILD_KUBEFLOW" = true ]; then + docker push "${KUBEFLOW_REPO}:${KUBEFLOW_IMAGE_VERSION}" + docker tag "${KUBEFLOW_REPO}:${KUBEFLOW_IMAGE_VERSION}" "${KUBEFLOW_REPO}:latest" + docker push "${KUBEFLOW_REPO}:latest" + fi + + if [ "$BUILD_SPARK" = true ]; then + docker push "${SPARK_REPO}:${SPARK_IMAGE_VERSION}" + docker tag "${SPARK_REPO}:${SPARK_IMAGE_VERSION}" "${SPARK_REPO}:latest" + docker push "${SPARK_REPO}:latest" + fi +fi diff --git a/scripts/mapr-dsr/centos7/Dockerfile b/scripts/mapr-dsr/centos7/Dockerfile new file mode 100644 index 00000000000..08751dec306 --- /dev/null +++ b/scripts/mapr-dsr/centos7/Dockerfile @@ -0,0 +1,65 @@ +# syntax=docker/dockerfile:1.0.2-experimental +FROM centos:centos7 as zeppelin_builder + +ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1911" +ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" + +RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ + mkdir /opt/maven && \ + wget https://www.apache.org/dist/maven/maven-3/3.5.4/binaries/apache-maven-3.5.4-bin.tar.gz -O maven.tar.gz && \ + tar xf maven.tar.gz --strip-components=1 -C /opt/maven && \ + ln -s /opt/maven/bin/mvn /usr/local/bin/mvn + +RUN --mount=type=cache,id=mvncache,target=/root/.m2 --mount=type=cache,id=npmcache,target=/root/.npm --mount=type=ssh \ + mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts && \ + git clone "$ZEPPELIN_GIT_REPO" zeppelin && \ + cd zeppelin && \ + git checkout "$ZEPPELIN_GIT_TAG" && \ + mvn clean package -DskipTests -P 'scala-2.11,build-distr,vendor-repo-mapr' && \ + ZEPPELIN_MAVEN_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) && \ + mv "./zeppelin-distribution/target/zeppelin-${ZEPPELIN_MAVEN_VERSION}/zeppelin-${ZEPPELIN_MAVEN_VERSION}" /zeppelin_build + + +FROM centos:centos7 + +ARG ZEPPELIN_VERSION="0.8.2" +ARG MAPR_VERSION_CORE="6.1.0" +ARG MAPR_VERSION_MEP="6.3.0" +ARG MAPR_REPO_ROOT="https://package.mapr.com/releases" + +LABEL mapr.os=centos7 mapr.version=$MAPR_VERSION_CORE mapr.mep_version=$MAPR_VERSION_MEP + +ENV container docker + +RUN yum install -y curl initscripts net-tools sudo wget which syslinux openssl file java-1.8.0-openjdk-devel unzip + +RUN mkdir -p /opt/mapr/installer/docker/ && \ + wget "${MAPR_REPO_ROOT}/installer/redhat/mapr-setup.sh" -P /opt/mapr/installer/docker/ && \ + chmod +x /opt/mapr/installer/docker/mapr-setup.sh + +RUN /opt/mapr/installer/docker/mapr-setup.sh -r "$MAPR_REPO_ROOT" container client "$MAPR_VERSION_CORE" "$MAPR_VERSION_MEP" mapr-client mapr-posix-client-container mapr-hbase mapr-pig mapr-spark mapr-kafka mapr-livy + +RUN mkdir -p /opt/mapr/zeppelin && \ + echo "$ZEPPELIN_VERSION" > /opt/mapr/zeppelin/zeppelinversion + +RUN yum install -y gcc python-devel python-setuptools && \ + easy_install pip && \ + pip install matplotlib numpy pandas + +COPY --from=zeppelin_builder /zeppelin_build "/opt/mapr/zeppelin/zeppelin-$ZEPPELIN_VERSION" + +RUN ZEPPELIN_HOME="/opt/mapr/zeppelin/zeppelin-${ZEPPELIN_VERSION}" ;\ + ln -s "${ZEPPELIN_HOME}/bin/entrypoint.sh" "/entrypoint.sh" ;\ + cat "${ZEPPELIN_HOME}/scripts/mapr-dsr/misc/profile.d/mapr.sh" >> /etc/profile.d/mapr.sh + +RUN rm /etc/yum.repos.d/mapr_*.repo && \ + yum -q clean all && \ + rm -rf /var/lib/yum/history/* && \ + find /var/lib/yum/yumdb/ -name origin_url -exec rm {} \; + +EXPOSE 9995 +EXPOSE 10000-10010 +EXPOSE 11000-11010 + +ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/scripts/mapr-dsr/kubeflow/Dockerfile b/scripts/mapr-dsr/kubeflow/Dockerfile new file mode 100644 index 00000000000..bc3f1301b54 --- /dev/null +++ b/scripts/mapr-dsr/kubeflow/Dockerfile @@ -0,0 +1,77 @@ +# syntax=docker/dockerfile:1.0.2-experimental +FROM centos:centos7 as zeppelin_builder + +ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1911" +ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" + +RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ + mkdir /opt/maven && \ + wget https://www.apache.org/dist/maven/maven-3/3.5.4/binaries/apache-maven-3.5.4-bin.tar.gz -O maven.tar.gz && \ + tar xf maven.tar.gz --strip-components=1 -C /opt/maven && \ + ln -s /opt/maven/bin/mvn /usr/local/bin/mvn + +RUN --mount=type=cache,id=mvncache,target=/root/.m2 --mount=type=cache,id=npmcache,target=/root/.npm --mount=type=ssh \ + mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts && \ + git clone "$ZEPPELIN_GIT_REPO" zeppelin && \ + cd zeppelin && \ + git checkout "$ZEPPELIN_GIT_TAG" && \ + mvn clean package -DskipTests -P 'scala-2.11,build-distr,vendor-repo-mapr' && \ + ZEPPELIN_MAVEN_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) && \ + mv "./zeppelin-distribution/target/zeppelin-${ZEPPELIN_MAVEN_VERSION}/zeppelin-${ZEPPELIN_MAVEN_VERSION}" /zeppelin_build + + +FROM centos:centos7 + +ARG ZEPPELIN_VERSION="0.8.2" +ARG MAPR_VERSION_CORE="6.1.0" +ARG MAPR_VERSION_MEP="6.3.0" +ARG MAPR_REPO_ROOT="https://package.mapr.com/releases" + +LABEL mapr.os=centos7 mapr.version=$MAPR_VERSION_CORE mapr.mep_version=$MAPR_VERSION_MEP + +ENV container docker + +RUN yum install -y curl initscripts net-tools sudo wget which syslinux openssl file java-1.8.0-openjdk-devel unzip + +RUN mkdir -p /opt/mapr/installer/docker/ && \ + wget "${MAPR_REPO_ROOT}/installer/redhat/mapr-setup.sh" -P /opt/mapr/installer/docker/ && \ + chmod +x /opt/mapr/installer/docker/mapr-setup.sh + +RUN /opt/mapr/installer/docker/mapr-setup.sh -r "$MAPR_REPO_ROOT" container client "$MAPR_VERSION_CORE" "$MAPR_VERSION_MEP" mapr-client mapr-hbase mapr-spark mapr-kafka + +RUN mkdir -p /opt/mapr/zeppelin && \ + echo "$ZEPPELIN_VERSION" > /opt/mapr/zeppelin/zeppelinversion + +RUN yum install -y gcc python-devel python-setuptools && \ + easy_install pip && \ + pip install matplotlib numpy pandas requests + +COPY --from=zeppelin_builder /zeppelin_build "/opt/mapr/zeppelin/zeppelin-$ZEPPELIN_VERSION" + +RUN ZEPPELIN_HOME="/opt/mapr/zeppelin/zeppelin-${ZEPPELIN_VERSION}" ;\ + ln -s "${ZEPPELIN_HOME}/bin/entrypoint.sh" "/entrypoint.sh" ;\ + cat "${ZEPPELIN_HOME}/scripts/mapr-dsr/misc/profile.d/mapr.sh" >> /etc/profile.d/mapr.sh + +RUN echo -e "\n\ +[kubernetes] \n\ +name=Kubernetes \n\ +baseurl=https://packages.cloud.google.com/yum/repos/cloud-sdk-el7-x86_64 \n\ +enabled=1 \n\ +gpgcheck=1 \n\ +repo_gpgcheck=1 \n\ +gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg \n\ + https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg \n\ +" > /etc/yum.repos.d/cloud-sdk-el7-x86_64.repo && \ + yum install -y kubectl + +RUN rm /etc/yum.repos.d/mapr_*.repo && \ + yum -q clean all && \ + rm -rf /var/lib/yum/history/* && \ + find /var/lib/yum/yumdb/ -name origin_url -exec rm {} \; + +EXPOSE 9995 +EXPOSE 10000-10010 +EXPOSE 11000-11010 + +ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/scripts/mapr-dsr/misc/profile.d/mapr.sh b/scripts/mapr-dsr/misc/profile.d/mapr.sh new file mode 100644 index 00000000000..5b0a40e21ce --- /dev/null +++ b/scripts/mapr-dsr/misc/profile.d/mapr.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# USER variable may be not set on Ubuntu +if [ -x /usr/bin/id ]; then + USER=${USER:-$(/usr/bin/id -un)} + LOGNAME=${LOGNAME:-$USER} + MAIL=${MAIL:-"/var/spool/mail/$USER"} + export USER LOGNAME MAIL + + if [ -x /usr/bin/getent ]; then + HOME=$(/usr/bin/getent passwd $USER | cut -d ':' -f 6) + export HOME + fi +fi + +export DRILL_HOME="${DRILL_HOME:-/opt/mapr}" + +cd ~ diff --git a/scripts/mapr-dsr/spark/Dockerfile b/scripts/mapr-dsr/spark/Dockerfile new file mode 100644 index 00000000000..1361d320ee6 --- /dev/null +++ b/scripts/mapr-dsr/spark/Dockerfile @@ -0,0 +1,34 @@ +# syntax=docker/dockerfile:1.0.2-experimental +FROM centos:centos7 as zeppelin_builder + +ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1911" +ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" + +RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ + mkdir /opt/maven && \ + wget https://www.apache.org/dist/maven/maven-3/3.5.4/binaries/apache-maven-3.5.4-bin.tar.gz -O maven.tar.gz && \ + tar xf maven.tar.gz --strip-components=1 -C /opt/maven && \ + ln -s /opt/maven/bin/mvn /usr/local/bin/mvn + +RUN --mount=type=cache,id=mvncache,target=/root/.m2 --mount=type=cache,id=npmcache,target=/root/.npm --mount=type=ssh \ + mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts && \ + git clone "$ZEPPELIN_GIT_REPO" zeppelin && \ + cd zeppelin && \ + git checkout "$ZEPPELIN_GIT_TAG" && \ + mvn clean package -DskipTests -P 'scala-2.11,build-distr,vendor-repo-mapr' -pl 'spark/interpreter,zeppelin-display,zeppelin-interpreter,spark/scala-2.11,spark/spark2-shims,spark/spark-shims,python,zeppelin-distribution,zeppelin-server,zeppelin-web,zeppelin-zengine' && \ + ZEPPELIN_MAVEN_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) && \ + mv "./zeppelin-distribution/target/zeppelin-${ZEPPELIN_MAVEN_VERSION}/zeppelin-${ZEPPELIN_MAVEN_VERSION}" /zeppelin_build + + +FROM us.gcr.io/mapreng-1/maprtech/spark-2.4.4:latest + +ARG ZEPPELIN_VERSION="0.8.2" + +RUN yum install -y gcc python-devel python-setuptools && \ + easy_install pip && \ + pip install matplotlib numpy pandas && \ + yum install -y R-core R-core-devel && \ + Rscript -e 'install.packages(c("data.table", "ggplot2", "googleVis", "knitr"), repos="https://cloud.r-project.org/")' + +COPY --from=zeppelin_builder /zeppelin_build "/opt/mapr/zeppelin/zeppelin-$ZEPPELIN_VERSION" diff --git a/scripts/mapr-dsr/ubuntu16/Dockerfile b/scripts/mapr-dsr/ubuntu16/Dockerfile new file mode 100644 index 00000000000..edce7c2d6d1 --- /dev/null +++ b/scripts/mapr-dsr/ubuntu16/Dockerfile @@ -0,0 +1,71 @@ +# syntax=docker/dockerfile:1.0.2-experimental +FROM ubuntu:16.04 as zeppelin_builder + +ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1911" +ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" + +RUN export DEBIAN_FRONTEND=noninteractive && \ + apt update -qq && \ + apt install --no-install-recommends -q -y git openssh-client wget openjdk-8-jdk bzip2 && \ + mkdir /opt/maven && \ + wget https://www.apache.org/dist/maven/maven-3/3.5.4/binaries/apache-maven-3.5.4-bin.tar.gz -O maven.tar.gz && \ + tar xf maven.tar.gz --strip-components=1 -C /opt/maven && \ + ln -s /opt/maven/bin/mvn /usr/local/bin/mvn + +RUN --mount=type=cache,id=mvncache,target=/root/.m2 --mount=type=cache,id=npmcache,target=/root/.npm --mount=type=ssh \ + mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts && \ + git clone "$ZEPPELIN_GIT_REPO" zeppelin && \ + cd zeppelin && \ + git checkout "$ZEPPELIN_GIT_TAG" && \ + mvn clean package -DskipTests -P 'scala-2.11,build-distr,vendor-repo-mapr' && \ + ZEPPELIN_MAVEN_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) && \ + mv "./zeppelin-distribution/target/zeppelin-${ZEPPELIN_MAVEN_VERSION}/zeppelin-${ZEPPELIN_MAVEN_VERSION}" /zeppelin_build + + +FROM ubuntu:16.04 + +ARG ZEPPELIN_VERSION="0.8.2" +ARG MAPR_VERSION_CORE="6.1.0" +ARG MAPR_VERSION_MEP="6.3.0" +ARG MAPR_REPO_ROOT="https://package.mapr.com/releases" + +LABEL mapr.os=ubuntu16 mapr.version=$MAPR_VERSION_CORE mapr.mep_versiona=$MAPR_VERSION_MEP + +ENV container docker + +RUN export DEBIAN_FRONTEND=noninteractive && \ + apt update -qq && \ + apt install --no-install-recommends -q -y curl sudo tzdata wget apt-transport-https apt-utils dnsutils file iputils-ping net-tools nfs-common openssl syslinux sysv-rc-conf libssl1.0.0 openjdk-8-jdk unzip + +RUN mkdir -p /opt/mapr/installer/docker/ && \ + wget "${MAPR_REPO_ROOT}/installer/ubuntu/mapr-setup.sh" -P /opt/mapr/installer/docker/ && \ + chmod +x /opt/mapr/installer/docker/mapr-setup.sh + +RUN /opt/mapr/installer/docker/mapr-setup.sh -r "$MAPR_REPO_ROOT" container client "$MAPR_VERSION_CORE" "$MAPR_VERSION_MEP" mapr-client mapr-posix-client-container mapr-hbase mapr-pig mapr-spark mapr-kafka mapr-livy + +RUN mkdir -p /opt/mapr/zeppelin && \ + echo "$ZEPPELIN_VERSION" > /opt/mapr/zeppelin/zeppelinversion + +RUN export DEBIAN_FRONTEND=noninteractive && \ + apt update && \ + apt install --no-install-recommends -q -y gcc python-dev python-setuptools && \ + easy_install pip && \ + pip install matplotlib numpy pandas + +COPY --from=zeppelin_builder /zeppelin_build "/opt/mapr/zeppelin/zeppelin-$ZEPPELIN_VERSION" + +RUN ZEPPELIN_HOME="/opt/mapr/zeppelin/zeppelin-${ZEPPELIN_VERSION}" && \ + ln -s "${ZEPPELIN_HOME}/bin/entrypoint.sh" "/entrypoint.sh" && \ + cat "${ZEPPELIN_HOME}/scripts/mapr-dsr/misc/profile.d/mapr.sh" >> /etc/profile.d/mapr.sh + +RUN rm /etc/apt/sources.list.d/mapr_* && \ + apt-get autoremove --purge -q -y && \ + rm -rf /var/lib/apt/lists/* && \ + apt-get clean -q + +EXPOSE 9995 +EXPOSE 10000-10010 +EXPOSE 11000-11010 + +ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/zeppelin-distribution/src/assemble/distribution.xml b/zeppelin-distribution/src/assemble/distribution.xml index 5c369e256a3..1f16e36f6b3 100644 --- a/zeppelin-distribution/src/assemble/distribution.xml +++ b/zeppelin-distribution/src/assemble/distribution.xml @@ -107,5 +107,11 @@ /lib/node_modules/zeppelin-spell ../zeppelin-web/src/app/spell + + ../scripts/mapr-dsr + + build.sh + + From f5373cf65764295df43ebe6e73dbd0b32df50ec2 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Fri, 22 Mar 2019 19:48:44 +0200 Subject: [PATCH 474/492] Modify Tutorial notebooks (cherry picked from commit ce22d43) #Squashed DSR-88 Refactor Spark MapR-DB Binary tutorials notebook according to ZEPPELIN-3587 (#132) --- notebook/2A94M5J1Z/note.json | 58 +-- notebook/2BWJFTXKJ/note.json | 338 ++++++------- notebook/2BYEZ5EVK/note.json | 887 ----------------------------------- notebook/2C2AUG798/note.json | 475 ++++++++++--------- notebook/2C35YU814/note.json | 806 ------------------------------- notebook/2C57UKYWR/note.json | 48 +- notebook/2D23Y84Q3/note.json | 257 ++++++++++ notebook/2D25QSMZD/note.json | 632 +++++++++++++++++++++++++ notebook/2D3979PMW/note.json | 263 +++++++++++ notebook/2D4T6X5AR/note.json | 265 +++++++++++ notebook/2D4WBY923/note.json | 309 ++++++++++++ notebook/2D6JT1W6P/note.json | 112 +++++ 12 files changed, 2284 insertions(+), 2166 deletions(-) delete mode 100644 notebook/2BYEZ5EVK/note.json delete mode 100644 notebook/2C35YU814/note.json create mode 100644 notebook/2D23Y84Q3/note.json create mode 100644 notebook/2D25QSMZD/note.json create mode 100644 notebook/2D3979PMW/note.json create mode 100644 notebook/2D4T6X5AR/note.json create mode 100644 notebook/2D4WBY923/note.json create mode 100644 notebook/2D6JT1W6P/note.json diff --git a/notebook/2A94M5J1Z/note.json b/notebook/2A94M5J1Z/note.json index 6e8e06fe296..c5ae02d9ebd 100644 --- a/notebook/2A94M5J1Z/note.json +++ b/notebook/2A94M5J1Z/note.json @@ -3,7 +3,6 @@ { "text": "%md\n## Welcome to Zeppelin.\n##### This is a live tutorial, you can run the code yourself. (Shift-Enter to Run)", "user": "anonymous", - "dateUpdated": "Dec 17, 2016 3:32:15 PM", "config": { "colWidth": 12.0, "editorHide": true, @@ -44,17 +43,13 @@ "apps": [], "jobName": "paragraph_1423836981412_-1007008116", "id": "20150213-231621_168813393", - "dateCreated": "Feb 13, 2015 11:16:21 PM", - "dateStarted": "Dec 17, 2016 3:32:15 PM", - "dateFinished": "Dec 17, 2016 3:32:18 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Load data into table", - "text": "import org.apache.commons.io.IOUtils\nimport java.net.URL\nimport java.nio.charset.Charset\n\n// Zeppelin creates and injects sc (SparkContext) and sqlContext (HiveContext or SqlContext)\n// So you don\u0027t need create them manually\n\n// load bank data\nval bankText \u003d sc.parallelize(\n IOUtils.toString(\n new URL(\"https://s3.amazonaws.com/apache-zeppelin/tutorial/bank/bank.csv\"),\n Charset.forName(\"utf8\")).split(\"\\n\"))\n\ncase class Bank(age: Integer, job: String, marital: String, education: String, balance: Integer)\n\nval bank \u003d bankText.map(s \u003d\u003e s.split(\";\")).filter(s \u003d\u003e s(0) !\u003d \"\\\"age\\\"\").map(\n s \u003d\u003e Bank(s(0).toInt, \n s(1).replaceAll(\"\\\"\", \"\"),\n s(2).replaceAll(\"\\\"\", \"\"),\n s(3).replaceAll(\"\\\"\", \"\"),\n s(5).replaceAll(\"\\\"\", \"\").toInt\n )\n).toDF()\nbank.registerTempTable(\"bank\")", + "text": "%spark\nimport org.apache.commons.io.IOUtils\nimport java.net.URL\nimport java.nio.charset.Charset\n\n// Zeppelin creates and injects sc (SparkContext) and sqlContext (HiveContext or SqlContext)\n// So you don\u0027t need create them manually\n\n// load bank data\nval bankText \u003d sc.parallelize(\n IOUtils.toString(\n new URL(\"http://s3.amazonaws.com/apache-zeppelin/tutorial/bank/bank.csv\"),\n Charset.forName(\"utf8\")).split(\"\\n\"))\n\ncase class Bank(age: Integer, job: String, marital: String, education: String, balance: Integer)\n\nval bank \u003d bankText.map(s \u003d\u003e s.split(\";\")).filter(s \u003d\u003e s(0) !\u003d \"\\\"age\\\"\").map(\n s \u003d\u003e Bank(s(0).toInt, \n s(1).replaceAll(\"\\\"\", \"\"),\n s(2).replaceAll(\"\\\"\", \"\"),\n s(3).replaceAll(\"\\\"\", \"\"),\n s(5).replaceAll(\"\\\"\", \"\").toInt\n )\n).toDF()\nbank.registerTempTable(\"bank\")", "user": "anonymous", - "dateUpdated": "Dec 17, 2016 3:30:09 PM", "config": { "colWidth": 12.0, "title": true, @@ -90,16 +85,12 @@ "apps": [], "jobName": "paragraph_1423500779206_-1502780787", "id": "20150210-015259_1403135953", - "dateCreated": "Feb 10, 2015 1:52:59 AM", - "dateStarted": "Dec 17, 2016 3:30:09 PM", - "dateFinished": "Dec 17, 2016 3:30:58 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { - "text": "%sql \nselect age, count(1) value\nfrom bank \nwhere age \u003c 30 \ngroup by age \norder by age", + "text": "%spark.sql \nselect age, count(1) value\nfrom bank \nwhere age \u003c 30 \ngroup by age \norder by age", "user": "anonymous", - "dateUpdated": "Mar 17, 2017 12:18:02 PM", "config": { "colWidth": 4.0, "results": [ @@ -135,16 +126,12 @@ "apps": [], "jobName": "paragraph_1423500782552_-1439281894", "id": "20150210-015302_1492795503", - "dateCreated": "Feb 10, 2015 1:53:02 AM", - "dateStarted": "Dec 17, 2016 3:30:13 PM", - "dateFinished": "Dec 17, 2016 3:31:04 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { - "text": "%sql \nselect age, count(1) value \nfrom bank \nwhere age \u003c ${maxAge\u003d30} \ngroup by age \norder by age", + "text": "%spark.sql \nselect age, count(1) value \nfrom bank \nwhere age \u003c ${maxAge\u003d30} \ngroup by age \norder by age", "user": "anonymous", - "dateUpdated": "Mar 17, 2017 12:17:39 PM", "config": { "colWidth": 4.0, "results": [ @@ -188,16 +175,12 @@ "apps": [], "jobName": "paragraph_1423720444030_-1424110477", "id": "20150212-145404_867439529", - "dateCreated": "Feb 12, 2015 2:54:04 PM", - "dateStarted": "Dec 17, 2016 3:30:58 PM", - "dateFinished": "Dec 17, 2016 3:31:07 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { - "text": "%sql \nselect age, count(1) value \nfrom bank \nwhere marital\u003d\"${marital\u003dsingle,single|divorced|married}\" \ngroup by age \norder by age", + "text": "%spark.sql \nselect age, count(1) value \nfrom bank \nwhere marital\u003d\"${marital\u003dsingle,single|divorced|married}\" \ngroup by age \norder by age", "user": "anonymous", - "dateUpdated": "Mar 17, 2017 12:18:18 PM", "config": { "colWidth": 4.0, "results": [ @@ -215,7 +198,8 @@ "language": "sql", "editOnDblClick": false }, - "editorMode": "ace/mode/sql" + "editorMode": "ace/mode/sql", + "runOnSelectionChange": true }, "settings": { "params": { @@ -252,16 +236,12 @@ "apps": [], "jobName": "paragraph_1423836262027_-210588283", "id": "20150213-230422_1600658137", - "dateCreated": "Feb 13, 2015 11:04:22 PM", - "dateStarted": "Dec 17, 2016 3:31:05 PM", - "dateFinished": "Dec 17, 2016 3:31:09 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%md\n## Congratulations, it\u0027s done.\n##### You can create your own notebook in \u0027Notebook\u0027 menu. Good luck!", "user": "anonymous", - "dateUpdated": "Dec 17, 2016 3:30:24 PM", "config": { "colWidth": 12.0, "editorHide": true, @@ -298,16 +278,12 @@ "apps": [], "jobName": "paragraph_1423836268492_216498320", "id": "20150213-230428_1231780373", - "dateCreated": "Feb 13, 2015 11:04:28 PM", - "dateStarted": "Dec 17, 2016 3:30:24 PM", - "dateFinished": "Dec 17, 2016 3:30:29 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%md\n\nAbout bank data\n\n```\nCitation Request:\n This dataset is public available for research. The details are described in [Moro et al., 2011]. \n Please include this citation if you plan to use this database:\n\n [Moro et al., 2011] S. Moro, R. Laureano and P. Cortez. Using Data Mining for Bank Direct Marketing: An Application of the CRISP-DM Methodology. \n In P. Novais et al. (Eds.), Proceedings of the European Simulation and Modelling Conference - ESM\u00272011, pp. 117-121, Guimarães, Portugal, October, 2011. EUROSIS.\n\n Available at: [pdf] http://hdl.handle.net/1822/14838\n [bib] http://www3.dsi.uminho.pt/pcortez/bib/2011-esm-1.txt\n```", "user": "anonymous", - "dateUpdated": "Dec 17, 2016 3:30:34 PM", "config": { "colWidth": 12.0, "editorHide": true, @@ -344,31 +320,13 @@ "apps": [], "jobName": "paragraph_1427420818407_872443482", "id": "20150326-214658_12335843", - "dateCreated": "Mar 26, 2015 9:46:58 PM", - "dateStarted": "Dec 17, 2016 3:30:34 PM", - "dateFinished": "Dec 17, 2016 3:30:34 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 - }, - { - "config": {}, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1435955447812_-158639899", - "id": "20150703-133047_853701097", - "dateCreated": "Jul 3, 2015 1:30:47 PM", - "status": "READY", - "progressUpdateIntervalMs": 500 } ], - "name": "Zeppelin Tutorial/Basic Features (Spark)", + "name": "Zeppelin Tutorial/Spark • Basic Features (Spark)", "id": "2A94M5J1Z", - "angularObjects": { - "2C73DY9P9:shared_process": [] - }, + "angularObjects": {}, "config": { "looknfeel": "default" }, diff --git a/notebook/2BWJFTXKJ/note.json b/notebook/2BWJFTXKJ/note.json index 7196dd0bd85..61366aa4559 100644 --- a/notebook/2BWJFTXKJ/note.json +++ b/notebook/2BWJFTXKJ/note.json @@ -2,9 +2,8 @@ "paragraphs": [ { "title": "Hello R", - "text": "%r\nfoo \u003c- TRUE\nprint(foo)\nbare \u003c- c(1, 2.5, 4)\nprint(bare)\ndouble \u003c- 15.0\nprint(double)", + "text": "%spark.r\nfoo \u003c- TRUE\nprint(foo)\nbare \u003c- c(1, 2.5, 4)\nprint(bare)\ndouble \u003c- 15.0\nprint(double)", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:45:40 AM", "config": { "colWidth": 12.0, "editorMode": "ace/mode/r", @@ -44,17 +43,13 @@ "apps": [], "jobName": "paragraph_1429882946244_-381648689", "id": "20150424-154226_261270952", - "dateCreated": "Apr 24, 2015 3:42:26 AM", - "dateStarted": "Jan 29, 2017 2:42:27 AM", - "dateFinished": "Jan 29, 2017 2:42:40 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Load R Librairies", - "text": "%r\nlibrary(data.table)\ndt \u003c- data.table(1:3)\nprint(dt)\nfor (i in 1:5) {\n print(i*2)\n}\nprint(1:50)", + "text": "%spark.r\nlibrary(data.table)\ndt \u003c- data.table(1:3)\nprint(dt)\nfor (i in 1:5) {\n print(i*2)\n}\nprint(1:50)", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:45:44 AM", "config": { "colWidth": 12.0, "editorMode": "ace/mode/r", @@ -94,16 +89,12 @@ "apps": [], "jobName": "paragraph_1429882976611_1352445253", "id": "20150424-154256_645296307", - "dateCreated": "Apr 24, 2015 3:42:56 AM", - "dateStarted": "Jan 29, 2017 2:42:43 AM", - "dateFinished": "Jan 29, 2017 2:42:43 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%md\n\n## Zeppelin SparkR Tutorial\n\n##### This is a live tutorial, you can run the code yourself. (Shift-Enter to Run)", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 3:17:08 AM", "config": { "colWidth": 12.0, "enabled": true, @@ -132,17 +123,13 @@ "apps": [], "jobName": "paragraph_1485571833509_-1037808822", "id": "20170128-115033_693473992", - "dateCreated": "Jan 28, 2017 11:50:33 AM", - "dateStarted": "Jan 29, 2017 3:17:08 AM", - "dateFinished": "Jan 29, 2017 3:17:08 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Load Iris Dataset", - "text": "%r\ncolnames(iris)\niris$Petal.Length\niris$Sepal.Length", + "text": "%spark.r\ncolnames(iris)\niris$Petal.Length\niris$Sepal.Length", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:45:47 AM", "config": { "colWidth": 12.0, "enabled": true, @@ -182,17 +169,13 @@ "apps": [], "jobName": "paragraph_1455138077044_161383897", "id": "20160210-220117_115873183", - "dateCreated": "Feb 10, 2016 10:01:17 AM", - "dateStarted": "Jan 29, 2017 2:42:46 AM", - "dateFinished": "Jan 29, 2017 2:42:46 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "TABLE Display", - "text": "%r print(\"%table name\\tsize\\nsmall\\t100\\nlarge\\t1000\")", + "text": "%spark.r print(\"%table name\\tsize\\nsmall\\t100\\nlarge\\t1000\")", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 3:20:33 AM", "config": { "colWidth": 6.0, "enabled": true, @@ -202,7 +185,7 @@ { "graph": { "mode": "table", - "height": 408.6458435058594, + "height": 362.641, "optionOpen": false, "keys": [ { @@ -256,17 +239,13 @@ "apps": [], "jobName": "paragraph_1456216582752_6855525", "id": "20160223-093622_330111284", - "dateCreated": "Feb 23, 2016 9:36:22 AM", - "dateStarted": "Jan 29, 2017 2:42:50 AM", - "dateFinished": "Jan 29, 2017 2:42:50 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "HTML Display", - "text": "%r \n\nprint(\"%html \u003ch3\u003eHello HTML\u003c/h3\u003e\")\nprint(\"\u003cfont color\u003d\u0027blue\u0027\u003e\u003cspan class\u003d\u0027fa fa-bars\u0027\u003e Easy...\u003c/font\u003e\u003c/span\u003e\")\nfor (i in 1:10) {\n print(paste0(\"\u003ch4\u003e\", i, \" * 2 \u003d \", i*2, \"\u003c/h4\u003e\"))\n}", + "text": "%spark.r \n\nprint(\"%html \u003ch3\u003eHello HTML\u003c/h3\u003e\")\nprint(\"\u003cfont color\u003d\u0027blue\u0027\u003e\u003cspan class\u003d\u0027fa fa-bars\u0027\u003e Easy...\u003c/font\u003e\u003c/span\u003e\")\nfor (i in 1:10) {\n print(paste0(\"\u003ch4\u003e\", i, \" * 2 \u003d \", i*2, \"\u003c/h4\u003e\"))\n}", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:52:20 AM", "config": { "colWidth": 6.0, "enabled": true, @@ -306,9 +285,40 @@ "apps": [], "jobName": "paragraph_1456140102445_51059930", "id": "20160222-122142_1323614681", - "dateCreated": "Feb 22, 2016 12:21:42 PM", - "dateStarted": "Jan 29, 2017 2:42:48 AM", - "dateFinished": "Jan 29, 2017 2:42:48 AM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n---", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": false, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003chr/\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512671835592_1756326537", + "id": "20171207-183715_1844685698", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, @@ -316,7 +326,6 @@ "title": "Write Scala To R", "text": "%spark\nval s \u003d \"Hello R from Scala\"\nz.put(\"s\", s)\nval b \u003d new Integer(42)\nz.put(\"b\", b)\nval a: Array[Double] \u003d Array[Double](30.1, 20.0)\nz.put(\"a\", a)\nval m \u003d Array(Array(1, 4), Array(8, 16))\nz.put(\"m\", m)\nval v \u003d Vector(1, 2, 3, 4)\nz.put(\"v\", v)", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:52:50 AM", "config": { "colWidth": 3.0, "editorMode": "ace/mode/scala", @@ -358,17 +367,13 @@ "apps": [], "jobName": "paragraph_1429862281402_-79250404", "id": "20150424-095801_125725189", - "dateCreated": "Apr 24, 2015 9:58:01 AM", - "dateStarted": "Jan 29, 2017 2:43:19 AM", - "dateFinished": "Jan 29, 2017 2:43:21 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Read from R the Scala Variables", - "text": "%r\nz.get(\"s\")\nz.get(\"b\")\nprint(unlist(z.get(\"a\")))\nprint(unlist(z.get(\"m\")))\nz.get(\"v\")", + "text": "%spark.r\nz.get(\"s\")\nz.get(\"b\")\nprint(unlist(z.get(\"a\")))\nprint(unlist(z.get(\"m\")))\nz.get(\"v\")", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:52:53 AM", "config": { "colWidth": 3.0, "editorMode": "ace/mode/r", @@ -402,24 +407,20 @@ "msg": [ { "type": "TEXT", - "data": "\n[1] “Hello R from Scala”\n[1] 42\n[1] 30.1 20.0\n[1] 1 4 8 16\nJava ref type scala.collection.immutable.Vector id 97 \n\n\n\n" + "data": "\n[1] “Hello R from Scala”\n[1] 42\n[1] 30.1 20.0\n[1] 1 4 8 16\nJava ref type scala.collection.immutable.Vector id 28 \n\n\n\n" } ] }, "apps": [], "jobName": "paragraph_1438930802740_-1781296534", "id": "20150807-090002_1514685133", - "dateCreated": "Aug 7, 2015 9:00:02 AM", - "dateStarted": "Jan 29, 2017 2:51:36 AM", - "dateFinished": "Jan 29, 2017 2:51:36 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Write R to Scala", - "text": "%r\ns \u003c- \"Hello Scala from R\"\nprint(s)\nz.put(\"rs\", s)\nb \u003c- TRUE\nprint(b)\nz.put(\"rb\", b)\nd \u003c- 15.0\nprint(d)\nz.put(\"rd\", d)\nm \u003c- c(2.4, 2.5, 4)\nprint(m)\nz.put(\"rm\", m)", + "text": "%spark.r\ns \u003c- \"Hello Scala from R\"\nprint(s)\nz.put(\"rs\", s)\nb \u003c- TRUE\nprint(b)\nz.put(\"rb\", b)\nd \u003c- 15.0\nprint(d)\nz.put(\"rd\", d)\nm \u003c- c(2.4, 2.5, 4)\nprint(m)\nz.put(\"rm\", m)", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:52:55 AM", "config": { "colWidth": 3.0, "enabled": true, @@ -460,17 +461,13 @@ "apps": [], "jobName": "paragraph_1455137934157_-1786381957", "id": "20160210-215854_620520530", - "dateCreated": "Feb 10, 2016 9:58:54 AM", - "dateStarted": "Jan 29, 2017 2:43:28 AM", - "dateFinished": "Jan 29, 2017 2:43:28 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Read from Scala the R Variables", - "text": "%spark\nprintln(\"rs \u003d \"+ z.get(\"rs\"))\nprintln(\"rb \u003d \"+ z.get(\"rb\"))\nprintln(\"rd \u003d \"+ z.get(\"rd\"))\nprintln(\"rm \u003d \"+ z.get(\"rm\"))\n// println(z.get(\"rm\").getClass)\n// println(\"rm \u003d \"+ z.get(\"rm\").asInstanceOf[Array[Double]].toSeq)", + "text": "%spark\nprintln(\"rs \u003d \"+ z.get(\"rs\"))\nprintln(\"rb \u003d \"+ z.get(\"rb\"))\nprintln(\"rd \u003d \"+ z.get(\"rd\"))\n// println(\"rm \u003d \"+ z.get(\"rm\"))\n// println(z.get(\"rm\").getClass)\nprintln(\"rm \u003d \"+ z.get(\"rm\").asInstanceOf[Array[Double]].toSeq)", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:52:58 AM", "config": { "colWidth": 3.0, "enabled": true, @@ -505,24 +502,54 @@ "msg": [ { "type": "TEXT", - "data": "rs \u003d Hello Scala from R\nrb \u003d true\nrd \u003d 15.0\nrm \u003d 2.4\n" + "data": "rs \u003d Hello Scala from R\nrb \u003d true\nrd \u003d 15.0\nrm \u003d WrappedArray(2.4, 2.5, 4.0)\n" } ] }, "apps": [], "jobName": "paragraph_1455138066039_1048230112", "id": "20160210-220106_141884849", - "dateCreated": "Feb 10, 2016 10:01:06 AM", - "dateStarted": "Jan 29, 2017 2:43:35 AM", - "dateFinished": "Jan 29, 2017 2:43:35 AM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n---", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": false, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003chr/\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512671848888_-627947073", + "id": "20171207-183728_963393674", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Create a Spark Dataframe", - "text": "%spark\nimport org.apache.commons.io.IOUtils\nimport java.net.URL\nimport java.nio.charset.Charset\n\nval bankText \u003d sc.parallelize(\n IOUtils.toString(\n new URL(\"https://s3.amazonaws.com/apache-zeppelin/tutorial/bank/bank.csv\"),\n Charset.forName(\"utf8\")).split(\"\\n\"))\n\ncase class Bank(age: Integer, job: String, marital: String, education: String, balance: Integer)\n\nval bank \u003d bankText.map(s \u003d\u003e s.split(\";\")).filter(s \u003d\u003e s(0) !\u003d \"\\\"age\\\"\").map(\n s \u003d\u003e Bank(s(0).toInt, \n s(1).replaceAll(\"\\\"\", \"\"),\n s(2).replaceAll(\"\\\"\", \"\"),\n s(3).replaceAll(\"\\\"\", \"\"),\n s(5).replaceAll(\"\\\"\", \"\").toInt\n )\n).toDF()\nbank.registerTempTable(\"bank\")", + "text": "%spark\nimport org.apache.commons.io.IOUtils\nimport java.net.URL\nimport java.nio.charset.Charset\n\nval bankText \u003d sc.parallelize(\n IOUtils.toString(\n new URL(\"http://s3.amazonaws.com/apache-zeppelin/tutorial/bank/bank.csv\"),\n Charset.forName(\"utf8\")).split(\"\\n\"))\n\ncase class Bank(age: Integer, job: String, marital: String, education: String, balance: Integer)\n\nval bank \u003d bankText.map(s \u003d\u003e s.split(\";\")).filter(s \u003d\u003e s(0) !\u003d \"\\\"age\\\"\").map(\n s \u003d\u003e Bank(s(0).toInt, \n s(1).replaceAll(\"\\\"\", \"\"),\n s(2).replaceAll(\"\\\"\", \"\"),\n s(3).replaceAll(\"\\\"\", \"\"),\n s(5).replaceAll(\"\\\"\", \"\").toInt\n )\n).toDF()\nbank.registerTempTable(\"bank\")", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:46:25 AM", "config": { "colWidth": 6.0, "enabled": true, @@ -558,24 +585,20 @@ "msg": [ { "type": "TEXT", - "data": "\nimport org.apache.commons.io.IOUtils\n\nimport java.net.URL\n\nimport java.nio.charset.Charset\n\nbankText: org.apache.spark.rdd.RDD[String] \u003d ParallelCollectionRDD[0] at parallelize at \u003cconsole\u003e:32\n\ndefined class Bank\n\nbank: org.apache.spark.sql.DataFrame \u003d [age: int, job: string, marital: string, education: string, balance: int]\n" + "data": "\nimport org.apache.commons.io.IOUtils\n\nimport java.net.URL\n\nimport java.nio.charset.Charset\n\nbankText: org.apache.spark.rdd.RDD[String] \u003d ParallelCollectionRDD[0] at parallelize at \u003cconsole\u003e:34\n\ndefined class Bank\n\nbank: org.apache.spark.sql.DataFrame \u003d [age: int, job: string ... 3 more fields]\n\nwarning: there was one deprecation warning; re-run with -deprecation for details\n" } ] }, "apps": [], "jobName": "paragraph_1455142039343_-233762796", "id": "20160210-230719_2111095838", - "dateCreated": "Feb 10, 2016 11:07:19 AM", - "dateStarted": "Jan 29, 2017 2:43:40 AM", - "dateFinished": "Jan 29, 2017 2:43:45 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Read the Spark Dataframe from R", - "text": "%r\n\ndf \u003c- sql(sqlContext, \"select count(*) from bank\")\nprintSchema(df)\nSparkR::head(df)", + "text": "%spark.r\n\ndf \u003c- sql(sqlContext, \"select count(*) from bank\")\nprintSchema(df)\nSparkR::head(df)", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:43:46 AM", "config": { "colWidth": 6.0, "enabled": true, @@ -609,24 +632,20 @@ "msg": [ { "type": "TEXT", - "data": "\nroot\n |– _c0: long (nullable \u003d false)\n _c0\n1 4521\n\n\n\n" + "data": "\nroot\n |– count(1): long (nullable \u003d false)\n count(1)\n1 4521\n\n\n\n" } ] }, "apps": [], "jobName": "paragraph_1455142043062_1598026718", "id": "20160210-230723_1811469598", - "dateCreated": "Feb 10, 2016 11:07:23 AM", - "dateStarted": "Jan 29, 2017 2:43:47 AM", - "dateFinished": "Jan 29, 2017 2:43:49 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Query with Spark Dataframe with SQL", - "text": "%sql select count(*) as count from bank", + "text": "%spark.sql select count(*) as count from bank", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:47:05 AM", "config": { "colWidth": 6.0, "enabled": true, @@ -667,17 +686,13 @@ "apps": [], "jobName": "paragraph_1455142050697_-1353382095", "id": "20160210-230730_1259663883", - "dateCreated": "Feb 10, 2016 11:07:30 AM", - "dateStarted": "Jan 29, 2017 2:43:55 AM", - "dateFinished": "Jan 29, 2017 2:43:55 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Create a R Dataframe", - "text": "%r \n\nlocalNames \u003c- data.frame(name\u003dc(\"John\", \"Smith\", \"Sarah\"), budget\u003dc(19, 53, 18))\nnames \u003c- createDataFrame(sqlContext, localNames)\nprintSchema(names)\nregisterTempTable(names, \"names\")\n\n# SparkR::head(names)", + "text": "%spark.r \n\nlocalNames \u003c- data.frame(name\u003dc(\"John\", \"Smith\", \"Sarah\"), budget\u003dc(19, 53, 18))\nnames \u003c- createDataFrame(sqlContext, localNames)\nprintSchema(names)\nregisterTempTable(names, \"names\")\n\n# SparkR::head(names)", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 3:19:59 AM", "config": { "colWidth": 12.0, "enabled": true, @@ -718,17 +733,13 @@ "apps": [], "jobName": "paragraph_1455142112413_519883679", "id": "20160210-230832_1847721959", - "dateCreated": "Feb 10, 2016 11:08:32 AM", - "dateStarted": "Jan 29, 2017 2:43:58 AM", - "dateFinished": "Jan 29, 2017 2:43:58 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Read the R Dataframe from Spark", - "text": "sqlc.sql(\"select * from names\").head", + "text": "%spark\nsqlContext.sql(\"select * from names\").head", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:47:18 AM", "config": { "colWidth": 12.0, "enabled": true, @@ -749,7 +760,7 @@ ], "editorSetting": { "language": "scala", - "editOnDblClick": true + "editOnDblClick": false }, "editorHide": false, "tableHide": false @@ -763,24 +774,20 @@ "msg": [ { "type": "TEXT", - "data": "\nres11: org.apache.spark.sql.Row \u003d [John,19.0]\n" + "data": "\nres22: org.apache.spark.sql.Row \u003d [John,19.0]\n" } ] }, "apps": [], "jobName": "paragraph_1455188357108_95477841", "id": "20160211-115917_445850505", - "dateCreated": "Feb 11, 2016 11:59:17 AM", - "dateStarted": "Jan 29, 2017 2:44:08 AM", - "dateFinished": "Jan 29, 2017 2:44:09 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Query the R Datafame with SQL", - "text": "%sql select * from names\n", + "text": "%spark.sql select * from names\n", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:47:29 AM", "config": { "colWidth": 12.0, "enabled": true, @@ -843,17 +850,13 @@ "apps": [], "jobName": "paragraph_1455142115582_-1840950897", "id": "20160210-230835_19876971", - "dateCreated": "Feb 10, 2016 11:08:35 AM", - "dateStarted": "Jan 29, 2017 2:44:11 AM", - "dateFinished": "Jan 29, 2017 2:44:11 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "GoogleVis: Bar Chart", - "text": "%r\nlibrary(googleVis)\ndf\u003ddata.frame(country\u003dc(\"US\", \"GB\", \"BR\"), \n val1\u003dc(10,13,14), \n val2\u003dc(23,12,32))\nBar \u003c- gvisBarChart(df)\nprint(Bar, tag \u003d \u0027chart\u0027)\n", + "text": "%spark.r\n\n# Workaround for Spark issue with googleVis: SPARK-23780\ndetach(\"package:SparkR\")\nlibrary(googleVis)\nsuppressPackageStartupMessages(library(SparkR))\n\ndf\u003ddata.frame(country\u003dc(\"US\", \"GB\", \"BR\"), \n val1\u003dc(10,13,14), \n val2\u003dc(23,12,32))\nBar \u003c- gvisBarChart(df)\nprint(Bar, tag \u003d \u0027chart\u0027)\n", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 3:16:38 AM", "config": { "colWidth": 4.0, "enabled": true, @@ -884,24 +887,20 @@ "msg": [ { "type": "HTML", - "data": "\n\u003c!-- BarChart generated in R 3.3.2 by googleVis 0.6.2 package --\u003e\n\n\u003c!-- Sun Jan 29 03:11:34 2017 --\u003e\n\n\u003c!-- jsHeader --\u003e\n\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataBarChartID17e4878cf808c () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"US\",\n10,\n23\n],\n[\n\"GB\",\n13,\n12\n],\n[\n\"BR\",\n14,\n32\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027country\u0027);\ndata.addColumn(\u0027number\u0027,\u0027val1\u0027);\ndata.addColumn(\u0027number\u0027,\u0027val2\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartBarChartID17e4878cf808c() {\nvar data \u003d gvisDataBarChartID17e4878cf808c();\nvar options \u003d {};\noptions[\"allowHtml\"] \u003d true;\n\n var chart \u003d new google.visualization.BarChart(\n document.getElementById(\u0027BarChartID17e4878cf808c\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"corechart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartBarChartID17e4878cf808c);\n})();\nfunction displayChartBarChartID17e4878cf808c() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\n\u003c!-- jsChart --\u003e \n\n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartBarChartID17e4878cf808c\"\u003e\u003c/script\u003e\n \n\n\u003c!-- divChart --\u003e\n\n\u003cdiv id\u003d\"BarChartID17e4878cf808c\" style\u003d\"width: 500; height: automatic;\"\u003e\n\u003c/div\u003e\n\n\n\n" + "data": "\n\u003c!-- BarChart generated in R 3.4.2 by googleVis 0.6.2 package --\u003e\n\n\u003c!-- Thu Dec 7 18:40:17 2017 --\u003e\n\n\u003c!-- jsHeader --\u003e\n\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataBarChartID7e7aeb6bf91 () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"US\",\n10,\n23\n],\n[\n\"GB\",\n13,\n12\n],\n[\n\"BR\",\n14,\n32\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027country\u0027);\ndata.addColumn(\u0027number\u0027,\u0027val1\u0027);\ndata.addColumn(\u0027number\u0027,\u0027val2\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartBarChartID7e7aeb6bf91() {\nvar data \u003d gvisDataBarChartID7e7aeb6bf91();\nvar options \u003d {};\noptions[\"allowHtml\"] \u003d true;\n\n var chart \u003d new google.visualization.BarChart(\n document.getElementById(\u0027BarChartID7e7aeb6bf91\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"corechart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartBarChartID7e7aeb6bf91);\n})();\nfunction displayChartBarChartID7e7aeb6bf91() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\n\u003c!-- jsChart --\u003e \n\n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartBarChartID7e7aeb6bf91\"\u003e\u003c/script\u003e\n \n\n\u003c!-- divChart --\u003e\n\n\u003cdiv id\u003d\"BarChartID7e7aeb6bf91\" style\u003d\"width: 500; height: automatic;\"\u003e\n\u003c/div\u003e\n\n\n\n" } ] }, "apps": [], "jobName": "paragraph_1485626417184_-1153542135", "id": "20170129-030017_426747323", - "dateCreated": "Jan 29, 2017 3:00:17 AM", - "dateStarted": "Jan 29, 2017 3:11:33 AM", - "dateFinished": "Jan 29, 2017 3:11:34 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "GoogleVis: Candlestick Chart", - "text": "%r\nlibrary(googleVis)\n\nCandle \u003c- gvisCandlestickChart(OpenClose, \n options\u003dlist(legend\u003d\u0027none\u0027))\n\nprint(Candle, tag \u003d \u0027chart\u0027)", + "text": "%spark.r\n\n# Workaround for Spark issue with googleVis: SPARK-23780\ndetach(\"package:SparkR\")\nlibrary(googleVis)\nsuppressPackageStartupMessages(library(SparkR))\n\nCandle \u003c- gvisCandlestickChart(OpenClose, \n options\u003dlist(legend\u003d\u0027none\u0027))\n\nprint(Candle, tag \u003d \u0027chart\u0027)", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 3:16:35 AM", "config": { "colWidth": 4.0, "enabled": true, @@ -916,7 +915,7 @@ }, "editorSetting": { "language": "r", - "editOnDblClick": true + "editOnDblClick": false }, "editorMode": "ace/mode/r", "editorHide": false, @@ -932,24 +931,20 @@ "msg": [ { "type": "HTML", - "data": "\n\u003c!-- CandlestickChart generated in R 3.3.2 by googleVis 0.6.2 package --\u003e\n\n\u003c!-- Sun Jan 29 03:16:15 2017 --\u003e\n\n\u003c!-- jsHeader --\u003e\n\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataCandlestickChartID17e4862e18e15 () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"Mon\",\n20,\n28,\n38,\n45\n],\n[\n\"Tues\",\n31,\n38,\n55,\n66\n],\n[\n\"Wed\",\n50,\n55,\n77,\n80\n],\n[\n\"Thurs\",\n50,\n77,\n66,\n77\n],\n[\n\"Fri\",\n15,\n66,\n22,\n68\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027Weekday\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Low\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Open\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Close\u0027);\ndata.addColumn(\u0027number\u0027,\u0027High\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartCandlestickChartID17e4862e18e15() {\nvar data \u003d gvisDataCandlestickChartID17e4862e18e15();\nvar options \u003d {};\noptions[\"allowHtml\"] \u003d true;\noptions[\"legend\"] \u003d \"none\";\n\n var chart \u003d new google.visualization.CandlestickChart(\n document.getElementById(\u0027CandlestickChartID17e4862e18e15\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"corechart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartCandlestickChartID17e4862e18e15);\n})();\nfunction displayChartCandlestickChartID17e4862e18e15() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\n\u003c!-- jsChart --\u003e \n\n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartCandlestickChartID17e4862e18e15\"\u003e\u003c/script\u003e\n \n\n\u003c!-- divChart --\u003e\n\n\u003cdiv id\u003d\"CandlestickChartID17e4862e18e15\" style\u003d\"width: 500; height: automatic;\"\u003e\n\u003c/div\u003e\n\n\n\n" + "data": "\n\u003c!-- CandlestickChart generated in R 3.4.2 by googleVis 0.6.2 package --\u003e\n\n\u003c!-- Thu Dec 7 18:40:19 2017 --\u003e\n\n\u003c!-- jsHeader --\u003e\n\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataCandlestickChartID7e7a54aa1c95 () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"Mon\",\n20,\n28,\n38,\n45\n],\n[\n\"Tues\",\n31,\n38,\n55,\n66\n],\n[\n\"Wed\",\n50,\n55,\n77,\n80\n],\n[\n\"Thurs\",\n50,\n77,\n66,\n77\n],\n[\n\"Fri\",\n15,\n66,\n22,\n68\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027Weekday\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Low\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Open\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Close\u0027);\ndata.addColumn(\u0027number\u0027,\u0027High\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartCandlestickChartID7e7a54aa1c95() {\nvar data \u003d gvisDataCandlestickChartID7e7a54aa1c95();\nvar options \u003d {};\noptions[\"allowHtml\"] \u003d true;\noptions[\"legend\"] \u003d \"none\";\n\n var chart \u003d new google.visualization.CandlestickChart(\n document.getElementById(\u0027CandlestickChartID7e7a54aa1c95\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"corechart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartCandlestickChartID7e7a54aa1c95);\n})();\nfunction displayChartCandlestickChartID7e7a54aa1c95() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\n\u003c!-- jsChart --\u003e \n\n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartCandlestickChartID7e7a54aa1c95\"\u003e\u003c/script\u003e\n \n\n\u003c!-- divChart --\u003e\n\n\u003cdiv id\u003d\"CandlestickChartID7e7a54aa1c95\" style\u003d\"width: 500; height: automatic;\"\u003e\n\u003c/div\u003e\n\n\n\n" } ] }, "apps": [], "jobName": "paragraph_1485627113560_-130863711", "id": "20170129-031153_758721410", - "dateCreated": "Jan 29, 2017 3:11:53 AM", - "dateStarted": "Jan 29, 2017 3:16:15 AM", - "dateFinished": "Jan 29, 2017 3:16:15 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "GoogleVis: Line chart", - "text": "%r\nlibrary(googleVis)\ndf\u003ddata.frame(country\u003dc(\"US\", \"GB\", \"BR\"), \n val1\u003dc(10,13,14), \n val2\u003dc(23,12,32))\n\nLine \u003c- gvisLineChart(df)\n\nprint(Line, tag \u003d \u0027chart\u0027)\n", + "text": "%spark.r\n\n# Workaround for Spark issue with googleVis: SPARK-23780\ndetach(\"package:SparkR\")\nlibrary(googleVis)\nsuppressPackageStartupMessages(library(SparkR))\n\ndf\u003ddata.frame(country\u003dc(\"US\", \"GB\", \"BR\"), \n val1\u003dc(10,13,14), \n val2\u003dc(23,12,32))\n\nLine \u003c- gvisLineChart(df)\n\nprint(Line, tag \u003d \u0027chart\u0027)\n", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 3:17:31 AM", "config": { "colWidth": 4.0, "enabled": true, @@ -965,7 +960,7 @@ ], "editorSetting": { "language": "r", - "editOnDblClick": true + "editOnDblClick": false }, "editorHide": false, "tableHide": false, @@ -980,23 +975,53 @@ "msg": [ { "type": "HTML", - "data": "\n\u003c!-- LineChart generated in R 3.3.2 by googleVis 0.6.2 package --\u003e\n\n\u003c!-- Sun Jan 29 03:17:08 2017 --\u003e\n\n\u003c!-- jsHeader --\u003e\n\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataLineChartID17e4818619a5d () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"US\",\n10,\n23\n],\n[\n\"GB\",\n13,\n12\n],\n[\n\"BR\",\n14,\n32\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027country\u0027);\ndata.addColumn(\u0027number\u0027,\u0027val1\u0027);\ndata.addColumn(\u0027number\u0027,\u0027val2\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartLineChartID17e4818619a5d() {\nvar data \u003d gvisDataLineChartID17e4818619a5d();\nvar options \u003d {};\noptions[\"allowHtml\"] \u003d true;\n\n var chart \u003d new google.visualization.LineChart(\n document.getElementById(\u0027LineChartID17e4818619a5d\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"corechart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartLineChartID17e4818619a5d);\n})();\nfunction displayChartLineChartID17e4818619a5d() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\n\u003c!-- jsChart --\u003e \n\n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartLineChartID17e4818619a5d\"\u003e\u003c/script\u003e\n \n\n\u003c!-- divChart --\u003e\n\n\u003cdiv id\u003d\"LineChartID17e4818619a5d\" style\u003d\"width: 500; height: automatic;\"\u003e\n\u003c/div\u003e\n\n\n\n" + "data": "\n\u003c!-- LineChart generated in R 3.4.2 by googleVis 0.6.2 package --\u003e\n\n\u003c!-- Thu Dec 7 18:40:21 2017 --\u003e\n\n\u003c!-- jsHeader --\u003e\n\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataLineChartID7e7a6c769deb () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"US\",\n10,\n23\n],\n[\n\"GB\",\n13,\n12\n],\n[\n\"BR\",\n14,\n32\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027country\u0027);\ndata.addColumn(\u0027number\u0027,\u0027val1\u0027);\ndata.addColumn(\u0027number\u0027,\u0027val2\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartLineChartID7e7a6c769deb() {\nvar data \u003d gvisDataLineChartID7e7a6c769deb();\nvar options \u003d {};\noptions[\"allowHtml\"] \u003d true;\n\n var chart \u003d new google.visualization.LineChart(\n document.getElementById(\u0027LineChartID7e7a6c769deb\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"corechart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartLineChartID7e7a6c769deb);\n})();\nfunction displayChartLineChartID7e7a6c769deb() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\n\u003c!-- jsChart --\u003e \n\n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartLineChartID7e7a6c769deb\"\u003e\u003c/script\u003e\n \n\n\u003c!-- divChart --\u003e\n\n\u003cdiv id\u003d\"LineChartID7e7a6c769deb\" style\u003d\"width: 500; height: automatic;\"\u003e\n\u003c/div\u003e\n\n\n\n" } ] }, "apps": [], "jobName": "paragraph_1455138857313_92355963", "id": "20160210-221417_1400405266", - "dateCreated": "Feb 10, 2016 10:14:17 AM", - "dateStarted": "Jan 29, 2017 3:17:08 AM", - "dateFinished": "Jan 29, 2017 3:17:08 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { - "text": "%r pairs(iris)", + "text": "%md\n---", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": false, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003chr/\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512671861144_2103400693", + "id": "20171207-183741_594205361", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%spark.r pairs(iris)", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:58:27 AM", "config": { "colWidth": 4.0, "enabled": true, @@ -1028,23 +1053,19 @@ "msg": [ { "type": "HTML", - "data": "\u003cp\u003e\u003cimg src\u003d\"data:image/png;base64,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Lne5S3OZy1wmBd9LX/rSqT3X5z73ueYpT3lK82d/9mfNJS5xieYBD3hA79pPf/rTc0L6/Oc/3xxzzDHNXnvt1fttGhsHH3xwc5GLXKQ5/PDDe5d75zvf2ZxwwgmpVNzvfvdrbnzjG/d+qxury4FhfenqV79644Oe/exnN2c5y1mm9qCvfe1rm5e97GXNe97znl2uecc73rG5wAUukPse8pCHTK1vE9z77rtvc41rXKP50pe+1Bx//PG9+771rW/NcXSxi10sn3efffbp/VY3tg8H9tg+j1qftMuBy13ucs3d7na35gMf+EDzlre8pSfgCb3b3e52zf7779/c+973TqFLIE+DTKYveMELGtb0Ax/4wF0E/Ic+9KHGpPTxj388J6rHPOYx07hlXuMf/uEfGopLP7385S9PAc/a2W+//aqA72fQiv6/UV9iXV/1qldtKHMU2DOd6UxTe0L3/Ld/+7dmzz333O2a7lMUzP/3//7fbr9PusPzHHnkkTl2CXr3v+AFL5iXo0xT3C960Ys2F7rQhSa9RT1vxTlQXfQr/gInbT7h/stf/rJh7dz1rnftXYZrvkwIrA6TxrTo8pe/fHoFbnnLW6YSUa7LbX+2s50t/zVBTfOe//Ef/9GcfPLJqbCU+5VvQv+sZz1rTsp4UWn1OTCsL5122mnpzqbw6YPTdF5e5zrXaQ477LDdlAYhIOECivPNbnazVJinxeVLXepSKdw/8pGPNJ67CHfX1+/f//73N094whOaF77whdO6Zb3OinGgWvAr9sKm1VzuvXve857NIx/5yOayl71s77J/+Id/mJOFHYTeec5znt5vW91guXPNf+Yzn2lMiHe/+91zQiTcS2zSRHXuc597q7fqne/5WFXHHnts853vfCeVmaLAnPnMv9dvz372s/fOqRury4Fhfela17pW86Y3vanx3sXhWbklNDWrJ/7jP/7j5tRTT81QAE/Y3//936dnbFr3e9/73pcCvOued+2jjz467/l///d/zU1vetPm/ve//7RuWa+zQhz4/Qy3Qo2uTd0aB1gud77znVPAmoC4+mj8X/3qV5u/+Zu/aV73utc1rJ2vfe1rPWt+a3f87dni+a961auab37zm82f/umfpnD/8Ic/nBMuS/pf/uVfmuOOO67ZsWPHNG6X13j84x/fHHrooc2NbnSjnMzPe97zNp/+9KfT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iRdffHE0U0v817/+1e5RO/nkk+Pdd9+d286PPvoo9u3bN7cMJz///HPHwLIixXvvvddx51ildO6550YTpqLFZc6wpp2pn0aPHp3tn3322b5PP4wfPz47vtVWW2X7tJ1rp5lmmjhkyBDfX3bZZePuu+/u+8a4ozF937f8zdGSPvj+HXfcEYcNG+b7JmFHSwTh+++880487bTT4k477eSPduWVV0Zjut7evfbay683waTkY1900UVZe9KzlbM1wSMaQ4omwJa8R0cqcM0118QLL7yw5COZidP7yQS4mMYHuPJuJnx33XXXbN/WOn1/4YUXjg8//LDvX3755dEyfPk8YlpUyXvWcwHeD961PLrnnnsi76CoPARuvPHGaKliyytcUOqbb76Jxtgj21pSXWnwn376abjqqqsCGtahhx4ajNGG008/3d7tEA4//PBgwAVjAP7bJmTfpty7NvH7b7RCyBi6S/lI+BCSFRoU5z/77LMw11xz+XrJqquuGg477LAwzzzzuCZvk4UnDvCLOsk/tKb9998/rL766mHdddd1rfq9996r+OkxS2EZAVsIzfaoo47K6tluu+2yfTIwofX+/vvvrlVzAi1+7NixWZl+/fr5Phr7bLPN5v2JNSb1MX2OQwv07bffuoaP5eeHH37wNmCdmXrqqYMJjf43//zz+zh68cUX/RokbbR82muMPzz66KN+vJx/POsRRxwRBgwY4OOqnGu4H/g8++yz5RTvtGVuueUW14YSAIUWjyuuuMIPYwVCc4KYN6abbjqPB844wSrEeijjWiQEKkEARzne7R5mNS78M8UntxrmHXgH21tvvTV8/PHHueVb62RdrcEzwa+//vqBifvMM8/0SRnmCyOA0cMwrrvuOsfm0ksv9Qn/5Zdf9t/JJJcmg19//dUZtmmift60Nd/yD09H037dZMx97rvvvvDTTz8F03h93zSOrGxn2EFIuvPOO8Pw4cOdecL0MF1XSuuss46buvEoRcBCQMM8nghzeaL11lsv/Pbbb96H3Bd68sknfaJOZZIQR9+wnIKAR+5ls+x4keuvvz6kF++rr75y8zwvKPuvvfZaePXVV73OcePGhbfeeivcfvvtAeYx44wz+vWY+TfeeGN/drMQuKBRLlPA1E6KSMYk9yyHVlxxRRdIklBSzjWdrQzLLJtsskmDzFyY4xMl4fHnn3/O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\" alt\u003d\"plot of chunk unnamed-chunk-1\" width\u003d\"100%\"\u003e\u003c/p\u003e" + "data": "\u003cp\u003e\u003cimg src\u003d\"data:image/png;base64,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\u003d\" alt\u003d\"plot of chunk unnamed-chunk-1\" width\u003d\"100%\"\u003e\u003c/p\u003e" } ] }, "apps": [], "jobName": "paragraph_1455137735427_-1023869289", "id": "20160210-215535_1815168219", - "dateCreated": "Feb 10, 2016 9:55:35 AM", - "dateStarted": "Jan 29, 2017 2:58:27 AM", - "dateFinished": "Jan 29, 2017 2:58:28 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { - "text": "%r \nplot(iris, col \u003d heat.colors(3))", + "text": "%spark.r \nplot(iris, col \u003d heat.colors(3))", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 2:58:24 AM", "config": { "colWidth": 4.0, "enabled": true, @@ -1076,23 +1097,19 @@ "msg": [ { "type": "HTML", - "data": "\u003cp\u003e\u003cimg src\u003d\"data:image/png;base64,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wF+BL1UfSQkoASWgBJSACnitA0pACSgBJaAErkACfivgZalYyxJ3UmeDLG8o6ylLlH1fgixN67lG1j9OHTzHZXnYzAS57vxwIWWTZ/REWVIydZAyecqX+rjuX14EZHlhWXLT1yBLa3J5K1+TO56tMrM2tizD7Gt9vpA6llbdT+thYmNjU+q21HFf16NPKy89duEE0qufUk/279/vc8aHDtFj0nkhs31eREQEzu+DJUtpD7JUrC/tQtKk5alNlir39JfS52cUxMdDeovTyHLSaZUzozxz4rxfzqKPjIxEnTp1sGXLlnOcEYwZMwb79u1z1ojv3r07WrZsmSHTbt26pax3/Mgjjzg/L5OLpHLffvvtaNasmXNeViPKKEglfPHFF5300nBeeumllEsyWzbptB9+mC5xGf755x80atQIr7zySkp+TZo0gUQJsq6xyyXutDRcTgROnjyJvn37Qupir169Miza3XffjRo1amDlypX47LPPzqnbaV3cu3dv1K5dG8uWLcMcOqfxpQ4MGTLE+dnQo48+mlaW5xzLTB0TAT1s2DAn73Xr1uGLL744J6/z/1iyZAlmzJjhHJ45cyZkHfRq1aqdn0z/zkIC6dXP3bt3O77L5SfF4ntB+kVvYdWqVZB+VK5LHTLT58l6+PLT0M2bN+Pxxx9H/fr1naykH5W136+++mpHMX3vvfdS3+KcfRHcffr0QdOmTbFp0yZ8+umnKee/+eYbiM8SWf9d6rW0nfTCtGnT8McffzgK6IMPPgguU5uSdNCgQc5PqNesWQPJMyNnYykX5tQONR6/C7Ie+80332xzVHtO2VkRbDo6sDliOue4tz9YMW1WKpsa6znJ6LPXpnOLTK0j/uSTT9qyhjI10X+tf3whZZMCUQu2b731VptejFLKRw3VZsWzKfhtWjFSjuvO5UXgueeesylI7c8//zzDgnG0YM+bN89J9/rrr9sU2F6v4QjYpiLgpOncubNT57xewJOS/4ABA+wJEyZklNTObB2jsLbFB4JcJ+2JAj/De0iC+fPn23TM41NaTXRpCaRXPyls7fXr1zs369ixo9ebcuRucyBit2nT5l/pMtPnedrI4sWL7WeeeSYlLzprceqVHLj33nttWntSzp2/I/WOgtc5TAFuc5CWkoTLvdiSNweAKcfS25F00q+KDBg/fnxKMo7cbVnbXoKs2+9p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\u003d\" alt\u003d\"plot of chunk unnamed-chunk-1\" width\u003d\"100%\"\u003e\u003c/p\u003e" + "data": "\u003cp\u003e\u003cimg src\u003d\"data:image/png;base64,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\u003d\u003d\" alt\u003d\"plot of chunk unnamed-chunk-1\" width\u003d\"100%\"\u003e\u003c/p\u003e" } ] }, "apps": [], "jobName": "paragraph_1455137737773_-549089146", "id": "20160210-215537_582262164", - "dateCreated": "Feb 10, 2016 9:55:37 AM", - "dateStarted": "Jan 29, 2017 2:58:24 AM", - "dateFinished": "Jan 29, 2017 2:58:25 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { - "text": "%r\nlibrary(ggplot2)\npres_rating \u003c- data.frame(\n rating \u003d as.numeric(presidents),\n year \u003d as.numeric(floor(time(presidents))),\n quarter \u003d as.numeric(cycle(presidents))\n)\np \u003c- ggplot(pres_rating, aes(x\u003dyear, y\u003dquarter, fill\u003drating))\np + geom_raster()", + "text": "%spark.r\nlibrary(ggplot2)\npres_rating \u003c- data.frame(\n rating \u003d as.numeric(presidents),\n year \u003d as.numeric(floor(time(presidents))),\n quarter \u003d as.numeric(cycle(presidents))\n)\np \u003c- ggplot(pres_rating, aes(x\u003dyear, y\u003dquarter, fill\u003drating))\np + geom_raster()", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 3:04:03 AM", "config": { "colWidth": 4.0, "enabled": true, @@ -1125,24 +1142,54 @@ "msg": [ { "type": "HTML", - "data": "\u003cp\u003e\u003cimg src\u003d\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAfgAAAH4CAYAAACmKP9/AAAEDWlDQ1BJQ0MgUHJvZmlsZQAAOI2NVV1oHFUUPrtzZyMkzlNsNIV0qD8NJQ2TVjShtLp/3d02bpZJNtoi6GT27s6Yyc44M7v9oU9FUHwx6psUxL+3gCAo9Q/bPrQvlQol2tQgKD60+INQ6Ium65k7M5lpurHeZe58853vnnvuuWfvBei5qliWkRQBFpquLRcy4nOHj4g9K5CEh6AXBqFXUR0rXalMAjZPC3e1W99Dwntf2dXd/p+tt0YdFSBxH2Kz5qgLiI8B8KdVy3YBevqRHz/qWh72Yui3MUDEL3q44WPXw3M+fo1pZuQs4tOIBVVTaoiXEI/MxfhGDPsxsNZfoE1q66ro5aJim3XdoLFw72H+n23BaIXzbcOnz5mfPoTvYVz7KzUl5+FRxEuqkp9G/Ajia219thzg25abkRE/BpDc3pqvphHvRFys2weqvp+krbWKIX7nhDbzLOItiM8358pTwdirqpPFnMF2xLc1WvLyOwTAibpbmvHHcvttU57y5+XqNZrLe3lE/Pq8eUj2fXKfOe3pfOjzhJYtB/yll5SDFcSDiH+hRkH25+L+sdxKEAMZahrlSX8ukqMOWy/jXW2m6M9LDBc31B9LFuv6gVKg/0Szi3KAr1kGq1GMjU/aLbnq6/lRxc4XfJ98hTargX++DbMJBSiYMIe9Ck1YAxFkKEAG3xbYaKmDDgYyFK0UGYpfoWYXG+fAPPI6tJnNwb7ClP7IyF+D+bjOtCpkhz6CFrIa/I6sFtNl8auFXGMTP34sNwI/JhkgEtmDz14ySfaRcTIBInmKPE32kxyyE2Tv+thKbEVePDfW/byMM1Kmm0XdObS7oGD/MypMXFPXrCwOtoYjyyn7BV29/MZfsVzpLDdRtuIZnbpXzvlf+ev8MvYr/Gqk4H/kV/G3csdazLuyTMPsbFhzd1UabQbjFvDRmcWJxR3zcfHkVw9GfpbJmeev9F08WW8uDkaslwX6avlWGU6NRKz0g/SHtCy9J30o/ca9zX3Kfc19zn3BXQKRO8ud477hLnAfc1/G9mrzGlrfexZ5GLdn6ZZrrEohI2wVHhZywjbhUWEy8icMCGNCUdiBlq3r+xafL549HQ5jH+an+1y+LlYBifuxAvRN/lVVVOlwlCkdVm9NOL5BE4wkQ2SMlDZU97hX86EilU/lUmkQUztTE6mx1EEPh7OmdqBtAvv8HdWpbrJS6tJj3n0CWdM6busNzRV3S9KTYhqvNiqWmuroiKgYhshMjmhTh9ptWhsF7970j/SbMrsPE1suR5z7DMC+P/Hs+y7ijrQAlhyAgccjbhjPygfeBTjzhNqy28EdkUh8C+DU9+z2v/oyeH791OncxHOs5y2AtTc7nb/f73TWPkD/qwBnjX8BoJ98VVBg/m8AADkzSURBVHgB7d0JkBxl+cfxZ3dnj+wVch8kISScCTcRBRRQCaegnB6UBWqBAoURgRRYgFj457AUFBQtBESUS0SlvDgSAWMg3FfkSkggm2NDNtmQvXdnZ/7zvDDjzk7PbPd2evv6dmqzM93v2/2+n3d3f9M9Pd1l6cwkTAgggAACCCAQKYHySPWGziCAAAIIIICAESDg+UFAAAEEEEAgggIEfAQHlS4hgAACCCBAwPMzgAACCCCAQAQFEn72qa2tzfPNl5WVSRzPIywvL5dUKuW5bxA3ENcx137rFMef97iOuY53XH/XhzPmDQ0NShabydeA7+jo8PSPUSKRkMrKSunq6orNgGY7Om7cONEXUL29vdlZsfmuv8Tt7e2x6W+2o9k/XnHtexz7XVVVJY2NjdLS0pL9MYjN95qaGkkmk+bLbqezvyN2y4e9HIfowz6CtB8BBBBAAAELAQLeAoVZCCCAAAIIhF2AgA/7CNJ+BBBAAAEELAQIeAsUZiGAAAIIIBB2AQI+7CNI+xFAAAEEELAQIOAtUJiFAAIIIIBA2AUI+LCPIO1HAAEEEEDAQoCAt0BhFgIIIIAAAmEXIODDPoK0HwEEEEAAAQsBAt4ChVkIIIAAAgiEXYCAD/sI0n4EEEAAAQQsBAh4CxRmIYAAAgggEHYBAj7sI0j7EUAAAQQQsBAg4C1QmIUAAggggEDYBQj4sI8g7UcAAQQQQMBCgIC3QGEWAggggAACYRcg4MM+grQfAQQQQAABCwEC3gKFWQgggAACCIRdgIAP+wjSfgQQQAABBCwEEhbzRmxWXV2dlJWVud5eZ2+/PPduR8F6ysqSmfX3SyqVLljW3rS8YN5QM6oaJwxVJG/5pOquvOd2njR3VtoplivT31PYb11YVVUlyWQy0/dUrmz2QbKrLfvQ9vfyymrbZbVgzZipjspr4XSq31GdyvqxluUTiTbTd8uF5RWWs4vN7Nu2qdiiovNT/X1Fl1ktqKwdbTW75Lz+ns6C5YnEFjNPx33wVL3D5MGzhnzutB99bZuHXOfgAolRDYNnlXze39djuTyRaC865o7bVT6M/R6L3zPLhn40s2JUfanFlsvS/YXjqn8/Kysrpbe317qOw9+pKXXOfgd1o2uat1puu9jMdLrwb1Kxsjp/yqQJMm+/uQVFEomE+ftm9TeuoHBMZ/ga8B0dHZJOF4av07FYu7VPrvn7BkfVVj5wo6PyWnjsnoc5qnPkxPWOymvhR9Y0OqrT+f5qR+W1cEfz247rVDU4e3Ez8cATHG8j1evsBVHj7HmOt1FeWeOoztY3/+OovBbu6/zAUZ2G6Xs5Kq+Fuzc3Oaozft+jHJXXwn2d2xzV+WDFMkfltXDdlN0c1en54H1H5bXw1pXPOqpTnqhyVF4Lp5LWAVtsRXWTZxdbVHR+sru96LJiC5z+Th07q7vYqorOv/eRl4sus1qQSlq/SLMqq/OO+cwnZferLylYXFNTY17UWb2gLSj80Yz6eucvrIqtKwzzh/FSNQzdoo0IIIAAAgjEW4CAj/f403sEEEAAgYgKEPARHVi6hQACCCAQbwECPt7jT+8RQAABBCIqQMBHdGDpFgIIIIBAvAUI+HiPP71HAAEEEIioAAEf0YGlWwgggAAC8RYg4OM9/vQeAQQQQCCiAgR8RAeWbiGAAAIIxFuAgI/3+NN7BBBAAIGIChDwER1YuoUAAgggEG8BAj7e40/vEUAAAQQiKkDAR3Rg6RYCCCCAQLwFCPh4jz+9RwABBBCIqAABH9GBpVsIIIAAAvEWIODjPf70HgEEEEAgogIEfEQHlm4hgAACCMRbgICP9/jTewQQQACBiAoQ8BEdWLqFAAIIIBBvAQI+3uNP7xFAAAEEIipAwEd0YOkWAggggEC8BQj4eI8/vUcAAQQQiKgAAR/RgaVbCCCAAALxFiDg4z3+9B4BBBBAIKICBHxEB5ZuIYAAAgjEW4CAj/f403sEEEAAgYgKEPARHVi6hQACCCAQbwECPt7jT+8RQAABBCIqQMBHdGDpFgIIIIBAvAUI+HiPP71HAAEEEIioAAEf0YGlWwgggAAC8RYg4OM9/vQeAQQQQCCiAgR8RAeWbiGAAAIIxFuAgI/3+NN7BBBAAIGIChDwER1YuoUAAgggEG8BAj7e40/vEUAAAQQiKkDAR3Rg6RYCCCCAQLwFCPh4jz+9RwABBBCIqAABH9GBpVsIIIAAAvEWIODjPf70HgEEEEAgogIEfEQHlm4hgAACCMRbgICP9/jTewQQQACBiAoQ8BEdWLqFAAIIIBBvAQI+3uNP7xFAAAEEIipAwEd0YOkWAggggEC8BQj4eI8/vUcAAQQQiKgAAR/RgaVbCCCAAALxFiDg4z3+9B4BBBBAIKICngZ8T0+PrFy5MqJ0dAsBBBBAAIHgCnga8Lfccos88MADwe09LUMAAQQQQCCiAp4F/LJly6S3tzeibHQLAQQQQACBYAskvGje1q1b5Z///KecddZZct999+Vt4qKLLpIlS5aYeYsXL5a6urq85cN50lXRlam2YThVqYMAAgggEGCBmpoamTRpUoBbGNymeRLwN9xwgxx++OGyevVq0bBvaWmR8ePHG4VLL71UFixYYB53dnZKV5eGs7uptZUjBe4EqY0AAggEU0DP5dIMGTxVV1dLMpmU/v7+wYuKPp84cWLRZVFc4EnA77333tLU1GTCfdOmTbJhw4ZcwE+YMCHn2NzcLKlUKvd8uA+2xzqGu23qIYAAAgh4J5BOpy1DXP/u65eTgPeulcFcsycBf9ppp5nerlmzxuyha+AzIYAAAggggMDICXh2kp12YcaMGXLZZZeNXG/YEgIIIIAAAggYAU8DHmMEEEAAAQQQ8EeAgPfHna0igAACCCDgqQAB7ykvK0cAAQQQQMAfAQLeH3e2igACCCCAgKcCBLynvKwcAQQQQAABfwQIeH/c2SoCCCCAAAKeChDwnvKycgQQQAABBPwRIOD9cWerCCCAAAIIeCpAwHvKy8oRQAABBBDwR4CA98edrSKAAAIIIOCpAAHvKS8rRwABBBBAwB8BAt4fd7aKAAIIIICApwIEvKe8rBwBBBBAAAF/BAh4f9zZKgIIIIAAAp4KEPCe8rJyBBBAAAEE/BEg4P1xZ6sIIIAAAgh4KkDAe8rLyhFAAAEEEPBHgID3x52tIoAAAggg4KkAAe8pLytHAAEEEEDAHwEC3h93tooAAggggICnAgS8p7ysHAEEEEAAAX8ECHh/3NkqAggggAACngoQ8J7ysnIEEEAAAQT8ESDg/XFnqwgggAACCHgqQMB7ysvKEUAAAQQQ8EeAgPfHna0igAACCCDgqQAB7ykvK0cAAQQQQMAfAQLeH3e2igACCCCAgKcCBLynvKwcAQQQQAABfwQIeH/c2SoCCCCAAAKeChDwnvKycgQQQAABBPwRIOD9cWerCCCAAAIIeCpAwHvKy8oRQAABBBDwR4CA98edrSKAAAIIIOCpAAHvKS8rRwABBBBAwB8BAt4fd7aKAAIIIICApwIEvKe8rBwBBBBAAAF/BAh4f9zZKgIIIIAAAp4KEPCe8rJyBBBAAAEE/BEg4P1xZ6sIIIAAAgh4KkDAe8rLyhFAAAEEEPBHgID3x52tIoAAAggg4KkAAe8pLytHAAEEEEDAHwEC3h93tooAAggggICnAgS8p7ysHAEEEEAAAX8ECHh/3NkqAggggAACngoQ8J7ysnIEEEAAAQT8ESDg/XFnqwgggAACCHgqkPB07UOsvK6uTsrKyoYoNfTi2t7uoQtRAgEEEEAgdAKJREIaGhoK2q3zU6mU+SpYyAwj4GvAd3R0SDqddj0UXT0iNWOmOlpPzdhpjspr4XSq33EdpxUq68c6qlLZuc1ReS1c1TDBcZ3TD9/FUZ0XJx7oqLwW7u/6wFGd6tETHZXXwu1NbziqU1k/zlF5Ldzdus5Rnb6OVkfltXBFTb2jOj1bNzgqr4W7NjU5qpOo3cFR+eEUHs6Y102e7WhTH6x6wVF5LZxKOtvJGDv3CMfbSHQ5/13v3bbJ4Xac9UNXXjN+pqNtdDa/5ah8MpmUtra2gjo1NTWiy/TL7lRf7+z3xu56g1qOQ/RBHRnahQACCCCAgAsBAt4FHlURQAABBBAIqgABH9SRoV0IIIAAAgi4ECDgXeBRFQEEEEAAgaAKEPBBHRnahQACCCCAgAsBAt4FHlURQAABBBAIqgABH9SRoV0IIIAAAgi4ECDgXeBRFQEEEEAAgaAKEPBBHRnahQACCCCAgAsBAt4FHlURQAABBBAIqgABH9SRoV0IIIAAAgi4ECDgXeBRFQEEEEAAgaAKEPBBHRnahQACCCCAgAsBAt4FHlURQAABBBAIqgABH9SRoV0IIIAAAgi4ECDgXeBRFQEEEEAAgaAKEPBBHRnahQACCCCAgAsBAt4FHlURQAABBBAIqgABH9SRoV0IIIAAAgi4ECDgXeBRFQEEEEAAgaAKEPBBHRnahQACCCCAgAsBAt4FHlURQAABBBAIqgABH9SRoV0IIIAAAgi4ECDgXeBRFQEEEEAAgaAKEPBBHRnahQACCCCAgAsBAt4FHlURQAABBBAIqgABH9SRoV0IIIAAAgi4ECDgXeBRFQEEEEAAgaAKEPBBHRnahQACCCCAgAsBAt4FHlURQAABBBAIqgABH9SRoV0IIIAAAgi4ECDgXeBRFQEEEEAAgaAKEPBBHRnahQACCCCAgAsBAt4FHlURQAABBBAIqgABH9SRoV0IIIAAAgi4ECDgXeBRFQEEEEAAgaAKEPBBHRnahQACCCCAgAsBAt4FHlURQAABBBAIqgABH9SRoV0IIIAAAgi4ECDgXeBRFQEEEEAAgaAKEPBBHRnahQACCCCAgAsBAt4FHlURQAABBBAIqgABH9SRoV0IIIAAAgi4ECDgXeBRFQEEEEAAgaAKEPBBHRnahQACCCCAgAsBAt4FHlURQAABBBAIqgABH9SRoV0IIIAAAgi4ECDgXeBRFQEEEEAAgaAKeBbwbW1t8uKLL0pXV1dQ+067EEAAAQQQiKyAJwG/du1aufzyy+W9996Tiy++WJLJZGQB6RgCCCCAAAJBFPAk4Ht7e2XhwoVy0kknSXl5uWzdujXX976+Punp6TFfuZk8QAABBBBAAIHtKpDYrmv7aGWzZs2S7u5uueCCC2TMmDEyfvz43GZ03uOPP26eP//889LQ0JBbNtwHvTV6hGDDcKtTDwEEEEAgoAKjRo2SKVOmBLR1wW6WJwGvXa6pqZGf/OQncv3118vTTz8tBx98sJG4+eabJZVKmcetra3S3t5uHrv5b1Obm9rURQABBBAIqoCex7VhQ+EOnGaMvv3r5C3guL1Q8CTg//Wvf5k99/33319mzpxp9uazPzyVlZXZh3xHAAEEEEAAAY8EPAn4fffd1+y9P/LII9Lf3y9f/vKXPWo+q0UAAQQQQAABKwFPAn7cuHFyzTXXmBPpqqurrbbLPAQQQAABBBDwUMCTs+iz7SXcsxJ8RwABBBCIk4B+muzqq682XdbzBBobG0e8+54G/Ij3hg0igAACCCAQAIEtW7bInXfeaVqSSCRk5cqVI94qAn7EydkgAggggEBYBfQ6LkceeaQcd9xxst9++5nzzPSCbnPnzpVp06bJKaecYt6evuiii2TdunVyxhlnmDP9tY5Op556qtmz33333UU/Uv7oo4+a+frR8q985Stm3vHHHy/HHHOMNDU1mWXD/c+T9+CH2xjqIYAAAgggEGQB/Zj34sWL5b777pNDDz1UnnnmGXnjjTfktddeE72Q2yGHHCL6STL9mPiyZcvk7rvvNgH/7rvvmm6tX79eamtrZfny5fLXv/5Vvv/978tRRx0lV111ldTV1clbb71l5usLBScfAbQyI+CtVJiHAAIIIIBAEQH9uLfuiVdUVJi99ltvvdUE/ksvvSQtLS2ybdu2IjU/nK2fLNN1zJs3z5TXufqi4Kc//amZr1eBnTRpUsl12FnIIXo7SpRBAAEEEEDgI4H6+noT7vr03//+t9mT1z3vo48+WvRj4ul0uqTV5MmTzXJ9gZAtq3v1+rFyncrKysyXeeLiPwLeBR5VEUAAAQTiLaDvoet75z/4wQ/koIMOMofq9dB6VVWV6Jn0dqcTTzxR7rnnHnNY/uGHH5bm5ma7VYuWI+CL0rAAAQQQQACB0gJ6Et3f//530RPj9H3zAw44QPT99rFjx5p7rWjo25m+/vWvi74/v9dee8ljjz1mDtHre/JuJt6Dd6NHXQQQQACBWAnozW/0I3DZac8995RXXnnFzNNQHzi9/vrrotfS14/JZd+Xf+qpp3JFdtxxx9zH55577jm58cYbzVn0eo8WPYlv4sSJubLDeUDAD0eNOggggAACCAwQGBzu2UX6gsDOpBeG+/znP2/OqF+yZImcf/75dqqVLEPAl+RhIQIIIIAAAt4LHHbYYeZjdbonr5+hnzp1quuNEvCuCVkBAggggEBcBB5/p1eefq/PVXd3GlMhZ+xfU7AOfc/9iCOOKJg/3BkE/HDlqIcAAgggEDuBVZv75Yl3elz1e7+plZYB72qlFpU5i94ChVkIIIAAAgiEXYCAD/sI0n4EEEAAAQQsBDhEb4HCLAQQQAABBKwFMlepG+JKddb1Bsx1W3/Aqko9JOBL6bAMAQQQQACBgQJ6FdoRCuiBmx3OYw7RD0eNOggggAACsRRISyqT7/2uv6zwOjo6ZOnSpXl3kVu7dq3o5+Kz16m3qldsHgFfTIb5CCCAAAIIDBJIJfukv7fL1VcqWXiNer0Lnd4D/tVXXzX3m9dA10vWnnfeeaLXpj/nnHMGtWTopxyiH9qIEggggAACCHwkkDZ772449AjA4EnvHa8Xuzn33HPliSeekBUrVshNN90kd911l4wZM8Zc614vkVvsinmD16fPCXgrFeYhgAACCCBgITC+rlyO3rPeLOlNpuXxt9stShXOOmD6KJnQ8GHkjqktjN6jjjpKrrnmGnNHup122kn22GMP2bx5swl3Xdu0adOkqamJgC+kZQ4CCCCAAALuBVra++WR/25zvKIX13Tm6uw/vTb3OPvg1ltvNfeTX7hwoTksr7ehHTjpLWj1nvFOJt6Dd6JFWQQQQACBWAtkDtBvl3+DEXVv/fDDDxe9Oc3HPvYx2bhxo0yfPl3WrFljiq5atUp0z97JVHicwEltyiKAAAIIIBAngVTmLPrMCXCupsw6Bk9nn322XHLJJeY2sXq72Ntuu0323XdfufDCC81Z9Xqv+aqqqsHVSj4n4EvysBABBBBAAIH/CaTTGvDubjaTTiX/t8KPHund4+6++27p7u6WmpoPb0Szzz77yIMPPig9PT2it5N1OhHwTsUojwACCCAQX4FU5jPwHgR8FjQb7tnn+n044a71CHhVYEIAAQQQQMCGQDpzFbu0xSF2G1VzRdIpvRye9xMB770xW0AAAQQQiIqAXqbW7aVq3da3aUnA24SiGAIIIIAAApl0z+R74UlyzmTYg3fmRWkEEEAAAQQ8FjCH6F0GvPsXCPY6yR68PSdKIYAAAgggkBHQQ/Qu9+CDcIj+oYceMqfnn3766QwrAggggAACCOjH5Cw+5uYExupa9E7q2y1b8kp2erH75557zu66KIcAAggggECkBfQM+nR/0uWXywvl2BQueYher6LzxS9+0dzZRi90X1ZWZlZ79dVXy9y5c21ugmIIIIAAAghERGA77MFnXh2MCEbJgN9ll13MlXUGt2TKlCmDZ/EcAQQQQACB6AuE6GNyJQ/R77zzznLIIYfIpk2bzG3q9E428+bNc3S7uuiPNj1EAAEEEIiVQDbk3XwfAbCSAd/R0SEf//jHzfvwTz75pLk3rT7v7e0dgaaxCQQQQAABBIImoJ9hd/s1Mn0qGfB64fsFCxbIBRdcYFpz8skny2mnnSb/+c9/RqZ1bAUBBBBAAIEACWQvVWtOttMT7obz5fZjdjY9Sr4H39DQIM3NzXmreuedd2TGjBl583iCAAIIIIBAHARSmTPok73drrra3zcyR8FLBrzef1ZvV6eH59977z059thjTaf05DsmBBBAAAEE4iaQ3YN3029dx0hMJQNeby7/2muvyeLFi2Xp0qVy1FFHySc/+cmRaBfbQAABBBBAIIAC2fff3TQtAAH/m9/8RmbNmiXHHHOM+dLuLFy4UI488kgT9m66R10EEEAAAQRCJ2Dy3WVA+7kH//rrr8uJJ54oW7dulcrKStGPx+mkhxVaW1vlW9/6VujGhAYjgAACCCDgWiCTg65vFuNnwM+ZM0eeeuopeeCBB0SvYHfQQQcZk4qKChk3bpzodyYEEEAAAQTiJ5DZe3d9FrzLm9XYRC/6HvzEiRNNuPf09AhXrrOpSTEEEEAAgUgLpFP9mSvN9rnqo17Lvtj0zDPPmEvB19fXmyJr166V1atXm4vOOd25Lvk5eG42U2wImI8AAgggEEsB/dx7MnOzGTdfmRcJgye9YuxJJ50kL7/8sjnnTd8Of+yxx+S8886Thx9+WM4555zBVYZ8XnQPXmtys5kh/SiAAAIIIBArge1wiN7iPXg9qf3MM8+U4447TubPny/JzAuIm266Se666y4ZM2aMHH/88bJlyxZHl4ovGfDcbCZWP7V0FgEEEEBgCIEJO9TKiYfsakr19PXLw8+uHKLGh4s/MWeaTBpTZ56MbfzwxPWBFd9++23Rw/N33nmnObldryS7efNmE+5aTs+Ha2pq2n4Brzeb0TPpV65cKf39/ZLKHJrQDc6ePdvRRgZ2YuDjurq63C1oB853+rgu84KqsqHkuw0Fq6xqGFcwb6gZlfVjhyqSt/zPz7+e99zOk9GzZtoplivTszX/SoO5BSUe9LVvLrHUetEfnrSeX2zu7JOLv8dUrE7nxtXFFlnO7+/uspxfamai9sP3tUqVGbisr6N14FNbj8vKnJ2EWlZe8nW2rW0OVah++t5DFXG9vKd1o+N1VNQ2OqrTvWmNo/JauKulyVGduqm7OyqvhVO9zn8WnW6kZ+sGp1Uk2bXNUZ0nO/dyVF4LV1S/6KxOubPfj0Tmeix6VdXBUyKRMJmkuTSS0/ut7fLQf950vMll//3fz+FBe04rqK/90L33b3zjG3LdddfJ3/72t7wyukef/URb3oIST0r+ZdHj/1/5yldk1113lb6+PikvLxe9Ac2pp55aYpX2F+m6tscVfTo79QfG2R9u+62kJAIIIICAXwIabG1tbQWbr6mpMYexdbndKXvimt3yluX08LrFIXbLskVnZtYxaNJPr+nF5XTKZu306dNlzZo15vLwq1atkp122mlQrdJPSwb8448/LjfccIPohm+//Xa55ZZbTOBzN7nSqCxFAAEEEIioQCbcvfgc/Nlnny1XXHGF/Pvf/5aNGzfKvffea86Du/DCC80LGb10fPYFgF3ZkgGvh9D1VdJee+1l3hvQlR544IHywgsvOH4lYbdBlEMAAQQQQCCoAhru6ZT9owZW/dCP2g2eRo8ebU6q6+7uNrmry/VeMA8++KDox9Wrq6sHVxnyecmA/8IXvmBuMKMB39jYKBdddJH89a9/NdemH3LNFEAAAQQQQCBiAhrwKZefg0+VeIGgO9WDp+GEu66j5Jlpc+fOlUWLFsn48eNFz+gbNWqU/PznPxd9X4AJAQQQQACB2Alk34N3+30E4Eruwetp+6+++mquGfvtt59s27ZN9AP5EyZMyM3nAQIIIIAAAvEQ8OYkOy/sSga8fjzuoYceMtvVj8nple30/QGdR8B7MRysEwEEEEAg0AJ6Arzbs+gLT6L3pMslA14/k6dfA6fTTz9dOIt+oAiPEUAAAQTiIpA5h978c9ffkUn4kgFv1YHJkyeL3k52jz32sFrMPAQQQAABBKIroDebSfa66p/bm9XY3XjJgNcz5vU6uDrpBWn0qnbLly83n9WzuwHKIYAAAgggEBWBdCqzB595y9rNlB6hq++VDHg9i/6ss87K9UNP1Z83b57ssMMOuXk8QAABBBBAID4CepKdy8vjun0P3yZ2yYDXSwB2df3vGsv6WD82l530rHq9IQ0TAggggAAC8RBw/x68rmEkppIBv379enPhe725zAEHHCDLli2T5uZmc/EbbZzeAICAH4lhYhsIIIAAAoEQyH7+3U1jgrAHrx+T0+vgXnXVVbmu6A3pFyxYYA7V52byAAEEEEAAgRgI6L6365ukBSHgN2zYUHDNeb0bj14InwkBBBBAAIHYCWTC2e1Jcq5fINhEL3mIXj/zfsIJJ8jSpUtlzz33NHe50dD/7Gc/a3P1FEMAAQQQQCA6Al7dbMYLoZLXot99993l0UcfNeHe0tIiCxcuNHeVs7oYvheNY50IIIAAAggESkDPoNebxbj5Srv7mJ1dj5J78LqSmTNnmrvI2V0h5RBAAAEEEIisQOYz7K4P0Qfhc/CRHSA6hgACCCCAwDAEtstJdsPY7nCqDLkHP5yVUgcBBBBAAIFICkTlY3KRHBw6hQACCCCAgCsBtxeqcVvfXuPZg7fnRCkEEEAAAQQyApmPybn8HLvb+naHgYC3K0U5BBBAAIHYC2yXj8m5vZa9zVEo+TE5m+ugGAIIIIAAArEQ0DPoU5n7tLj5KnU3Or3nyyuvvJKzXLt2rSxZskT6h3EHOwI+x8gDBBBAAAEEhhIw59FnCrn9br0dvd7Mz372M7Pwsccek/POO08efvhhOeecc6wrlJjLIfoSOCxCAAEEEEBgoEBNVaVMmzTOzOrP7M1veH/LwMVFH48b0yijqqvM8tH1oyzLaZB3d3fnlt10001y1113yZgxY+T444+XLVu2yNixY3PLh3rAHvxQQixHAAEEEEDgI4FqDfjJ48zX1Ekatvb25MePacjVG91QV+CpV4u98847zQ3esgs3b95swl2fT5s2TZqamrKLbH1nD94WE4UQQAABBBAQ+aCtQ5a99IZjirfe+V84V5QX7luff/75cvLJJ8vy5ctl06ZNordrHzglM+/719bWDpw15GMCfkgiCiCAAAIIIPChQDpzHfl0qs8VRzqduZb9oOmQQw6Rt99+24T7unXr5N1335Xp06fLmjVrZMaMGbJq1aqCu7sOWkXBUwK+gIQZCCCAAAIIWAukU2lJpdzdLCZlcS36BQsWmA2+9dZb0t7eLhr4env2Cy+8UHTv/ZRTTpGqqg/fw7duWeFcAr7QhDkIIIAAAggUEcjcTc7t59hL1Ne7uN5xxx1m2/vss488+OCD0tPTI9XV1UXaU3w2AV/chiUIIIAAAgjkC5hz6vQ/N5Oz+sMJd20dAe9mjKiLAAIIIBAzgexZ88HvNgEf/DGihQgggAACARJwfS15l9eyt0tBwNuVohwCCCCAAALmdrGZ9+HdTCXeg3ez2sF1CfjBIjxHAAEEEECgmIAGvMVZ8MWKW85nD96ShZkIIIAAAgj4JvDh3eTcfUxOb1gzEhN78COhzDYQQAABBCIikNmDd32InYCPyA8D3UAAAQQQiIxA5vC625Ps3Na3a8kevF0pyiGAAAIIIGAEnH2O3S80At4vebaLAAIIIBBOAbf57ra+TTUC3iYUxRBAAAEEENDD624Psbutb3cUCHi7UpRDAAEEEEAgc4Kd1d3gnMG4Owvf7rYIeLtSlEMAAQQQiL2Afkwu1V94u1cnMKl+At6JF2URQAABBBDwXkDfPx+h99DddoY9eLeC1EcAAQQQiJmA24R3W98ed7m9YpRCAAEEEEAAgTAJEPBhGi3aigACCCCAgE0BDtHbhKIYAggggAACM2fsKJ86+EBXEPvM2c1VfbuVPQv4zs5OWbFihcyZM0cqKyvttodyCCCAAAIIBFbgzC99XvQrDJMnh+ibmprk0ksvldWrV8uCBQukra0tDBa0EQEEEEAAgcgIeLIH39LSYoJ99uzZsm3bNlm2bJnMnz/foL3//vvS1dVlHo8aNWq7QJaXe/I6Zbu0jZUggAACCAxfoKysTCoqKgpWoH/39ctqWUHhmM7wJOD3339/w9na2ipLliyRa6+9Nsf7ox/9yMzTGYsWLZK6urrcsuE+6KzUjxz0DLc69RBAAAEEAipQXV0t48ePD2jrgt0sTwJeu6x78VdeeaV85zvfyRucH//4xzmR5uZmaW9vzz0f7oPNHfrqrn641amHAAIIIBBQge7ubtm4cWNB62pqaiSZTJqvgoVFZkyZMqXIkmjO9iTgNdyvuuoqueSSS2TnnXeOphy9QgABBBBAIMACngT8HXfcYfbgb7zxRtP1U089VQ477LAAM9A0BBBAAAEEoiXgScAvXLgwWkr0BgEEEEAAgZAJcPp5yAaM5iKAAAIIIGBHgIC3o0QZBBBAAAEEQiZAwIdswGguAggggAACdgQIeDtKlEEAAQQQQCBkAgR8yAaM5iKAAAIIIGBHgIC3o0QZBBBAAAEEQiZAwIdswGguAggggAACdgQIeDtKlEEAAQQQQCBkAgR8yAaM5iKAAAIIIGBHgIC3o0QZBBBAAAEEQiZAwIdswGguAggggAACdgQIeDtKlEEAAQQQQCBkAgR8yAaM5iKAAAIIIGBHgIC3o0QZBBBAAAEEQiZAwIdswGguAggggAACdgQIeDtKlEEAAQQQQCBkAgR8yAaM5iKAAAIIIGBHgIC3o0QZBBBAAAEEQiZAwIdswGguAggggAACdgQIeDtKlEEAAQQQQCBkAgR8yAaM5iKAAAIIIGBHgIC3o0QZBBBAAAEEQiZAwIdswGguAggggAACdgQIeDtKlEEAAQQQQCBkAgR8yAaM5iKAAAIIIGBHgIC3o0QZBBBAAAEEQiZAwIdswGguAggggAACdgQIeDtKlEEAAQQQQCBkAgR8yAaM5iKAAAIIIGBHgIC3o0QZBBBAAAEEQiZAwIdswGguAggggAACdgQIeDtKlEEAAQQQQCBkAgR8yAaM5iKAAAIIIGBHgIC3o0QZBBBAAAEEQiZAwIdswGguAggggAACdgQIeDtKlEEAAQQQQCBkAgR8yAaM5iKAAAIIIGBHgIC3o0QZBBBAAAEEQiZAwIdswGguAggggAACdgQIeDtKlEEAAQQQQCBkAgR8yAaM5iKAAAIIIGBHgIC3o0QZBBBAAAEEQiZAwIdswGguAggggAACdgQIeDtKlEEAAQQQQCBkAgR8yAaM5iKAAAIIIGBHgIC3o0QZBBBAAAEEQiZAwIdswGguAggggAACdgQIeDtKlEEAAQQQQCBkAgk/21tXVydlZWWum7BD50bZ9fmrHa2neeqBjspr4Y3P/9lZnfIKZ+Uzpeum7OaoTkfzO47Ka+G09Duu09O6wVGdbe++6Ki8Fq6furvjOk4rVNaNdVSlZtx0R+W1cCrZ46hOsnOro/JauGGnvR3VaV/3hqPyWthp353a6jYStaP1m+0p7dBWVzx6lrPf9da3n7bdnmzB3vZN2Ye2vtdMmGGr3MBC9TvOGfjU1uO2916xVS5bqLN5Zfah7e8T9z/Odlkt2LHhTUflK6tqpKGhoaBOIpGQVCplvgoWMsMI+BrwHR0dkk6nXQ9FZ2en63WwAgQQQACB4Akkk0lpa2sraFhNTY3oMv2yO9XX19stGolyHKKPxDDSCQQQQAABBPIFCPh8D54hgAACCCAQCQECPhLDSCcQQAABBBDIFyDg8z14hgACCCCAQCQECPhIDCOdQAABBBBAIF+AgM/34BkCCCCAAAKRECDgIzGMdAIBBBBAAIF8AQI+34NnCCCAAAIIREKAgI/EMNIJBBBAAAEE8gUI+HwPniGAAAIIIBAJAQI+EsNIJxBAAAEEEMgXIODzPXiGAAIIIIBAJAQI+EgMI51AAAEEEEAgX4CAz/fgGQIIIIAAApEQIOAjMYx0AgEEEEAAgXwBAj7fg2cIIIAAAghEQoCAj8Qw0gkEEEAAAQTyBQj4fA+eIYAAAgggEAkBAj4Sw0gnEEAAAQQQyBcg4PM9eIYAAggggEAkBAj4SAwjnUAAAQQQQCBfgIDP9+AZAggggAACkRAg4CMxjHQCAQQQQACBfAECPt+DZwgggAACCERCgICPxDDSCQQQQAABBPIFCPh8D54hgAACCCAQCQECPhLDSCcQQAABBBDIFyDg8z14hgACCCCAQCQECPhIDCOdQAABBBBAIF+AgM/34BkCCCCAAAKRECDgIzGMdAIBBBBAAIF8AQI+34NnCCCAAAIIREKAgI/EMNIJBBBAAAEE8gUI+HwPniGAAAIIIBAJAQI+EsNIJxBAAAEEEMgXIODzPXiGAAIIIIBAJAQI+EgMI51AAAEEEEAgX4CAz/fgGQIIIIAAApEQIOAjMYx0AgEEEEAAgXwBAj7fg2cIIIAAAghEQoCAj8Qw0gkEEEAAAQTyBQj4fA+eIYAAAgggEAkBAj4Sw0gnEEAAAQQQyBcg4PM9eIYAAggggEAkBAj4SAwjnUAAAQQQQCBfgIDP9+AZAggggAACkRAg4CMxjHQCAQQQQACBfAECPt+DZwgggAACCERCwNOA37hxo2zZsiUSUHQCAQQQQACBMAl4FvDr1q2T7373u7JixYowedBWBBBAAAEEIiGQ8KIXvb29cvPNN8vcuXMLVq+B39raaubvvPPOUl7u/jVGZWVlwXaYgQACCCAQfgHNiKqqqoKOJBIJKSsr2y4ZUrDyiMzwJOB1MK677jr5zW9+U8B0//33y7PPPmvm/+53v5Pa2tqCMk5ndHZ2Oq1CeQQQQACBEAjoDlxjY2NBSzXcdUqn0wXLmPGhgCcBXwr38ssvzy1ubm6W7RHOW7duza2TBwgggAAC0RHo6emRlpaWgg7V1NRIMpk0XwULi8yYMmVKkSXRnO3++Hg0XegVAggggAACoRYg4EM9fDQeAQQQQAABawFPD9F/7Wtfs94qcxFAAAEEEEDAUwH24D3lZeUIIIAAAgj4I0DA++POVhFAAAEEEPBUgID3lJeVI4AAAggg4I8AAe+PO1tFAAEEEEDAUwEC3lNeVo4AAggggIA/AgS8P+5sFQEEEEAAAU8FCHhPeVk5AggggAAC/ggQ8P64s1UEEEAAAQQ8FSDgPeVl5QgggAACCPgjQMD7485WEUAAAQQQ8FSAgPeUl5UjgAACCCDgjwAB7487W0UAAQQQQMBTAQLeU15WjgACCCCAgD8CBLw/7mwVAQQQQAABTwUIeE95WTkCCCCAAAL+CBDw/rizVQQQQAABBDwVIOA95WXlCCCAAAII+CNAwPvjzlYRQAABBBDwVICA95SXlSOAAAIIIOCPAAHvjztbRQABBBBAwFMBAt5TXlaOAAIIIICAPwIEvD/ubBUBBBBAAAFPBQh4T3lZOQIIIIAAAv4IEPD+uLNVBBBAAAEEPBUg4D3lZeUIIIAAAgj4I0DA++POVhFAAAEEEPBUgID3lJeVI4AAAggg4I8AAe+PO1tFAAEEEEDAUwEC3lNeVo4AAggggIA/AgS8P+5sFQEEEEAAAU8FCHhPeVk5AggggAAC/ggQ8P64s1UEEEAAAQQ8FSDgPeVl5QgggAACCPgjQMD7485WEUAAAQQQ8FSAgPeUl5UjgAACCCDgjwAB7487W0UAAQQQQMBTAQLeU15WjgACCCCAgD8CBLw/7mwVAQQQQAABTwUIeE95WTkCCCCAAAL+CBDw/rizVQQQQAABBDwVIOA95WXlCCCAAAII+CNAwPvjzlYRQAABBBDwVICA95SXlSOAAAIIIOCPAAHvjztbRQABBBBAwFMBAt5TXlaOAAIIIICAPwIEvD/ubBUBBBBAAAFPBQh4T3lZOQIIIIAAAv4IEPD+uLNVBBBAAAEEPBUoS2cmT7dQYuXbtm2TsrKyEiXsLerr65P169cXFNZ1l5eXS39/f8Gyben6gnlDzehr3zJUkfzlw+hadeOk/HUM8ayvo9WyRHV1tahLKpUqWJ7q7y2YN9SMVNJZnaqGCUOtsmB5RdWognnDmZFIJCSZTFpWLatIWM4vNrOsvKLYoqLzk50fFF1mtSBtMUZW5QbOq6iuHfjUPNZ+68+8jnvBNIzfs/JEVcFqSs1Ipwp/z0qV12Vl5c7GI9XXbbnKUmPu9Gc32dVmuY1SM9MpC/MSFRKjRpdYar3Iajz071tlZaX09PRYVurv6bCcX3RmuvDvRdGyHy1IjGocqkje8u7Wwr/VeQUGPdmhsUFmz5w2aK6Ijrn+fbP6G1dQ+KMZDQ0NxRZFcr6vAd/c3Cxevr7QHwD94e/q6ork4JXq1Lhx46StrU16e50Fc6l1hmWZ/hJr3+M2Zf94xbXvcex3VVWVNDY2SktLS9x+3KWmpsa8kC/2Yt4KZMqUKVazIzuPQ/SRHVo6hgACCCAQZwECPs6jT98RQAABBCIrQMBHdmjpGAIIIIBAnAUI+DiPPn1HAAEEEIisAAEf2aGlYwgggAACcRYg4OM8+vQdAQQQQCCyAgR8ZIeWjiGAAAIIxFmAgI/z6NN3BBBAAIHIChDwkR1aOoYAAgggEGcBAj7Oo0/fEUAAAQQiK0DAR3Zo6RgCCCCAQJwFCPg4jz59RwABBBCIrAABH9mhpWMIIIAAAnEWIODjPPr0HQEEEEAgsgIEfGSHlo4hgAACCMRZgICP8+jTdwQQQACByAoQ8JEdWjqGAAIIIBBnAQI+zqNP3xFAAAEEIitAwEd2aOkYAggggECcBcrSmSnOAFHt+6233iqf/exnZfbs2VHtIv0aJLB48WJJpVIyf/78QUt4GlWB9957T/7xj3/IueeeG9Uu0i8XAuzBu8ALctVHHnlEmpubg9xE2radBV555RV58cUXt/NaWV2QBd5//30T8EFuI23zT4CA98+eLSOAAAIIIOCZAIfoPaP1d8Xr16+XMWPGyKhRo/xtCFsfMYHW1lbRd9zGjh07YttkQ/4KdHd3y+bNm2XHHXf0tyFsPZACBHwgh4VGIYAAAggg4E6AQ/Tu/Hyt/dZbb5mTqrKNaGlpEZ2XPW9S9+hef/1186Un42SnlStXin4xhU9g48aNsmXLllzDdQ/u1Vdfld7e3tw8ffDf//5XOjs7c/P0vVp9j76/vz83jwfhEXjzzTdzjdXf79dee022bdtm5ul3He/s17vvvmvmd3R0yAsvvCD6nSmeAhVXZaZ4dj3cvX7uuefku9/9rnz5y1+WiooKefTRR+WBBx4Q/YOvjw899FD53e9+Jy+//LLoH3f9Y7/77rvLr3/9a1mxYoUsXbrUzNttt93CDRGj1q9bt04uvvhi2XXXXWXatGmyZs0a0V/f+vp6+f3vf2/GvL29Xa6++mqpra2VX/7yl3LYYYeZFwC33367Ge9FixbJpz71qRiphburGuZ333233H///XLCCSeYF/QLFiyQmpoaue+++2SXXXYx4/rEE0+Ynwcd39WrV8ucOXPke9/7nvk50LE/8sgjJZFIhBuD1jsWYMQdk/lfYfny5bJkyRKZNWtWrjH6UZn/+7//k7q6Ornkkkvkgw8+kHfeeUcuu+wyqaqqMr/oWvill16SW265RZLJpHz729+W448/PrcOHgRXQPfQb775Zpk7d26ukfpC7owzzpBPfOIT0tfXJ88++6zoi4BjjjlGDj74YPnYxz5mxvmPf/yjXH755dLQ0GB+NnSPr7GxMbceHgRX4M9//rNUV1dLWVmZaaT+/mp4f+lLX5J9991XHnroIbnwwgvlm9/8pnR1dZnxPf/88+Vvf/ubfPGLXzQv8PSjk88884x5HNye0jIvBDhE74Wqx+vca6+9zN67vorPTuPHj5e1a9dKT0+P+SOvf+hXrVolv/3tb+XKK680fwj0kP3o0aNNFX01z+HarF7wv+uLtOuuu06mTJmSa6w+1sOxupenY61j3tTUJPoRSR1z3YPXvXt9safhrtPEiRPNEZ3cSngQaIGTTz5ZTjvttFzA65jrGOvvbnbMsx34wx/+IMcee6wZ6w0bNuR+ViZNmiT61g5T/ATYg4/ImJ911llyxx13mF/8mTNnmj/sd955p9mj1731c845R4444oi89+w5ZBfuwf/MZz4jevj10ksvFf0jriGuYa9775/73OfMYfunnnoqr5P6s6B7hEzhFJg6darMmzfPHJnbY489ci/cdC9dj+r94he/MB0rLy/P/a7riwHGPJzj7bbV7MG7FQxIfT1pTt+f/cEPfmAO1ememga+TtlfcN171705nfTEGz5CZyhC+5++B6+H46+//nrzQm7nnXcWfXGXfeGmh3X1sK3+LGT34PTjk5MnTw5tn+PecP391RdzP/rRj8zbNTrmOr399tvm3IxskO+0007mvXhdpkd5ZsyYoQ+ZYibAHnxEBlwPxV5zzTXm5Bs9TKeH7/Xz0D/84Q9FX81/7WtfMz09/fTTzeFbPVyv79sxhVdAg1vHXP/g64u1vffeW/QP/m233WbOmNez7fUkPD0R66abbjIv9PQoTmVlZXg7HfOW64v0xx9/XPTITFtbmzmRTkn0xLqBl6U++uij5cc//rE8/fTTZu99v/32i7lcPLvP5+AjNO56+FWn7B6cPtaTrwb/QddyGvr6xRR+AT3vIrvnlu2N1Tw9UU/fy2cKv4DV+Fr1ijG3UonPPAI+PmNNTxFAAAEEYiTALlyMBpuuIoAAAgjER4CAj89Y01MEEEAAgRgJEPAxGmy6igACCCAQHwECPj5jTU8RQAABBGIkQMDHaLDpKgIIIIBAfAQI+PiMNT0NuMAFF1wgf/rTn3Kt1GsX6PXl9dKk8+fPNxex0avUvfHGG6aM3ilMn+sNg/Sz7nr9cZ30Iih6Q5IJEybIvffea+bxHwIIxE+AgI/fmNPjgAropWf18sI66UVqnsjcIUwvS6qBfdxxx5mLmWSvVqhl9OZCeiEjvYrZtddea+4ip/M3b95sXhjojUm0HhMCCMRTgICP57jT6wAK6J39nn/+edGrDOqtf7M3GfnLX/4imzZtMmGu9/xevHixuamQ3iVObxV8ww03mD317P3BtWsa7HpL2ezNhQLYXZqEAAIeCxDwHgOzegTsCuhV5k466SRzmP6ee+6Rr371q+ZKhHrfAL1N7J577mluFap3idMrlGmI/+xnPzMhrofz9UYz2Ylgz0rwHYH4CnAt+viOPT0PoMCZZ55pbgWsN4nRa8vrpPcW0OnUU081h+P1vXqd9+STT5rD8bW1teZe8dlLFZvC/IcAArEXIOBj/yMAQJAEDjroIGlpaZGzzz4716zLLrtMrrjiCvnVr34lnZ2d5gYydXV1ct5558nhhx9ubio0Z84cc6fA7u7uXD0eIIBAvAW4Fn28x5/eB1Bgn332kUWLFpnbvA5sXnt7u+hdAwdOevheby40+GYzA8vwGAEE4inAe/DxHHd6HUCBf/3rX6K3c/30pz9dEO7a3MHhrvN0T55wVwkmBBAYLMAe/GARniPgk4B+vE0/466fbdez45kQQAABNwIEvBs96iKAAAIIIBBQAQ7RB3RgaBYCCCCAAAJuBAh4N3rURQABBBBAIKACBHxAB4ZmIYAAAggg4EaAgHejR10EEEAAAQQCKvD/uneGoDceIf8AAAAASUVORK5CYII\u003d\" alt\u003d\"plot of chunk unnamed-chunk-1\" width\u003d\"100%\"\u003e\u003c/p\u003e" + "data": "\u003cp\u003e\u003cimg src\u003d\"data:image/png;base64,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\" alt\u003d\"plot of chunk unnamed-chunk-1\" width\u003d\"100%\"\u003e\u003c/p\u003e" } ] }, "apps": [], "jobName": "paragraph_1438930880648_-1572054429", "id": "20150807-090120_1060568667", - "dateCreated": "Aug 7, 2015 9:01:20 AM", - "dateStarted": "Jan 29, 2017 3:04:03 AM", - "dateFinished": "Jan 29, 2017 3:04:04 AM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n---", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": false, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003chr/\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512671867808_-2048809437", + "id": "20171207-183747_668656216", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "GoogleViz: Bubble Chart", - "text": "%r\nlibrary(googleVis)\nbubble \u003c- gvisBubbleChart(Fruits, idvar\u003d\"Fruit\", \n xvar\u003d\"Sales\", yvar\u003d\"Expenses\",\n colorvar\u003d\"Year\", sizevar\u003d\"Profit\",\n options\u003dlist(\n hAxis\u003d\u0027{minValue:75, maxValue:125}\u0027))\nprint(bubble, tag \u003d \u0027chart\u0027)", + "text": "%spark.r\n\n# Workaround for Spark issue with googleVis: SPARK-23780\ndetach(\"package:SparkR\")\nlibrary(googleVis)\nsuppressPackageStartupMessages(library(SparkR))\n\nbubble \u003c- gvisBubbleChart(Fruits, idvar\u003d\"Fruit\", \n xvar\u003d\"Sales\", yvar\u003d\"Expenses\",\n colorvar\u003d\"Year\", sizevar\u003d\"Profit\",\n options\u003dlist(\n hAxis\u003d\u0027{minValue:75, maxValue:125}\u0027))\nprint(bubble, tag \u003d \u0027chart\u0027)", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 3:14:35 AM", "config": { "colWidth": 6.0, "enabled": true, @@ -1176,24 +1223,20 @@ "msg": [ { "type": "HTML", - "data": "\n\u003c!-- BubbleChart generated in R 3.3.2 by googleVis 0.6.2 package --\u003e\n\n\u003c!-- Sun Jan 29 03:14:35 2017 --\u003e\n\n\u003c!-- jsHeader --\u003e\n\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataBubbleChartID17e4815dd498c () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"Apples\",\n98,\n78,\n\"2008\",\n20\n],\n[\n\"Apples\",\n111,\n79,\n\"2009\",\n32\n],\n[\n\"Apples\",\n89,\n76,\n\"2010\",\n13\n],\n[\n\"Oranges\",\n96,\n81,\n\"2008\",\n15\n],\n[\n\"Bananas\",\n85,\n76,\n\"2008\",\n9\n],\n[\n\"Oranges\",\n93,\n80,\n\"2009\",\n13\n],\n[\n\"Bananas\",\n94,\n78,\n\"2009\",\n16\n],\n[\n\"Oranges\",\n98,\n91,\n\"2010\",\n7\n],\n[\n\"Bananas\",\n81,\n71,\n\"2010\",\n10\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027Fruit\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Sales\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Expenses\u0027);\ndata.addColumn(\u0027string\u0027,\u0027Year\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Profit\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartBubbleChartID17e4815dd498c() {\nvar data \u003d gvisDataBubbleChartID17e4815dd498c();\nvar options \u003d {};\noptions[\"hAxis\"] \u003d {minValue:75, maxValue:125};\n\n var chart \u003d new google.visualization.BubbleChart(\n document.getElementById(\u0027BubbleChartID17e4815dd498c\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"corechart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartBubbleChartID17e4815dd498c);\n})();\nfunction displayChartBubbleChartID17e4815dd498c() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\n\u003c!-- jsChart --\u003e \n\n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartBubbleChartID17e4815dd498c\"\u003e\u003c/script\u003e\n \n\n\u003c!-- divChart --\u003e\n\n\u003cdiv id\u003d\"BubbleChartID17e4815dd498c\" style\u003d\"width: 500; height: automatic;\"\u003e\n\u003c/div\u003e\n\n\n\n" + "data": "\n\u003c!-- BubbleChart generated in R 3.4.2 by googleVis 0.6.2 package --\u003e\n\n\u003c!-- Thu Dec 7 18:40:37 2017 --\u003e\n\n\u003c!-- jsHeader --\u003e\n\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataBubbleChartID7e7a5b5fd722 () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"Apples\",\n98,\n78,\n\"2008\",\n20\n],\n[\n\"Apples\",\n111,\n79,\n\"2009\",\n32\n],\n[\n\"Apples\",\n89,\n76,\n\"2010\",\n13\n],\n[\n\"Oranges\",\n96,\n81,\n\"2008\",\n15\n],\n[\n\"Bananas\",\n85,\n76,\n\"2008\",\n9\n],\n[\n\"Oranges\",\n93,\n80,\n\"2009\",\n13\n],\n[\n\"Bananas\",\n94,\n78,\n\"2009\",\n16\n],\n[\n\"Oranges\",\n98,\n91,\n\"2010\",\n7\n],\n[\n\"Bananas\",\n81,\n71,\n\"2010\",\n10\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027Fruit\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Sales\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Expenses\u0027);\ndata.addColumn(\u0027string\u0027,\u0027Year\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Profit\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartBubbleChartID7e7a5b5fd722() {\nvar data \u003d gvisDataBubbleChartID7e7a5b5fd722();\nvar options \u003d {};\noptions[\"hAxis\"] \u003d {minValue:75, maxValue:125};\n\n var chart \u003d new google.visualization.BubbleChart(\n document.getElementById(\u0027BubbleChartID7e7a5b5fd722\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"corechart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartBubbleChartID7e7a5b5fd722);\n})();\nfunction displayChartBubbleChartID7e7a5b5fd722() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\n\u003c!-- jsChart --\u003e \n\n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartBubbleChartID7e7a5b5fd722\"\u003e\u003c/script\u003e\n \n\n\u003c!-- divChart --\u003e\n\n\u003cdiv id\u003d\"BubbleChartID7e7a5b5fd722\" style\u003d\"width: 500; height: automatic;\"\u003e\n\u003c/div\u003e\n\n\n\n" } ] }, "apps": [], "jobName": "paragraph_1455141578555_-1713165000", "id": "20160210-225938_1538591791", - "dateCreated": "Feb 10, 2016 10:59:38 AM", - "dateStarted": "Jan 29, 2017 3:14:35 AM", - "dateFinished": "Jan 29, 2017 3:14:35 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "GoogleViz: Geo Chart", - "text": "%r\nlibrary(googleVis)\ngeo \u003d gvisGeoChart(Exports, locationvar \u003d \"Country\", colorvar\u003d\"Profit\", options\u003dlist(Projection \u003d \"kavrayskiy-vii\"))\nprint(geo, tag \u003d \u0027chart\u0027)", + "text": "%spark.r\n\n# Workaround for Spark issue with googleVis: SPARK-23780\ndetach(\"package:SparkR\")\nlibrary(googleVis)\nsuppressPackageStartupMessages(library(SparkR))\n\ngeo \u003d gvisGeoChart(Exports, locationvar \u003d \"Country\", colorvar\u003d\"Profit\", options\u003dlist(Projection \u003d \"kavrayskiy-vii\"))\nprint(geo, tag \u003d \u0027chart\u0027)", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 3:14:38 AM", "config": { "colWidth": 6.0, "enabled": true, @@ -1227,23 +1270,19 @@ "msg": [ { "type": "HTML", - "data": "\n\u003c!-- GeoChart generated in R 3.3.2 by googleVis 0.6.2 package --\u003e\n\n\u003c!-- Sun Jan 29 03:14:38 2017 --\u003e\n\n\u003c!-- jsHeader --\u003e\n\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataGeoChartID17e485996beba () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"Germany\",\n3\n],\n[\n\"Brazil\",\n4\n],\n[\n\"United States\",\n5\n],\n[\n\"France\",\n4\n],\n[\n\"Hungary\",\n3\n],\n[\n\"India\",\n2\n],\n[\n\"Iceland\",\n1\n],\n[\n\"Norway\",\n4\n],\n[\n\"Spain\",\n5\n],\n[\n\"Turkey\",\n1\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027Country\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Profit\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartGeoChartID17e485996beba() {\nvar data \u003d gvisDataGeoChartID17e485996beba();\nvar options \u003d {};\noptions[\"width\"] \u003d 556;\noptions[\"height\"] \u003d 347;\noptions[\"Projection\"] \u003d \"kavrayskiy-vii\";\n\n var chart \u003d new google.visualization.GeoChart(\n document.getElementById(\u0027GeoChartID17e485996beba\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"geochart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartGeoChartID17e485996beba);\n})();\nfunction displayChartGeoChartID17e485996beba() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\n\u003c!-- jsChart --\u003e \n\n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartGeoChartID17e485996beba\"\u003e\u003c/script\u003e\n \n\n\u003c!-- divChart --\u003e\n\n\u003cdiv id\u003d\"GeoChartID17e485996beba\" style\u003d\"width: 556; height: 347;\"\u003e\n\u003c/div\u003e\n\n\n\n" + "data": "\n\u003c!-- GeoChart generated in R 3.4.2 by googleVis 0.6.2 package --\u003e\n\n\u003c!-- Thu Dec 7 18:40:38 2017 --\u003e\n\n\u003c!-- jsHeader --\u003e\n\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataGeoChartID7e7a250c9d79 () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"Germany\",\n3\n],\n[\n\"Brazil\",\n4\n],\n[\n\"United States\",\n5\n],\n[\n\"France\",\n4\n],\n[\n\"Hungary\",\n3\n],\n[\n\"India\",\n2\n],\n[\n\"Iceland\",\n1\n],\n[\n\"Norway\",\n4\n],\n[\n\"Spain\",\n5\n],\n[\n\"Turkey\",\n1\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027Country\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Profit\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartGeoChartID7e7a250c9d79() {\nvar data \u003d gvisDataGeoChartID7e7a250c9d79();\nvar options \u003d {};\noptions[\"width\"] \u003d 556;\noptions[\"height\"] \u003d 347;\noptions[\"Projection\"] \u003d \"kavrayskiy-vii\";\n\n var chart \u003d new google.visualization.GeoChart(\n document.getElementById(\u0027GeoChartID7e7a250c9d79\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"geochart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartGeoChartID7e7a250c9d79);\n})();\nfunction displayChartGeoChartID7e7a250c9d79() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\n\u003c!-- jsChart --\u003e \n\n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartGeoChartID7e7a250c9d79\"\u003e\u003c/script\u003e\n \n\n\u003c!-- divChart --\u003e\n\n\u003cdiv id\u003d\"GeoChartID7e7a250c9d79\" style\u003d\"width: 556; height: 347;\"\u003e\n\u003c/div\u003e\n\n\n\n" } ] }, "apps": [], "jobName": "paragraph_1455140544963_1486338978", "id": "20160210-224224_735421242", - "dateCreated": "Feb 10, 2016 10:42:24 AM", - "dateStarted": "Jan 29, 2017 3:14:38 AM", - "dateFinished": "Jan 29, 2017 3:14:38 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%md\n\n## Congratulations, it\u0027s done.\n### You can create your own notebook in \u0027Notebook\u0027 menu. Good luck!", "user": "anonymous", - "dateUpdated": "Jan 29, 2017 3:12:06 AM", "config": { "colWidth": 12.0, "enabled": true, @@ -1272,36 +1311,13 @@ "apps": [], "jobName": "paragraph_1485626988585_-946362813", "id": "20170129-030948_1379298104", - "dateCreated": "Jan 29, 2017 3:09:48 AM", - "dateStarted": "Jan 29, 2017 3:12:06 AM", - "dateFinished": "Jan 29, 2017 3:12:06 AM", "status": "FINISHED", "progressUpdateIntervalMs": 500 } ], - "name": "Zeppelin Tutorial/R (SparkR)", + "name": "Zeppelin Tutorial/Spark • R (SparkR)", "id": "2BWJFTXKJ", - "angularObjects": { - "2C9A5UJ3F:shared_process": [], - "2C8D7ETGW:shared_process": [], - "2C9S19J4N:shared_process": [], - "2C734G5GU:shared_process": [], - "2C7F9HJVB:shared_process": [], - "2C7RMKT4T:shared_process": [], - "2C6UPN6XS:shared_process": [], - "2C8MCDYHM:shared_process": [], - "2C7GPJHE2:shared_process": [], - "2C81DYM51:shared_process": [], - "2C7Z5PJKA:shared_process": [], - "2C7KKEX6R:shared_process": [], - "2C8G679A7:shared_process": [], - "2C96T367K:shared_process": [], - "2C79TYUDA:shared_process": [], - "2C82EG3YP:shared_process": [], - "2C8C4BYC9:shared_process": [], - "2C77RB7Q2:shared_process": [], - "2C9ENJ461:shared_process": [] - }, + "angularObjects": {}, "config": { "looknfeel": "default" }, diff --git a/notebook/2BYEZ5EVK/note.json b/notebook/2BYEZ5EVK/note.json deleted file mode 100644 index 83a79135f7e..00000000000 --- a/notebook/2BYEZ5EVK/note.json +++ /dev/null @@ -1,887 +0,0 @@ -{ - "paragraphs": [ - { - "text": "%md\n\n### The [Apache Mahout](http://mahout.apache.org/)™ project\u0027s goal is to build an environment for quickly creating scalable performant machine learning applications.\n\n#### Apache Mahout software provides three major features:\n\n- A simple and extensible programming environment and framework for building scalable algorithms\n- A wide variety of premade algorithms for Scala + Apache Spark, H2O, Apache Flink\n- Samsara, a vector math experimentation environment with R-like syntax which works at scale\n\n#### In other words:\n\n*Apache Mahout provides a unified API for quickly creating machine learning algorithms on a variety of engines.*\n\n#### Getting Started\n\nApache Mahout is a collection of Libraries that enhance Apache Flink, Apache Spark, and others. Currently Zeppelin support the Flink and Spark Engines. A convenience script is provided to setup the nessecary imports and configurations to run Mahout on Spark and Flink. \n\nWe can use Apache Mahout\u0027s R-Like Domain Specific Language (DSL) inline with native Flink or Spark code. We must however, first declare a few imports that are different for Spark and Flink\n\n__References:__\n\n[Mahout-Samsara\u0027s In-Core Linear Algebra DSL Reference](http://mahout.apache.org/users/environment/in-core-reference.html)\n[Mahout-Samsara\u0027s Distributed Linear Algebra DSL Reference](http://mahout.apache.org/users/environment/out-of-core-reference.html)\n[Getting Started with the Mahout-Samsara Shell](http://mahout.apache.org/users/sparkbindings/play-with-shell.html)\n", - "dateUpdated": "Sep 28, 2016 10:01:52 AM", - "config": { - "colWidth": 12.0, - "enabled": true, - "editorMode": "ace/mode/scala", - "editorHide": true, - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475013396125_39313566", - "id": "20160927-155636_1798325301", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003ch3\u003eThe \u003ca href\u003d\"http://mahout.apache.org/\"\u003eApache Mahout\u003c/a\u003e™ project\u0027s goal is to build an environment for quickly creating scalable performant machine learning applications.\u003c/h3\u003e\n\u003ch4\u003eApache Mahout software provides three major features:\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eA simple and extensible programming environment and framework for building scalable algorithms\u003c/li\u003e\n\u003cli\u003eA wide variety of premade algorithms for Scala + Apache Spark, H2O, Apache Flink\u003c/li\u003e\n\u003cli\u003eSamsara, a vector math experimentation environment with R-like syntax which works at scale\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4\u003eIn other words:\u003c/h4\u003e\n\u003cp\u003e\u003cem\u003eApache Mahout provides a unified API for quickly creating machine learning algorithms on a variety of engines.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eReferences:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href\u003d\"http://mahout.apache.org/users/environment/in-core-reference.html\"\u003eMahout-Samsara\u0027s In-Core Linear Algebra DSL Reference\u003c/a\u003e\n\u003cbr /\u003e\u003ca href\u003d\"http://mahout.apache.org/users/environment/out-of-core-reference.html\"\u003eMahout-Samsara\u0027s Distributed Linear Algebra DSL Reference\u003c/a\u003e\n\u003cbr /\u003e\u003ca href\u003d\"http://mahout.apache.org/users/sparkbindings/play-with-shell.html\"\u003eGetting Started with the Mahout-Samsara Shell\u003c/a\u003e\u003c/p\u003e\n" - } - ] - }, - "dateCreated": "Sep 27, 2016 3:56:36 AM", - "dateStarted": "Sep 27, 2016 4:02:55 AM", - "dateFinished": "Sep 27, 2016 4:02:55 AM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%md\n\n#### \"Installing\" the Apache Mahout dependencies and configuring a new Spark and Flink interpreter\n\nThe following two paragraphs are convenience paragraphs. You **only need to run them once** to create two new interpreters `%spark.mahout` and `%flink.mahout`. These are intended for users who don\u0027t have Apache Mahout already installed. They assume you started Apache Zeppelin from the top level directory or from the bin. You can tell which one is you by weather you started Zeppelin by typing `./zeppelin-daemon.sh start` or `bin/zeppelin-daemon.sh start`. If you started Zeppelin from somewhere else you will also need to run them from the command line.\n\nThey both run a python script which may be found at `ZEPPELIN_HOME/scripts/mahout/add_mahout.py`\n\nIn short this script:\n- Downloads Apache Mahout\n- Creates a new Flink interpreter with dependencies.\n- Creates a new Spark interpreter with dependencies and modified configuration to use Kryo serialization.\n\n__You only need to run this script once ever.__ (Maybe again if for some reason you delete `conf/interpreter.json`) \n", - "dateUpdated": "Sep 27, 2016 4:31:15 AM", - "config": { - "colWidth": 12.0, - "enabled": true, - "editorMode": "ace/mode/scala", - "editorHide": true, - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475015019489_-1704057033", - "id": "20160927-162339_341514150", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003ch4\u003e\u0026ldquo;Installing\u0026rdquo; the Apache Mahout dependencies and configuring a new Spark and Flink interpreter\u003c/h4\u003e\n\u003cp\u003eThe following two paragraphs are convenience paragraphs. You \u003cstrong\u003eonly need to run them once\u003c/strong\u003e to create two new interpreters \u003ccode\u003e%spark.mahout\u003c/code\u003e and \u003ccode\u003e%flink.mahout\u003c/code\u003e. These are intended for users who don\u0027t have Apache Mahout already installed. They assume you started Apache Zeppelin from the top level directory or from the bin. You can tell which one is you by weather you started Zeppelin by typing \u003ccode\u003e./zeppelin-daemon.sh start\u003c/code\u003e or \u003ccode\u003ebin/zeppelin-daemon.sh start\u003c/code\u003e. If you started Zeppelin from somewhere else you will also need to run them from the command line.\u003c/p\u003e\n\u003cp\u003eThey both run a python script which may be found at \u003ccode\u003eZEPPELIN_HOME/scripts/mahout/add_mahout.py\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eIn short this script:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDownloads Apache Mahout\u003c/li\u003e\n\u003cli\u003eCreates a new Flink interpreter with dependencies.\u003c/li\u003e\n\u003cli\u003eCreates a new Spark interpreter with dependencies and modified configuration to use Kryo serialization.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eYou only need to run this script once ever.\u003c/strong\u003e (Maybe again if for some reason you delete \u003ccode\u003econf/interpreter.json\u003c/code\u003e)\u003c/p\u003e\n" - } - ] - }, - "dateCreated": "Sep 27, 2016 4:23:39 AM", - "dateStarted": "Sep 27, 2016 4:31:12 AM", - "dateFinished": "Sep 27, 2016 4:31:13 AM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "title": "Convenience Paragraph if you started Zeppelin by \u0027./zeppelin-daemon.sh start\u0027", - "text": "%sh\n\npython ../scripts/mahout/add_mahout.py", - "dateUpdated": "Dec 17, 2016 3:41:45 PM", - "config": { - "colWidth": 12.0, - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - }, - "enabled": true, - "editorMode": "ace/mode/sh", - "title": true, - "results": {}, - "editorSetting": { - "language": "sh", - "editOnDblClick": false - } - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475014957043_-748248820", - "id": "20160927-162237_1864782562", - "dateCreated": "Sep 27, 2016 4:22:37 AM", - "status": "READY", - "progressUpdateIntervalMs": 500 - }, - { - "title": "Convenience Paragraph if you started Zeppelin by \u0027bin/zeppelin-daemon.sh start\u0027", - "text": "%sh\npython scripts/mahout/add_mahout_interpreters.py", - "dateUpdated": "Dec 17, 2016 3:41:46 PM", - "config": { - "colWidth": 12.0, - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - }, - "enabled": true, - "editorMode": "ace/mode/sh", - "title": true, - "results": {}, - "editorSetting": { - "language": "sh", - "editOnDblClick": false - } - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475018789604_-139338572", - "id": "20160927-172629_1189436716", - "dateCreated": "Sep 27, 2016 5:26:29 AM", - "status": "READY", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%md\n\nAfter the interpreters are created you will need to \u0027bind\u0027 them by clicking on the little gear in the top right corner, scrolling to the top, and clicking on `mahoutFlink` and `mahoutSpark` so that they are highlighted in blue.\n\n#### Running Mahout code\n\nYou will need to import certain libraries, and declare the _Mahout Distributed Context_ when you first start your notebook using the interpreters. \n\nIf using Apache Flink the code you need to run is:\n```scala\n%flinkMahout\n\nimport org.apache.flink.api.scala._\nimport org.apache.mahout.math.drm._\nimport org.apache.mahout.math.drm.RLikeDrmOps._\nimport org.apache.mahout.flinkbindings._\nimport org.apache.mahout.math._\nimport scalabindings._\nimport RLikeOps._\n\n\nimplicit val ctx \u003d new FlinkDistributedContext(benv)\n```\n\nIf using Apache Spark the code you need to run is\n```scala\n%sparkMahout\n\nimport org.apache.mahout.math._\nimport org.apache.mahout.math.scalabindings._\nimport org.apache.mahout.math.drm._\nimport org.apache.mahout.math.scalabindings.RLikeOps._\nimport org.apache.mahout.math.drm.RLikeDrmOps._\nimport org.apache.mahout.sparkbindings._\n\nimplicit val sdc: org.apache.mahout.sparkbindings.SparkDistributedContext \u003d sc2sdc(sc)\n```\n\n__Note: For Apache Mahout on Apache Spark you must be running Spark 1.5.x or 1.6.x. We are working hard on supporting Spark 2.0__\nIn the meantime, feel free to play with Mahout on Flink and then simple _copy and paste your Mahout code to Spark once it is supported!_\n\n### A Side by Side Example\n", - "dateUpdated": "Sep 28, 2016 12:36:44 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "editorMode": "ace/mode/markdown", - "editorHide": true, - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475014730618_1513783554", - "id": "20160927-161850_1560940440", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003cp\u003eAfter the interpreters are created you will need to \u0027bind\u0027 them by clicking on the little gear in the top right corner, scrolling to the top, and clicking on \u003ccode\u003emahoutFlink\u003c/code\u003e and \u003ccode\u003emahoutSpark\u003c/code\u003e so that they are highlighted in blue.\u003c/p\u003e\n\u003ch4\u003eRunning Mahout code\u003c/h4\u003e\n\u003cp\u003eYou will need to import certain libraries, and declare the \u003cem\u003eMahout Distributed Context\u003c/em\u003e when you first start your notebook using the interpreters.\u003c/p\u003e\n\u003cp\u003eIf using Apache Flink the code you need to run is:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class\u003d\"scala\"\u003e%flinkMahout\n\nimport org.apache.flink.api.scala._\nimport org.apache.mahout.math.drm._\nimport org.apache.mahout.math.drm.RLikeDrmOps._\nimport org.apache.mahout.flinkbindings._\nimport org.apache.mahout.math._\nimport scalabindings._\nimport RLikeOps._\n\n\n@transient implicit val ctx \u003d new FlinkDistributedContext(benv)\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eIf using Apache Spark the code you need to run is\u003c/p\u003e\n\u003cpre\u003e\u003ccode class\u003d\"scala\"\u003e%sparkMahout\n\nimport org.apache.mahout.math._\nimport org.apache.mahout.math.scalabindings._\nimport org.apache.mahout.math.drm._\nimport org.apache.mahout.math.scalabindings.RLikeOps._\nimport org.apache.mahout.math.drm.RLikeDrmOps._\nimport org.apache.mahout.sparkbindings._\n\nimplicit val sdc: org.apache.mahout.sparkbindings.SparkDistributedContext \u003d sc2sdc(sc)\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eNote: For Apache Mahout on Apache Spark you must be running Spark 1.5.x or 1.6.x. We are working hard on supporting Spark 2.0\u003c/strong\u003e\n\u003cbr /\u003eIn the meantime, feel free to play with Mahout on Flink and then simple \u003cem\u003ecopy and paste your Mahout code to Spark once it is supported!\u003c/em\u003e\u003c/p\u003e\n\u003ch3\u003eA Side by Side Example\u003c/h3\u003e\n" - } - ] - }, - "dateCreated": "Sep 27, 2016 4:18:50 AM", - "dateStarted": "Sep 28, 2016 10:17:05 AM", - "dateFinished": "Sep 28, 2016 10:17:06 AM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%flinkMahout\n\n// Imports and creating the distributed context, similar but not exactly the same ///////////////////////////////////////////\nimport org.apache.flink.api.scala._\nimport org.apache.mahout.math.drm._\nimport org.apache.mahout.math.drm.RLikeDrmOps._\nimport org.apache.mahout.flinkbindings._\nimport org.apache.mahout.math._\nimport scalabindings._\nimport RLikeOps._\n\n\nimplicit val ctx \u003d new FlinkDistributedContext(benv)\n\n// CODE IS EXACTLY THE SAME FROM HERE ON - R-Like DSL ////////////////////////////////////////////////////////////////////////////////\n\nval drmData \u003d drmParallelize(dense(\n (2, 2, 10.5, 10, 29.509541), // Apple Cinnamon Cheerios\n (1, 2, 12, 12, 18.042851), // Cap\u0027n\u0027Crunch\n (1, 1, 12, 13, 22.736446), // Cocoa Puffs\n (2, 1, 11, 13, 32.207582), // Froot Loops\n (1, 2, 12, 11, 21.871292), // Honey Graham Ohs\n (2, 1, 16, 8, 36.187559), // Wheaties Honey Gold\n (6, 2, 17, 1, 50.764999), // Cheerios\n (3, 2, 13, 7, 40.400208), // Clusters\n (3, 3, 13, 4, 45.811716)), numPartitions \u003d 2)\n \ndrmData.collect(::, 0 until 4)\n\nval drmX \u003d drmData(::, 0 until 4)\nval y \u003d drmData.collect(::, 4)\nval drmXtX \u003d drmX.t %*% drmX\nval drmXty \u003d drmX.t %*% y\n\n\nval XtX \u003d drmXtX.collect\nval Xty \u003d drmXty.collect(::, 0)\nval beta \u003d solve(XtX, Xty)\n\n", - "dateUpdated": "Sep 28, 2016 1:41:59 PM", - "config": { - "colWidth": 6.0, - "enabled": true, - "editorMode": "ace/mode/markdown", - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475015779325_-1869239670", - "id": "20160927-163619_899520006", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "import org.apache.flink.api.scala._\nimport org.apache.mahout.math.drm._\nimport org.apache.mahout.math.drm.RLikeDrmOps._\nimport org.apache.mahout.flinkbindings._\nimport org.apache.mahout.math._\nimport scalabindings._\nimport RLikeOps._\nctx: org.apache.mahout.flinkbindings.FlinkDistributedContext \u003d org.apache.mahout.flinkbindings.FlinkDistributedContext@4452b0a5\nwarning: Class it.unimi.dsi.fastutil.ints.Int2DoubleOpenHashMap not found - continuing with a stub.\ndrmData: org.apache.mahout.math.drm.CheckpointedDrm[Int] \u003d org.apache.mahout.flinkbindings.drm.CheckpointedFlinkDrm@445242be\n(5,9)\nres1: org.apache.mahout.math.Matrix \u003d \n{\n 0 \u003d\u003e\t{0:2.0,1:2.0,2:10.5,3:10.0}\n 1 \u003d\u003e\t{0:1.0,1:2.0,2:12.0,3:12.0}\n 2 \u003d\u003e\t{0:1.0,1:1.0,2:12.0,3:13.0}\n 3 \u003d\u003e\t{0:2.0,1:1.0,2:11.0,3:13.0}\n 4 \u003d\u003e\t{0:1.0,1:2.0,2:12.0,3:11.0}\n 5 \u003d\u003e\t{0:2.0,1:1.0,2:16.0,3:8.0}\n 6 \u003d\u003e\t{0:6.0,1:2.0,2:17.0,3:1.0}\n 7 \u003d\u003e\t{0:3.0,1:2.0,2:13.0,3:7.0}\n 8 \u003d\u003e\t{0:3.0,1:3.0,2:13.0,3:4.0}\n}\ndrmX: org.apache.mahout.math.drm.DrmLike[Int] \u003d OpMapBlock(org.apache.mahout.flinkbindings.drm.CheckpointedFlinkDrm@445242be,\u003cfunction1\u003e,4,-1,true)\n(5,9)\ny: org.apache.mahout.math.Vector \u003d {0:29.509541,1:18.042851,2:22.736446,3:32.207582,4:21.871292,5:36.187559,6:50.764999,7:40.400208,8:45.811716}\ndrmXtX: org.apache.mahout.math.drm.DrmLike[Int] \u003d OpABAnyKey(OpAt(OpMapBlock(org.apache.mahout.flinkbindings.drm.CheckpointedFlinkDrm@445242be,\u003cfunction1\u003e,4,-1,true)),OpMapBlock(org.apache.mahout.flinkbindings.drm.CheckpointedFlinkDrm@445242be,\u003cfunction1\u003e,4,-1,true))\ndrmXty: org.apache.mahout.math.drm.DrmLike[Int] \u003d OpAx(OpAt(OpMapBlock(org.apache.mahout.flinkbindings.drm.CheckpointedFlinkDrm@445242be,\u003cfunction1\u003e,4,-1,true)),{0:29.509541,1:18.042851,2:22.736446,3:32.207582,4:21.871292,5:36.187559,6:50.764999,7:40.400208,8:45.811716})\n(4,4)\nXtX: org.apache.mahout.math.Matrix \u003d \n{\n 0 \u003d\u003e\t{0:69.0,1:40.0,2:291.0,3:137.0}\n 1 \u003d\u003e\t{0:40.0,1:32.0,2:207.0,3:128.0}\n 2 \u003d\u003e\t{0:291.0,1:207.0,2:1546.25,3:968.0}\n 3 \u003d\u003e\t{0:137.0,1:128.0,2:968.0,3:833.0}\n}\n(1,4)\nXty: org.apache.mahout.math.Vector \u003d {0:821.6857190000001,1:549.744517,2:3978.7015895000004,3:2272.7799889999997}\nbeta: org.apache.mahout.math.Vector \u003d {0:5.247349465378393,1:2.7507945784675067,2:1.1527813010791783,3:0.10312017617607437}\n" - } - ] - }, - "dateCreated": "Sep 27, 2016 4:36:19 AM", - "dateStarted": "Sep 28, 2016 1:41:59 PM", - "dateFinished": "Sep 28, 2016 1:42:25 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%sparkMahout\n\n// Imports and creating the distributed context, similar but not exactly the same ///////////////////////////////////////////\n\nimport org.apache.mahout.math._\nimport org.apache.mahout.math.scalabindings._\nimport org.apache.mahout.math.drm._\nimport org.apache.mahout.math.scalabindings.RLikeOps._\nimport org.apache.mahout.math.drm.RLikeDrmOps._\nimport org.apache.mahout.sparkbindings._\n\nimplicit val sdc: org.apache.mahout.sparkbindings.SparkDistributedContext \u003d sc2sdc(sc)\n\n\n// CODE IS EXACTLY THE SAME FROM HERE ON - R-Like DSL ////////////////////////////////////////////////////////////////////////////////\n\nval drmData \u003d drmParallelize(dense(\n (2, 2, 10.5, 10, 29.509541), // Apple Cinnamon Cheerios\n (1, 2, 12, 12, 18.042851), // Cap\u0027n\u0027Crunch\n (1, 1, 12, 13, 22.736446), // Cocoa Puffs\n (2, 1, 11, 13, 32.207582), // Froot Loops\n (1, 2, 12, 11, 21.871292), // Honey Graham Ohs\n (2, 1, 16, 8, 36.187559), // Wheaties Honey Gold\n (6, 2, 17, 1, 50.764999), // Cheerios\n (3, 2, 13, 7, 40.400208), // Clusters\n (3, 3, 13, 4, 45.811716)), numPartitions \u003d 2)\n \ndrmData.collect(::, 0 until 4)\n\nval drmX \u003d drmData(::, 0 until 4)\nval y \u003d drmData.collect(::, 4)\nval drmXtX \u003d drmX.t %*% drmX\nval drmXty \u003d drmX.t %*% y\n\n\nval XtX \u003d drmXtX.collect\nval Xty \u003d drmXty.collect(::, 0)\nval beta \u003d solve(XtX, Xty)\n", - "dateUpdated": "Sep 28, 2016 1:45:09 PM", - "config": { - "colWidth": 6.0, - "enabled": true, - "editorMode": "ace/mode/scala", - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475016737629_-774084480", - "id": "20160927-165217_1266863511", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "\nimport org.apache.mahout.math._\n\nimport org.apache.mahout.math.scalabindings._\n\nimport org.apache.mahout.math.drm._\n\nimport org.apache.mahout.math.scalabindings.RLikeOps._\n\nimport org.apache.mahout.math.drm.RLikeDrmOps._\n\nimport org.apache.mahout.sparkbindings._\n\nsdc: org.apache.mahout.sparkbindings.SparkDistributedContext \u003d org.apache.mahout.sparkbindings.SparkDistributedContext@32c46474\n\ndrmData: org.apache.mahout.math.drm.CheckpointedDrm[Int] \u003d org.apache.mahout.sparkbindings.drm.CheckpointedDrmSpark@783484b9\n\n\n\n\n\n\n\n\n\n\n\n\nres2: org.apache.mahout.math.Matrix \u003d \n{\n 0 \u003d\u003e\t{0:2.0,1:2.0,2:10.5,3:10.0}\n 1 \u003d\u003e\t{0:1.0,1:2.0,2:12.0,3:12.0}\n 2 \u003d\u003e\t{0:1.0,1:1.0,2:12.0,3:13.0}\n 3 \u003d\u003e\t{0:2.0,1:1.0,2:11.0,3:13.0}\n 4 \u003d\u003e\t{0:1.0,1:2.0,2:12.0,3:11.0}\n 5 \u003d\u003e\t{0:2.0,1:1.0,2:16.0,3:8.0}\n 6 \u003d\u003e\t{0:6.0,1:2.0,2:17.0,3:1.0}\n 7 \u003d\u003e\t{0:3.0,1:2.0,2:13.0,3:7.0}\n 8 \u003d\u003e\t{0:3.0,1:3.0,2:13.0,3:4.0}\n}\n\ndrmX: org.apache.mahout.math.drm.DrmLike[Int] \u003d OpMapBlock(org.apache.mahout.sparkbindings.drm.CheckpointedDrmSpark@783484b9,\u003cfunction1\u003e,4,-1,true)\n\ny: org.apache.mahout.math.Vector \u003d {0:29.509541,1:18.042851,2:22.736446,3:32.207582,4:21.871292,5:36.187559,6:50.764999,7:40.400208,8:45.811716}\n\ndrmXtX: org.apache.mahout.math.drm.DrmLike[Int] \u003d OpABAnyKey(OpAt(OpMapBlock(org.apache.mahout.sparkbindings.drm.CheckpointedDrmSpark@783484b9,\u003cfunction1\u003e,4,-1,true)),OpMapBlock(org.apache.mahout.sparkbindings.drm.CheckpointedDrmSpark@783484b9,\u003cfunction1\u003e,4,-1,true))\n\ndrmXty: org.apache.mahout.math.drm.DrmLike[Int] \u003d OpAx(OpAt(OpMapBlock(org.apache.mahout.sparkbindings.drm.CheckpointedDrmSpark@783484b9,\u003cfunction1\u003e,4,-1,true)),{0:29.509541,1:18.042851,2:22.736446,3:32.207582,4:21.871292,5:36.187559,6:50.764999,7:40.400208,8:45.811716})\n\n\n\n\n\n\n\nXtX: org.apache.mahout.math.Matrix \u003d \n{\n 0 \u003d\u003e\t{0:69.0,1:40.0,2:291.0,3:137.0}\n 1 \u003d\u003e\t{0:40.0,1:32.0,2:207.0,3:128.0}\n 2 \u003d\u003e\t{0:291.0,1:207.0,2:1546.25,3:968.0}\n 3 \u003d\u003e\t{0:137.0,1:128.0,2:968.0,3:833.0}\n}\n\nXty: org.apache.mahout.math.Vector \u003d {0:821.6857190000001,1:549.744517,2:3978.7015894999995,3:2272.779989}\n\nbeta: org.apache.mahout.math.Vector \u003d {0:5.247349465378446,1:2.750794578467531,2:1.1527813010791554,3:0.10312017617608908}\n" - } - ] - }, - "dateCreated": "Sep 27, 2016 4:52:17 AM", - "dateStarted": "Sep 28, 2016 1:45:09 PM", - "dateFinished": "Sep 28, 2016 1:45:23 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "title": "Use Resource Pools with Zeppelin", - "text": "%md\n\n### Taking advantage of Zeppelin Resource Pools\n\nOne of the major motivations for integrating Apache Mahout with Apache Zeppelin was the many benefits that come from leveraging the resource pools. A resource pool is a block of memory that can be acccessed by all interpreters and is useful for sharing small variables between the interpreters. \n\nThe Spark interpreter has a simple interface for accessing the ResourcePools, the Flink interface is less documented but can be reverse engineered from code (thanks open source!)\n\n\nCollect betas from Spark and Flink- compare in Python\n\nCreate Matrix in Flink and Spark - visualize with R", - "dateUpdated": "Sep 27, 2016 5:55:31 AM", - "config": { - "colWidth": 12.0, - "enabled": true, - "title": true, - "editorMode": "ace/mode/markdown", - "editorHide": true, - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475016792277_-1100474141", - "id": "20160927-165312_1668894932", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003ch3\u003eTaking advantage of Zeppelin Resource Pools\u003c/h3\u003e\n\u003cp\u003eOne of the major motivations for integrating Apache Mahout with Apache Zeppelin was the many benefits that come from leveraging the resource pools. A resource pool is a block of memory that can be acccessed by all interpreters and is useful for sharing small variables between the interpreters.\u003c/p\u003e\n\u003cp\u003eThe Spark interpreter has a simple interface for accessing the ResourcePools, the Flink interface is less documented but can be reverse engineered from code (thanks open source!)\u003c/p\u003e\n\u003cp\u003eCollect betas from Spark and Flink- compare in Python\u003c/p\u003e\n\u003cp\u003eCreate Matrix in Flink and Spark - visualize with R\u003c/p\u003e\n" - } - ] - }, - "dateCreated": "Sep 27, 2016 4:53:12 AM", - "dateStarted": "Sep 27, 2016 5:40:35 AM", - "dateFinished": "Sep 27, 2016 5:40:36 AM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "title": "Flink ResourcePools", - "text": "%flinkMahout\n\nimport org.apache.zeppelin.interpreter.InterpreterContext\n\nval resourcePool \u003d InterpreterContext.get().getResourcePool()\n\nresourcePool.put(\"flinkBeta\", beta.asFormatString)\n", - "dateUpdated": "Sep 28, 2016 1:42:35 PM", - "config": { - "colWidth": 6.0, - "enabled": true, - "editorMode": "ace/mode/scala", - "title": true, - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475019635571_-1705373112", - "id": "20160927-174035_1591078106", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "import org.apache.zeppelin.interpreter.InterpreterContext\nresourcePool: org.apache.zeppelin.resource.ResourcePool \u003d org.apache.zeppelin.resource.DistributedResourcePool@3fdd93cc\n" - } - ] - }, - "dateCreated": "Sep 27, 2016 5:40:35 AM", - "dateStarted": "Sep 28, 2016 1:42:35 PM", - "dateFinished": "Sep 28, 2016 1:42:36 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "title": "Spark ResourcePools", - "text": "%sparkMahout\n\n\n\n\nz.put(\"sparkBeta\", beta.asFormatString)", - "dateUpdated": "Sep 28, 2016 1:45:35 PM", - "config": { - "colWidth": 6.0, - "enabled": true, - "editorMode": "ace/mode/scala", - "title": true, - "results": [] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475019751650_-1885234738", - "id": "20160927-174231_1288588876", - "results": { - "code": "SUCCESS", - "msg": [] - }, - "dateCreated": "Sep 27, 2016 5:42:31 AM", - "dateStarted": "Sep 28, 2016 1:45:35 PM", - "dateFinished": "Sep 28, 2016 1:45:36 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "title": "Collect Results in Python and Evaluate Differences", - "text": "%spark.pyspark\n\nimport ast\n\nflinkBetaDict \u003d ast.literal_eval(z.get(\"flinkBeta\"))\nsparkBetaDict \u003d ast.literal_eval(z.get(\"sparkBeta\"))\n\nprint \"----------------- differences between betas calulated in Flink and Spark-----------------\"\nfor i in range(0,4):\n print \"beta\", i, \": \" , flinkBetaDict[i] - sparkBetaDict[i]", - "dateUpdated": "Sep 28, 2016 1:45:37 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "editorMode": "ace/mode/python", - "title": true, - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475020470280_1661203311", - "id": "20160927-175430_1451783515", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "----------------- differences between betas calulated in Flink and Spark-----------------\nbeta 0 : -5.24025267623e-14\nbeta 1 : -2.44249065418e-14\nbeta 2 : 2.28705943073e-14\nbeta 3 : -1.47104550763e-14\n" - } - ] - }, - "dateCreated": "Sep 27, 2016 5:54:30 AM", - "dateStarted": "Sep 28, 2016 1:45:38 PM", - "dateFinished": "Sep 28, 2016 1:45:38 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%md\n\n## Plotting Mahout with R\n\nThe following examples show how we can leverage R to plot our results from Mahout\n", - "dateUpdated": "Sep 28, 2016 12:34:33 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "editorMode": "ace/mode/markdown", - "editorHide": true, - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475087633007_-566041383", - "id": "20160928-123353_147363530", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003ch2\u003ePlotting Mahout with R\u003c/h2\u003e\n\u003cp\u003eThe following examples show how we can leverage R to plot our results from Mahout\u003c/p\u003e\n" - } - ] - }, - "dateCreated": "Sep 28, 2016 12:33:53 PM", - "dateStarted": "Sep 28, 2016 12:34:30 PM", - "dateFinished": "Sep 28, 2016 12:34:30 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%flinkMahout\nval mxRnd \u003d Matrices.symmetricUniformView(5000, 2, 1234)\nval drmRand \u003d drmParallelize(mxRnd)\n\n\nval drmSin \u003d drmRand.mapBlock() {case (keys, block) \u003d\u003e \n val blockB \u003d block.like()\n for (i \u003c- 0 until block.nrow) {\n blockB(i, 0) \u003d block(i, 0) \n blockB(i, 1) \u003d Math.sin((block(i, 0) * 8))\n }\n keys -\u003e blockB\n}\n\nresourcePool.put(\"flinkSinDrm\", drm.drmSampleToTSV(drmSin, 0.85))", - "dateUpdated": "Sep 28, 2016 1:52:44 PM", - "config": { - "colWidth": 6.0, - "enabled": true, - "editorMode": "ace/mode/scala", - "results": [ - { - "graph": { - "mode": "table", - "height": 284.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475020580886_2102494975", - "id": "20160927-175620_816809523", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "mxRnd: org.apache.mahout.math.Matrix \u003d \n{\n 0 \u003d\u003e\t{0:0.4586377101191827,1:0.07261898163580698}\n 1 \u003d\u003e\t{0:0.48977896201757654,1:0.2695201068510176}\n 2 \u003d\u003e\t{0:0.33215452109376786,1:0.2148377346657124}\n 3 \u003d\u003e\t{0:0.4497098649240723,1:0.4331127334380502}\n 4 \u003d\u003e\t{0:-0.03782634247193647,1:-0.32353833540588983}\n 5 \u003d\u003e\t{0:0.15137106418749705,1:0.422446220403861}\n 6 \u003d\u003e\t{0:0.2714115385692545,1:-0.4495233989067956}\n 7 \u003d\u003e\t{0:0.02468155133492185,1:0.49474128114887833}\n 8 \u003d\u003e\t{0:-0.2269662536373416,1:-0.14808249195411455}\n 9 \u003d\u003e\t{0:0.050870692759856756,1:-0.4797329808849356}\n... }\ndrmRand: org.apache.mahout.math.drm.CheckpointedDrm[Int] \u003d org.apache.mahout.flinkbindings.drm.CheckpointedFlinkDrm@72c5b7be\ndrmSin: org.apache.mahout.math.drm.DrmLike[Int] \u003d OpMapBlock(org.apache.mahout.flinkbindings.drm.CheckpointedFlinkDrm@72c5b7be,\u003cfunction1\u003e,-1,-1,true)\n(2,5000)\n" - } - ] - }, - "dateCreated": "Sep 27, 2016 5:56:20 AM", - "dateStarted": "Sep 28, 2016 1:42:42 PM", - "dateFinished": "Sep 28, 2016 1:42:52 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%sparkMahout\nval mxRnd \u003d Matrices.symmetricUniformView(5000, 2, 1234)\nval drmRand \u003d drmParallelize(mxRnd)\n\n\nval drmSin \u003d drmRand.mapBlock() {case (keys, block) \u003d\u003e \n val blockB \u003d block.like()\n for (i \u003c- 0 until block.nrow) {\n blockB(i, 0) \u003d block(i, 0) \n blockB(i, 1) \u003d Math.sin((block(i, 0) * 8))\n }\n keys -\u003e blockB\n}\n\nz.put(\"sparkSinDrm\", org.apache.mahout.math.drm.drmSampleToTSV(drmSin, 0.85))\n", - "dateUpdated": "Sep 27, 2016 6:38:39 AM", - "config": { - "colWidth": 6.0, - "enabled": true, - "editorMode": "ace/mode/scala", - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475021390512_-2030189316", - "id": "20160927-180950_1754833838", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "\n\n\n\n\n\n\n\n\n\n\n\n\nmxRnd: org.apache.mahout.math.Matrix \u003d \n{\n 0 \u003d\u003e\t{0:0.4586377101191827,1:0.07261898163580698}\n 1 \u003d\u003e\t{0:0.48977896201757654,1:0.2695201068510176}\n 2 \u003d\u003e\t{0:0.33215452109376786,1:0.2148377346657124}\n 3 \u003d\u003e\t{0:0.4497098649240723,1:0.4331127334380502}\n 4 \u003d\u003e\t{0:-0.03782634247193647,1:-0.32353833540588983}\n 5 \u003d\u003e\t{0:0.15137106418749705,1:0.422446220403861}\n 6 \u003d\u003e\t{0:0.2714115385692545,1:-0.4495233989067956}\n 7 \u003d\u003e\t{0:0.02468155133492185,1:0.49474128114887833}\n 8 \u003d\u003e\t{0:-0.2269662536373416,1:-0.14808249195411455}\n 9 \u003d\u003e\t{0:0.050870692759856756,1:-0.4797329808849356}\n... }\n\ndrmRand: org.apache.mahout.math.drm.CheckpointedDrm[Int] \u003d org.apache.mahout.sparkbindings.drm.CheckpointedDrmSpark@1d6a6ecf\n\ndrmSin: org.apache.mahout.math.drm.DrmLike[Int] \u003d OpMapBlock(org.apache.mahout.sparkbindings.drm.CheckpointedDrmSpark@1d6a6ecf,\u003cfunction1\u003e,-1,-1,true)\n" - } - ] - }, - "dateCreated": "Sep 27, 2016 6:09:50 AM", - "dateStarted": "Sep 27, 2016 6:38:39 AM", - "dateFinished": "Sep 27, 2016 6:38:40 AM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%spark.r {\"imageWidth\": \"400px\"}\n\nlibrary(\"ggplot2\")\n\nflinkSinStr \u003d z.get(\"flinkSinDrm\")\nsparkSinStr \u003d z.get(\"sparkSinDrm\")\n\nflinkData \u003c- read.table(text\u003d flinkSinStr, sep\u003d\"\\t\", header\u003dFALSE)\nsparkData \u003c- read.table(text\u003d sparkSinStr, sep\u003d\"\\t\", header\u003dFALSE)\n\nplot(flinkData, col\u003d\"red\")\n# Graph trucks with red dashed line and square points\npoints(sparkData, col\u003d\"blue\")\n\n# Create a title with a red, bold/italic font\ntitle(main\u003d\"Sampled Mahout Sin Graph in R\", col.main\u003d\"black\", font.main\u003d4)\n\nlegend(\"bottomright\", c(\"Apache Flink\", \"Apache Spark\"), col\u003d c(\"red\", \"blue\"), pch\u003d c(22, 22)) \n\n", - "dateUpdated": "Sep 28, 2016 1:52:26 PM", - "config": { - "colWidth": 6.0, - "enabled": true, - "editorMode": "ace/mode/r", - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475021654999_1062405375", - "id": "20160927-181414_1420533932", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003cp\u003e\u003cimg src\u003d\"data:image/png;base64,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\" alt\u003d\"plot of chunk unnamed-chunk-1\" width\u003d\"400px\" /\u003e\u003c/p\u003e" - } - ] - }, - "dateCreated": "Sep 27, 2016 6:14:14 AM", - "dateStarted": "Sep 27, 2016 6:42:20 AM", - "dateFinished": "Sep 27, 2016 6:42:20 AM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "title": "Create a Gaussian Matrix", - "text": "%flinkMahout\n\nval mxRnd3d \u003d Matrices.symmetricUniformView(5000, 3, 1234)\nval drmRand3d \u003d drmParallelize(mxRnd3d)\n\nval drmGauss \u003d drmRand3d.mapBlock() {case (keys, block) \u003d\u003e\n val blockB \u003d block.like()\n for (i \u003c- 0 until block.nrow) {\n val x: Double \u003d block(i, 0)\n val y: Double \u003d block(i, 1)\n val z: Double \u003d block(i, 2)\n\n blockB(i, 0) \u003d x\n blockB(i, 1) \u003d y\n blockB(i, 2) \u003d Math.exp(-((Math.pow(x, 2)) + (Math.pow(y, 2)))/2)\n }\n keys -\u003e blockB\n}\n\nresourcePool.put(\"flinkGaussDrm\", drm.drmSampleToTSV(drmGauss, 50.0))", - "dateUpdated": "Sep 28, 2016 1:53:22 PM", - "config": { - "colWidth": 6.0, - "enabled": true, - "editorMode": "ace/mode/scala", - "tableHide": true, - "title": true, - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475021740078_127388926", - "id": "20160927-181540_1706054053", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "mxRnd3d: org.apache.mahout.math.Matrix \u003d \n{\n 0 \u003d\u003e\t{0:0.4586377101191827,1:0.07261898163580698,2:-0.4120814898385057}\n 1 \u003d\u003e\t{0:0.48977896201757654,1:0.2695201068510176,2:0.2035624121801051}\n 2 \u003d\u003e\t{0:0.33215452109376786,1:0.2148377346657124,2:0.22923597484837382}\n 3 \u003d\u003e\t{0:0.4497098649240723,1:0.4331127334380502,2:-0.26063522630725094}\n 4 \u003d\u003e\t{0:-0.03782634247193647,1:-0.32353833540588983,2:-0.4423256266785404}\n 5 \u003d\u003e\t{0:0.15137106418749705,1:0.422446220403861,2:-0.20452218901606223}\n 6 \u003d\u003e\t{0:0.2714115385692545,1:-0.4495233989067956,2:0.13402344186662743}\n 7 \u003d\u003e\t{0:0.02468155133492185,1:0.49474128114887833,2:-0.484577970998106}\n 8 \u003d\u003e\t{0:-0.2269662536373416,1:-0.14808249195411455,2:-0.16159073199184967}\n 9 \u003d\u003e\t{0:0.050870692759856756,1:-0.4797329808849356,2:0.30230792168515175}\n... }\ndrmRand3d: org.apache.mahout.math.drm.CheckpointedDrm[Int] \u003d org.apache.mahout.flinkbindings.drm.CheckpointedFlinkDrm@448a1f4e\ndrmGauss: org.apache.mahout.math.drm.DrmLike[Int] \u003d OpMapBlock(org.apache.mahout.flinkbindings.drm.CheckpointedFlinkDrm@448a1f4e,\u003cfunction1\u003e,-1,-1,true)\n(3,5000)\n" - } - ] - }, - "dateCreated": "Sep 27, 2016 6:15:40 AM", - "dateStarted": "Sep 28, 2016 1:50:54 PM", - "dateFinished": "Sep 28, 2016 1:51:00 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%spark.r {\"imageWidth\": \"400px\"}\n\nlibrary(scatterplot3d)\n\n\nflinkGaussStr \u003d z.get(\"flinkGaussDrm\")\nflinkData \u003c- read.table(text\u003d flinkGaussStr, sep\u003d\"\\t\", header\u003dFALSE)\n\nscatterplot3d(flinkData, color\u003d\"green\")\n\n", - "dateUpdated": "Sep 28, 2016 1:54:56 PM", - "config": { - "colWidth": 6.0, - "enabled": true, - "editorMode": "ace/mode/r", - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475023444293_-1038534869", - "id": "20160927-184404_773885252", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003cp\u003e\u003cimg src\u003d\"data:image/png;base64,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\u003d\u003d\" alt\u003d\"plot of chunk unnamed-chunk-1\" width\u003d\"400px\" /\u003e\u003c/p\u003e" - } - ] - }, - "dateCreated": "Sep 27, 2016 6:44:04 AM", - "dateStarted": "Sep 28, 2016 1:52:10 PM", - "dateFinished": "Sep 28, 2016 1:52:10 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%md\n\n**NOTE** To install `scatterplot3d` on Ubuntu use:\n\n```sh\nsudo apt-get install r-cran-scatterplot3d\n```\n\n", - "dateUpdated": "Sep 28, 2016 1:54:37 PM", - "config": { - "colWidth": 6.0, - "enabled": true, - "editorMode": "ace/mode/markdown", - "editorHide": true, - "results": [ - { - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {}, - "map": { - "baseMapType": "Streets", - "isOnline": true, - "pinCols": [] - } - } - } - ] - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475091302527_1223653372", - "id": "20160928-133502_1743267136", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003cp\u003e\u003cstrong\u003eNOTE\u003c/strong\u003e To install \u003ccode\u003escatterplot3d\u003c/code\u003e on Ubuntu use:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class\u003d\"sh\"\u003esudo apt-get install r-cran-scatterplot3d\n\u003c/code\u003e\u003c/pre\u003e\n" - } - ] - }, - "dateCreated": "Sep 28, 2016 1:35:02 AM", - "dateStarted": "Sep 28, 2016 1:54:32 PM", - "dateFinished": "Sep 28, 2016 1:54:33 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%md\n", - "dateUpdated": "Sep 28, 2016 1:54:32 PM", - "config": {}, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1475092472681_-955530461", - "id": "20160928-135432_2099340527", - "dateCreated": "Sep 28, 2016 1:54:32 PM", - "status": "READY", - "progressUpdateIntervalMs": 500 - } - ], - "name": "Zeppelin Tutorial/Using Mahout", - "id": "2BYEZ5EVK", - "angularObjects": { - "2C6WUGPNH:shared_process": [], - "2C4A8RJNB:shared_process": [], - "2C4DTK2ZT:shared_process": [], - "2C6XKJWBR:shared_process": [], - "2C6AHZPMK:shared_process": [], - "2C5SU66WQ:shared_process": [], - "2C6AMJ98Q:shared_process": [], - "2C4AJZK72:shared_process": [], - "2C3STPSD7:shared_process": [], - "2C4FJN9CK:shared_process": [], - "2C3CW6JBY:shared_process": [], - "2C5UPQX6Q:shared_process": [], - "2C5873KN4:shared_process": [], - "2C5719XN4:shared_process": [], - "2C52DE5G3:shared_process": [], - "2C4G28E63:shared_process": [], - "2C6CU96BC:shared_process": [], - "2C49A6WY3:shared_process": [], - "2C3NE73HG:shared_process": [] - }, - "config": {}, - "info": {} -} diff --git a/notebook/2C2AUG798/note.json b/notebook/2C2AUG798/note.json index 23ab3df67c8..0546326c98f 100644 --- a/notebook/2C2AUG798/note.json +++ b/notebook/2C2AUG798/note.json @@ -3,7 +3,6 @@ { "text": "%md\n## Introduction\nIn this tutorial we will go through some of the basic features of Zeppelin\u0027s built-in matplotlib integration. \n\n### Prerequisites\n`matplotlib` must be installed to your local python installation. (use `pip install matplotlib` or `conda install matplotlib` if you have `conda`). Additionally, you will need Zeppelin\u0027s matplotlib backend files which are usually found in `$ZEPPELIN_HOME/lib/python`. Although Zeppelin should automatically find this directory, it might be a good idea to add it to your `PYTHONPATH` just in case. \n\n### Interpreters\nMost of the examples shown in this tutorial can be used interchangeably with either the `python` or `pyspark` interpreters. Iterative plotting using the Angular Display System is currently only available for `pyspark`, but this functionality will eventually be added to the base `python` interpreter. \n\n### macOS\nMake sure locale is set, to avoid `ValueError: unknown locale: UTF-8`\n\n### virtualenv\nIn case you want to use virtualenv or conda env:\n - configure python interpreter property -\u003e `absolute/path/to/venv/bin/python`\n - see *Working with Matplotlib in Virtual environments* in the [Matplotlib FAQ](http://matplotlib.org/faq/virtualenv_faq.html)\n \n### A simple example\nLet\u0027s start by making a very simple line plot:", "user": "anonymous", - "dateUpdated": "Dec 17, 2016 3:33:25 PM", "config": { "tableHide": false, "colWidth": 12.0, @@ -32,27 +31,24 @@ "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627954_-1473548609", - "id": "20160614-174657_1772993700", "results": { "code": "SUCCESS", "msg": [ { "type": "HTML", - "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch2\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eIn this tutorial we will go through some of the basic features of Zeppelin\u0026rsquo;s built-in matplotlib integration. \u003c/p\u003e\n\u003ch3\u003ePrerequisites\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003ematplotlib\u003c/code\u003e must be installed to your local python installation. (use \u003ccode\u003epip install matplotlib\u003c/code\u003e or \u003ccode\u003econda install matplotlib\u003c/code\u003e if you have \u003ccode\u003econda\u003c/code\u003e). Additionally, you will need Zeppelin\u0026rsquo;s matplotlib backend files which are usually found in \u003ccode\u003e$ZEPPELIN_HOME/interpreter/lib/python\u003c/code\u003e. Although Zeppelin should automatically find this directory, it might be a good idea to add it to your \u003ccode\u003ePYTHONPATH\u003c/code\u003e just in case. \u003c/p\u003e\n\u003ch3\u003eInterpreters\u003c/h3\u003e\n\u003cp\u003eMost of the examples shown in this tutorial can be used interchangeably with either the \u003ccode\u003epython\u003c/code\u003e or \u003ccode\u003epyspark\u003c/code\u003e interpreters. Iterative plotting using the Angular Display System is currently only available for \u003ccode\u003epyspark\u003c/code\u003e, but this functionality will eventually be added to the base \u003ccode\u003epython\u003c/code\u003e interpreter. \u003c/p\u003e\n\u003ch3\u003emacOS\u003c/h3\u003e\n\u003cp\u003eMake sure locale is set, to avoid \u003ccode\u003eValueError: unknown locale: UTF-8\u003c/code\u003e\u003c/p\u003e\n\u003ch3\u003evirtualenv\u003c/h3\u003e\n\u003cp\u003eIn case you want to use virtualenv or conda env:\u003cbr/\u003e - configure python interpreter property -\u0026gt; \u003ccode\u003eabsolute/path/to/venv/bin/python\u003c/code\u003e\u003cbr/\u003e - see \u003cem\u003eWorking with Matplotlib in Virtual environments\u003c/em\u003e in the \u003ca href\u003d\"http://matplotlib.org/faq/virtualenv_faq.html\"\u003eMatplotlib FAQ\u003c/a\u003e\u003c/p\u003e\n\u003ch3\u003eA simple example\u003c/h3\u003e\n\u003cp\u003eLet\u0026rsquo;s start by making a very simple line plot:\u003c/p\u003e\n\u003c/div\u003e" + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch2\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eIn this tutorial we will go through some of the basic features of Zeppelin\u0026rsquo;s built-in matplotlib integration. \u003c/p\u003e\n\u003ch3\u003ePrerequisites\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003ematplotlib\u003c/code\u003e must be installed to your local python installation. (use \u003ccode\u003epip install matplotlib\u003c/code\u003e or \u003ccode\u003econda install matplotlib\u003c/code\u003e if you have \u003ccode\u003econda\u003c/code\u003e). Additionally, you will need Zeppelin\u0026rsquo;s matplotlib backend files which are usually found in \u003ccode\u003e$ZEPPELIN_HOME/lib/python\u003c/code\u003e. Although Zeppelin should automatically find this directory, it might be a good idea to add it to your \u003ccode\u003ePYTHONPATH\u003c/code\u003e just in case. \u003c/p\u003e\n\u003ch3\u003eInterpreters\u003c/h3\u003e\n\u003cp\u003eMost of the examples shown in this tutorial can be used interchangeably with either the \u003ccode\u003epython\u003c/code\u003e or \u003ccode\u003epyspark\u003c/code\u003e interpreters. Iterative plotting using the Angular Display System is currently only available for \u003ccode\u003epyspark\u003c/code\u003e, but this functionality will eventually be added to the base \u003ccode\u003epython\u003c/code\u003e interpreter. \u003c/p\u003e\n\u003ch3\u003emacOS\u003c/h3\u003e\n\u003cp\u003eMake sure locale is set, to avoid \u003ccode\u003eValueError: unknown locale: UTF-8\u003c/code\u003e\u003c/p\u003e\n\u003ch3\u003evirtualenv\u003c/h3\u003e\n\u003cp\u003eIn case you want to use virtualenv or conda env:\u003cbr/\u003e - configure python interpreter property -\u0026gt; \u003ccode\u003eabsolute/path/to/venv/bin/python\u003c/code\u003e\u003cbr/\u003e - see \u003cem\u003eWorking with Matplotlib in Virtual environments\u003c/em\u003e in the \u003ca href\u003d\"http://matplotlib.org/faq/virtualenv_faq.html\"\u003eMatplotlib FAQ\u003c/a\u003e\u003c/p\u003e\n\u003ch3\u003eA simple example\u003c/h3\u003e\n\u003cp\u003eLet\u0026rsquo;s start by making a very simple line plot:\u003c/p\u003e\n\u003c/div\u003e" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "dateStarted": "Dec 17, 2016 3:33:25 PM", - "dateFinished": "Dec 17, 2016 3:33:25 PM", + "apps": [], + "jobName": "paragraph_1478123627954_-1473548609", + "id": "20160614-174657_1772993700", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%python\nimport matplotlib.pyplot as plt\nplt.plot([1, 2, 3])", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "user": "anonymous", "config": { "colWidth": 12.0, "editorMode": "ace/mode/python", @@ -69,32 +65,38 @@ "scatter": {} } } - ] + ], + "editorSetting": { + "language": "python", + "editOnDblClick": false + } }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627958_-1475087605", - "id": "20161101-192232_289486976", "results": { "code": "SUCCESS", "msg": [ + { + "type": "TEXT", + "data": "[\u003cmatplotlib.lines.Line2D object at 0x26201d0\u003e]\n" + }, { "type": "HTML", - "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAGQCAYAAAByNR6YAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAIABJREFUeJzt3XtsVfWe/vF3vcQbTISgNHZAI3AsKNgLTEWR9OAFUcFLCBijIKKIckRHnRhGx4M/8XJMdERB8RJxIJgh4AUMWCVyU6BQoUWDjqgEhIoooHVAtLRdvz++5zAid9jt2nvt9yshad3rkE/c7tMnz1r9fnKiKIqQJElSyhwV9wCSJElJY8CSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKscQHrN9++42SkhIKCwvp3LkzDz/88F6vGzlyJB06dKCgoICqqqomnlKSJCXJMXEP0NiOO+445s2bx4knnkh9fT0XXHABffr04V/+5V92XfPuu+/y9ddf8+WXX7J06VKGDx9OeXl5jFNLkqRMlvgGC+DEE08EQptVV1dHTk7Obq/PmDGDQYMGAVBSUkJNTQ2bNm1q8jklSVIyZEXAamhooLCwkNzcXC655BK6deu22+vV1dW0adNm1/d5eXlUV1c39ZiSJCkhsiJgHXXUUVRWVrJhwwaWLl3KZ599FvdIkiQpwRL/DNbv/dM//RN//vOfKSsro1OnTrv+eV5eHuvXr9/1/YYNG8jLy9vjf//HW4uSJOngRVEU9whNJvEN1ubNm6mpqQFgx44dzJkzh/z8/N2u6devH5MmTQKgvLyck08+mdatW+/174uiyD8J+fPXv/419hn843vqH9/PpP5ZsiSiQ4eIG26I+PHH7AlW/5D4Bmvjxo0MHjyYhoYGGhoaGDhwIJdffjkvvvgiOTk5DBs2jMsvv5zZs2fTvn17TjrpJCZOnBj32JIkZaTaWnjkEXj5ZRg3Dvr3j3uieCQ+YHXu3JkVK1bs8c9vu+223b4fN25cU40kSVIirVoFN94IeXlQVQW5uXFPFJ/E3yKU9qW0tDTuEZRivqfJ4vuZOerr4amnoLQURoyAmTOzO1wB5ERRlH03Rg9TTk4O/uuSJOn/rF0LgwdDFMFrr8GZZ+79umz7GWqDJUmSDlkUwauvQrdu0LcvzJu373CVjRL/DJYkSUqtTZvg1lth/foQrM45J+6J0o8NliRJOmhvvgnnngtdusDSpYarfbHBkiRJB/TTTzByJCxZAm+9Bd27xz1RerPBkiRJ+/XBB6G1at48HL9guDowGyxJkrRXv/wCo0aF24KvvAK9e8c9UeawwZIkSXuoqICiIti8GT75xHB1qGywJEnSLjt3wpgxMGECPPccDBgQ90SZyYAlSZIA+PzzsOrm1FOhshJOOy3uiTKXtwglScpyDQ3wzDPQsycMGwazZhmujpQNliRJWWzdOrjppnBrsLwc2rWLe6JksMGSJCkL/WN3YNeucNllsGCB4SqVbLAkScoy338Pt90Ga9aEM666dIl7ouSxwZIkKYvMmBEODc3Ph2XLDFeNxQZLkqQsUFMDd98NH34I06fDBRfEPVGy2WBJkpRw8+aF1uq448KqG8NV47PBkiQpoXbsgAcegKlTw6qbPn3inih72GBJkpRAy5dDcTFUV4dVN4arpmWDJUlSguzcCY8/DuPHw9ixcN11cU+UnQxYkiQlxBdfhFU3LVrAihWQlxf3RNnLW4SSJGW4hoawmLlHDxgyBMrKDFdxs8GSJCmDrV8fQtX27bB4MXToEPdEAhssSZIyUhTB5MnhQfaLLgrnWxmu0ocNliRJGeaHH2D4cFi9Gt5/HwoK4p5If2SDJUlSBnnnnXBoaLt2UFFhuEpXNliSJGWAn3+Ge+6BuXPDwaEXXhj3RNofGyxJktLcwoWhtTrqKFi50nCVCWywJElKU7/+Cg8+CK+/Di+9BFdeGfdEOlgGLEmS0lBlZTg0ND8/rLpp1SruiXQovEUoSVIaqauDRx+F3r1h1CiYNs1wlYlssCRJShOrV8OgQdC8eVjW3KZN3BPpcNlgSZIUsygKy5nPPx9uuAHee89wlelssCRJitGGDXDzzVBTA4sWwVlnxT2RUsEGS5KkGERR+O3AoiLo2dNwlTQ2WJIkNbEtW+D222HVKigrCyFLyWKDJUlSE5o1C7p0Cc9YLV9uuEoqGyxJkprAtm1w771hOfOUKVBaGvdEakw2WJIkNbKPPgqrburqwqobw1Xy2WBJktRIfvsNHnoIJk+GCROgX7+4J1JTMWBJktQIVq4Mq27atw9fn3JK3BOpKXmLUJKkFKqvhyeegEsugfvugzfeMFxlIxssSZJS5KuvYPBgOP54+PhjaNs27okUFxssSZKOUBSFZ6y6d4eBA2HOHMNVtrPBkiTpCHz7LQwdCps3w8KF0LFj3BMpHdhgSZJ0mKZOhcJCOO88WLzYcKX/Y4MlSdIh2roVRoyAqqpwMnvXrnFPpHRjgyVJ0iEoKwurbnJzYcUKw5X2zgZLkqSDsH17OHZh9myYNAl69Yp7IqUzGyxJkg5g8WIoKIAdO+CTTwxXOjAbLEmS9qG2FkaPhokT4fnn4Zpr4p5ImcKAJUnSXnz6aVh1c/rp4WH21q3jnkiZxFuEkiT9Tn09PPlkuA14113w9tuGKx06GyxJkv5uzZqw6uboo6GiAs44I+6JlKlssCRJWS+K4OWXoaQErr0W5s41XOnI2GBJkrLaxo1wyy3w3XewYAF06hT3REoCGyxJUtaaNi0cv1BcDOXlhiuljg2WJCnr/Pgj3HlneM5q5sxwa1BKJRssSVJWmTMnrLpp2RIqKw1Xahw2WJKkrLB9O9x/f2isJk6Eiy+OeyIlmQ2WJCnxysuhsBBqasKqG8OVGpsNliQpsWpr4ZFHwhEM48ZB//5xT6RsYcCSJCXSqlVh1U1eXlh1k5sb90TKJt4ilCQlSn09PPUUlJbCiBHhmSvDlZqaDZYkKTHWrg2rbqIIli6FM8+MeyJlKxssSVLGiyJ49VXo1g369oV58wxXipcNliQpo23aBLfeCuvXh2B1zjlxTyTZYEmSMtibb8K554aDQ5cuNVwpfdhgSZIyzk8/wciRsGQJvPUWdO8e90TS7mywJEkZ5YMPQmvVvHk4fsFwpXSU+IC1YcMGevXqxdlnn03nzp159tln97hmwYIFnHzyyRQVFVFUVMSYMWNimFSStD+//AJ33QU33QQvvQTjx8NJJ8U9lbR3ib9FeMwxx/D0009TUFDAtm3bKC4u5tJLLyU/P3+363r27MnMmTNjmlKStD8VFeHQ0OLisOqmRYu4J5L2L/ENVm5uLgUFBQA0a9aMjh07Ul1dvcd1URQ19WiSpAPYuRP++le48kr4f/8PpkwxXCkzJD5g/d7atWupqqqipKRkj9eWLFlCQUEBV1xxBZ999lkM00mSfu/zz8PzVRUVUFkJAwbEPZF08LImYG3bto3+/fszduxYmjVrtttrxcXFfPPNN1RVVfGXv/yFq6++OqYpJUkNDfDMM9CzJwwbBrNmwWmnxT2VdGgS/wwWQF1dHf379+fGG2/kqquu2uP13weuPn36cMcdd7B161Zatmy5x7WjR4/e9XVpaSmlpaWNMbIkZaV168JD7Dt3Qnk5tGsX90Q6XPPnz2f+/PlxjxGbnCgLHj4aNGgQrVq14umnn97r65s2baJ169YALFu2jAEDBrB27do9rsvJyfFZLUlqBFEE//Vf8G//BvfdF/4cfXTcUymVsu1naOIbrEWLFjFlyhQ6d+5MYWEhOTk5PPbYY6xbt46cnByGDRvG9OnTeeGFFzj22GM54YQTmDp1atxjS1LW+P57uO02WLMmnHHVpUvcE0lHLisarFTJtvQtSY1txgwYPjzcFhw9Go47Lu6J1Fiy7Wdo4hssSVL6qamBu++GDz+E6dPhggvinkhKraz5LUJJUnqYNy+sujnuuLDqxnClJLLBkiQ1iR074IEHYOpUeOUV6NMn7omkxmODJUlqdMuXhzU31dVh1Y3hSklngyVJajQ7d8Ljj4fFzGPHwnXXxT2R1DQMWJKkRvHFF2FBc4sWsGIF5OXFPZHUdLxFKElKqYYGeO456NEDhgyBsjLDlbKPDZYkKWXWrw+havt2WLwYOnSIeyIpHjZYkqQjFkUweXJ4kP2ii8L5VoYrZTMbLEnSEfnhh3Aa++rV8P77UFAQ90RS/GywJEmH7Z13wqGh7dpBRYXhSvoHGyxJ0iH7+We45x6YOzccHHrhhXFPJKUXGyxJ0iFZuDC0VkcdBStXGq6kvbHBkiQdlF9/hQcfhNdfh5degiuvjHsiKX0ZsCRJB1RZGQ4Nzc8Pq25atYp7Iim9eYtQkrRPdXXw6KPQuzeMGgXTphmupINhgyVJ2qvVq2HQIGjePCxrbtMm7omkzGGDJUnaTRSF5cznnw833ADvvWe4kg6VDZYkaZcNG+Dmm6GmBhYtgrPOinsiKTPZYEmSiKLw24FFRdCzp+FKOlI2WJKU5bZsgdtvh1WroKwshCxJR8YGS5Ky2KxZ0KVLeMZq+XLDlZQqNliSlIW2bYN77w3LmadMgdLSuCeSksUGS5KyzEcfhVU3dXVh1Y3hSko9GyxJyhK//QYPPQSTJ8OECdCvX9wTScllwJKkLLByZVh10759+PqUU+KeSEo2bxFKUoLV18MTT8All8B998EbbxiupKZggyVJCfXVVzB4MBx/PHz8MbRtG/dEUvawwZKkhImi8IxV9+4wcCDMmWO4kpqaDZYkJci338LQobB5MyxcCB07xj2RlJ1ssCQpIaZOhcJCOO88WLzYcCXFyQZLkjLc1q0wYgRUVYWT2bt2jXsiSTZYkpTBysrCqpvcXFixwnAlpQsbLEnKQNu3h2MXZs+GSZOgV6+4J5L0ezZYkpRhFi+GggLYsQM++cRwJaUjGyxJyhC1tTB6NEycCM8/D9dcE/dEkvbFgCVJGeDTT8Oqm9NPDw+zt24d90SS9sdbhJKUxurr4cknw23Au+6Ct982XEmZwAZLktLUmjVh1c3RR0NFBZxxRtwTSTpYNliSlGaiCF5+GUpK4NprYe5cw5WUaWywJCmNbNwIt9wC330HCxZAp05xTyTpcNhgSVKamDYtHL9QXAzl5YYrKZPZYElSzH78Ee68MzxnNXNmuDUoKbPZYElSjObMCatuWraEykrDlZQUNliSFIPt2+H++0NjNXEiXHxx3BNJSiUbLElqYuXlUFgINTVh1Y3hSkoeGyxJaiK1tfDII+EIhnHjoH//uCeS1FgMWJLUBFatCqtu8vLCqpvc3LgnktSYvEUoSY2ovh6eegpKS2HEiPDMleFKSj4bLElqJGvXhlU3UQRLl8KZZ8Y9kaSmYoMlSSkWRfDqq9CtG/TtC/PmGa6kbGODJUkptGkT3HorrF8fgtU558Q9kaQ42GBJUoq8+Sace244OHTpUsOVlM1ssCTpCP30E4wcCUuWwFtvQffucU8kKW42WJJ0BD74ILRWzZuH4xcMV5LABkuSDssvv8CoUeG24CuvQO/ecU8kKZ3YYEnSIaqogKIi2Lw5rLoxXEn6IxssSTpIO3fCmDEwYQI89xwMGBD3RJLSlQFLkg7C55+HVTenngqVlXDaaXFPJCmdeYtQkvajoQGeeQZ69oRhw2DWLMOVpAOzwZKkfVi3Dm66KdwaLC+Hdu3inkhSprDBkqQ/iCJ47TXo2hUuuwwWLDBcSTo0NliS9Dvffw+33QZr1oQzrrp0iXsiSZnIBkuS/m7GjHBoaH4+LFtmuJJ0+GywJGW9mhq4+2748EOYPh0uuCDuiSRlOhssSVlt3rzQWh13XFh1Y7iSlAo2WJKy0o4d8MADMHVqWHXTp0/cE0lKEhssSVln+XIoLobq6rDqxnAlKdVssCRljZ074fHHYfx4GDsWrrsu7okkJZUBS1JW+OKLsOqmRQtYsQLy8uKeSFKSeYtQUqI1NITFzD16wJAhUFZmuJLU+BIfsDZs2ECvXr04++yz6dy5M88+++xerxs5ciQdOnSgoKCAqqqqJp5SUmNYvx4uvRRefx0WL4bbb4ecnLinkpQNEh+wjjnmGJ5++mlWrVrFkiVLGD9+PP/zP/+z2zXvvvsuX3/9NV9++SUvvvgiw4cPj2laSakQRTB5cniQ/aKLwvlWHTrEPZWkbJL4Z7Byc3PJzc0FoFmzZnTs2JHq6mry8/N3XTNjxgwGDRoEQElJCTU1NWzatInWrVvHMrOkw/fDDzB8OKxeDe+/DwUFcU8kKRslvsH6vbVr11JVVUVJSclu/7y6upo2bdrs+j4vL4/q6uqmHk/SEXrnnXBoaLt2UFFhuJIUn8Q3WP+wbds2+vfvz9ixY2nWrFnc40hKoZ9/hnvugblzw8GhF14Y90SSsl1WBKy6ujr69+/PjTfeyFVXXbXH63l5eaxfv37X9xs2bCBvH79mNHr06F1fl5aWUlpamupxJR2ChQth8GC4+GJYuRKaN497IkkA8+fPZ/78+XGPEZucKIqiuIdobIMGDaJVq1Y8/fTTe3199uzZjB8/nlmzZlFeXs7dd99NeXn5Htfl5OSQBf+6pIzw66/w4IPhNwRfegmuvDLuiSTtT7b9DE18g7Vo0SKmTJlC586dKSwsJCcnh8cee4x169aRk5PDsGHDuPzyy5k9ezbt27fnpJNOYuLEiXGPLWk/KivDoaH5+WHVTatWcU8kSbvLigYrVbItfUvppq4O/va3sObmP/8Trr/ec62kTJFtP0MT32BJSobVq2HQoPCM1fLl8Ltf/JWktJNVxzRIyjxRFJYzn38+3HADvPee4UpS+rPBkpS2NmyAm2+GmhpYtAjOOivuiSTp4NhgSUo7URR+O7CoCHr2NFxJyjw2WJLSypYtYSnzqlVQVhZCliRlGhssSWlj1izo0iU8Y7V8ueFKUuaywZIUu23b4N57w3LmKVPABQmSMp0NlqRYffRRWNBcVxdW3RiuJCWBDZakWPz2Gzz0EEyeDBMmQL9+cU8kSaljwJLU5FauDKtu2rcPX59yStwTSVJqeYtQUpOpr4cnnoBLLoH77oM33jBcSUomGyxJTeKrr2DwYDj+ePj4Y2jbNu6JJKnx2GBJalRRFJ6x6t4dBg6EOXMMV5KSzwZLUqP59lsYOhQ2b4aFC6Fjx7gnkqSmYYMlqVFMnQqFhXDeebB4seFKUnaxwZKUUlu3wogRUFUVTmbv2jXuiSSp6dlgSUqZsrKw6iY3F1asMFxJyl42WJKO2Pbt4diF2bNh0iTo1SvuiSQpXjZYko7I4sVQUAA7dsAnnxiuJAlssCQdptpaGD0aJk6E55+Ha66JeyJJSh8GLEmH7NNPw6qb008PD7O3bh33RJKUXrxFKOmg1dfDk0+G24B33QVvv224kqS9scGSdFDWrAmrbo4+Gioq4Iwz4p5IktKXDZak/YoiePllKCmBa6+FuXMNV5J0IDZYkvZp40a45Rb47jtYsAA6dYp7IknKDDZYkvZq2rRw/EJxMZSXG64k6VDYYEnazY8/wp13huesZs4MtwYlSYfGBkvSLnPmhFU3LVtCZaXhSpIOlw2WJLZvh/vvD43VxIlw8cVxTyRJmc0GS8py5eVQWAg1NWHVjeFKko6cDZaUpWpr4ZFHwhEM48ZB//5xTyRJyWHAkrLQqlVh1U1eXlh1k5sb90SSlCzeIpSySH09PPUUlJbCiBHhmSvDlSSlng2WlCXWrg2rbqIIli6FM8+MeyJJSi4bLCnhoghefRW6dYO+fWHePMOVJDU2GywpwTZtgltvhfXrQ7A655y4J5Kk7GCDJSXUm2/CueeGg0OXLjVcSVJTssGSEuann2DkSFiyBN56C7p3j3siSco+NlhSgnzwQWitmjcPxy8YriQpHjZYUgL88guMGhVuC77yCvTuHfdEkpTdbLCkDFdRAUVFsHlzWHVjuJKk+NlgSRlq504YMwYmTIDnnoMBA+KeSJL0DwYsKQN9/nlYdXPqqVBZCaedFvdEkqTf8xahlEEaGuCZZ6BnTxg2DGbNMlxJUjqywZIyxLp1cNNN4dZgeTm0axf3RJKkfbHBktJcFMFrr0HXrnDZZbBggeFKktKdDZaUxr7/Hm67DdasCWdcdekS90SSpINhgyWlqRkzwqGh+fmwbJnhSpIyiQ2WlGZqauDuu+HDD2H6dLjggrgnkiQdKhssKY3Mmxdaq+OOC6tuDFeSlJlssKQ0sGMHPPAATJ0aVt306RP3RJKkI2GDJcVs+XIoLobq6rDqxnAlSZnPBkuKyc6d8PjjMH48jB0L110X90SSpFQxYEkx+OKLsOqmRQtYsQLy8uKeSJKUSt4ilJpQQ0NYzNyjBwwZAmVlhitJSiIbLKmJrF8fQtX27bB4MXToEPdEkqTGYoMlNbIogsmTw4PsF10UzrcyXElSstlgSY3ohx9g+HBYvRrefx8KCuKeSJLUFGywpEbyzjvh0NB27aCiwnAlSdnEBktKsZ9/hnvugblzw8GhF14Y90SSpKZmgyWl0MKFobU66ihYudJwJUnZygZLSoFff4UHH4TXX4eXXoIrr4x7IklSnAxY0hGqrAyHhubnh1U3rVrFPZEkKW7eIpQOU10dPPoo9O4No0bBtGmGK0lSYIMlHYbVq2HQIGjePCxrbtMm7okkSenEBks6BFEUljOffz7ccAO8957hSpK0Jxss6SBt2AA33ww1NbBoEZx1VtwTSZLSlQ2WdABRFH47sKgIevY0XEmSDswGS9qPLVvg9tth1SooKwshS5KkA7HBkvZh1izo0iU8Y7V8ueFKknTwbLCkP9i2De69NyxnnjIFSkvjnkiSlGkS32ANHTqU1q1b06VLl72+vmDBAk4++WSKioooKipizJgxTTyh0slHH4VVN3V1YdWN4UqSdDgS32ANGTKEO++8k0GDBu3zmp49ezJz5swmnErp5rff4KGHYPJkmDAB+vWLeyJJUiZLfIPVo0cPWrRosd9roihqommUjlauhG7d4Msvw9eGK0nSkUp8wDoYS5YsoaCggCuuuILPPvss7nHUROrr4Ykn4JJL4L774I034JRT4p5KkpQEib9FeCDFxcV88803nHjiibz77rtcffXVrF69Ou6x1Mi++goGD4bjj4ePP4a2beOeSJKUJFkfsJo1a7br6z59+nDHHXewdetWWrZsudfrR48evevr0tJSSn0KOqNEEbz4IvzHf4Q/f/kLHGWPK0kpN3/+fObPnx/3GLHJibLgAaS1a9fSt29fPv300z1e27RpE61btwZg2bJlDBgwgLVr1+7178nJyfF5rQz27bcwdChs3gyTJkHHjnFPJEnZI9t+hia+wbr++uuZP38+W7ZsoW3btjz88MPU1taSk5PDsGHDmD59Oi+88ALHHnssJ5xwAlOnTo17ZDWCqVNh5Ei44w7493+HY4+NeyJJUpJlRYOVKtmWvpNg61YYMQKqqsIRDF27xj2RJGWnbPsZ6tMnSqyysrDqJjcXVqwwXEmSmk7ibxEq+2zfHo5dmD07PGvVq1fcE0mSso0NlhJl8WIoKIAdO+CTTwxXkqR42GApEWprYfRomDgRnn8errkm7okkSdnMgKWM9+mncOONcPrp4WH2v5+6IUlSbLxFqIxVXw9PPhluA951F7z9tuFKkpQebLCUkdasCatujj4aKirgjDPinkiSpP9jg6WMEkXw8stQUgLXXgtz5xquJEnpxwZLGWPjRrjlFvjuO1iwADp1insiSZL2zgZLGWHatHD8QnExlJcbriRJ6c0GS2ntxx/hzjvDc1YzZ4Zbg5IkpTsbLKWtOXPCqpuWLaGy0nAlScocNlhKO9u3w/33h8Zq4kS4+OK4J5Ik6dDYYCmtlJdDYSHU1IRVN4YrSVImssFSWqithUceCUcwjBsH/fvHPZEkSYfPgKXYrVoVVt3k5YVVN7m5cU8kSdKR8RahYlNfD089BaWlMGJEeObKcCVJSgIbLMVi7dqw6iaKYOlSOPPMuCeSJCl1bLDUpKIIXn0VunWDvn1h3jzDlSQpeWyw1GQ2bYJbb4X160OwOuecuCeSJKlx2GCpSbz5Jpx7bjg4dOlSw5UkKdlssNSofvoJRo6EJUvgrbege/e4J5IkqfHZYKnRfPBBaK2aNw/HLxiuJEnZwgZLKffLLzBqVLgt+Mor0Lt33BNJktS0bLCUUhUVUFQEmzeHVTeGK0lSNrLBUkrs3AljxsCECfDcczBgQNwTSZIUHwOWjtjnn4dVN6eeCpWVcNppcU8kSVK8vEWow9bQAM88Az17wrBhMGuW4UqSJLDB0mFatw5uuincGiwvh3bt4p5IkqT0YYOlQxJF8Npr0LUrXHYZLFhguJIk6Y9ssHTQvv8ebrsN1qwJZ1x16RL3RJIkpScbLB2UGTPCoaH5+bBsmeFKkqT9scHSftXUwN13w4cfwvTpcMEFcU8kSVL6s8HSPs2bF1qr444Lq24MV5IkHRwbLO1hxw544AGYOjWsuunTJ+6JJEnKLDZY2s3y5VBcDNXVYdWN4UqSpENngyUgnGf1+OMwfjyMHQvXXRf3RJIkZS4Dlvjii7DqpkULWLEC8vLinkiSpMzmLcIs1tAQFjP36AFDhkBZmeFKkqRUsMHKUuvXh1C1fTssXgwdOsQ9kSRJyWGDlWWiCCZPDg+yX3RRON/KcCVJUmrZYGWRH36A4cNh9Wp4/30oKIh7IkmSkskGK0u88044NLRdO6ioMFxJktSYbLAS7uef4Z57YO7ccHDohRfGPZEkSclng5VgCxeG1uqoo2DlSsOVJElNxQYrgX79FR58EF5/HV56Ca68Mu6JJEnKLgashKmsDIeG5ueHVTetWsXOfgTxAAAHEklEQVQ9kSRJ2cdbhAlRVwePPgq9e8OoUTBtmuFKkqS42GAlwOrVMGgQNG8eljW3aRP3RJIkZTcbrAwWRWE58/nnww03wHvvGa4kSUoHNlgZasMGuPlmqKmBRYvgrLPinkiSJP2DDVaGiaLw24FFRdCzp+FKkqR0ZIOVQbZsgdtvh1WroKwshCxJkpR+bLAyxKxZ0KVLeMZq+XLDlSRJ6cwGK81t2wb33huWM0+ZAqWlcU8kSZIOxAYrjX30UVh1U1cXVt0YriRJygw2WGnot9/goYdg8mSYMAH69Yt7IkmSdCgMWGlm5cqw6qZ9+/D1KafEPZEkSTpU3iJME/X18MQTcMklcN998MYbhitJkjKVDVYa+OorGDwYjj8ePv4Y2raNeyJJknQkbLBiFEXhGavu3WHgQJgzx3AlSVIS2GDF5NtvYehQ2LwZFi6Ejh3jnkiSJKWKDVYMpk6FwkI47zxYvNhwJUlS0thgNaGtW2HECKiqCiezd+0a90SSJKkx2GA1kbKysOomNxdWrDBcSZKUZDZYjWz79nDswuzZMGkS9OoV90SSJKmx2WA1osWLoaAAduyATz4xXEmSlC1ssBpBbS2MHg0TJ8Lzz8M118Q9kSRJakoGrBT79NOw6ub008PD7K1bxz2RJElqat4iTJH6enjyyXAb8K674O23DVeSJGUrG6wUWLMmrLo5+mioqIAzzoh7IkmSFKfEN1hDhw6ldevWdOnSZZ/XjBw5kg4dOlBQUEBVVdVB/91RBC+/DCUlcO21MHeu4UqSJGVBwBoyZAjvvffePl9/9913+frrr/nyyy958cUXGT58+EH9vRs3wpVXhl2CCxbAv/4rHJX4f5vJMn/+/LhHUIr5niaL76cyWeIjQY8ePWjRosU+X58xYwaDBg0CoKSkhJqaGjZt2rTfv3PatHD8QnExlJdDp04pHVlNxP/zTh7f02Tx/VQmy/pnsKqrq2nTps2u7/Py8qiurqb1Pp5Qv+GG8JzVzJnh1qAkSdIfZX3AOlQtWkBlJZx4YtyTSJKkdJUTRVEU9xCNbd26dfTt25dPPvlkj9eGDx/On//8ZwYOHAhAfn4+CxYs2GuDlZOT0+izSpKUVFkQOXbJigYriqJ9vqn9+vVj/PjxDBw4kPLyck4++eR93h7Mpv8wJEnS4Ut8wLr++uuZP38+W7ZsoW3btjz88MPU1taSk5PDsGHDuPzyy5k9ezbt27fnpJNOYuLEiXGPLEmSMlxW3CKUJElqSok/puFwlJWVkZ+fz5/+9Cf+9re/7fWawz2cVE3vQO/nggULOPnkkykqKqKoqIgxY8bEMKUOVmMeHqymd6D3089nZtmwYQO9evXi7LPPpnPnzjz77LN7vS4rPqORdlNfXx+1a9cuWrt2bVRbWxude+650eeff77bNbNnz44uv/zyKIqiqLy8PCopKYljVB2Eg3k/58+fH/Xt2zemCXWoPvzww6iysjLq3LnzXl/385lZDvR++vnMLBs3bowqKyujKIqi//3f/43+9Kc/Ze3PUBusP1i2bBkdOnTg9NNP59hjj+W6665jxowZu11zOIeTKh4H836Cv8CQSRrj8GDF50DvJ/j5zCS5ubkUFBQA0KxZMzp27Eh1dfVu12TLZ9SA9Qd/PHj0n//5n/f4j2Nfh5Mq/RzM+wmwZMkSCgoKuOKKK/jss8+ackSlmJ/P5PHzmZnWrl1LVVUVJX84lTtbPqOJ/y1C6UCKi4v55ptvOPHEE3n33Xe5+uqrWb16ddxjScLPZ6batm0b/fv3Z+zYsTRr1izucWJhg/UHeXl5fPPNN7u+37BhA3l5eXtcs379+v1eo/RwMO9ns2bNOPHvR/P36dOHnTt3snXr1iadU6nj5zNZ/Hxmnrq6Ovr378+NN97IVVddtcfr2fIZNWD9Qbdu3fjqq69Yt24dtbW1/Pd//zf9+vXb7Zp+/foxadIkgAMeTqp4Hcz7+ft7/8uWLSOKIlq2bNnUo+oQRAc4PNjPZ2bZ3/vp5zPz3HzzzXTq1Im77rprr69ny2fUW4R/cPTRRzNu3DguvfRSGhoaGDp0KB07duTFF1/0cNIMdDDv5/Tp03nhhRc49thjOeGEE5g6dWrcY2s/PDw4WQ70fvr5zCyLFi1iypQpdO7cmcLCQnJycnjsscdYt25d1n1GPWhUkiQpxbxFKEmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKXY/wfkKCsZlpS9sAAAAABJRU5ErkJggg\u003d\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" + "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627958_-1475087605", + "id": "20161101-192232_289486976", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%md\nNotice how an explicit call to `show()` is not necessary. This is accomplished via a post-execute hook which tells Zeppelin to plot all currently open matplotlib figures after executing the rest of the paragraph.\n### Plotting multiple figures\nWe can easily plot multiple figures at once too:", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "user": "anonymous", "config": { "colWidth": 12.0, "editorMode": "ace/mode/markdown", @@ -112,32 +114,31 @@ "scatter": {} } } - ] + ], + "editorSetting": {} }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627958_-1475087605", - "id": "20160617-002131_1552178409", "results": { "code": "SUCCESS", "msg": [ { "type": "HTML", - "data": "\u003cp\u003eNotice how an explicit call to \u003ccode\u003eshow()\u003c/code\u003e is not necessary. This is accomplished via a post-execute hook which tells Zeppelin to plot all currently open matplotlib figures after executing the rest of the paragraph.\u003c/p\u003e\n\u003ch3\u003ePlotting multiple figures\u003c/h3\u003e\n\u003cp\u003eWe can easily plot multiple figures at once too:\u003c/p\u003e\n" + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003cp\u003eNotice how an explicit call to \u003ccode\u003eshow()\u003c/code\u003e is not necessary. This is accomplished via a post-execute hook which tells Zeppelin to plot all currently open matplotlib figures after executing the rest of the paragraph.\u003c/p\u003e\n\u003ch3\u003ePlotting multiple figures\u003c/h3\u003e\n\u003cp\u003eWe can easily plot multiple figures at once too:\u003c/p\u003e\n\u003c/div\u003e" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627958_-1475087605", + "id": "20160617-002131_1552178409", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%python\n# Figure 1\nplt.plot([1, 2, 3])\n\n# Figure 2\nplt.figure()\nplt.plot([3, 2, 1])", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "user": "anonymous", "config": { "colWidth": 12.0, "editorMode": "ace/mode/python", @@ -154,32 +155,38 @@ "scatter": {} } } - ] + ], + "editorSetting": { + "language": "python", + "editOnDblClick": false + } }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627959_-1475472354", - "id": "20161101-193533_2096366908", "results": { "code": "SUCCESS", "msg": [ + { + "type": "TEXT", + "data": "[\u003cmatplotlib.lines.Line2D object at 0x2889f90\u003e]\n" + }, { "type": "HTML", - "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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\u003d\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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 style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" + "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAGQCAYAAAByNR6YAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4wLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvpW3flQAAIABJREFUeJzt3XlAlXX+/v/rCO4ICooSqIDgwiYpilk5WeFWmYqV2mSlDdnyaWb6pC0zLVqZo9O0TMsnJqfMsZwRLXK3cinNpKMGihsuKODG4gIoCpz374/5Dr8cNbUO3Occno+/4F6O17ubw7l6nQWbMcYIAAAATtPA6gAAAACehoIFAADgZBQsAAAAJ6NgAQAAOBkFCwAAwMkoWAAAAE5GwQIAAHAyChYAAICTUbAAAACcjIIFAADgZBQsAAAAJ6NgAQAAOBkFCwAAwMkoWAAAAE5GwQIAAHAyChYAAICTUbAAAACcjIIFAADgZBQsAAAAJ6NgAQAAOBkFCwAAwMkoWAAAAE5GwQIAAHAyChYAAICTUbAAAACcjIIFAADgZBQsAAAAJ6NgAQAAOBkFCwAAwMkoWAAAAE5GwQIAAHAyChYAAICTUbAAAACcjIIFAADgZBQsAAAAJ6NgAQAAOBkFCwAAwMkoWAAAAE5GwQIAAHAyChYAAICTUbAAAACcjIIFAADgZBQsAAAAJ6NgAQAAOBkFCwAAwMkoWAAAAE5GwQIAAHAyb6sDuJPWrVsrNDTU6hgAALid3NxcFRUVWR2jzlCwrkBoaKjsdrvVMQAAcDsJCQlWR6hTPEUIAADgZBQsAAAAJ6NgAQAAOBkFCwAAwMkoWAAAAE5GwQIAAHAyChYAAICTUbAAAACczK0LVkVFhXr37q3u3bsrOjpazz///HnHnDlzRnfddZciIiKUmJio3Nzcmn2vvPKKIiIi1KVLFy1fvrwOkwMAAE/m1p/k3rhxY61cuVI+Pj6qrKzUddddp8GDB6tPnz41x8ycOVOtWrXS7t27NXfuXD355JP65z//qW3btmnu3LnKzs7WwYMHdfPNN2vXrl3y8vKycEUAAMATuPUEy2azycfHR5JUWVmpyspK2Wy2c45JT0/XvffeK0kaOXKkvvrqKxljlJ6erlGjRqlx48YKCwtTRESEMjIy6nwNAAC4GmOM1RHcnlsXLEmqrq5WfHy8AgMDlZSUpMTExHP2FxQUqH379pIkb29v+fn5qbi4+JztkhQSEqKCgoI6zQ4AgCspKjujR+Zs0off5lodxe25fcHy8vLSDz/8oPz8fGVkZGjr1q1Ovf3U1FQlJCQoISFBhYWFTr1tAABcgTFGCzMPasBrX+uLbUdU7WCC9Uu5fcH6j5YtW6p///5atmzZOduDg4OVl5cnSaqqqtKJEycUEBBwznZJys/PV3Bw8Hm3m5KSIrvdLrvdrjZt2tTuIgAAqGOFpWf00D826X8+2az2rZpq0WPX6YHrw62O5fbcumAVFhbq+PHjkqTTp0/riy++UNeuXc85ZujQoZo1a5YkKS0tTTfeeKNsNpuGDh2quXPn6syZM9q3b59ycnLUu3fvOl8DAABWMMYo/YcCJb22Rit3HtVTg7tq/kN91bltC6ujeQS3fhfhoUOHdO+996q6uloOh0N33nmnbr31Vj333HNKSEjQ0KFDNX78eN1zzz2KiIiQv7+/5s6dK0mKjo7WnXfeqaioKHl7e+vtt9/mHYQAgHrh6MkK/eGzrfpi2xFd3aGlZoyMU0QgxcqZbIa3Cly2hIQE2e12q2MAAPCzGGP06eYCTV64TRWV1XpiQBeNuy5MXg1slz75F6pvj6FuPcECAACX5/CJCj3z6Rat3HFUCR1bafrIOIW38bE6lseiYAEA4MGMMZq3MV8vLtqmymqHnr01Svf1Da2TqVV9RsECAMBDHTx+Wk8v2KI1uwrVO9Rf00fGKbR1c6tj1QsULAAAPIwxRv/8Pk8vL96uKofR5KHRuqdPRzVgalVnKFgAAHiQguOn9dT8LH2TU6Q+4f6antxdHQKaWR2r3qFgAQDgAYwx+jjjgKYu3i4j6cVhMbq7dwemVhahYAEA4ObySk7pyflZ+nZPsa6NCNC0EXFq78/UykoULAAA3JTDYTRnw369snSHGthsmjo8VqN7t5fNxtTKahQsAADc0P7icj05P0vf7S3R9ZGtNS05TsEtm1odC/8PBQsAADficBh9tD5Xf1q2U94NbJqeHKc7EkKYWrkYChYAAG5iX1G5nkzLUkZuiW7o0kavjIhVkB9TK1dEwQIAwMVVO4w+WLdPf16xUw29GujPd3RXco9gplYujIIFAIAL21NYpklpWdq4/5hu6hqoqSNi1da3idWxcAkULAAAXFC1w2jm2r16dcUuNWnopdfu6q5h8Uyt3AUFCwAAF7P7aKmemJelH/KOa0BUW700LEaBTK3cCgULAAAXUVXt0N++2afXvtyl5o289Oboq3VbXBBTKzdEwQIAwAXsPFyqSWmZysw/ocEx7TTl9hi1adHY6lj4mShYAABYqLLaoffW7NGbX+2WTxNvvT2mh26JC7I6Fn4hChYAABbZfuikJqZlamvBSd0SF6QpQ6MV4MPUyhNQsAAAqGOV1Q69s2qP3lqVI7+mDfXu3T00OJaplSehYAEAUIeyD57QE/OytP3QSd0ef5Wevy1a/s0bWR0LTkbBAgCgDpytcuitVbv1zqrdatW8kVLv6akB0e2sjoVaQsECAKCWbck/oYlpmdpxuFQjrg7Wc7dFqWUzplaejIIFAEAtOVNVrTe/ytH/rdmr1j6NNPPeBN3Ura3VsVAHKFgAANSCzLzjmpiWqV1HyjSyZ4ievSVKfs0aWh0LdYSCBQCAE1VUVuv1L3OU+vUeBbZoog/u76X+XQKtjoU65rYFKy8vT2PHjtWRI0dks9mUkpKi3/72t+ccM2PGDM2ZM0eSVFVVpe3bt6uwsFD+/v4KDQ1VixYt5OXlJW9vb9ntdiuWAQDwIJsOHNPEeZnaU1iuUb3a65lbusm3CVOr+shmjDFWh/g5Dh06pEOHDqlHjx4qLS1Vz5499dlnnykqKuqCxy9cuFCvvfaaVq5cKUkKDQ2V3W5X69atL/vfTEhIoIgBAM5TUVmtv3yxS+9/s1ftfJtoWnKc+nVuY3Usl1LfHkPddoIVFBSkoKB/fyhbixYt1K1bNxUUFFy0YH3yyScaPXp0XUYEANQD9twSTUrL0t6ico1J7KCnB3dVC6ZW9V4DqwM4Q25urjZv3qzExMQL7j916pSWLVum5OTkmm02m00DBgxQz549lZqaWldRAQAe4vTZak1ZuE13vLdeZ6ocmvNAoqYOj6VcQZIbT7D+o6ysTMnJyXr99dfl6+t7wWMWLlyoa6+9Vv7+/jXb1q5dq+DgYB09elRJSUnq2rWr+vXrd965qampNQWssLCwdhYBAHArG/YW68n5WcotPqV7+nTUk4O7yqex2z+kwonceoJVWVmp5ORk3X333RoxYsRFj5s7d+55Tw8GBwdLkgIDAzV8+HBlZGRc8NyUlBTZ7XbZ7Xa1acPz6QBQn506W6UXPs/WXanfqdoYffKbPnpxWAzlCudx24JljNH48ePVrVs3Pf744xc97sSJE1qzZo1uv/32mm3l5eUqLS2t+XrFihWKiYmp9cwAAPe1fk+xBr3+jT78Nlf39Q3V8t/10zWdAqyOBRfltpV73bp1mj17tmJjYxUfHy9Jmjp1qg4cOCBJmjBhgiTp008/1YABA9S8efOac48cOaLhw4dL+vfHN4wZM0aDBg2q4xUAANxB+ZkqTVu6Q7O/26/QgGb614PXqHeY/6VPRL3mth/TYIX69hZTAKjv1u0u0qS0LB08cVrjrg3TEwO6qGkjL6tjuaX69hjqthMsAABqS2lFpV5ZukMfbzig8NbNlTbhGvXsyNQKl4+CBQDAj3y9q1BPzc/S4ZMVSukXrseTOqtJQ6ZWuDIULAAAJJ2sqNTLi7brn/Y8dWrTXGkP9VWPDq2sjgU3RcECANR7q3Ye1TMLtujIyQo9dEMn/famSKZW+EUoWACAeuvEqUq9uHib0jbmq3NbH/3fr69V9/YtrY4FD0DBAgDUS19uO6JnPt2i4vKzerR/hP7npgg19mZqBeegYAEA6pXjp85qysJtWrC5QF3btdDMe3spNsTP6ljwMBQsAEC9sTz7sP742VYdKz+rx26K1KP9I9TI223/qAlcGAULAODxSsrP6oXPs/V55kF1C/LVh/f3UvRVTK1QeyhYAACPtnTLIT2bvlUnTlfq9zd31sP9O6mhF1Mr1C4KFgDAIxWXndFzn2drcdYhxQT7avb4RHUL8rU6FuoJChYAwKMYY7R4yyE9l56tsooqTRzYRSn9wplaoU5RsAAAHqOw9IyeS9+qpVsPq3uIn2bc0V2d27awOhbqIQoWAMDtGWP0eeZBvfB5tsrPVuvJQV31m+vD5M3UChahYAEA3NrRkxX6w2db9cW2I4pv31J/viNOEYFMrWAtChYAwC0ZY/Tp5gJNXrhNFZXVemZIV42/LlxeDWxWRwMoWAAA93PkZIWeWbBFX+04qp4dW2n6yDh1auNjdSygBgULAOA2jDFK25ivFxdt09lqh569NUr39Q1lagWXQ8ECALiFQydO6+kFW7R6Z6F6h/pr+sg4hbZubnUs4IIoWAAAl2aM0b/seXpp0XZVOYxeuC1KY68JVQOmVnBhFCwAgMsqOH5aT83P0jc5ReoT7q/pyd3VIaCZ1bGAS6JgAQBcjjFGH2cc0CtLdshhjF68PVp3J3ZkagW3QcECALiUvJJTempBltbtLlbfTgH6U3Kc2vsztYJ7oWABAFyCw2E0Z8N+vbJ0hxrYbJo6PFaje7eXzcbUCu6HggUAsNyB4lOaND9T3+0t0fWRrTUtOU7BLZtaHQv42ShYAADLOBxGH63P1Z+W7ZR3A5v+lByrOxOYWsH9ue1fwczLy1P//v0VFRWl6OhovfHGG+cds3r1avn5+Sk+Pl7x8fGaMmVKzb5ly5apS5cuioiI0LRp0+oyOgBAUm5RuUalfqcXFm5TYri/lv++n+7q1YFyBY/gthMsb29vvfrqq+rRo4dKS0vVs2dPJSUlKSoq6pzjrr/+ei1atOicbdXV1XrkkUf0xRdfKCQkRL169dLQoUPPOxcA4HzVDqMP1u3Tn1fsVEOvBpoxMk4je4ZQrOBR3LZgBQUFKSgoSJLUokULdevWTQUFBZdVkjIyMhQREaHw8HBJ0qhRo5Senk7BAoBatqewTJPSsrRx/zHd1DVQLw+PVTu/JlbHApzObZ8i/LHc3Fxt3rxZiYmJ5+1bv369unfvrsGDBys7O1uSVFBQoPbt29ccExISooKCgjrLCwD1TbXDKPXrPRryxjfafbRMr93VXe/fm0C5gsdy2wnWf5SVlSk5OVmvv/66fH19z9nXo0cP7d+/Xz4+PlqyZImGDRumnJycK7r91NRUpaamSpIKCwudlhsA6ovdR0s1MS1Lmw8cV1JUW708LEaBvhQreDa3nmBVVlYqOTlZd999t0aMGHHefl9fX/n4+EiShgwZosrKShUVFSk4OFh5eXk1x+Xn5ys4OPiC/0ZKSorsdrvsdrvatGlTOwsBAA9UVe3Qu6v3aMiba7WvqFxvjIpX6j09KVeoF9x2gmWM0fjx49WtWzc9/vjjFzzm8OHDatu2rWw2mzIyMuRwOBQQEKCWLVsqJydH+/btU3BwsObOnauPP/64jlcAAJ5r15FSTZyXqcz8ExoU3U4vDotRmxaNrY4F1Bm3LVjr1q3T7NmzFRsbq/j4eEnS1KlTdeDAAUnShAkTlJaWpnfffVfe3t5q2rSp5s6dK5vNJm9vb7311lsaOHCgqqurNW7cOEVHR1u5HADwCJXVDr23Zo/e/Gq3fJp4660xV+uW2CDeIYh6x2aMMVaHcBcJCQmy2+1WxwAAl7T90ElNTMvU1oKTuiUuSFOGRivAh6kV/q2+PYa67QQLAOAaKqsdemfVHr21Kkd+TRvq3bt7aHBskNWxAEtRsAAAP1v2wROaOC9L2w6d1NDuV+mFodHyb97I6liA5ShYAIArdrbKobdW7dY7q3arZbNGeu+enhoY3c7qWIDLoGABAK7I1oITemJepnYcLtXwq4P1/G1RatmMqRXwYxQsAMBlOVNVrb9+tVvvrtmjgOaN9P7YBN0c1dbqWIBLomABAC4pM++4JqZlateRMo3sGaJnb4mSX7OGVscCXBYFCwBwURWV1Xrjqxy9t2aPAls00Qf391L/LoFWxwJcHgULAHBBmw4c06S0LO0+Wqa7EtrrD7d2k28TplbA5aBgAQDOUVFZrb98sUvvf7NX7XybaNa43vpVZ/4WK3AlKFgAgBob95do4rws7S0q1+jeHfTMkK5qwdQKuGIULACATp+t1p9X7NTf1+3TVX5N9Y/xibousrXVsQC3RcECgHouY1+JJqVlKrf4lO7p01FPDu4qn8Y8PAC/BPcgAKinTp2t0vRlOzVrfa5CWjXVx79JVN9OTK0AZ6BgAUA9tH5PsZ6cn6UDJad0X99QTRzYRc2ZWgFOw70JAOqR8jNVmrZ0h2Z/t18dA5rpnyl9lBgeYHUswONQsACgnli3u0hPzs9SwfHTGndtmCYO7KKmjbysjgV4JAoWAHi40opKvbJ0hz7ecEBhrZtr3oPXKCHU3+pYgEejYAGAB/t6V6GeXrBFh06cVkq/cD2e1FlNGjK1AmobBQsAPNDJikpNXbxdc7/PU6c2zZX2UF/16NDK6lhAvUHBAgAPs2rnUT2zYIuOnKzQhF910u9ujmRqBdQxChYAeIgTpyv10qJtmrcxX5GBPnr34WsV376l1bGAeomCBQAe4KvtR/TMp1tUVHZWj/TvpMduilRjb6ZWgFUoWADgxo6fOqspC7dpweYCdWnbQu+P7aXYED+rYwH1HgULANzUiuzD+sNnW3Ws/KweuylSj/aPUCPvBlbHAiAKFgC4nWPlZ/XCwmyl/3BQ3YJ89cF9vRQTzNQKcCUULABwI8u2HtIfP9uq46cq9fubO+uhGzoxtQJckNveK/Py8tS/f39FRUUpOjpab7zxxnnHzJkzR3FxcYqNjVXfvn2VmZlZsy80NFSxsbGKj49XQkJCXUYHgCtWXHZGj368SRP+sUnt/Jpo4f9cp9/eHEm5AlyU206wvL299eqrr6pHjx4qLS1Vz549lZSUpKioqJpjwsLCtGbNGrVq1UpLly5VSkqKNmzYULN/1apVat26tRXxAeCyLc46pOfSt+pkRaWeGNBZD/6qkxp6UawAV+a2BSsoKEhBQUGSpBYtWqhbt24qKCg4p2D17du35us+ffooPz+/znMCwM9VWHpGz6Vv1dKthxUX4qePR/ZRl3YtrI4F4DK4bcH6sdzcXG3evFmJiYkXPWbmzJkaPHhwzfc2m00DBgyQzWbTgw8+qJSUlLqICgCXZIzR55kH9cLn2So/U61Jg7oo5fpweTO1AtyG2xessrIyJScn6/XXX5evr+8Fj1m1apVmzpyptWvX1mxbu3atgoODdfToUSUlJalr167q16/feeempqYqNTVVklRYWFg7iwCA/+doaYX++OlWrdh2RPHtW2rGyDhFtmVqBbgbmzHGWB3i56qsrNStt96qgQMH6vHHH7/gMVlZWRo+fLiWLl2qzp07X/CYF154QT4+PnriiSd+8t9LSEiQ3W7/xbkB4L8ZY/TZDwV64fNtOl1ZrScGdNb468Ll1cBmdTTAKerbY6jbzpuNMRo/fry6det20XJ14MABjRgxQrNnzz6nXJWXl6u0tLTm6xUrVigmJqZOcgPAfztyskK/+ciu3/8zUxGBPlr62+uV0q8T5QpwY277FOG6des0e/bsmo9akKSpU6fqwIEDkqQJEyZoypQpKi4u1sMPPyzp3+88tNvtOnLkiIYPHy5Jqqqq0pgxYzRo0CBrFgKg3jLGaP6mAk1ZmK2z1Q798ZZuuv/aMIoV4AHc+inCulbfxpsAas+hE6f19IItWr2zUL1CW2n6yO4Ka93c6lhAralvj6FuO8ECAHdkjNG/7Hl6adF2VTmMnr8tSvdeE6oGTK0Aj0LBAoA6UnD8tJ6an6VvcoqUGOav6SPj1DGAqRXgiShYAFDLjDH6JCNPU5dsl8MYvXh7tO5O7MjUCvBgFCwAqEV5Jaf09IItWru7SH07BehPyXFq79/M6lgAahkFCwBqgcNhNCfjgKYt2S5Jenl4jMb07iCbjakVUB9QsADAyQ4Un9Kk+Zn6bm+Jro9srVdGxCqkFVMroD6hYAGAkzgcRh+tz9Wflu2UdwObpo2I1V292jO1AuohChYAOEFuUbkmzc9Sxr4S/apzG70yIlZXtWxqdSwAFqFgAcAvUO0w+vDbXM1YvkMNvRpoxsg4jewZwtQKqOcoWADwM+0tLNOktCzZ9x/TjV0DNXV4rNr5NbE6FgAXQMECgCtU7TD6+9p9+vOKnWrS0Et/ubO7hl8dzNQKQA0KFgBcgd1HyzQxLVObDxzXzd3aaurwGAX6MrUCcC4KFgBchqpqh/72zT699uUuNWvkpTdGxWto96uYWgG4IAoWAFzCriOlmjgvU5n5JzQoup1eHBajNi0aWx0LgAujYAHARVRVO/Te13v1xpc58mnirbfGXK1bYoOYWgG4JAoWAFzAjsMnNXFelrYUnNAtcUGaMjRaAT5MrQBcHgoWAPxIZbVD767eo7+uzJFvk4Z65+4eGhIbZHUsAG6GggUA/0/2wROaOC9L2w6d1NDuV+mFodHyb97I6lgA3BAFC0C9d7bKobdX7dbbq3arZbNGeu+enhoY3c7qWADcGAULQL22teCEnpiXqR2HSzX86mA9f1uUWjZjagXgl6FgAaiXzlRV669f7da7a/YooHkjvT82QTdHtbU6FgAPQcECUO9k5R/XE/MytetImUb2DNGzt0TJr1lDq2MB8CAULAD1RkVltd74KkepX+9VG5/G+uC+XurfNdDqWAA8EAULQL2w+cAxTUzL0u6jZbozIUR/uCVKfk2ZWgGoHRQsAB6torJar32xS3/7Zq/a+jbRrHG99avObayOBcDDUbAAeKyN+0s0cV6W9haVa3TvDnpmSFe1aMLUCkDta2B1gF8iLy9P/fv3V1RUlKKjo/XGG2+cd4wxRo899pgiIiIUFxenTZs21eybNWuWIiMjFRkZqVmzZtVldAC16PTZar24aJtG/t96naly6B/jE/XKiFjKFYA649YTLG9vb7366qvq0aOHSktL1bNnTyUlJSkqKqrmmKVLlyonJ0c5OTnasGGDHnroIW3YsEElJSWaPHmy7Ha7bDabevbsqaFDh6pVq1YWrgjAL5Wxr0ST0jKVW3xKv+7TQU8N7iafxm79qw6AG3LrCVZQUJB69OghSWrRooW6deumgoKCc45JT0/X2LFjZbPZ1KdPHx0/flyHDh3S8uXLlZSUJH9/f7Vq1UpJSUlatmyZFcsA4ASnzlbphc+zdVfqelUbo49/k6iXhsVSrgBYwmN+8+Tm5mrz5s1KTEw8Z3tBQYHat29f831ISIgKCgouuh2A+/lub7EmpWXpQMkp3XtNR00a1FXNKVYALOQRv4HKysqUnJys119/Xb6+vk697dTUVKWmpkqSCgsLnXrbAH6Z8jNV+tOyHfpo/X51DGimuSl91Cc8wOpYAODeTxFKUmVlpZKTk3X33XdrxIgR5+0PDg5WXl5ezff5+fkKDg6+6Pb/lpKSIrvdLrvdrjZteGs34Cq+3V2kga9/rdnf7de4a8O09LfXU64AuAy3LljGGI0fP17dunXT448/fsFjhg4dqo8++kjGGH333Xfy8/NTUFCQBg4cqBUrVujYsWM6duyYVqxYoYEDB9bxCgBcqdKKSj3z6RaNeX+DGno10LwHr9Fzt0WpWSOPGMgD8BBu/Rtp3bp1mj17tmJjYxUfHy9Jmjp1qg4cOCBJmjBhgoYMGaIlS5YoIiJCzZo10wcffCBJ8vf317PPPqtevXpJkp577jn5+/tbsxAAl+WbnEI9NX+LDp44rd9cH6bHk7qoaSMvq2MBwHlsxhhjdQh3kZCQILvdbnUMoN45WVGpqYu3a+73eQpv01wzRnZXz458pArgTurbY6hbT7AAeL7VO4/q6QVbdORkhR78Vbh+f3NnNWnI1AqAa6NgAXBJJ05X6qVF2zRvY74iA3307sPXKr59S6tjAcBloWABcDkrdxzR0wu2qKjsrB7p30mP3RSpxt5MrQC4DwoWAJdx4lSlJi/K1oJNBerStoXeH9tLsSF+VscCgCtGwQLgEr7YdkTPfLpFx8rP6rEbI/TIjRFMrQC4LQoWAEsdKz+ryQuz9dkPB9UtyFcf3NdLMcFMrQC4NwoWAMss23pIf/wsW8dPndXvbo7UwzdEqJG3W3/+MQBIomABsEBx2Rk9/3m2FmUdUvRVvvpoXG9FXeXcvyMKAFaiYAGoU4uzDum59K06WVGp/03qrAk3dFJDL6ZWADwLBQtAnSgqO6Pn0rdqyZbDig3208d39FGXdi2sjgUAtYKCBaBWGWO0MOuQnk/fqvIz1Zo0qItSrg+XN1MrAB6MggWg1hwtrdCzn23V8uwj6t6+pf48Mk6RbZlaAfB8FCwATmeMUfoPB/XCwmydOlutpwd31fjrwphaAag3KFgAnOrIyQr94dMt+nL7UfXo0FLTR3ZXRKCP1bEAoE5RsAA4hTFG8zcVaMrCbJ2pcuiPt3TT/deGyauBzepoAFDnKFgAfrHDJyr09IIsrdpZqF6hrTR9ZHeFtW5udSwAsAwFC8DPZozRPHu+Xly8TZXVDj1/W5TuvSZUDZhaAajnKFgAfpaDx0/rqQVb9PWuQiWG+Wv6yDh1DGBqBQASBQvAFTLGaO73eXp58XY5jNGU26P168SOTK0A4EcoWAAuW/6xU3pq/hat3V2ka8IDNH1knNr7N7M6FgC4HAoWgEtyOIzmZBzQtCXbJUkvDYvRmN4dmFoBwEVQsAD8pLySU5qUlqX1e4t1XURrTUuOVUgrplYA8FMoWAAuyOEwmv3dfv1p2Q41sNk0bUSs7urVXjYbUysAuBQKFoA3mdtuAAAdIUlEQVTz7C8u18S0LGXsK9GvOrfRKyNidVXLplbHAgC3QcECUMPhMPrw21xNX75DDb0aaPrION3RM4SpFQBcIQoWAEnS3sIyTUrLkn3/Md3YNVBTh8eqnV8Tq2MBgFty64I1btw4LVq0SIGBgdq6det5+2fMmKE5c+ZIkqqqqrR9+3YVFhbK399foaGhatGihby8vOTt7S273V7X8QGXUO0w+vvaffrzip1q7N1Ar97RXSN6BDO1AoBfwGaMMVaH+Lm+/vpr+fj4aOzYsRcsWD+2cOFCvfbaa1q5cqUkKTQ0VHa7Xa1bt77sfy8hIYEiBo+y+2iZJqZlavOB47q5W1tNHR6jQF+mVgCcr749hrr1BKtfv37Kzc29rGM/+eQTjR49unYDAW6iqtqh99fu01++2KVmjbz0xqh4De1+FVMrAHASty5Yl+vUqVNatmyZ3nrrrZptNptNAwYMkM1m04MPPqiUlBQLEwJ1J+dIqZ5Iy1Jm3nENjG6rF4fFKLAFUysAcKZ6UbAWLlyoa6+9Vv7+/jXb1q5dq+DgYB09elRJSUnq2rWr+vXrd965qampSk1NlSQVFhbWWWbA2aqqHXrv671648sc+TTx1l9HX61b44KYWgFALWhgdYC6MHfu3POeHgwODpYkBQYGavjw4crIyLjguSkpKbLb7bLb7WrTpk2tZwVqw47DJzX8nW81Y/lOJUW11Yrf99NtPCUIALXG4wvWiRMntGbNGt1+++0128rLy1VaWlrz9YoVKxQTE2NVRKDWVFY79OZXObrtr2t18PhpvXN3D719dw+19mlsdTQA8Ghu/RTh6NGjtXr1ahUVFSkkJESTJ09WZWWlJGnChAmSpE8//VQDBgxQ8+bNa847cuSIhg8fLunfH98wZswYDRo0qO4XANSibQdPamJaprIPntRt3a/S5KHR8m/eyOpYAFAvuPXHNNS1+vYWU7ins1UOvb1qt95etVstmzXSS8NiNCimndWxANRz9e0x1K0nWADOtbXghJ6Yl6kdh0s1/OpgPXdrlFoxtQKAOkfBAjzAmapqvbVyt95ZvUcBzRvpb2MTlBTV1upYAFBvUbAAN5eVf1wT52Vp55FSJfcI0XO3RsmvWUOrYwFAvUbBAtxURWW13vwqR+99vVdtfBrr7/cl6MauTK0AwBVQsAA3tPnAMU1My9Luo2W6MyFEf7glSn5NmVoBgKugYAFupKKyWq99sUt/+2av2vo20Yf399INXQKtjgUA+C8ULMBNbNx/TBPTMrW3sFyje3fQM0O6qkUTplYA4IooWICLO322Wq+u2KmZ6/bpKr+mmj2+t66P5M82AYAro2ABLuz73BJNSsvSvqJy/bpPBz01uJt8GnO3BQBXx29qwAWdOlulGct36sNvcxXSqqk+fiBRfSNaWx0LAHCZKFiAi/lub7EmpWXpQMkp3XtNR00a1FXNmVoBgFvhtzbgIsrPVOlPy3boo/X71TGgmeam9FGf8ACrYwEAfgYKFuACvt1dpEnzs1Rw/LTuvzZUEwd2UbNG3D0BwF3xGxywUNmZKr2yZLvmbDigsNbN9a8Hr1GvUH+rYwEAfiEKFmCRtTlFenJ+lg6eOK3fXB+mx5O6qGkjL6tjAQCcgIIF1LGTFZV6Zcl2fZKRp/A2zZU2oa96dmxldSwAgBNRsIA6tHrnUT29YIuOnKzQg78K1+9v7qwmDZlaAYCnoWABdeDE6Uq9tGib5m3MV2Sgj955qK+u7sDUCgA8FQULqGUrdxzR0wu2qKjsrB6+oZMeuymSqRUAeDgKFlBLTpyq1ORF2VqwqUBd2rbQ38YmKC6kpdWxAAB1gIIF1IIvth3RHz7douLys/qfGyP06I0RauzN1AoA6gsKFuBEx8rPavLCbH32w0F1bddCf7+vl2KC/ayOBQCoYxQswEmWbT2sP362VcdPndXvbo7UwzdEqJF3A6tjAQAsQMECfqHisjN6/vNsLco6pKggX300rreirvK1OhYAwEIULOAXWLLlkJ79bKtOVlTqf5M6a8INndTQi6kVANR3FCzgZygqO6Pn0rdqyZbDig3205w7EtW1HVMrAMC/ufX/ao8bN06BgYGKiYm54P7Vq1fLz89P8fHxio+P15QpU2r2LVu2TF26dFFERISmTZtWV5Hh5owxWph5UEl/WaMvtx3VxIFd9OnDfSlXAIBzuPUE67777tOjjz6qsWPHXvSY66+/XosWLTpnW3V1tR555BF98cUXCgkJUa9evTR06FBFRUXVdmS4saOlFXr2s61ann1E3du31J9HximybQurYwEAXJBbF6x+/fopNzf3is/LyMhQRESEwsPDJUmjRo1Seno6BQsXZIxR+g8H9cLCbJ06W62nBnfVA9eFyZvXWgEALsLjHyHWr1+v7t27a/DgwcrOzpYkFRQUqH379jXHhISEqKCgwKqIcGFHT1boNx9t1O/++YPCWjfXkseu14RfdaJcAQB+kltPsC6lR48e2r9/v3x8fLRkyRINGzZMOTk5V3QbqampSk1NlSQVFhbWRky4IGOMFmwq0OSF2TpT5dAfb+mm+68Nk1cDm9XRAABuwKP/N9zX11c+Pj6SpCFDhqiyslJFRUUKDg5WXl5ezXH5+fkKDg6+4G2kpKTIbrfLbrerTZs2dZIb1jp8okLjPvxe/zsvU53bttDS316vB64Pp1wBAC6bR0+wDh8+rLZt28pmsykjI0MOh0MBAQFq2bKlcnJytG/fPgUHB2vu3Ln6+OOPrY4LixljNG9jvl5ctE2V1Q49d2uU7u0bSrECAFwxty5Yo0eP1urVq1VUVKSQkBBNnjxZlZWVkqQJEyYoLS1N7777rry9vdW0aVPNnTtXNptN3t7eeuuttzRw4EBVV1dr3Lhxio6Otng1sNLB46f11IIt+npXoXqH+Wt6cpxCWze3OhYAwE3ZjDHG6hDuIiEhQXa73eoYcCJjjOZ+n6eXF2+Xwxg9Oair7unTUQ2YWgGAU9W3x1C3nmABv0T+sVN6esEWfZNTpGvCA/Sn5Dh1CGhmdSwAgAegYKHecTiMPs44oFeWbJckvTQsRmN6d2BqBQBwGgoW6pW8klN6cn6Wvt1TrOsiWmtacqxCWjG1AgA4FwUL9YLDYfSPDfs1bekONbDZ9MqIWI3q1V42G1MrAIDzUbDg8fYXl2tSWpY27CtRv85t9MqIWAW3bGp1LACAB6NgwWM5HEYffpurGct3ytvLpunJcbojIYSpFQCg1lGw4JH2FZVrUlqmvs89pv5d2mjqiFgF+TG1AgDUDQoWPEq1w+iDdfs0Y/lONfZuoFfv6K4RPYKZWgEA6hQFCx5jT2GZJs7L1KYDx3Vzt0C9PDxWbX2bWB0LAFAPUbDg9qodRu9/s1evfrFLzRp56fW74nV7/FVMrQAAlqFgwa3lHCnVE2lZysw7rgFRbfXS8BgFtmBqBQCwFgULbqmq2qH3vt6rN77MUfPGXnpz9NW6LS6IqRUAwCVQsOB2dh4u1cS0TGXln9CQ2HaacnuMWvs0tjoWAAA1KFhwG5XVDv3f6j16c2WOfJs01NtjeuiWuCCrYwEAcB4KFtzC9kMn9cS8TGUfPKnbul+lF26LUgBTKwCAi6JgwaWdrXLondW79dbK3WrZrKH+79c9NCiGqRUAwLVRsOCythac0MS0LG0/dFLD4q/S87dFq1XzRlbHAgDgkihYcDlnqxx6a2WO3lm9R62aN9LfxiYoKaqt1bEAALhsFCy4lKz845o4L0s7j5RqRI9gPXdrlFo2Y2oFAHAvFCy4hDNV1Xrjyxy99/VetfZppL/fl6AbuzK1AgC4JwoWLPdD3nFNnJepnKNlujMhRH+4JUp+TRtaHQsAgJ+NggXLVFRW67Uvd+lvX+9VW98m+vD+XrqhS6DVsQAA+MUoWLDExv3HNCktU3sKyzW6d3s9PaSbfJswtQIAeAYKFupURWW1Xl2xU++v3aer/Jpq9vjeuj6yjdWxAABwKgoW6sz3uSWalJalfUXlujuxg54e0k0+jfkRBAB4Hh7dUOtOna3SjOU79eG3uQpu2VQfP5CovhGtrY4FAECtaWB1gF9i3LhxCgwMVExMzAX3z5kzR3FxcYqNjVXfvn2VmZlZsy80NFSxsbGKj49XQkJCXUWudzbsLdbgN77RB+tydU+fjlr+u36UKwCAx3PrCdZ9992nRx99VGPHjr3g/rCwMK1Zs0atWrXS0qVLlZKSog0bNtTsX7VqlVq35sG+NpSfqdL0ZTs0a/1+dfBvpk9+00fXdAqwOhYAAHXCrQtWv379lJube9H9ffv2rfm6T58+ys/Pr4NU+HZPkZ6cn6X8Y6d1/7Whmjiwi5o1cusfNQAArki9edSbOXOmBg8eXPO9zWbTgAEDZLPZ9OCDDyolJcXCdJ6h7EyVpi3drn98d0BhrZvrXw9eo16h/lbHAgCgztWLgrVq1SrNnDlTa9eurdm2du1aBQcH6+jRo0pKSlLXrl3Vr1+/885NTU1VamqqJKmwsLDOMrubtTn/nlodPHFaD1wXpv8d0EVNG3lZHQsAAEu49YvcL0dWVpYeeOABpaenKyDg/38NUHBwsCQpMDBQw4cPV0ZGxgXPT0lJkd1ul91uV5s2fF7TfyutqNTTC7L065kb1LhhA6VNuEZ/vDWKcgUAqNc8umAdOHBAI0aM0OzZs9W5c+ea7eXl5SotLa35esWKFRd9JyIubs2uQg187Wv98/s8PdgvXEseu149O/KUIAAAbv0U4ejRo7V69WoVFRUpJCREkydPVmVlpSRpwoQJmjJlioqLi/Xwww9Lkry9vWW323XkyBENHz5cklRVVaUxY8Zo0KBBlq3D3Zw4XamXF2/Tv+z5igj00fyH+urqDq2sjgUAgMuwGWOM1SHcRUJCgux2u9UxLLVqx1E9vWCLjpZWaMKvOumxmyLVpCFPBwIAflp9ewx16wkW6s6JU5Wasmib5m/KV5e2LZQ6tqfiQlpaHQsAAJdEwcIlfbntiJ75dIuKy8/qf26M0KM3RqixN1MrAAAuhoKFizpWflaTF2brsx8Oqmu7Fvr7fb0UE+xndSwAAFweBQsXtDz7sP7w6VYdP3VWv70pUo/0j1Ajb49+0ykAAE5DwcI5SsrP6vnPs7Uw86Cignw1a1wvRV/F1AoAgCtBwUKNJVsO6dnPtupkRaX+N6mzJtzQSQ29mFoBAHClKFhQUdkZPZ+ercVbDik22E9z7khU13a+VscCAMBtUbDqMWOMFmUd0vOfZ6usokoTB3bRg/3C5c3UCgCAX4SCVU8Vlp7Rs59t1bLsw+revqVmjIxT57YtrI4FAIBHoGDVM8YYfZ55UM9/nq1TZ6v11OCueuC6MKZWAAA4EQWrHjl6skLPfLpVX24/oqs7tNSMkd0VEehjdSwAADwOBaseMMbo080FeuHzbJ2pcuiPt3TT/deGyauBzepoAAB4JAqWhzt8okLPfLpFK3ccVULHVpo+Mk7hbZhaAQBQmyhYHsoYo3kb8/Xiom2qrHbouVujdG/fUKZWAADUAQqWBzp4/LSeXrBFa3YVqneYv6Ynxym0dXOrYwEAUG9QsDyIMUb//D5PLy3ermqH0eSh0bqnT0c1YGoFAECdomB5iPxjp/T0gi36JqdI14QH6E/JceoQ0MzqWAAA1EsULDdnjNHHGQc0dfF2SdJLw2I0pncHplYAAFiIguXG8kpO6cn5Wfp2T7GujQjQtBFxau/P1AoAAKtRsNyQw2H0jw37NW3pDjWw2TR1eKxG924vm42pFQAAroCC5Wb2F5drUlqWNuwr0fWRrTUtOU7BLZtaHQsAAPwIBctNOBxGs9bnavqynfJuYNP05DjdkRDC1AoAABdEwXID+4rKNSktU9/nHlP/Lm00dUSsgvyYWgEA4KooWC6s2mH0wbp9mrF8pxp7N9Cf7+iu5B7BTK0AAHBxFCwXtaewTBPnZWrTgeO6qWugpo6IVVvfJlbHAgAAl4GC5WKqHUbvf7NXr36xS00beum1u7prWDxTKwAA3EkDqwP8EuPGjVNgYKBiYmIuuN8Yo8cee0wRERGKi4vTpk2bavbNmjVLkZGRioyM1KxZs+oq8k/KOVKq5He/1StLd+iGzm30xeP9NPxqXsgOAIC7ceuCdd9992nZsmUX3b906VLl5OQoJydHqampeuihhyRJJSUlmjx5sjZs2KCMjAxNnjxZx44dq6vY56mqduid1bt1y5trtb+4XG+Ovlrv3dNTgS14ShAAAHfk1gWrX79+8vf3v+j+9PR0jR07VjabTX369NHx48d16NAhLV++XElJSfL391erVq2UlJT0k0WtNu08XKoR736r6ct26qZugVrx+19paPermFoBAODGPPo1WAUFBWrfvn3N9yEhISooKLjo9rr2t6/3avryHWrRpKHeHtNDt8QF1XkGAADgfB5dsJwhNTVVqampkqTCwkKn3rZXA5sGRrfT5KHRCvBp7NTbBgAA1nHrpwgvJTg4WHl5eTXf5+fnKzg4+KLbLyQlJUV2u112u11t2rRxar77rw3VW2N6UK4AAPAwHl2whg4dqo8++kjGGH333Xfy8/NTUFCQBg4cqBUrVujYsWM6duyYVqxYoYEDB9Z5Pl5nBQCAZ3LrpwhHjx6t1atXq6ioSCEhIZo8ebIqKyslSRMmTNCQIUO0ZMkSRUREqFmzZvrggw8kSf7+/nr22WfVq1cvSdJzzz33ky+WBwAAuBI2Y4yxOoS7SEhIkN1utzoGAABup749hnr0U4QAAABWoGABAAA4GQULAADAyShYAAAATkbBAgAAcDIKFgAAgJNRsAAAAJyMggUAAOBkFCwAAAAn45Pcr0Dr1q0VGhrq1NssLCx0+h+Rthprcg+syfV52nok1uQuamNNubm5KioqcuptujIKlsU88U8HsCb3wJpcn6etR2JN7sIT11TXeIoQAADAyShYAAAATub1wgsvvGB1iPquZ8+eVkdwOtbkHliT6/O09UisyV144prqEq/BAgAAcDKeIgQAAHAyClYtWrZsmbp06aKIiAhNmzbtvP1nzpzRXXfdpYiICCUmJio3N7dm3yuvvKKIiAh16dJFy5cvr8PUF3ep9fzlL39RVFSU4uLidNNNN2n//v01+7y8vBQfH6/4+HgNHTq0LmP/pEut6cMPP1SbNm1qsr///vs1+2bNmqXIyEhFRkZq1qxZdRn7J11qTb///e9r1tO5c2e1bNmyZp+rXqdx48YpMDBQMTExF9xvjNFjjz2miIgIxcXFadOmTTX7XPE6XWo9c+bMUVxcnGJjY9W3b19lZmbW7AsNDVVsbKzi4+OVkJBQV5Ev6VJrWr16tfz8/Gp+vqZMmVKz71I/s1a51JpmzJhRs56YmBh5eXmppKREkutep7y8PPXv319RUVGKjo7WG2+8cd4x7nZ/clkGtaKqqsqEh4ebPXv2mDNnzpi4uDiTnZ19zjFvv/22efDBB40xxnzyySfmzjvvNMYYk52dbeLi4kxFRYXZu3evCQ8PN1VVVXW+hh+7nPWsXLnSlJeXG2OMeeedd2rWY4wxzZs3r9O8l+Ny1vTBBx+YRx555Lxzi4uLTVhYmCkuLjYlJSUmLCzMlJSU1FX0i7qcNf3Ym2++ae6///6a713xOhljzJo1a8zGjRtNdHT0BfcvXrzYDBo0yDgcDrN+/XrTu3dvY4zrXqdLrWfdunU1OZcsWVKzHmOM6dixoyksLKyTnFfiUmtatWqVueWWW87bfqU/s3XpUmv6sc8//9z079+/5ntXvU4HDx40GzduNMYYc/LkSRMZGXnef293uz+5KiZYtSQjI0MREREKDw9Xo0aNNGrUKKWnp59zTHp6uu69915J0siRI/XVV1/JGKP09HSNGjVKjRs3VlhYmCIiIpSRkWHFMmpcznr69++vZs2aSZL69Omj/Px8K6JetstZ08UsX75cSUlJ8vf3V6tWrZSUlKRly5bVcuJLu9I1ffLJJxo9enQdJvx5+vXrJ39//4vuT09P19ixY2Wz2dSnTx8dP35chw4dctnrdKn19O3bV61atZLkHvcl6dJruphfcj+sbVeyJne5LwUFBalHjx6SpBYtWqhbt24qKCg45xh3uz+5KgpWLSkoKFD79u1rvg8JCTnvh/jHx3h7e8vPz0/FxcWXdW5du9JMM2fO1ODBg2u+r6ioUEJCgvr06aPPPvusVrNerstd0/z58xUXF6eRI0cqLy/vis6ta1eSa//+/dq3b59uvPHGmm2ueJ0ux8XW7arX6Ur8933JZrNpwIAB6tmzp1JTUy1MduXWr1+v7t27a/DgwcrOzpbkuvelK3Hq1CktW7ZMycnJNdvc4Trl5uZq8+bNSkxMPGe7J9+f6pK31QHgef7xj3/IbrdrzZo1Ndv279+v4OBg7d27VzfeeKNiY2PVqVMnC1Nenttuu02jR49W48aN9d577+nee+/VypUrrY7lFHPnztXIkSPl5eVVs81dr5OnWrVqlWbOnKm1a9fWbFu7dq2Cg4N19OhRJSUlqWvXrurXr5+FKS9Pjx49tH//fvn4+GjJkiUaNmyYcnJyrI7lFAsXLtS11157zrTL1a9TWVmZkpOT9frrr8vX19fqOB6JCVYtCQ4Orpl2SFJ+fr6Cg4MvekxVVZVOnDihgICAyzq3rl1upi+//FIvv/yyPv/8czVu3Pic8yUpPDxcN9xwgzZv3lz7oS/hctYUEBBQs44HHnhAGzduvOxzrXAluebOnXveUxqueJ0ux8XW7arX6XJkZWXpgQceUHp6ugICAmq2/yd/YGCghg8fbvnLBy6Xr6+vfHx8JElDhgxRZWWlioqK3Poa/cdP3Zdc8TpVVlYqOTlZd999t0aMGHHefk+8P1nC6heBearKykoTFhZm9u7dW/PCza1bt55zzFtvvXXOi9zvuOMOY4wxW7duPedF7mFhYZa/yP1y1rNp0yYTHh5udu3adc72kpISU1FRYYwxprCw0ERERLjEi1gvZ00HDx6s+XrBggUmMTHRGPPvF3uGhoaakpISU1JSYkJDQ01xcXGd5r+Qy1mTMcZs377ddOzY0Tgcjpptrnqd/mPfvn0XfbHxokWLznlRbq9evYwxrnudjPnp9ezfv9906tTJrFu37pztZWVl5uTJkzVfX3PNNWbp0qW1nvVy/dSaDh06VPPztmHDBtO+fXvjcDgu+2fWKj+1JmOMOX78uGnVqpUpKyur2ebK18nhcJh77rnH/Pa3v73oMe54f3JFFKxatHjxYhMZGWnCw8PNSy+9ZIwx5tlnnzXp6enGGGNOnz5tRo4caTp16mR69epl9uzZU3PuSy+9ZMLDw03nzp3NkiVLLMn/3y61nptuuskEBgaa7t27m+7du5vbbrvNGPPvd0TFxMSYuLg4ExMTY95//33L1vDfLrWmp556ykRFRZm4uDhzww03mO3bt9ecO3PmTNOpUyfTqVMn8/e//92S/BdyqTUZY8zzzz9vnnzyyXPOc+XrNGrUKNOuXTvj7e1tgoODzfvvv2/effdd8+677xpj/v2g8fDDD5vw8HATExNjvv/++5pzXfE6XWo948ePNy1btqy5L/Xs2dMYY8yePXtMXFyciYuLM1FRUTXX1xVcak1//etfa+5LiYmJ55THC/3MuoJLrcmYf7/T+K677jrnPFe+Tt98842RZGJjY2t+vhYvXuzW9ydXxSe5AwAAOBmvwQIAAHAyChYAAICTUbAAAACcjIIFAADgZBQsAAAAJ6NgAQAAOBkFCwAAwMkoWAAAAE5GwQIAAHAyChYAAICTUbAAAACcjIIFAADgZBQsAAAAJ6NgAQAAOBkFCwAAwMkoWAAAAE5GwQIAAHAyChYAAICTUbAAAACcjIIFAADgZBQsAAAAJ6NgAQAAOBkFCwAAwMkoWAAAAE5GwQIAAHAyChYAAICTUbAAAACcjIIFAADgZP8f+3qaOt3KImkAAAAASUVORK5CYII\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627959_-1475472354", + "id": "20161101-193533_2096366908", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%md\n### Changing the default inline plotting behavior\nBoth the `python` and `pyspark` include a built-in function for changing some default inline plotting behavior. For example, we can change the default size of each figure in pixels to 400x300 in svg format using: ", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "user": "anonymous", "config": { "tableHide": false, "colWidth": 12.0, @@ -198,35 +205,34 @@ "scatter": {} } } - ] + ], + "editorSetting": {} }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627959_-1475472354", - "id": "20160614-174421_274483707", "results": { "code": "SUCCESS", "msg": [ { "type": "HTML", - "data": "\u003ch3\u003eChanging the default inline plotting behavior\u003c/h3\u003e\n\u003cp\u003eBoth the \u003ccode\u003epython\u003c/code\u003e and \u003ccode\u003epyspark\u003c/code\u003e include a built-in function for changing some default inline plotting behavior. For example, we can change the default size of each figure in pixels to 400x300 in svg format using:\u003c/p\u003e\n" + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch3\u003eChanging the default inline plotting behavior\u003c/h3\u003e\n\u003cp\u003eBoth the \u003ccode\u003epython\u003c/code\u003e and \u003ccode\u003epyspark\u003c/code\u003e include a built-in function for changing some default inline plotting behavior. For example, we can change the default size of each figure in pixels to 400x300 in svg format using:\u003c/p\u003e\n\u003c/div\u003e" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627959_-1475472354", + "id": "20160614-174421_274483707", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%python\nz.configure_mpl(width\u003d400, height\u003d300, fmt\u003d\u0027svg\u0027)\nplt.plot([1, 2, 3])", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "user": "anonymous", "config": { "colWidth": 12.0, - "editorMode": "ace/mode/scala", + "editorMode": "ace/mode/python", "enabled": true, "results": [ { @@ -240,7 +246,11 @@ "scatter": {} } } - ] + ], + "editorSetting": { + "language": "python", + "editOnDblClick": false + } }, "settings": { "params": { @@ -248,26 +258,28 @@ }, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627959_-1475472354", - "id": "20160616-234947_579056637", "results": { "code": "SUCCESS", "msg": [ + { + "type": "TEXT", + "data": "[\u003cmatplotlib.lines.Line2D object at 0x28723d0\u003e]\n" + }, { "type": "HTML", - "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAEsCAYAAADtt+XCAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAHapJREFUeJzt3X9MlVeex/HP7VRaKUwodsTkTjusLY3SUPkxHbrRtEiiCbYqSfFHasAtTZTUlmmy7obY7Fgy2uj+Yaq2UUzWH00Y1NUSmwCdoDuXAOHHwBSd6ExkGtBeUkyoSnAchgGe/eN03SqgcIV7nnvv+5WQCPcp/fbp7f30fM9zzvE4juMIAIApesR2AQCA0ESAAAACQoAAAAJCgAAAAkKAAAACQoAAAAJCgAAAAkKAAAACQoAAAAJCgAAAAkKAAAACQoAAAAJCgAAAAkKAAAACQoAAAAJCgAAAAkKAAAACQoAAAAJCgAAAAkKAAAACQoAAAAJCgAAAAkKAAAACQoAAAAJiNUD+/ve/KzMzU2lpaUpJSVFpaem41xUXFyspKUmpqanq6OgIcpUAgPE8avNv/thjj+l3v/udoqOjNTIyosWLFysnJ0e/+MUv7lxTU1Ojr7/+Wp2dnWppaVFRUZGam5stVg0AkFzQwoqOjpZkRiPDw8PyeDx3vX7mzBkVFBRIkjIzM9Xf369r164FvU4AwN2sB8jo6KjS0tI0b948LVu2TC+99NJdr/f09Ojpp5++873X61VPT0+wywQA3MN6gDzyyCP66quv5Pf71dLSokuXLtkuCQAwCVbnQH7oxz/+sZYuXaovv/xSycnJd37u9Xr1zTff3Pne7/fL6/WO+evvbX0BQDhwHMd2CROyOgLp6+tTf3+/JOlvf/ubamtrtWDBgruuWbVqlT777DNJUnNzs+Li4pSQkDDu73Mch68pfm3fvt16DaH2xT3jvs3U1/XrjlaudPTP/+zI73dvcPwfqyOQb7/9Vhs3btTo6KhGR0e1bt06rVixQmVlZfJ4PNq0aZNWrFih6upqPffcc3riiSd05MgRmyUDwIzo6JDeeEN6/XXp1CkpKsp2RQ9mNUBSUlL0hz/8YczPN2/efNf3n3zySbBKAoCgO3pU+rd/k/bvl9avt13N5LlmDgR2ZGVl2S4h5HDPAsN9G2twUCoulurrpbo66QfTvyHB4ziO+xttk+DxeBQm/ygAIkBXl5SXJz37rPRf/yXFxo69xu2fa9Yf4wWASFNdLb38spSfL504MX54hAJaWAAQJCMjUmmpdPiwdPq0tGSJ7YoeDgECAEHQ1ydt2CANDUnt7dIEqxFCCi0sAJhhra1SRoaUlibV1oZHeEiMQABgxjiOdPCgtH27dOiQlJtru6LpRYAAwAy4fVvavFk6f15qbJSSkmxXNP1oYQHANOvsNE9ZeTxSc3N4hodEgADAtKqslBYvlrZskY4dk74/8igs0cICgGkwPCxt22bWdVRVSfccbRSWCBAAeEi9vWYPq8ceM4/oPvWU7YqCgxYWADyEhgbp5z+XsrLMCvNICQ+JEQgABMRxpI8/lnbtMrvp5uTYrij4CBAAmKKBAamw0GyI2NIiJSbarsgOWlgAMAUXL5oJ8vh4076K1PCQCBAAmLSKCjPXUVIilZVJjz9uuyK7aGEBwAMMDUlbt5pJ8tpaKTXVdkXuQIAAwH34/dKaNdLcuVJbmxQXZ7si96CFBQATOHfOzHesXm1WmBMed2MEAgD3GB2Vdu+W9u2Tysul7GzbFbkTAQIAP3DjhrRxozkAqq1N8nptV+RetLAA4HsdHWZV+T/9k+TzER4PQoAAgMxq8mXLpJ07pb17pago2xW5Hy0sABFtcFAqLpbq66W6Oik52XZFoYMRCICI1dVlzu64edOcW054TA0BAiAiVVebUwPz880ZHrGxtisKPbSwAESUkRGptFQ6fFg6fVpassR2RaGLAAEQMfr6pA0bzNYk7e1SQoLtikIbLSwAEaG1VcrIkNLSzH5WhMfDYwQCIKw5jnTwoLR9u3TokJSba7ui8EGAAAhbt29LmzdL589LjY1SUpLtisILLSwAYamz0zxl5fFIzc2Ex0wgQACEncpKs75jyxbp2DEpOtp2ReGJFhaAsDE8LG3bJp08KVVVma3YMXMIEABhobdXWr/eHDPb3i7NmWO7ovBHCwtAyGtoMLvoZmWZkQfhERyMQACELMeRPv5Y2rXL7Kabk2O7oshCgAAISQMDUmGh2RCxpUVKTLRdUeShhQUg5Fy8aCbI4+NN+4rwsIMAARBSKirMXEdJiVRWZibNYQctLAAhYWhI2rrVbMN+9qy0aJHtikCAAHA9v19as0aaO1dqa5Pi4mxXBIkWFgCXO3fOzHfk5poV5oSHezACAeBKo6PS7t3Svn1SebmUnW27ItyLAAHgOjduSBs3mgOg2tokr9d2RRgPLSwArtLRYVaVz58v+XyEh5sRIABc4+hRadkyaedOs8I8Ksp2RbgfWlgArBsclIqLpfp6qa5OSk62XREmgxEIAKu6uszZHf395txywiN0ECAArKmuNqcG5udLx49LsbG2K8JUWA0Qv9+v7OxsvfDCC0pJSdG+ffvGXFNXV6e4uDilp6crPT1dO3bssFApgOk0MiL96lfSpk3S6dPS+++bo2cRWqzOgTz66KPas2ePUlNTdevWLWVkZGj58uVasGDBXde98sor+uKLLyxVCWA69fVJGzaYrUna26WEBNsVIVBWRyDz5s1TamqqJCkmJkYLFy5UT0/PmOscxwl2aQBmQGurlJEhpaVJtbWER6hzzRxId3e3Ojo6lJmZOea1pqYmpaam6rXXXtOlS5csVAfgYTiOdOCA9Prr0t695gCoR3kGNOS54l/hrVu3lJeXp7179yomJuau1zIyMnT16lVFR0erpqZGubm5unz5sqVKAUzV7dvS5s3ShQtSY6OUlGS7IkwX6wEyPDysvLw85efna/Xq1WNe/2Gg5OTk6J133tH169cVHx8/5toPP/zwzp+zsrKUlZU1EyUDmKTOTumNN6TUVKmpSYqOtl2Ru/l8Pvl8PttlTJrHsTzBUFBQoKeeekp79uwZ9/Vr164p4ftGaWtrq9auXavu7u4x13k8HuZKABeprDQjj1//2jxtxVNWU+f2zzWrI5DGxkaVl5crJSVFaWlp8ng8+uijj3TlyhV5PB5t2rRJp06d0oEDBzRr1izNnj1bJ06csFkygAcYHpa2bZNOnpSqqsxW7AhP1kcg08XtSQ1Egt5eaf16c8xsebk0Z47tikKb2z/XXPMUFoDQ1tBgdtHNyjIjD8Ij/FmfRAcQ2hzH7Jy7a5fZTTcnx3ZFCBYCBEDABgakwkKzIWJLi5SYaLsiBBMtLAABuXjRTJDHx5v2FeEReQgQAFNWUWHmOkpKpLIyM2mOyEMLC8CkDQ1JW7eabdjPnpUWLbJdEWwiQABMit8vrVkjzZ0rtbVJcXG2K4JttLAAPNC5c2a+IzfXrDAnPCAxAgFwH6Oj0u7d0r59ZmFgdrbtiuAmBAiAcd28KRUUmAOg2tokr9d2RXAbWlgAxujoMKvK58+XfD7CA+MjQADc5ehRadkyaccOs8I8Ksp2RXArWlgAJEmDg1JxsVRfL9XVScnJtiuC2zECAaCuLmnxYqm/35xbTnhgMggQIMJVV0svvyzl50vHj0uxsbYrQqighQVEqJERqbRUOnxYOn1aWrLEdkUINQQIEIH6+qQNG8zWJO3t0venRgNTQgsLiDCtrVJGhpSWJtXWEh4IHCMQIEI4jnTwoLR9u3TokNmWBHgYBAgQAW7floqKpPPnpcZGKSnJdkUIB7SwgDDX2WmespKkpibCA9OHAAHCWGWlWd+xZYt07JgUHW27IoQTWlhAGBoelj74QDpxQqqqMluxA9ONAAHCTG+vtH69OWa2vV2aM8d2RQhXtLCAMNLQYHbRzcoyIw/CAzOJEQgQBhzH7Jy7e7d05IiUk2O7IkQCAgQIcQMDUmGh2RCxuVlKTLRdESIFLSwghF26ZCbI4+NN+4rwQDARIECIqqiQXn1VKimRysrMpDkQTLSwgBAzNCRt3Wq2YT97Vlq0yHZFiFQECBBC/H5p7VrpJz+R2tqkuDjbFSGS0cICQsS5c2a+Y/Vqs8Kc8IBtjEAAlxsdNY/n7t8v/eY30tKltisCDAIEcLGbN6WCAnMA1O9/L3m9tisC/h8tLMClOjrMqvL58yWfj/CA+xAggAsdPSotXy7t3GlWmEdF2a4IGIsWFuAig4NScbFUX29GHcnJtisCJsYIBHCJri5zdkd/vzm3nPCA2xEggAtUV5tTAwsKpOPHpdhY2xUBD0YLC7BoZEQqLZUOH5Y+/9yMQIBQQYAAlvT1SRs2mK1J2tulhATbFQFTQwsLsKC1VcrIkNLSpNpawgOhiREIEESOIx08KG3fLh06JOXm2q4ICBwBAgTJ7dtSUZF0/rzU2CglJdmuCHg4tLCAIOjsNE9ZeTxSUxPhgfBAgAAzrLLSPF21ZYtZYR4dbbsiYHrQwgJmyPCw9MEH0okTUlWV2YodCCcECDADenul9evNMbPt7dKcObYrAqYfLSxgmjU0mF10s7LMyIPwQLhiBAJME8cxO+fu3i0dOSLl5NiuCJhZBAgwDQYGpMJCsyFic7OUmGi7ImDmWW1h+f1+ZWdn64UXXlBKSor27ds37nXFxcVKSkpSamqqOjo6glwlcH+XLpkJ8vh4074iPBAprAbIo48+qj179ujixYtqamrSp59+qj//+c93XVNTU6Ovv/5anZ2dKisrU1FRkaVqgbEqKqRXX5VKSqSyMjNpDkQKqy2sefPmad68eZKkmJgYLVy4UD09PVqwYMGda86cOaOCggJJUmZmpvr7+3Xt2jUlsHkQLBoakrZuNduwnz0rLVpkuyIg+FzzFFZ3d7c6OjqUmZl51897enr09NNP3/ne6/Wqp6cn2OUBd/j95gmrK1ektjbCA5HLFQFy69Yt5eXlae/evYqJibFdDjCh//kfM9+xerVZYR4XZ7siwB7rT2ENDw8rLy9P+fn5Wr169ZjXvV6vvvnmmzvf+/1+eb3ecX/Xhx9+eOfPWVlZysrKmu5yEaFGR83jufv3S+XlUna27YoQjnw+n3w+n+0yJs3jOI5js4CCggI99dRT2rNnz7ivV1dX69NPP1VVVZWam5v1/vvvq7m5ecx1Ho9Hlv9REKZu3jRHzfb1Sf/939IE//8CTDu3f65ZDZDGxka98sorSklJkcfjkcfj0UcffaQrV67I4/Fo06ZNkqR3331XX375pZ544gkdOXJE6enpY36X2280QlNHh5SXJ73+uvSf/ylFRdmuCJHE7Z9r1kcg08XtNxqh5+hR6d//3bSt1q2zXQ0ikds/16zPgQBuMzgoFRdL9fWSzyclJ9uuCHAnVzyFBbhFV5c5u6O/35xbTngAEyNAgO9VV5tTAwsKpOPHpdhY2xUB7kYLCxFvZEQqLZUOH5Y+/9yMQAA8GAGCiNbXJ23YYLYmaW+X2CEHmDxaWIhYra1SRoaUlibV1hIewFQxAkHEcRzp4EFp+3bp0CEpN9d2RUBoIkAQUW7floqKpPPnpcZGKSnJdkVA6KKFhYjR2WmesvJ4pKYmwgN4WAQIIkJlpXm6assWs8I8Otp2RUDoo4WFsDY8LH3wgXTihFRVZbZiBzA9CBCErd5eaf16c8xse7s0Z47tioDwQgsLYamhQfr5z83JgVVVhAcwExiBIKw4jvTxx+bwpyNHpJwc2xUB4YsAQdgYGJAKC82GiM3NUmKi7YqA8EYLC2Hh0iUzQR4fb9pXhAcw8wgQhLyKCunVV6WSEqmszEyaA5h5tLAQsoaGpK1bzTbsZ89KixbZrgiILAQIQpLfL61dK/3kJ1JbmxQXZ7siIPLQwkLIOXfOzHesXm1WmBMegB2MQBAyRkfN47n790u/+Y20dKntioDIRoAgJNy8aY6a7euTfv97yeu1XREAWlhwvY4Os6p8/nzJ5yM8ALcgQOBqR49Ky5dLO3eaFeZRUbYrAvB/aGHBlQYHpeJiqb7ejDqSk21XBOBejEDgOl1d5uyO/n5zbjnhAbgTAQJXqa42pwYWFEjHj0uxsbYrAjARWlhwhZERqbRUOnxY+vxzMwIB4G4ECKzr65M2bDBbk7S3SwkJtisCMBm0sGBVa6uUkSGlpUm1tYQHEEoYgcAKx5EOHpS2b5cOHZJyc21XBGCqCBAE3e3b0ubN0oULUmOjlJRkuyIAgaCFhaDq7DRPWT3yiNTURHgAoYwAQdBUVpqnq7ZsMSvMo6NtVwTgYdDCwowbHpa2bZNOnpSqqsxW7ABCHwGCGdXbK61fb46ZbW+X5syxXRGA6UILCzOmocHsopuVZUYehAcQXhiBYNo5jtk5d9cuM9eRk2O7IgAzgQDBtBoYkAoLzYaILS1SYqLtigDMFFpYmDYXL5oJ8vh4074iPIDwRoBgWlRUmLmOkhKprMxMmgMIb7Sw8FCGhqStW8027GfPSosW2a4IQLAQIAiY3y+tWSPNnSu1tUlxcbYrAhBMtLAQkHPnzHxHbq5ZYU54AJGHEQimZHRU2r1b2rdPKi+XsrNtVwTAFgIEk3bjhrRxozkAqq1N8nptVwTAJlpYmJSODrOqfP58yecjPAAQIJiEo0elZcuknTvNCvOoKNsVAXADWliY0OCgVFws1ddLdXVScrLtigC4CSMQjKury5zd0d9vzi0nPADciwDBGNXV5tTA/Hzp+HEpNtZ2RQDcyGqAvP3220pISNCLL7447ut1dXWKi4tTenq60tPTtWPHjiBXGFlGRqRf/UratEk6fVp6/33J47FdFQC3sjoH8tZbb+m9995TQUHBhNe88sor+uKLL4JYVWTq65M2bDBbk7S3SwkJtisC4HZWRyBLlizRk08+ed9rHMcJUjWRq7VVysiQ0tKk2lrCA8DkuH4OpKmpSampqXrttdd06dIl2+WEFceRDhyQXn9d2rvXHAD1KM/lAZgkV39cZGRk6OrVq4qOjlZNTY1yc3N1+fJl22WFhdu3pc2bpQsXpMZGKSnJdkUAQo2rAyQmJubOn3NycvTOO+/o+vXrio+PH/f6Dz/88M6fs7KylJWVNcMVhqbOTumNN6TUVKmpSYqOtl0RAEny+Xzy+Xy2y5g0j2N5kqG7u1srV67UH//4xzGvXbt2TQnfN+RbW1u1du1adXd3j/t7PB4P8yWTUFlpRh6//rV52oqnrAD3cvvnmtURyJtvvimfz6fvvvtOzzzzjEpLSzU0NCSPx6NNmzbp1KlTOnDggGbNmqXZs2frxIkTNssNacPD0rZt0smTUlWV2YodAB6G9RHIdHF7UtvU2yutX2+OmS0vl+bMsV0RgMlw++ea65/CwsNpaDC76GZlmZEH4QFgurh6Eh2Bcxyzc+6uXWY33Zwc2xUBCDcESBgaGJAKC82GiC0tUmKi7YoAhCNaWGHm4kUzQR4fb9pXhAeAmUKAhJGKCjPXUVIilZWZSXMAmCm0sMLA0JC0davZhv3sWWnRItsVAYgEBEiI8/ulNWukuXOltjYpLs52RQAiBS2sEHbunJnvyM01K8wJDwDBxAgkBI2OSrt3S/v2mYWB2dm2KwIQiQiQEHPjhrRxozkAqq1N8nptVwQgUtHCCiEdHWZV+fz5ks9HeACwiwAJEUePSsuWSTt3mhXmUVG2KwIQ6WhhudzgoFRcLNXXS3V1UnKy7YoAwGAE4mJdXdLixVJ/vzm3nPAA4CYEiEtVV0svvyzl50vHj0uxsbYrAoC70cJymZERqbRUOnxYOn1aWrLEdkUAMD4CxEX6+qQNG8zWJO3t0ven+QKAK9HCconWVikjQ0pLk2prCQ8A7scIxDLHkQ4elLZvlw4dMtuSAEAoIEAs+utfpaIi6cIFqbFRSkqyXREATB4tLEsuXzZPWXk8UlMT4QEg9BAgFlRWmqer3n1XOnZMio62XREATB0trCAaHpa2bZNOnpSqqsxW7AAQqgiQIOntldatk2bPNo/ozpljuyIAeDi0sIKgocHsort0qRl5EB4AwgEjkBnkOGbn3F27zG66OTm2KwKA6UOAzJCBAamw0GyI2NIiJSbarggAphctrBlw8aKZII+PN+0rwgNAOCJApllFhZSVJZWUSGVl0uOP264IAGYGLaxpMjQk/eu/SjU10tmz0qJFtisCgJlFgEwDv19as0aaO1dqa5Pi4mxXBAAzjxbWQzp3zsx35OaaFeaEB4BIwQgkQKOj5vHc/ful8nIpO9t2RQAQXARIAG7ckDZuNAdAtbVJXq/tigAg+GhhTVFHh1lVPn++5PMRHgAiFwEyBUeOSMuWSTt3mhXmUVG2KwIAe2hhTcLgoFRcLNXXS3V1UnKy7YoAwD5GIA/Q1SUtXiz195tzywkPADAIkPuorjanBubnS8ePS7GxtisCAPeghTWOkRGptFQ6fFg6fdqcHggAuBsBco++PunNN6V//MMc/JSQYLsiAHAnWlg/0NoqZWRI6elSbS3hAQD3wwhE5uCngwel7dulQ4fMtiQAgPuL+AD561+loiLpwgWpsVFKSrJdEQCEhohuYV2+bJ6y8nikpibCAwCmImIDpLLSPF317rvSsWNSdLTtigAgtERcC2t4WNq2TTp5UqqqMluxAwCmLqICpLdXWrdOmj3bPKI7Z47tigAgdEVMC6uhweyiu3SpGXkQHgDwcMJ+BOI4ZufcXbuko0elnBzbFQFAeAjrABkYkAoLzYaILS1SYqLtigAgfFhtYb399ttKSEjQiy++OOE1xcXFSkpKUmpqqjo6Oib9uy9eNBPk8fGmfUV4AMD0shogb731ln77299O+HpNTY2+/vprdXZ2qqysTEVFRZP6vRUVUlaWVFIilZVJjz8+TQWHIZ/PZ7uEkMM9Cwz3LfxYDZAlS5boySefnPD1M2fOqKCgQJKUmZmp/v5+Xbt2bcLrh4ak996T/uM/pLNnpX/5l+muOPzwH/XUcc8Cw30LP66eA+np6dHTTz9953uv16uenh4lTLDL4auvSnPnSm1tUlxcsKoEgMgUVo/xrl5tVpgTHgAw8zyO4zg2C7hy5YpWrlypCxcujHmtqKhIS5cu1bp16yRJCxYsUF1d3bgjEI/HM+O1AkCwWf6Ivi/rLSzHcSa8QatWrdKnn36qdevWqbm5WXFxcRO2r9x8kwEgHFkNkDfffFM+n0/fffednnnmGZWWlmpoaEgej0ebNm3SihUrVF1dreeee05PPPGEjhw5YrNcAMAPWG9hAQBCU8hNon/55ZdasGCBnn/+ee3evXvcawJdfBiuHnTP6urqFBcXp/T0dKWnp2vHjh0WqnSXmVzkGs4edN94r43l9/uVnZ2tF154QSkpKdq3b9+417ny/eaEkJGREefZZ591uru7naGhIWfRokXOn/70p7uuqa6udlasWOE4juM0Nzc7mZmZNkp1jcncM5/P56xcudJShe5UX1/vfPXVV05KSsq4r/M+G9+D7hvvtbG+/fZb56uvvnIcx3EGBgac559/PmQ+10JqBNLa2qqkpCT97Gc/06xZs7R+/XqdOXPmrmumuvgw3E3mnkk8hHCv6V7kGikedN8k3mv3mjdvnlJTUyVJMTExWrhwoXp6eu66xq3vt5AKkHsXFv70pz8dc6MnWnwYqSZzzySpqalJqampeu2113Tp0qVglhiSeJ8FjvfaxLq7u9XR0aHMzMy7fu7W95v1x3hhX0ZGhq5evaro6GjV1NQoNzdXly9ftl0WwhDvtYndunVLeXl52rt3r2JiYmyXMykhNQLxer26evXqne/9fr+8Xu+Ya7755pv7XhNJJnPPYmJiFP39ofA5OTn6xz/+oevXrwe1zlDD+ywwvNfGNzw8rLy8POXn52v16tVjXnfr+y2kAuSll17SX/7yF125ckVDQ0M6fvy4Vq1addc1q1at0meffSZJD1x8GAkmc89+2EttbW2V4ziKj48Pdqmu4zxgkSvvs/Hd777xXhtfYWGhkpOT9ctf/nLc1936fgupFtaPfvQjffLJJ1q+fLlGR0f19ttva+HChSorK2Px4QQmc89OnTqlAwcOaNasWZo9e7ZOnDhhu2zrWOQamAfdN95rYzU2Nqq8vFwpKSlKS0uTx+PRRx99pCtXrrj+/cZCQgBAQEKqhQUAcA8CBAAQEAIEABAQAgQAEBACBAAQEAIEABAQAgQAEBACBAAQEAIEABAQAgQAEBACBAAQEAIEABAQAgQAEBACBAAQEAIEABAQAgQAEBACBAAQkP8FIFQxVkgtI/8AAAAASUVORK5CYII\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" + "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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 style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627959_-1475472354", + "id": "20160616-234947_579056637", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%md\n### Iteratively updating a plot\n#### (a) Using multiple plots\nNow let\u0027s show an example where we update each element of the plot in a separate paragraph. However, you may have noticed that each matplotlib figure instance gets closed immediately after its shown. To fix this, we set the `close` property to `False` in our configuration:", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "user": "anonymous", "config": { "colWidth": 12.0, "editorMode": "ace/mode/markdown", @@ -285,33 +297,32 @@ "scatter": {} } } - ] + ], + "editorSetting": {} }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627960_-1477396098", - "id": "20160617-140439_1111727405", "results": { "code": "SUCCESS", "msg": [ { "type": "HTML", - "data": "\u003ch3\u003eIteratively updating a plot\u003c/h3\u003e\n\u003ch4\u003e(a) Using multiple plots\u003c/h4\u003e\n\u003cp\u003eNow let\u0027s show an example where we update each element of the plot in a separate paragraph. However, you may have noticed that each matplotlib figure instance gets closed immediately after its shown. To fix this, we set the \u003ccode\u003eclose\u003c/code\u003e property to \u003ccode\u003eFalse\u003c/code\u003e in our configuration:\u003c/p\u003e\n" + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch3\u003eIteratively updating a plot\u003c/h3\u003e\n\u003ch4\u003e(a) Using multiple plots\u003c/h4\u003e\n\u003cp\u003eNow let\u0026rsquo;s show an example where we update each element of the plot in a separate paragraph. However, you may have noticed that each matplotlib figure instance gets closed immediately after its shown. To fix this, we set the \u003ccode\u003eclose\u003c/code\u003e property to \u003ccode\u003eFalse\u003c/code\u003e in our configuration:\u003c/p\u003e\n\u003c/div\u003e" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627960_-1477396098", + "id": "20160617-140439_1111727405", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "First line", "text": "%python\nplt.close() # Added here to reset the first plot when rerunning the paragraph\nz.configure_mpl(width\u003d600, height\u003d400, fmt\u003d\u0027png\u0027, close\u003dFalse)\nplt.plot([1, 2, 3], label\u003dr\u0027$y\u003dx$\u0027)", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "user": "anonymous", "config": { "colWidth": 12.0, "title": true, @@ -328,33 +339,40 @@ "scatter": {} } } - ] + ], + "editorSetting": { + "language": "python", + "editOnDblClick": false + }, + "editorMode": "ace/mode/python" }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627960_-1477396098", - "id": "20161101-195657_1336292109", "results": { "code": "SUCCESS", "msg": [ + { + "type": "TEXT", + "data": "[\u003cmatplotlib.lines.Line2D object at 0x2a76fd0\u003e]\n" + }, { "type": "HTML", - "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAGQCAYAAAByNR6YAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAIABJREFUeJzt3XtsVfWe/vF3vcQbTISgNHZAI3AsKNgLTEWR9OAFUcFLCBijIKKIckRHnRhGx4M/8XJMdERB8RJxIJgh4AUMWCVyU6BQoUWDjqgEhIoooHVAtLRdvz++5zAid9jt2nvt9yshad3rkE/c7tMnz1r9fnKiKIqQJElSyhwV9wCSJElJY8CSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKscQHrN9++42SkhIKCwvp3LkzDz/88F6vGzlyJB06dKCgoICqqqomnlKSJCXJMXEP0NiOO+445s2bx4knnkh9fT0XXHABffr04V/+5V92XfPuu+/y9ddf8+WXX7J06VKGDx9OeXl5jFNLkqRMlvgGC+DEE08EQptVV1dHTk7Obq/PmDGDQYMGAVBSUkJNTQ2bNm1q8jklSVIyZEXAamhooLCwkNzcXC655BK6deu22+vV1dW0adNm1/d5eXlUV1c39ZiSJCkhsiJgHXXUUVRWVrJhwwaWLl3KZ599FvdIkiQpwRL/DNbv/dM//RN//vOfKSsro1OnTrv+eV5eHuvXr9/1/YYNG8jLy9vjf//HW4uSJOngRVEU9whNJvEN1ubNm6mpqQFgx44dzJkzh/z8/N2u6devH5MmTQKgvLyck08+mdatW+/174uiyD8J+fPXv/419hn843vqH9/PpP5ZsiSiQ4eIG26I+PHH7AlW/5D4Bmvjxo0MHjyYhoYGGhoaGDhwIJdffjkvvvgiOTk5DBs2jMsvv5zZs2fTvn17TjrpJCZOnBj32JIkZaTaWnjkEXj5ZRg3Dvr3j3uieCQ+YHXu3JkVK1bs8c9vu+223b4fN25cU40kSVIirVoFN94IeXlQVQW5uXFPFJ/E3yKU9qW0tDTuEZRivqfJ4vuZOerr4amnoLQURoyAmTOzO1wB5ERRlH03Rg9TTk4O/uuSJOn/rF0LgwdDFMFrr8GZZ+79umz7GWqDJUmSDlkUwauvQrdu0LcvzJu373CVjRL/DJYkSUqtTZvg1lth/foQrM45J+6J0o8NliRJOmhvvgnnngtdusDSpYarfbHBkiRJB/TTTzByJCxZAm+9Bd27xz1RerPBkiRJ+/XBB6G1at48HL9guDowGyxJkrRXv/wCo0aF24KvvAK9e8c9UeawwZIkSXuoqICiIti8GT75xHB1qGywJEnSLjt3wpgxMGECPPccDBgQ90SZyYAlSZIA+PzzsOrm1FOhshJOOy3uiTKXtwglScpyDQ3wzDPQsycMGwazZhmujpQNliRJWWzdOrjppnBrsLwc2rWLe6JksMGSJCkL/WN3YNeucNllsGCB4SqVbLAkScoy338Pt90Ga9aEM666dIl7ouSxwZIkKYvMmBEODc3Ph2XLDFeNxQZLkqQsUFMDd98NH34I06fDBRfEPVGy2WBJkpRw8+aF1uq448KqG8NV47PBkiQpoXbsgAcegKlTw6qbPn3inih72GBJkpRAy5dDcTFUV4dVN4arpmWDJUlSguzcCY8/DuPHw9ixcN11cU+UnQxYkiQlxBdfhFU3LVrAihWQlxf3RNnLW4SSJGW4hoawmLlHDxgyBMrKDFdxs8GSJCmDrV8fQtX27bB4MXToEPdEAhssSZIyUhTB5MnhQfaLLgrnWxmu0ocNliRJGeaHH2D4cFi9Gt5/HwoK4p5If2SDJUlSBnnnnXBoaLt2UFFhuEpXNliSJGWAn3+Ge+6BuXPDwaEXXhj3RNofGyxJktLcwoWhtTrqKFi50nCVCWywJElKU7/+Cg8+CK+/Di+9BFdeGfdEOlgGLEmS0lBlZTg0ND8/rLpp1SruiXQovEUoSVIaqauDRx+F3r1h1CiYNs1wlYlssCRJShOrV8OgQdC8eVjW3KZN3BPpcNlgSZIUsygKy5nPPx9uuAHee89wlelssCRJitGGDXDzzVBTA4sWwVlnxT2RUsEGS5KkGERR+O3AoiLo2dNwlTQ2WJIkNbEtW+D222HVKigrCyFLyWKDJUlSE5o1C7p0Cc9YLV9uuEoqGyxJkprAtm1w771hOfOUKVBaGvdEakw2WJIkNbKPPgqrburqwqobw1Xy2WBJktRIfvsNHnoIJk+GCROgX7+4J1JTMWBJktQIVq4Mq27atw9fn3JK3BOpKXmLUJKkFKqvhyeegEsugfvugzfeMFxlIxssSZJS5KuvYPBgOP54+PhjaNs27okUFxssSZKOUBSFZ6y6d4eBA2HOHMNVtrPBkiTpCHz7LQwdCps3w8KF0LFj3BMpHdhgSZJ0mKZOhcJCOO88WLzYcKX/Y4MlSdIh2roVRoyAqqpwMnvXrnFPpHRjgyVJ0iEoKwurbnJzYcUKw5X2zgZLkqSDsH17OHZh9myYNAl69Yp7IqUzGyxJkg5g8WIoKIAdO+CTTwxXOjAbLEmS9qG2FkaPhokT4fnn4Zpr4p5ImcKAJUnSXnz6aVh1c/rp4WH21q3jnkiZxFuEkiT9Tn09PPlkuA14113w9tuGKx06GyxJkv5uzZqw6uboo6GiAs44I+6JlKlssCRJWS+K4OWXoaQErr0W5s41XOnI2GBJkrLaxo1wyy3w3XewYAF06hT3REoCGyxJUtaaNi0cv1BcDOXlhiuljg2WJCnr/Pgj3HlneM5q5sxwa1BKJRssSVJWmTMnrLpp2RIqKw1Xahw2WJKkrLB9O9x/f2isJk6Eiy+OeyIlmQ2WJCnxysuhsBBqasKqG8OVGpsNliQpsWpr4ZFHwhEM48ZB//5xT6RsYcCSJCXSqlVh1U1eXlh1k5sb90TKJt4ilCQlSn09PPUUlJbCiBHhmSvDlZqaDZYkKTHWrg2rbqIIli6FM8+MeyJlKxssSVLGiyJ49VXo1g369oV58wxXipcNliQpo23aBLfeCuvXh2B1zjlxTyTZYEmSMtibb8K554aDQ5cuNVwpfdhgSZIyzk8/wciRsGQJvPUWdO8e90TS7mywJEkZ5YMPQmvVvHk4fsFwpXSU+IC1YcMGevXqxdlnn03nzp159tln97hmwYIFnHzyyRQVFVFUVMSYMWNimFSStD+//AJ33QU33QQvvQTjx8NJJ8U9lbR3ib9FeMwxx/D0009TUFDAtm3bKC4u5tJLLyU/P3+363r27MnMmTNjmlKStD8VFeHQ0OLisOqmRYu4J5L2L/ENVm5uLgUFBQA0a9aMjh07Ul1dvcd1URQ19WiSpAPYuRP++le48kr4f/8PpkwxXCkzJD5g/d7atWupqqqipKRkj9eWLFlCQUEBV1xxBZ999lkM00mSfu/zz8PzVRUVUFkJAwbEPZF08LImYG3bto3+/fszduxYmjVrtttrxcXFfPPNN1RVVfGXv/yFq6++OqYpJUkNDfDMM9CzJwwbBrNmwWmnxT2VdGgS/wwWQF1dHf379+fGG2/kqquu2uP13weuPn36cMcdd7B161Zatmy5x7WjR4/e9XVpaSmlpaWNMbIkZaV168JD7Dt3Qnk5tGsX90Q6XPPnz2f+/PlxjxGbnCgLHj4aNGgQrVq14umnn97r65s2baJ169YALFu2jAEDBrB27do9rsvJyfFZLUlqBFEE//Vf8G//BvfdF/4cfXTcUymVsu1naOIbrEWLFjFlyhQ6d+5MYWEhOTk5PPbYY6xbt46cnByGDRvG9OnTeeGFFzj22GM54YQTmDp1atxjS1LW+P57uO02WLMmnHHVpUvcE0lHLisarFTJtvQtSY1txgwYPjzcFhw9Go47Lu6J1Fiy7Wdo4hssSVL6qamBu++GDz+E6dPhggvinkhKraz5LUJJUnqYNy+sujnuuLDqxnClJLLBkiQ1iR074IEHYOpUeOUV6NMn7omkxmODJUlqdMuXhzU31dVh1Y3hSklngyVJajQ7d8Ljj4fFzGPHwnXXxT2R1DQMWJKkRvHFF2FBc4sWsGIF5OXFPZHUdLxFKElKqYYGeO456NEDhgyBsjLDlbKPDZYkKWXWrw+havt2WLwYOnSIeyIpHjZYkqQjFkUweXJ4kP2ii8L5VoYrZTMbLEnSEfnhh3Aa++rV8P77UFAQ90RS/GywJEmH7Z13wqGh7dpBRYXhSvoHGyxJ0iH7+We45x6YOzccHHrhhXFPJKUXGyxJ0iFZuDC0VkcdBStXGq6kvbHBkiQdlF9/hQcfhNdfh5degiuvjHsiKX0ZsCRJB1RZGQ4Nzc8Pq25atYp7Iim9eYtQkrRPdXXw6KPQuzeMGgXTphmupINhgyVJ2qvVq2HQIGjePCxrbtMm7omkzGGDJUnaTRSF5cznnw833ADvvWe4kg6VDZYkaZcNG+Dmm6GmBhYtgrPOinsiKTPZYEmSiKLw24FFRdCzp+FKOlI2WJKU5bZsgdtvh1WroKwshCxJR8YGS5Ky2KxZ0KVLeMZq+XLDlZQqNliSlIW2bYN77w3LmadMgdLSuCeSksUGS5KyzEcfhVU3dXVh1Y3hSko9GyxJyhK//QYPPQSTJ8OECdCvX9wTScllwJKkLLByZVh10759+PqUU+KeSEo2bxFKUoLV18MTT8All8B998EbbxiupKZggyVJCfXVVzB4MBx/PHz8MbRtG/dEUvawwZKkhImi8IxV9+4wcCDMmWO4kpqaDZYkJci338LQobB5MyxcCB07xj2RlJ1ssCQpIaZOhcJCOO88WLzYcCXFyQZLkjLc1q0wYgRUVYWT2bt2jXsiSTZYkpTBysrCqpvcXFixwnAlpQsbLEnKQNu3h2MXZs+GSZOgV6+4J5L0ezZYkpRhFi+GggLYsQM++cRwJaUjGyxJyhC1tTB6NEycCM8/D9dcE/dEkvbFgCVJGeDTT8Oqm9NPDw+zt24d90SS9sdbhJKUxurr4cknw23Au+6Ct982XEmZwAZLktLUmjVh1c3RR0NFBZxxRtwTSTpYNliSlGaiCF5+GUpK4NprYe5cw5WUaWywJCmNbNwIt9wC330HCxZAp05xTyTpcNhgSVKamDYtHL9QXAzl5YYrKZPZYElSzH78Ee68MzxnNXNmuDUoKbPZYElSjObMCatuWraEykrDlZQUNliSFIPt2+H++0NjNXEiXHxx3BNJSiUbLElqYuXlUFgINTVh1Y3hSkoeGyxJaiK1tfDII+EIhnHjoH//uCeS1FgMWJLUBFatCqtu8vLCqpvc3LgnktSYvEUoSY2ovh6eegpKS2HEiPDMleFKSj4bLElqJGvXhlU3UQRLl8KZZ8Y9kaSmYoMlSSkWRfDqq9CtG/TtC/PmGa6kbGODJUkptGkT3HorrF8fgtU558Q9kaQ42GBJUoq8+Sace244OHTpUsOVlM1ssCTpCP30E4wcCUuWwFtvQffucU8kKW42WJJ0BD74ILRWzZuH4xcMV5LABkuSDssvv8CoUeG24CuvQO/ecU8kKZ3YYEnSIaqogKIi2Lw5rLoxXEn6IxssSTpIO3fCmDEwYQI89xwMGBD3RJLSlQFLkg7C55+HVTenngqVlXDaaXFPJCmdeYtQkvajoQGeeQZ69oRhw2DWLMOVpAOzwZKkfVi3Dm66KdwaLC+Hdu3inkhSprDBkqQ/iCJ47TXo2hUuuwwWLDBcSTo0NliS9Dvffw+33QZr1oQzrrp0iXsiSZnIBkuS/m7GjHBoaH4+LFtmuJJ0+GywJGW9mhq4+2748EOYPh0uuCDuiSRlOhssSVlt3rzQWh13XFh1Y7iSlAo2WJKy0o4d8MADMHVqWHXTp0/cE0lKEhssSVln+XIoLobq6rDqxnAlKdVssCRljZ074fHHYfx4GDsWrrsu7okkJZUBS1JW+OKLsOqmRQtYsQLy8uKeSFKSeYtQUqI1NITFzD16wJAhUFZmuJLU+BIfsDZs2ECvXr04++yz6dy5M88+++xerxs5ciQdOnSgoKCAqqqqJp5SUmNYvx4uvRRefx0WL4bbb4ecnLinkpQNEh+wjjnmGJ5++mlWrVrFkiVLGD9+PP/zP/+z2zXvvvsuX3/9NV9++SUvvvgiw4cPj2laSakQRTB5cniQ/aKLwvlWHTrEPZWkbJL4Z7Byc3PJzc0FoFmzZnTs2JHq6mry8/N3XTNjxgwGDRoEQElJCTU1NWzatInWrVvHMrOkw/fDDzB8OKxeDe+/DwUFcU8kKRslvsH6vbVr11JVVUVJSclu/7y6upo2bdrs+j4vL4/q6uqmHk/SEXrnnXBoaLt2UFFhuJIUn8Q3WP+wbds2+vfvz9ixY2nWrFnc40hKoZ9/hnvugblzw8GhF14Y90SSsl1WBKy6ujr69+/PjTfeyFVXXbXH63l5eaxfv37X9xs2bCBvH79mNHr06F1fl5aWUlpamupxJR2ChQth8GC4+GJYuRKaN497IkkA8+fPZ/78+XGPEZucKIqiuIdobIMGDaJVq1Y8/fTTe3199uzZjB8/nlmzZlFeXs7dd99NeXn5Htfl5OSQBf+6pIzw66/w4IPhNwRfegmuvDLuiSTtT7b9DE18g7Vo0SKmTJlC586dKSwsJCcnh8cee4x169aRk5PDsGHDuPzyy5k9ezbt27fnpJNOYuLEiXGPLWk/KivDoaH5+WHVTatWcU8kSbvLigYrVbItfUvppq4O/va3sObmP/8Trr/ec62kTJFtP0MT32BJSobVq2HQoPCM1fLl8Ltf/JWktJNVxzRIyjxRFJYzn38+3HADvPee4UpS+rPBkpS2NmyAm2+GmhpYtAjOOivuiSTp4NhgSUo7URR+O7CoCHr2NFxJyjw2WJLSypYtYSnzqlVQVhZCliRlGhssSWlj1izo0iU8Y7V8ueFKUuaywZIUu23b4N57w3LmKVPABQmSMp0NlqRYffRRWNBcVxdW3RiuJCWBDZakWPz2Gzz0EEyeDBMmQL9+cU8kSaljwJLU5FauDKtu2rcPX59yStwTSVJqeYtQUpOpr4cnnoBLLoH77oM33jBcSUomGyxJTeKrr2DwYDj+ePj4Y2jbNu6JJKnx2GBJalRRFJ6x6t4dBg6EOXMMV5KSzwZLUqP59lsYOhQ2b4aFC6Fjx7gnkqSmYYMlqVFMnQqFhXDeebB4seFKUnaxwZKUUlu3wogRUFUVTmbv2jXuiSSp6dlgSUqZsrKw6iY3F1asMFxJyl42WJKO2Pbt4diF2bNh0iTo1SvuiSQpXjZYko7I4sVQUAA7dsAnnxiuJAlssCQdptpaGD0aJk6E55+Ha66JeyJJSh8GLEmH7NNPw6qb008PD7O3bh33RJKUXrxFKOmg1dfDk0+G24B33QVvv224kqS9scGSdFDWrAmrbo4+Gioq4Iwz4p5IktKXDZak/YoiePllKCmBa6+FuXMNV5J0IDZYkvZp40a45Rb47jtYsAA6dYp7IknKDDZYkvZq2rRw/EJxMZSXG64k6VDYYEnazY8/wp13huesZs4MtwYlSYfGBkvSLnPmhFU3LVtCZaXhSpIOlw2WJLZvh/vvD43VxIlw8cVxTyRJmc0GS8py5eVQWAg1NWHVjeFKko6cDZaUpWpr4ZFHwhEM48ZB//5xTyRJyWHAkrLQqlVh1U1eXlh1k5sb90SSlCzeIpSySH09PPUUlJbCiBHhmSvDlSSlng2WlCXWrg2rbqIIli6FM8+MeyJJSi4bLCnhoghefRW6dYO+fWHePMOVJDU2GywpwTZtgltvhfXrQ7A655y4J5Kk7GCDJSXUm2/CueeGg0OXLjVcSVJTssGSEuann2DkSFiyBN56C7p3j3siSco+NlhSgnzwQWitmjcPxy8YriQpHjZYUgL88guMGhVuC77yCvTuHfdEkpTdbLCkDFdRAUVFsHlzWHVjuJKk+NlgSRlq504YMwYmTIDnnoMBA+KeSJL0DwYsKQN9/nlYdXPqqVBZCaedFvdEkqTf8xahlEEaGuCZZ6BnTxg2DGbNMlxJUjqywZIyxLp1cNNN4dZgeTm0axf3RJKkfbHBktJcFMFrr0HXrnDZZbBggeFKktKdDZaUxr7/Hm67DdasCWdcdekS90SSpINhgyWlqRkzwqGh+fmwbJnhSpIyiQ2WlGZqauDuu+HDD2H6dLjggrgnkiQdKhssKY3Mmxdaq+OOC6tuDFeSlJlssKQ0sGMHPPAATJ0aVt306RP3RJKkI2GDJcVs+XIoLobq6rDqxnAlSZnPBkuKyc6d8PjjMH48jB0L110X90SSpFQxYEkx+OKLsOqmRQtYsQLy8uKeSJKUSt4ilJpQQ0NYzNyjBwwZAmVlhitJSiIbLKmJrF8fQtX27bB4MXToEPdEkqTGYoMlNbIogsmTw4PsF10UzrcyXElSstlgSY3ohx9g+HBYvRrefx8KCuKeSJLUFGywpEbyzjvh0NB27aCiwnAlSdnEBktKsZ9/hnvugblzw8GhF14Y90SSpKZmgyWl0MKFobU66ihYudJwJUnZygZLSoFff4UHH4TXX4eXXoIrr4x7IklSnAxY0hGqrAyHhubnh1U3rVrFPZEkKW7eIpQOU10dPPoo9O4No0bBtGmGK0lSYIMlHYbVq2HQIGjePCxrbtMm7okkSenEBks6BFEUljOffz7ccAO8957hSpK0Jxss6SBt2AA33ww1NbBoEZx1VtwTSZLSlQ2WdABRFH47sKgIevY0XEmSDswGS9qPLVvg9tth1SooKwshS5KkA7HBkvZh1izo0iU8Y7V8ueFKknTwbLCkP9i2De69NyxnnjIFSkvjnkiSlGkS32ANHTqU1q1b06VLl72+vmDBAk4++WSKioooKipizJgxTTyh0slHH4VVN3V1YdWN4UqSdDgS32ANGTKEO++8k0GDBu3zmp49ezJz5swmnErp5rff4KGHYPJkmDAB+vWLeyJJUiZLfIPVo0cPWrRosd9roihqommUjlauhG7d4Msvw9eGK0nSkUp8wDoYS5YsoaCggCuuuILPPvss7nHUROrr4Ykn4JJL4L774I034JRT4p5KkpQEib9FeCDFxcV88803nHjiibz77rtcffXVrF69Ou6x1Mi++goGD4bjj4ePP4a2beOeSJKUJFkfsJo1a7br6z59+nDHHXewdetWWrZsudfrR48evevr0tJSSn0KOqNEEbz4IvzHf4Q/f/kLHGWPK0kpN3/+fObPnx/3GLHJibLgAaS1a9fSt29fPv300z1e27RpE61btwZg2bJlDBgwgLVr1+7178nJyfF5rQz27bcwdChs3gyTJkHHjnFPJEnZI9t+hia+wbr++uuZP38+W7ZsoW3btjz88MPU1taSk5PDsGHDmD59Oi+88ALHHnssJ5xwAlOnTo17ZDWCqVNh5Ei44w7493+HY4+NeyJJUpJlRYOVKtmWvpNg61YYMQKqqsIRDF27xj2RJGWnbPsZ6tMnSqyysrDqJjcXVqwwXEmSmk7ibxEq+2zfHo5dmD07PGvVq1fcE0mSso0NlhJl8WIoKIAdO+CTTwxXkqR42GApEWprYfRomDgRnn8errkm7okkSdnMgKWM9+mncOONcPrp4WH2v5+6IUlSbLxFqIxVXw9PPhluA951F7z9tuFKkpQebLCUkdasCatujj4aKirgjDPinkiSpP9jg6WMEkXw8stQUgLXXgtz5xquJEnpxwZLGWPjRrjlFvjuO1iwADp1insiSZL2zgZLGWHatHD8QnExlJcbriRJ6c0GS2ntxx/hzjvDc1YzZ4Zbg5IkpTsbLKWtOXPCqpuWLaGy0nAlScocNlhKO9u3w/33h8Zq4kS4+OK4J5Ik6dDYYCmtlJdDYSHU1IRVN4YrSVImssFSWqithUceCUcwjBsH/fvHPZEkSYfPgKXYrVoVVt3k5YVVN7m5cU8kSdKR8RahYlNfD089BaWlMGJEeObKcCVJSgIbLMVi7dqw6iaKYOlSOPPMuCeSJCl1bLDUpKIIXn0VunWDvn1h3jzDlSQpeWyw1GQ2bYJbb4X160OwOuecuCeSJKlx2GCpSbz5Jpx7bjg4dOlSw5UkKdlssNSofvoJRo6EJUvgrbege/e4J5IkqfHZYKnRfPBBaK2aNw/HLxiuJEnZwgZLKffLLzBqVLgt+Mor0Lt33BNJktS0bLCUUhUVUFQEmzeHVTeGK0lSNrLBUkrs3AljxsCECfDcczBgQNwTSZIUHwOWjtjnn4dVN6eeCpWVcNppcU8kSVK8vEWow9bQAM88Az17wrBhMGuW4UqSJLDB0mFatw5uuincGiwvh3bt4p5IkqT0YYOlQxJF8Npr0LUrXHYZLFhguJIk6Y9ssHTQvv8ebrsN1qwJZ1x16RL3RJIkpScbLB2UGTPCoaH5+bBsmeFKkqT9scHSftXUwN13w4cfwvTpcMEFcU8kSVL6s8HSPs2bF1qr444Lq24MV5IkHRwbLO1hxw544AGYOjWsuunTJ+6JJEnKLDZY2s3y5VBcDNXVYdWN4UqSpENngyUgnGf1+OMwfjyMHQvXXRf3RJIkZS4Dlvjii7DqpkULWLEC8vLinkiSpMzmLcIs1tAQFjP36AFDhkBZmeFKkqRUsMHKUuvXh1C1fTssXgwdOsQ9kSRJyWGDlWWiCCZPDg+yX3RRON/KcCVJUmrZYGWRH36A4cNh9Wp4/30oKIh7IkmSkskGK0u88044NLRdO6ioMFxJktSYbLAS7uef4Z57YO7ccHDohRfGPZEkSclng5VgCxeG1uqoo2DlSsOVJElNxQYrgX79FR58EF5/HV56Ca68Mu6JJEnKLgashKmsDIeG5ueHVTetWsXOfgTxAAAHEklEQVQ9kSRJ2cdbhAlRVwePPgq9e8OoUTBtmuFKkqS42GAlwOrVMGgQNG8eljW3aRP3RJIkZTcbrAwWRWE58/nnww03wHvvGa4kSUoHNlgZasMGuPlmqKmBRYvgrLPinkiSJP2DDVaGiaLw24FFRdCzp+FKkqR0ZIOVQbZsgdtvh1WroKwshCxJkpR+bLAyxKxZ0KVLeMZq+XLDlSRJ6cwGK81t2wb33huWM0+ZAqWlcU8kSZIOxAYrjX30UVh1U1cXVt0YriRJygw2WGnot9/goYdg8mSYMAH69Yt7IkmSdCgMWGlm5cqw6qZ9+/D1KafEPZEkSTpU3iJME/X18MQTcMklcN998MYbhitJkjKVDVYa+OorGDwYjj8ePv4Y2raNeyJJknQkbLBiFEXhGavu3WHgQJgzx3AlSVIS2GDF5NtvYehQ2LwZFi6Ejh3jnkiSJKWKDVYMpk6FwkI47zxYvNhwJUlS0thgNaGtW2HECKiqCiezd+0a90SSJKkx2GA1kbKysOomNxdWrDBcSZKUZDZYjWz79nDswuzZMGkS9OoV90SSJKmx2WA1osWLoaAAduyATz4xXEmSlC1ssBpBbS2MHg0TJ8Lzz8M118Q9kSRJakoGrBT79NOw6ub008PD7K1bxz2RJElqat4iTJH6enjyyXAb8K674O23DVeSJGUrG6wUWLMmrLo5+mioqIAzzoh7IkmSFKfEN1hDhw6ldevWdOnSZZ/XjBw5kg4dOlBQUEBVVdVB/91RBC+/DCUlcO21MHeu4UqSJGVBwBoyZAjvvffePl9/9913+frrr/nyyy958cUXGT58+EH9vRs3wpVXhl2CCxbAv/4rHJX4f5vJMn/+/LhHUIr5niaL76cyWeIjQY8ePWjRosU+X58xYwaDBg0CoKSkhJqaGjZt2rTfv3PatHD8QnExlJdDp04pHVlNxP/zTh7f02Tx/VQmy/pnsKqrq2nTps2u7/Py8qiurqb1Pp5Qv+GG8JzVzJnh1qAkSdIfZX3AOlQtWkBlJZx4YtyTSJKkdJUTRVEU9xCNbd26dfTt25dPPvlkj9eGDx/On//8ZwYOHAhAfn4+CxYs2GuDlZOT0+izSpKUVFkQOXbJigYriqJ9vqn9+vVj/PjxDBw4kPLyck4++eR93h7Mpv8wJEnS4Ut8wLr++uuZP38+W7ZsoW3btjz88MPU1taSk5PDsGHDuPzyy5k9ezbt27fnpJNOYuLEiXGPLEmSMlxW3CKUJElqSok/puFwlJWVkZ+fz5/+9Cf+9re/7fWawz2cVE3vQO/nggULOPnkkykqKqKoqIgxY8bEMKUOVmMeHqymd6D3089nZtmwYQO9evXi7LPPpnPnzjz77LN7vS4rPqORdlNfXx+1a9cuWrt2bVRbWxude+650eeff77bNbNnz44uv/zyKIqiqLy8PCopKYljVB2Eg3k/58+fH/Xt2zemCXWoPvzww6iysjLq3LnzXl/385lZDvR++vnMLBs3bowqKyujKIqi//3f/43+9Kc/Ze3PUBusP1i2bBkdOnTg9NNP59hjj+W6665jxowZu11zOIeTKh4H836Cv8CQSRrj8GDF50DvJ/j5zCS5ubkUFBQA0KxZMzp27Eh1dfVu12TLZ9SA9Qd/PHj0n//5n/f4j2Nfh5Mq/RzM+wmwZMkSCgoKuOKKK/jss8+ackSlmJ/P5PHzmZnWrl1LVVUVJX84lTtbPqOJ/y1C6UCKi4v55ptvOPHEE3n33Xe5+uqrWb16ddxjScLPZ6batm0b/fv3Z+zYsTRr1izucWJhg/UHeXl5fPPNN7u+37BhA3l5eXtcs379+v1eo/RwMO9ns2bNOPHvR/P36dOHnTt3snXr1iadU6nj5zNZ/Hxmnrq6Ovr378+NN97IVVddtcfr2fIZNWD9Qbdu3fjqq69Yt24dtbW1/Pd//zf9+vXb7Zp+/foxadIkgAMeTqp4Hcz7+ft7/8uWLSOKIlq2bNnUo+oQRAc4PNjPZ2bZ3/vp5zPz3HzzzXTq1Im77rprr69ny2fUW4R/cPTRRzNu3DguvfRSGhoaGDp0KB07duTFF1/0cNIMdDDv5/Tp03nhhRc49thjOeGEE5g6dWrcY2s/PDw4WQ70fvr5zCyLFi1iypQpdO7cmcLCQnJycnjsscdYt25d1n1GPWhUkiQpxbxFKEmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKWYAUuSJCnFDFiSJEkpZsCSJElKMQOWJElSihmwJEmSUsyAJUmSlGIGLEmSpBQzYEmSJKXY/wfkKCsZlpS9sAAAAABJRU5ErkJggg\u003d\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" + "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627960_-1477396098", + "id": "20161101-195657_1336292109", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Second line", "text": "%python\nplt.plot([3, 2, 1], label\u003dr\u0027$y\u003d3-x$\u0027)", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "user": "anonymous", "config": { "colWidth": 12.0, "editorMode": "ace/mode/python", @@ -372,33 +390,39 @@ "scatter": {} } } - ] + ], + "editorSetting": { + "language": "python", + "editOnDblClick": false + } }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627961_-1477780847", - "id": "20161101-195937_907325325", "results": { "code": "SUCCESS", "msg": [ + { + "type": "TEXT", + "data": "[\u003cmatplotlib.lines.Line2D object at 0x263ccd0\u003e]\n" + }, { "type": "HTML", - "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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\u003d\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" + "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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yf/fyqunV69ext/f37Rv3960b9/eDBw40Bjj+Iuo8PBwY7fbTXh4uJk3b55lNfy38mr64x//aMLCwozdbjc9evQwe/bscR6blJRkQkJCTEhIiHnrrbcsyX855dVkjDGTJ082Tz311EXHufN5Gj58uLnhhhtMrVq1TEBAgJk3b56ZO3eumTt3rjHG8aYxfvx4ExwcbMLDw83XX3/tPNYdz1N59Tz00EOmcePGzmupc+fOxhhjMjMzjd1uN3a73YSFhTnPrzsor6ZZs2Y5r6WoqKiLmsfLvWbdQXk1GeP4S+Nhw4ZddJw7n6f169cbwERERDhfX8uXL/fo68ld6U7uIiIiIi6mNVgiIiIiLqYGS0RERMTF1GCJiIiIuJgaLBEREREXU4MlIiIi4mJqsERERERcTA2WiIiIiIupwRIRERFxMTVYIiIiIi6mBktERETExdRgiYiIiLiYGiwRERERF1ODJSIiIuJiarBEREREXEwNloiIiIiLqcESERERcTE1WCIiIiIupgZLRERExMXUYImIiIi4mBosERERERdTgyUiIiLiYmqwRERERFxMDZaIiIiIi6nBEhEREXExNVgiIiIiLqYGS0RERMTF1GCJiIiIuJgaLBEREREX+//HW6PCv5aPHgAAAABJRU5ErkJggg\u003d\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627961_-1477780847", + "id": "20161101-195937_907325325", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Label axes", "text": "%python\nplt.xlabel(r\u0027$x$\u0027, fontsize\u003d20)\nplt.ylabel(r\u0027$y$\u0027, fontsize\u003d20)", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "user": "anonymous", "config": { "colWidth": 12.0, "editorMode": "ace/mode/python", @@ -416,33 +440,39 @@ "scatter": {} } } - ] + ], + "editorSetting": { + "language": "python", + "editOnDblClick": false + } }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627962_-1476626600", - "id": "20161101-200014_2113468597", "results": { "code": "SUCCESS", "msg": [ + { + "type": "TEXT", + "data": "Text(41.625,0.5,u\u0027$y$\u0027)\n" + }, { "type": "HTML", - "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAGQCAYAAAByNR6YAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAIABJREFUeJzs3Xl4lOW9//F32CVEAkICRBaJERCzEyKLIYKcyCpwKLglYTExSAWPeE7Lz1r1aF1KsSKKCWCEaG25sCQBwSCKAcEAMWYTkUUMkrAJCpYUWZL5/fGc2lpBGZjMPTPP53VdXBeQYeZjM3B/+n2euW8/h8PhQERERERcppHpACIiIiK+RgVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMVUsERERERcTAVLRERExMV8vmCdPn2a+Ph4oqOjCQ8P5/HHHz/v42bMmEFYWBhRUVGUlZW5OaWIiIj4kiamAzS05s2b8/7779OyZUvq6uoYMGAAw4YNo2/fvt8/5u233+bzzz9n9+7dbN26lYyMDLZs2WIwtYiIiHgzn59gAbRs2RKwplnnzp3Dz8/vB1/Pz88nJSUFgPj4eE6cOMHhw4fdnlNERER8gy0KVn19PdHR0XTo0IGhQ4cSFxf3g6/X1NTQuXPn738dEhJCTU2Nu2OKiIiIj7BFwWrUqBGlpaVUV1ezdetWPv30U9ORRERExIf5/D1Y/+rKK6/k5ptvpqCggOuvv/773w8JCWH//v3f/7q6upqQkJAf/fl/v7QoIiIiF8/hcJiO4DY+P8E6evQoJ06cAODUqVOsW7eOnj17/uAxo0ePJicnB4AtW7YQGBhIcHDweZ/v7hV3c93869iyfwsOh0M/vPjHo48+ajyDfuh7qh/6fvrqj6IiB2FhDu6+28E339inWP2Dz0+wDh48SGpqKvX19dTX1zNx4kSGDx9OVlYWfn5+pKenM3z4cNasWcO1116Lv78/r7766gWf77Wxr/Hmp28y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 style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" + "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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\u003d\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627962_-1476626600", + "id": "20161101-200014_2113468597", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Add legend", "text": "%python\nplt.legend(loc\u003d\u0027upper center\u0027, fontsize\u003d20)", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "user": "anonymous", "config": { "colWidth": 12.0, "editorMode": "ace/mode/python", @@ -460,33 +490,39 @@ "scatter": {} } } - ] + ], + "editorSetting": { + "language": "python", + "editOnDblClick": false + } }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627962_-1476626600", - "id": "20161101-200141_1493024813", "results": { "code": "SUCCESS", "msg": [ + { + "type": "TEXT", + "data": "\u003cmatplotlib.legend.Legend object at 0x264c250\u003e\n" + }, { "type": "HTML", - "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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\u003d\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" + "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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Yy5dumR69uxpgoKCTJMmTcz+/fudzx0/frwJDAw0tWrVMnFxcZbk/2+51dOmTRvj5+dn6tevb+rXr286depkjHH8RVRoaKgJDw83oaGhZtasWZbV8N9yq+n55583devWNeHh4aZVq1Zmz549zufOnj3bBAUFmaCgIPPBBx9Ykv96cqvJGGNefvll89xzz13zPFc+Tr179zZVq1Y13t7ext/f38yaNcvMnDnTzJw50xjjeNMYNmyYCQwMNKGhoeann35yPtcVj1Nu9Tz22GOmYsWKznOpUaNGxhhj9u/fb8LDw014eLipW7eu8/i6gtxqeuedd5znUtOmTa9pHq/3mnUFudVkjOMvjXv16nXN81z5OK1bt84AJiwszPn6Wrp0qVufT65Kd3IXERERyWdagyUiIiKSz9RgiYiIiOQzNVgiIiIi+UwNloiIiEg+U4MlIiIiks/UYImIiIjkMzVYIiIiIvlMDZaIiIhIPlODJSIiIpLP1GCJiFtq27YtNpuNhQsXXrPdGEP//v2x2Ww8//zzFqUTkaJOH5UjIm5p+/btNGzYkJCQEHbs2IGXlxcAo0aNYtKkSQwePJj33nvPZ0ejNgAAAX5JREFU4pQiUlRpgiUibql+/fr07duXPXv28MknnwDw+uuvM2nSJB588EFmzpxpcUIRKco0wRIRt3X48GFq1apF1apVGTVqFCNGjCA6OpolS5ZQokQJq+OJSBGmBktE3NrYsWOZOHEiABEREXz33XeULl3a4lQiUtTpEqGIuDVfX1/n17Nnz1ZzJSIuQQ2WiLitzz77jGeffZaqVasCMHXqVIsTiYg4qMESEbcUFxdH//79CQ0NJSkpiZCQEGbNmsXevXutjiYiogZLRNxPQkICPXv2pFq1aixfvhxfX1/Gjx9PdnY2zz33nNXxRES0yF1E3Mu2bdto1aoVpUqVIiEhgaCgIOe+Jk2akJiYyNq1a2nZsqWFKUWkqNMES0Tcxi+//EK7du2w2WwsX778muYKYMKECQCMHj3aingiIk6aYImIiIjkM02wRERERPKZGiwRERGRfKYGS0RERCSfqcESERERyWdqsERERETymRosERERkXymBktEREQkn/0/RCBHmnlLjssAAAAASUVORK5CYII\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627962_-1476626600", + "id": "20161101-200141_1493024813", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Add title", "text": "%python\nplt.title(\u0027Inline plotting example\u0027, fontsize\u003d20)", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "user": "anonymous", "config": { "colWidth": 12.0, "editorMode": "ace/mode/python", @@ -504,32 +540,38 @@ "scatter": {} } } - ] + ], + "editorSetting": { + "language": "python", + "editOnDblClick": false + } }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627963_-1477011349", - "id": "20161101-200445_78775142", "results": { "code": "SUCCESS", "msg": [ + { + "type": "TEXT", + "data": "Text(0.5,1,u\u0027Inline plotting example\u0027)\n" + }, { "type": "HTML", - "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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 style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" + "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627963_-1477011349", + "id": "20161101-200445_78775142", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%md\n####(b) Using a single plot\nTo iteratively update a single plot, we can leverage Zeppelin\u0027s built-in Angular Display System. Currently this feature is only available for the `pyspark` interpreter for raster (png and jpg) formats. To enable this, we must set a special `angular` flag to `True` in our configuration:", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "user": "anonymous", "config": { "colWidth": 12.0, "editorMode": "ace/mode/markdown", @@ -547,34 +589,32 @@ "scatter": {} } } - ] + ], + "editorSetting": {} }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627963_-1477011349", - "id": "20161101-200541_1283841564", "results": { "code": "SUCCESS", "msg": [ { "type": "HTML", - "data": "\u003ch4\u003e(b) Using a single plot\u003c/h4\u003e\n\u003cp\u003eTo iteratively update a single plot, we can leverage Zeppelin\u0027s built-in Angular Display System. Currently this feature is only available for the \u003ccode\u003epyspark\u003c/code\u003e interpreter for raster (png and jpg) formats. To enable this, we must set a special \u003ccode\u003eangular\u003c/code\u003e flag to \u003ccode\u003eTrue\u003c/code\u003e in our configuration.\u003c/p\u003e\n" + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003cp\u003e####(b) Using a single plot\u003cbr/\u003eTo iteratively update a single plot, we can leverage Zeppelin\u0026rsquo;s built-in Angular Display System. Currently this feature is only available for the \u003ccode\u003epyspark\u003c/code\u003e interpreter for raster (png and jpg) formats. To enable this, we must set a special \u003ccode\u003eangular\u003c/code\u003e flag to \u003ccode\u003eTrue\u003c/code\u003e in our configuration:\u003c/p\u003e\n\u003c/div\u003e" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627963_-1477011349", + "id": "20161101-200541_1283841564", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "First line (figure will be displayed here)", - "text": "%pyspark\nimport matplotlib.pyplot as plt\nplt.close() # Added here to reset the plot when rerunning the paragraph\nz.configure_mpl(angular\u003dTrue, close\u003dFalse)\nplt.plot([1, 2, 3], label\u003dr\u0027$y\u003dx$\u0027)", + "text": "%spark.pyspark\nimport matplotlib.pyplot as plt\nplt.close() # Added here to reset the plot when rerunning the paragraph\nz.configure_mpl(angular\u003dTrue, close\u003dFalse)\nplt.plot([1, 2, 3], label\u003dr\u0027$y\u003dx$\u0027)", "user": "anonymous", - "dateUpdated": "Nov 2, 2016 2:55:37 PM", "config": { "colWidth": 7.0, "editorMode": "ace/mode/python", @@ -592,188 +632,191 @@ "scatter": {} } } - ] + ], + "editorSetting": { + "language": "python", + "editOnDblClick": false + } }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627963_-1477011349", - "id": "20161101-200754_739212093", "results": { "code": "SUCCESS", "msg": [ + { + "type": "TEXT", + "data": "[\u003cmatplotlib.lines.Line2D object at 0x3275390\u003e]\n" + }, { "type": "HTML", - "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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\u003d\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" + "data": "\u003cdiv style\u003d\u0027width:auto;height:auto\u0027\u003e\u003cimg src\u003ddata:image/png;base64,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\u003d style\u003d\u0027width\u003dauto;height:auto\u0027\u003e\u003cdiv\u003e\n" } ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "dateStarted": "Nov 2, 2016 2:54:53 PM", - "dateFinished": "Nov 2, 2016 2:55:04 PM", + "apps": [], + "jobName": "paragraph_1478123627963_-1477011349", + "id": "20161101-200754_739212093", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Second line", - "text": "%pyspark\nplt.plot([3, 2, 1], label\u003dr\u0027$y\u003d3-x$\u0027)", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "text": "%spark.pyspark\nplt.plot([3, 2, 1], label\u003dr\u0027$y\u003d3-x$\u0027)", + "user": "anonymous", "config": { "colWidth": 5.0, "title": true, "enabled": true, - "results": [] + "results": [], + "editorSetting": { + "language": "python", + "editOnDblClick": false + }, + "editorMode": "ace/mode/python" }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627964_-1478935094", - "id": "20161101-200854_1676504884", "results": { "code": "SUCCESS", - "msg": [] + "msg": [ + { + "type": "TEXT", + "data": "[\u003cmatplotlib.lines.Line2D object at 0x327b410\u003e]\n" + }, + { + "type": "ANGULAR", + "data": "" + } + ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627964_-1478935094", + "id": "20161101-200854_1676504884", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Label axes", - "text": "%pyspark\nplt.xlabel(r\u0027$x$\u0027, fontsize\u003d20)\nplt.ylabel(r\u0027$y$\u0027, fontsize\u003d20)", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "text": "%spark.pyspark\nplt.xlabel(r\u0027$x$\u0027, fontsize\u003d20)\nplt.ylabel(r\u0027$y$\u0027, fontsize\u003d20)", + "user": "anonymous", "config": { "colWidth": 5.0, "title": true, "enabled": true, - "results": [] + "results": [], + "editorSetting": { + "language": "python", + "editOnDblClick": false + }, + "editorMode": "ace/mode/python" }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627964_-1478935094", - "id": "20161101-200851_314384892", "results": { "code": "SUCCESS", - "msg": [] + "msg": [ + { + "type": "TEXT", + "data": "Text(41.625,0.5,u\u0027$y$\u0027)\n" + }, + { + "type": "ANGULAR", + "data": "" + } + ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627964_-1478935094", + "id": "20161101-200851_314384892", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Add legend", - "text": "%pyspark\nplt.legend(loc\u003d\u0027upper center\u0027, fontsize\u003d20)", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "text": "%spark.pyspark\nplt.legend(loc\u003d\u0027upper center\u0027, fontsize\u003d20)", + "user": "anonymous", "config": { "colWidth": 5.0, "editorMode": "ace/mode/python", "title": true, "enabled": true, - "results": [] + "results": [], + "editorSetting": { + "language": "python", + "editOnDblClick": false + } }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627964_-1478935094", - "id": "20161101-201552_651686132", "results": { "code": "SUCCESS", - "msg": [] + "msg": [ + { + "type": "TEXT", + "data": "\u003cmatplotlib.legend.Legend object at 0x360d650\u003e\n" + }, + { + "type": "ANGULAR", + "data": "" + } + ] }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "apps": [], + "jobName": "paragraph_1478123627964_-1478935094", + "id": "20161101-201552_651686132", + "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "title": "Add title", - "text": "%pyspark\nplt.title(\u0027Inline plotting example\u0027, fontsize\u003d20)", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", + "text": "%spark.pyspark\nplt.title(\u0027Inline plotting example\u0027, fontsize\u003d20)", + "user": "anonymous", "config": { "colWidth": 5.0, "editorMode": "ace/mode/python", "title": true, "enabled": true, - "results": [] + "results": [], + "editorSetting": { + "language": "python", + "editOnDblClick": false + } }, "settings": { "params": {}, "forms": {} }, - "apps": [], - "jobName": "paragraph_1478123627965_-1479319843", - "id": "20161101-202024_1645454710", "results": { "code": "SUCCESS", - "msg": [] - }, - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", - "progressUpdateIntervalMs": 500 - }, - { - "text": "", - "dateUpdated": "Nov 2, 2016 2:53:47 PM", - "config": { - "colWidth": 12.0, - "graph": { - "mode": "table", - "height": 300.0, - "optionOpen": false, - "keys": [], - "values": [], - "groups": [], - "scatter": {} - }, - "enabled": true - }, - "settings": { - "params": {}, - "forms": {} + "msg": [ + { + "type": "TEXT", + "data": "Text(0.5,1,u\u0027Inline plotting example\u0027)\n" + }, + { + "type": "ANGULAR", + "data": "" + } + ] }, "apps": [], "jobName": "paragraph_1478123627965_-1479319843", - "id": "20161102-124716_1703649609", - "dateCreated": "Nov 2, 2016 2:53:47 PM", - "status": "READY", - "errorMessage": "", + "id": "20161101-202024_1645454710", + "status": "FINISHED", "progressUpdateIntervalMs": 500 } ], - "name": "Zeppelin Tutorial/Matplotlib (Python • PySpark)", + "name": "Zeppelin Tutorial/Spark • Matplotlib (Python • PySpark)", "id": "2C2AUG798", - "angularObjects": { - "2C6WUGPNH:shared_process": [], - "2C4A8RJNB:shared_process": [], - "2C4DTK2ZT:shared_process": [], - "2C6XKJWBR:shared_process": [], - "2C6AHZPMK:shared_process": [], - "2C5SU66WQ:shared_process": [], - "2C6AMJ98Q:shared_process": [], - "2C4AJZK72:shared_process": [], - "2C3STPSD7:shared_process": [], - "2C4FJN9CK:shared_process": [], - "2C3CW6JBY:shared_process": [], - "2C5UPQX6Q:shared_process": [], - "2C5873KN4:shared_process": [], - "2C5719XN4:shared_process": [], - "2C52DE5G3:shared_process": [], - "2C4G28E63:shared_process": [], - "2C6CU96BC:shared_process": [], - "2C49A6WY3:shared_process": [], - "2C3NE73HG:shared_process": [] - }, + "angularObjects": {}, "config": {}, "info": {} -} +} \ No newline at end of file diff --git a/notebook/2C35YU814/note.json b/notebook/2C35YU814/note.json deleted file mode 100644 index 09ed8c6e01c..00000000000 --- a/notebook/2C35YU814/note.json +++ /dev/null @@ -1,806 +0,0 @@ -{ - "paragraphs": [ - { - "text": "%md\n### Intro\nThis notebook is an example of how to use **Apache Flink** for processing simple data sets. We will take an open airline data set from [stat-computing.org](http://stat-computing.org) and find out who was the most popular carrier during 1998-2000 years. Next we will build a chart that shows flights distribution by months and look how it changes from year to year. We will use Zeppelin `%table` display system to build charts.", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 11:55:42 AM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "markdown", - "editOnDblClick": true - }, - "editorMode": "ace/mode/markdown", - "editorHide": true, - "tableHide": false - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952101049_-1120777567", - "id": "20170109-115501_192763014", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch3\u003eIntro\u003c/h3\u003e\n\u003cp\u003eThis notebook is an example of how to use \u003cstrong\u003eApache Flink\u003c/strong\u003e for processing simple data sets. We will take an open airline data set from \u003ca href\u003d\"http://stat-computing.org\"\u003estat-computing.org\u003c/a\u003e and find out who was the most popular carrier during 1998-2000 years. Next we will build a chart that shows flights distribution by months and look how it changes from year to year. We will use Zeppelin \u003ccode\u003e%table\u003c/code\u003e display system to build charts.\u003c/p\u003e\n\u003c/div\u003e" - } - ] - }, - "dateCreated": "Jan 9, 2017 11:55:01 AM", - "dateStarted": "Jan 9, 2017 11:55:42 AM", - "dateFinished": "Jan 9, 2017 11:55:44 AM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%md\n### Getting the data\nFirst we need to download and unpack the data. We will get three big data sets with flight details (one pack for each year) and a small one with carriers names. In total we will get for about 1,5 GB of data. To be able to process such amount of data it is recommended to increase `shell.command.timeout.millisecs` value in `%sh` interpreter settings up to several minutes. You can find interpreters configuration by clicking on `Interpreter` in a drop-down menu from the top right corner of the Zeppelin web-ui.", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 11:56:08 AM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "scala", - "editOnDblClick": false - }, - "editorMode": "ace/mode/scala", - "editorHide": true - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952142017_284386712", - "id": "20170109-115542_1487437739", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch3\u003eGetting the data\u003c/h3\u003e\n\u003cp\u003eFirst we need to download and unpack the data. We will get three big data sets with flight details (one pack for each year) and a small one with carriers names. In total we will get for about 1,5 GB of data. To be able to process such amount of data it is recommended to increase \u003ccode\u003eshell.command.timeout.millisecs\u003c/code\u003e value in \u003ccode\u003e%sh\u003c/code\u003e interpreter settings up to several minutes. You can find interpreters configuration by clicking on \u003ccode\u003eInterpreter\u003c/code\u003e in a drop-down menu from the top right corner of the Zeppelin web-ui.\u003c/p\u003e\n\u003c/div\u003e" - } - ] - }, - "dateCreated": "Jan 9, 2017 11:55:42 AM", - "dateStarted": "Jan 9, 2017 11:56:07 AM", - "dateFinished": "Jan 9, 2017 11:56:07 AM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%sh\n\nrm /tmp/flights98.csv.bz2\ncurl -o /tmp/flights98.csv.bz2 \"http://stat-computing.org/dataexpo/2009/1998.csv.bz2\"\nrm /tmp/flights98.csv\nbzip2 -d /tmp/flights98.csv.bz2\nchmod 666 /tmp/flights98.csv", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 11:59:02 AM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "sh", - "editOnDblClick": false - }, - "editorMode": "ace/mode/sh", - "tableHide": true - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952167547_-566831096", - "id": "20170109-115607_1634441713", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "rm: cannot remove \u0027/tmp/flights98.csv.bz2\u0027: No such file or directory\n % Total % Received % Xferd Average Speed Time Time Time Current\n Dload Upload Total Spent Left Speed\n\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r 0 73.1M 0 64295 0 0 51646 0 0:24:44 0:00:01 0:24:43 51642\r 0 73.1M 0 358k 0 0 160k 0 0:07:47 0:00:02 0:07:45 160k\r 1 73.1M 1 1209k 0 0 373k 0 0:03:20 0:00:03 0:03:17 373k\r 4 73.1M 4 3204k 0 0 773k 0 0:01:36 0:00:04 0:01:32 773k\r 7 73.1M 7 5508k 0 0 1071k 0 0:01:09 0:00:05 0:01:04 1145k\r 10 73.1M 10 7875k 0 0 1280k 0 0:00:58 0:00:06 0:00:52 1592k\r 13 73.1M 13 10.1M 0 0 1458k 0 0:00:51 0:00:07 0:00:44 2049k\r 17 73.1M 17 12.7M 0 0 1608k 0 0:00:46 0:00:08 0:00:38 2422k\r 20 73.1M 20 14.9M 0 0 1671k 0 0:00:44 0:00:09 0:00:35 2413k\r 23 73.1M 23 17.1M 0 0 1728k 0 0:00:43 0:00:10 0:00:33 2403k\r 26 73.1M 26 19.4M 0 0 1787k 0 0:00:41 0:00:11 0:00:30 2411k\r 29 73.1M 29 21.7M 0 0 1837k 0 0:00:40 0:00:12 0:00:28 2379k\r 32 73.1M 32 24.1M 0 0 1879k 0 0:00:39 0:00:13 0:00:26 2322k\r 36 73.1M 36 26.4M 0 0 1916k 0 0:00:39 0:00:14 0:00:25 2365k\r 39 73.1M 39 28.5M 0 0 1930k 0 0:00:38 0:00:15 0:00:23 2341k\r 41 73.1M 41 30.6M 0 0 1943k 0 0:00:38 0:00:16 0:00:22 2292k\r 44 73.1M 44 32.6M 0 0 1947k 0 0:00:38 0:00:17 0:00:21 2215k\r 47 73.1M 47 34.6M 0 0 1952k 0 0:00:38 0:00:18 0:00:20 2145k\r 50 73.1M 50 36.6M 0 0 1960k 0 0:00:38 0:00:19 0:00:19 2082k\r 52 73.1M 52 38.3M 0 0 1947k 0 0:00:38 0:00:20 0:00:18 1998k\r 55 73.1M 55 40.4M 0 0 1956k 0 0:00:38 0:00:21 0:00:17 1996k\r 57 73.1M 57 42.2M 0 0 1951k 0 0:00:38 0:00:22 0:00:16 1965k\r 60 73.1M 60 44.0M 0 0 1948k 0 0:00:38 0:00:23 0:00:15 1932k\r 62 73.1M 62 45.4M 0 0 1927k 0 0:00:38 0:00:24 0:00:14 1803k\r 63 73.1M 63 46.5M 0 0 1896k 0 0:00:39 0:00:25 0:00:14 1688k\r 65 73.1M 65 47.7M 0 0 1868k 0 0:00:40 0:00:26 0:00:14 1496k\r 66 73.1M 66 48.8M 0 0 1843k 0 0:00:40 0:00:27 0:00:13 1363k\r 68 73.1M 68 50.0M 0 0 1820k 0 0:00:41 0:00:28 0:00:13 1227k\r 69 73.1M 69 51.1M 0 0 1786k 0 0:00:41 0:00:29 0:00:12 1126k\r 71 73.1M 71 52.0M 0 0 1769k 0 0:00:42 0:00:30 0:00:12 1131k\r 72 73.1M 72 53.0M 0 0 1744k 0 0:00:42 0:00:31 0:00:11 1098k\r 73 73.1M 73 54.0M 0 0 1723k 0 0:00:43 0:00:32 0:00:11 1070k\r 75 73.1M 75 55.1M 0 0 1702k 0 0:00:43 0:00:33 0:00:10 1040k\r 76 73.1M 76 56.0M 0 0 1681k 0 0:00:44 0:00:34 0:00:10 1048k\r 77 73.1M 77 56.9M 0 0 1659k 0 0:00:45 0:00:35 0:00:10 993k\r 79 73.1M 79 57.8M 0 0 1638k 0 0:00:45 0:00:36 0:00:09 972k\r 80 73.1M 80 58.7M 0 0 1618k 0 0:00:46 0:00:37 0:00:09 946k\r 81 73.1M 81 59.6M 0 0 1600k 0 0:00:46 0:00:38 0:00:08 921k\r 82 73.1M 82 60.5M 0 0 1582k 0 0:00:47 0:00:39 0:00:08 906k\r 83 73.1M 83 61.4M 0 0 1566k 0 0:00:47 0:00:40 0:00:07 917k\r 85 73.1M 85 62.1M 0 0 1546k 0 0:00:48 0:00:41 0:00:07 887k\r 86 73.1M 86 63.0M 0 0 1532k 0 0:00:48 0:00:42 0:00:06 892k\r 87 73.1M 87 63.9M 0 0 1517k 0 0:00:49 0:00:43 0:00:06 882k\r 88 73.1M 88 64.8M 0 0 1503k 0 0:00:49 0:00:44 0:00:05 878k\r 89 73.1M 89 65.6M 0 0 1489k 0 0:00:50 0:00:45 0:00:05 872k\r 91 73.1M 91 66.5M 0 0 1477k 0 0:00:50 0:00:46 0:00:04 904k\r 92 73.1M 92 67.4M 0 0 1465k 0 0:00:51 0:00:47 0:00:04 897k\r 93 73.1M 93 68.2M 0 0 1451k 0 0:00:51 0:00:48 0:00:03 889k\r 94 73.1M 94 69.2M 0 0 1441k 0 0:00:51 0:00:49 0:00:02 897k\r 95 73.1M 95 70.1M 0 0 1430k 0 0:00:52 0:00:50 0:00:02 904k\r 97 73.1M 97 71.0M 0 0 1421k 0 0:00:52 0:00:51 0:00:01 910k\r 98 73.1M 98 71.9M 0 0 1413k 0 0:00:52 0:00:52 --:--:-- 923k\r 99 73.1M 99 72.8M 0 0 1403k 0 0:00:53 0:00:53 --:--:-- 941k\r100 73.1M 100 73.1M 0 0 1401k 0 0:00:53 0:00:53 --:--:-- 941k\nrm: cannot remove \u0027/tmp/flights98.csv\u0027: No such file or directory\n" - } - ] - }, - "dateCreated": "Jan 9, 2017 11:56:07 AM", - "dateStarted": "Jan 9, 2017 11:57:37 AM", - "dateFinished": "Jan 9, 2017 11:58:50 AM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%sh\n\nrm /tmp/flights99.csv.bz2\ncurl -o /tmp/flights99.csv.bz2 \"http://stat-computing.org/dataexpo/2009/1999.csv.bz2\"\nrm /tmp/flights99.csv\nbzip2 -d /tmp/flights99.csv.bz2\nchmod 666 /tmp/flights99.csv", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 11:59:59 AM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "sh", - "editOnDblClick": false - }, - "editorMode": "ace/mode/sh", - "tableHide": true - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952257873_-1874269156", - "id": "20170109-115737_1346880844", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "rm: cannot remove \u0027/tmp/flights99.csv.bz2\u0027: No such file or directory\n % Total % Received % Xferd Average Speed Time Time Time Current\n Dload Upload Total Spent Left Speed\n\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r 0 75.7M 0 5520 0 0 9851 0 2:14:25 --:--:-- 2:14:25 9839\r 0 75.7M 0 88819 0 0 64302 0 0:20:35 0:00:01 0:20:34 64268\r 0 75.7M 0 181k 0 0 25316 0 0:52:18 0:00:07 0:52:11 25316\r 0 75.7M 0 548k 0 0 67331 0 0:19:39 0:00:08 0:19:31 67327\r 1 75.7M 1 817k 0 0 89344 0 0:14:49 0:00:09 0:14:40 89337\r 1 75.7M 1 1042k 0 0 100k 0 0:12:54 0:00:10 0:12:44 105k\r 3 75.7M 3 2461k 0 0 218k 0 0:05:55 0:00:11 0:05:44 239k\r 6 75.7M 6 5069k 0 0 412k 0 0:03:08 0:00:12 0:02:56 985k\r 11 75.7M 11 9165k 0 0 690k 0 0:01:52 0:00:13 0:01:39 1744k\r 14 75.7M 14 11.2M 0 0 796k 0 0:01:37 0:00:14 0:01:23 2109k\r 19 75.7M 19 14.8M 0 0 995k 0 0:01:17 0:00:15 0:01:02 2910k\r 24 75.7M 24 18.6M 0 0 1174k 0 0:01:06 0:00:16 0:00:50 3331k\r 29 75.7M 29 22.5M 0 0 1338k 0 0:00:57 0:00:17 0:00:40 3613k\r 35 75.7M 35 26.5M 0 0 1486k 0 0:00:52 0:00:18 0:00:34 3603k\r 40 75.7M 40 30.3M 0 0 1610k 0 0:00:48 0:00:19 0:00:29 4025k\r 45 75.7M 45 34.2M 0 0 1731k 0 0:00:44 0:00:20 0:00:24 3980k\r 50 75.7M 50 38.2M 0 0 1840k 0 0:00:42 0:00:21 0:00:21 4011k\r 55 75.7M 55 42.2M 0 0 1940k 0 0:00:39 0:00:22 0:00:17 4020k\r 60 75.7M 60 46.2M 0 0 2032k 0 0:00:38 0:00:23 0:00:15 4026k\r 65 75.7M 65 49.9M 0 0 2106k 0 0:00:36 0:00:24 0:00:12 4017k\r 70 75.7M 70 53.5M 0 0 2169k 0 0:00:35 0:00:25 0:00:10 3945k\r 75 75.7M 75 57.2M 0 0 2229k 0 0:00:34 0:00:26 0:00:08 3884k\r 80 75.7M 80 61.1M 0 0 2293k 0 0:00:33 0:00:27 0:00:06 3868k\r 86 75.7M 86 65.5M 0 0 2372k 0 0:00:32 0:00:28 0:00:04 3956k\r 92 75.7M 92 70.4M 0 0 2464k 0 0:00:31 0:00:29 0:00:02 4200k\r100 75.7M 100 75.7M 0 0 2565k 0 0:00:30 0:00:30 --:--:-- 4585k\nrm: cannot remove \u0027/tmp/flights99.csv\u0027: No such file or directory\n" - } - ] - }, - "dateCreated": "Jan 9, 2017 11:57:37 AM", - "dateStarted": "Jan 9, 2017 11:59:04 AM", - "dateFinished": "Jan 9, 2017 11:59:53 AM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%sh\n\nrm /tmp/flights00.csv.bz2\ncurl -o /tmp/flights00.csv.bz2 \"http://stat-computing.org/dataexpo/2009/2000.csv.bz2\"\nrm /tmp/flights00.csv\nbzip2 -d /tmp/flights00.csv.bz2\nchmod 666 /tmp/flights00.csv", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:01:42 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "sh", - "editOnDblClick": false - }, - "editorMode": "ace/mode/sh", - "tableHide": true - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952312038_-1315320949", - "id": "20170109-115832_608069986", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "rm: cannot remove \u0027/tmp/flights00.csv.bz2\u0027: No such file or directory\n % Total % Received % Xferd Average Speed Time Time Time Current\n Dload Upload Total Spent Left Speed\n\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r 0 0 0 0 0 0 0 0 --:--:-- 0:00:01 --:--:-- 0\r 0 78.7M 0 5520 0 0 3016 0 7:36:06 0:00:01 7:36:05 3014\r 0 78.7M 0 39987 0 0 15337 0 1:29:41 0:00:02 1:29:39 15332\r 0 78.7M 0 87755 0 0 24531 0 0:56:04 0:00:03 0:56:01 24526\r 0 78.7M 0 157k 0 0 33950 0 0:40:31 0:00:04 0:40:27 33944\r 0 78.7M 0 221k 0 0 40878 0 0:33:39 0:00:05 0:33:34 53734\r 0 78.7M 0 308k 0 0 47250 0 0:29:06 0:00:06 0:29:00 63943\r 0 78.7M 0 398k 0 0 52806 0 0:26:03 0:00:07 0:25:56 71903\r 0 78.7M 0 437k 0 0 36667 0 0:37:31 0:00:12 0:37:19 41697\r 0 78.7M 0 703k 0 0 57158 0 0:24:04 0:00:12 0:23:52 71137\r 1 78.7M 1 851k 0 0 64259 0 0:21:24 0:00:13 0:21:11 80471\r 1 78.7M 1 1171k 0 0 82442 0 0:16:41 0:00:14 0:16:27 109k\r 1 78.7M 1 1546k 0 0 79861 0 0:17:13 0:00:19 0:16:54 97134\r 3 78.7M 3 3181k 0 0 154k 0 0:08:41 0:00:20 0:08:21 327k\r 4 78.7M 4 3466k 0 0 160k 0 0:08:21 0:00:21 0:08:00 308k\r 4 78.7M 4 3565k 0 0 136k 0 0:09:50 0:00:26 0:09:24 216k\r 8 78.7M 8 7196k 0 0 270k 0 0:04:57 0:00:26 0:04:31 501k\r 10 78.7M 10 8459k 0 0 307k 0 0:04:22 0:00:27 0:03:55 894k\r 11 78.7M 11 9386k 0 0 327k 0 0:04:06 0:00:28 0:03:38 768k\r 15 78.7M 15 11.9M 0 0 413k 0 0:03:14 0:00:29 0:02:45 1093k\r 18 78.7M 18 14.5M 0 0 487k 0 0:02:45 0:00:30 0:02:15 2553k\r 22 78.7M 22 17.7M 0 0 574k 0 0:02:20 0:00:31 0:01:49 2195k\r 25 78.7M 25 19.9M 0 0 626k 0 0:02:08 0:00:32 0:01:36 2375k\r 28 78.7M 28 22.1M 0 0 676k 0 0:01:59 0:00:33 0:01:26 2726k\r 31 78.7M 31 24.7M 0 0 734k 0 0:01:49 0:00:34 0:01:15 2643k\r 34 78.7M 34 27.3M 0 0 789k 0 0:01:42 0:00:35 0:01:07 2638k\r 38 78.7M 38 30.0M 0 0 841k 0 0:01:35 0:00:36 0:00:59 2513k\r 40 78.7M 40 32.1M 0 0 874k 0 0:01:32 0:00:37 0:00:55 2457k\r 43 78.7M 43 34.1M 0 0 906k 0 0:01:28 0:00:38 0:00:50 2445k\r 45 78.7M 45 35.7M 0 0 925k 0 0:01:27 0:00:39 0:00:48 2250k\r 47 78.7M 47 37.4M 0 0 946k 0 0:01:25 0:00:40 0:00:45 2062k\r 49 78.7M 49 39.3M 0 0 968k 0 0:01:23 0:00:41 0:00:42 1907k\r 52 78.7M 52 41.0M 0 0 987k 0 0:01:21 0:00:42 0:00:39 1859k\r 54 78.7M 54 42.5M 0 0 1000k 0 0:01:20 0:00:43 0:00:37 1729k\r 55 78.7M 55 43.9M 0 0 1008k 0 0:01:19 0:00:44 0:00:35 1651k\r 57 78.7M 57 45.4M 0 0 1020k 0 0:01:18 0:00:45 0:00:33 1625k\r 59 78.7M 59 46.6M 0 0 1027k 0 0:01:18 0:00:46 0:00:32 1512k\r 60 78.7M 60 47.7M 0 0 1027k 0 0:01:18 0:00:47 0:00:31 1376k\r 61 78.7M 61 48.6M 0 0 1024k 0 0:01:18 0:00:48 0:00:30 1236k\r 62 78.7M 62 49.5M 0 0 1020k 0 0:01:18 0:00:49 0:00:29 1125k\r 64 78.7M 64 50.4M 0 0 1021k 0 0:01:18 0:00:50 0:00:28 1027k\r 65 78.7M 65 51.3M 0 0 1018k 0 0:01:19 0:00:51 0:00:28 941k\r 66 78.7M 66 52.1M 0 0 1016k 0 0:01:19 0:00:52 0:00:27 910k\r 67 78.7M 67 53.0M 0 0 1014k 0 0:01:19 0:00:53 0:00:26 909k\r 68 78.7M 68 53.7M 0 0 1006k 0 0:01:20 0:00:54 0:00:26 868k\r 69 78.7M 69 54.6M 0 0 1006k 0 0:01:20 0:00:55 0:00:25 858k\r 70 78.7M 70 55.3M 0 0 1002k 0 0:01:20 0:00:56 0:00:24 831k\r 71 78.7M 71 56.1M 0 0 998k 0 0:01:20 0:00:57 0:00:23 807k\r 72 78.7M 72 56.9M 0 0 994k 0 0:01:21 0:00:58 0:00:23 787k\r 73 78.7M 73 57.6M 0 0 991k 0 0:01:21 0:00:59 0:00:22 823k\r 74 78.7M 74 58.4M 0 0 988k 0 0:01:21 0:01:00 0:00:21 784k\r 75 78.7M 75 59.2M 0 0 985k 0 0:01:21 0:01:01 0:00:20 791k\r 76 78.7M 76 60.0M 0 0 982k 0 0:01:22 0:01:02 0:00:20 797k\r 77 78.7M 77 60.8M 0 0 980k 0 0:01:22 0:01:03 0:00:19 808k\r 78 78.7M 78 61.6M 0 0 977k 0 0:01:22 0:01:04 0:00:18 812k\r 79 78.7M 79 62.4M 0 0 975k 0 0:01:22 0:01:05 0:00:17 824k\r 80 78.7M 80 63.4M 0 0 976k 0 0:01:22 0:01:06 0:00:16 870k\r 82 78.7M 82 64.9M 0 0 984k 0 0:01:21 0:01:07 0:00:14 1006k\r 85 78.7M 85 66.9M 0 0 1000k 0 0:01:20 0:01:08 0:00:12 1254k\r 88 78.7M 88 69.4M 0 0 1022k 0 0:01:18 0:01:09 0:00:09 1602k\r 92 78.7M 92 72.5M 0 0 1053k 0 0:01:16 0:01:10 0:00:06 2064k\r 96 78.7M 96 76.1M 0 0 1089k 0 0:01:13 0:01:11 0:00:02 2600k\r100 78.7M 100 78.7M 0 0 1116k 0 0:01:12 0:01:12 --:--:-- 3022k\nrm: cannot remove \u0027/tmp/flights00.csv\u0027: No such file or directory\n" - } - ] - }, - "dateCreated": "Jan 9, 2017 11:58:32 AM", - "dateStarted": "Jan 9, 2017 12:00:01 PM", - "dateFinished": "Jan 9, 2017 12:01:34 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%sh\n\nrm /tmp/carriers.csv\ncurl -o /tmp/carriers.csv \"http://stat-computing.org/dataexpo/2009/carriers.csv\"\nchmod 666 /tmp/carriers.csv", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:01:48 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "sh", - "editOnDblClick": false - }, - "editorMode": "ace/mode/sh", - "tableHide": true - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952329229_2136292082", - "id": "20170109-115849_1794095031", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "rm: cannot remove \u0027/tmp/carriers.csv\u0027: No such file or directory\n % Total % Received % Xferd Average Speed Time Time Time Current\n Dload Upload Total Spent Left Speed\n\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r 9 43758 9 4140 0 0 7588 0 0:00:05 --:--:-- 0:00:05 7582\r100 43758 100 43758 0 0 46357 0 --:--:-- --:--:-- --:--:-- 46353\n" - } - ] - }, - "dateCreated": "Jan 9, 2017 11:58:49 AM", - "dateStarted": "Jan 9, 2017 12:01:44 PM", - "dateFinished": "Jan 9, 2017 12:01:45 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%md\n### Preparing the data\nThe `flights\u003cYY\u003e.csv` contains various data but we only need the information about the year, the month and the carrier who served the flight. Let\u0027s retrieve this information and create `DataSets`.", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:01:51 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "markdown", - "editOnDblClick": true - }, - "editorMode": "ace/mode/markdown", - "editorHide": true, - "tableHide": false - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952363836_-1769111757", - "id": "20170109-115923_963126574", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch3\u003ePreparing the data\u003c/h3\u003e\n\u003cp\u003eThe \u003ccode\u003eflights\u0026lt;YY\u0026gt;.csv\u003c/code\u003e contains various data but we only need the information about the year, the month and the carrier who served the flight. Let\u0026rsquo;s retrieve this information and create \u003ccode\u003eDataSets\u003c/code\u003e.\u003c/p\u003e\n\u003c/div\u003e" - } - ] - }, - "dateCreated": "Jan 9, 2017 11:59:23 AM", - "dateStarted": "Jan 9, 2017 12:01:51 PM", - "dateFinished": "Jan 9, 2017 12:01:53 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%flink\n\ncase class Flight(year: Int, month: Int, carrierCode: String)\ncase class Carrier(code: String, name: String)\n\nval flights98 \u003d benv.readCsvFile[Flight](\"/tmp/flights98.csv\", ignoreFirstLine \u003d true, includedFields \u003d Array(0, 1, 8))\nval flights99 \u003d benv.readCsvFile[Flight](\"/tmp/flights99.csv\", ignoreFirstLine \u003d true, includedFields \u003d Array(0, 1, 8))\nval flights00 \u003d benv.readCsvFile[Flight](\"/tmp/flights00.csv\", ignoreFirstLine \u003d true, includedFields \u003d Array(0, 1, 8))\nval flights \u003d flights98.union(flights99).union(flights00)\nval carriers \u003d benv.readCsvFile[Carrier](\"/tmp/carriers.csv\", ignoreFirstLine \u003d true, quoteCharacter \u003d \u0027\"\u0027)", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:02:38 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "scala", - "editOnDblClick": false - }, - "editorMode": "ace/mode/scala", - "lineNumbers": true, - "tableHide": true - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952511284_-589624871", - "id": "20170109-120151_872852428", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "defined class Flight\ndefined class Carrier\nflights98: org.apache.flink.api.scala.DataSet[Flight] \u003d org.apache.flink.api.scala.DataSet@7cd81fd5\nflights99: org.apache.flink.api.scala.DataSet[Flight] \u003d org.apache.flink.api.scala.DataSet@58242e79\nflights00: org.apache.flink.api.scala.DataSet[Flight] \u003d org.apache.flink.api.scala.DataSet@13f866c0\nflights: org.apache.flink.api.scala.DataSet[Flight] \u003d org.apache.flink.api.scala.DataSet@2aad2530\ncarriers: org.apache.flink.api.scala.DataSet[Carrier] \u003d org.apache.flink.api.scala.DataSet@148c977b\n" - } - ] - }, - "dateCreated": "Jan 9, 2017 12:01:51 PM", - "dateStarted": "Jan 9, 2017 12:02:10 PM", - "dateFinished": "Jan 9, 2017 12:02:29 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%md\n### Choosing the carrier\nNow we will search for the most popular carrier during the whole time period.", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:03:08 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "markdown", - "editOnDblClick": true - }, - "editorMode": "ace/mode/markdown", - "editorHide": true, - "tableHide": false - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952530113_212237809", - "id": "20170109-120210_773710997", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch3\u003eChoosing the carrier\u003c/h3\u003e\n\u003cp\u003eNow we will search for the most popular carrier during the whole time period.\u003c/p\u003e\n\u003c/div\u003e" - } - ] - }, - "dateCreated": "Jan 9, 2017 12:02:10 PM", - "dateStarted": "Jan 9, 2017 12:03:08 PM", - "dateFinished": "Jan 9, 2017 12:03:08 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%flink\n\nimport org.apache.flink.api.common.operators.Order\nimport org.apache.flink.api.java.aggregation.Aggregations\n\ncase class CarrierFlightsCount(carrierCode: String, count: Int)\ncase class CountByMonth(month: Int, count: Int)\n\nval carriersFlights \u003d flights\n .map(f \u003d\u003e CarrierFlightsCount(f.carrierCode, 1))\n .groupBy(\"carrierCode\")\n .sum(\"count\")\n\nval maxFlights \u003d carriersFlights\n .aggregate(Aggregations.MAX, \"count\")\n\nval bestCarrier \u003d carriersFlights\n .join(maxFlights)\n .where(\"count\")\n .equalTo(\"count\")\n .map(_._1)\n \nval carrierName \u003d bestCarrier\n .join(carriers)\n .where(\"carrierCode\")\n .equalTo(\"code\")\n .map(_._2.name)\n .collect\n .head", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:04:04 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "scala", - "editOnDblClick": false - }, - "editorMode": "ace/mode/scala", - "lineNumbers": true, - "tableHide": true - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952588708_-1770095793", - "id": "20170109-120308_1328511597", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "import org.apache.flink.api.common.operators.Order\nimport org.apache.flink.api.java.aggregation.Aggregations\ndefined class CarrierFlightsCount\ndefined class CountByMonth\ncarriersFlights: org.apache.flink.api.scala.AggregateDataSet[CarrierFlightsCount] \u003d org.apache.flink.api.scala.AggregateDataSet@2c59be0b\nmaxFlights: org.apache.flink.api.scala.AggregateDataSet[CarrierFlightsCount] \u003d org.apache.flink.api.scala.AggregateDataSet@53e5fad9\nbestCarrier: org.apache.flink.api.scala.DataSet[CarrierFlightsCount] \u003d org.apache.flink.api.scala.DataSet@64b7b1b3\ncarrierName: String \u003d Delta Air Lines Inc.\n" - } - ] - }, - "dateCreated": "Jan 9, 2017 12:03:08 PM", - "dateStarted": "Jan 9, 2017 12:03:41 PM", - "dateFinished": "Jan 9, 2017 12:03:58 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%flink\n\nprintln(s\"\"\"The most popular carrier is:\n$carrierName\n\"\"\")", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:09:18 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "scala", - "editOnDblClick": false - }, - "editorMode": "ace/mode/scala", - "lineNumbers": true - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952621624_-1222400539", - "id": "20170109-120341_952212268", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "The most popular carrier is:\nDelta Air Lines Inc.\n\n" - } - ] - }, - "dateCreated": "Jan 9, 2017 12:03:41 PM", - "dateStarted": "Jan 9, 2017 12:04:09 PM", - "dateFinished": "Jan 9, 2017 12:04:10 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%md\n### Calculating flights\nThe last step is to filter **Delta Air Lines** flights and group them by months.", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:04:26 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "markdown", - "editOnDblClick": true - }, - "editorMode": "ace/mode/markdown", - "editorHide": true, - "tableHide": false - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952649646_-1553253944", - "id": "20170109-120409_2003276881", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch3\u003eCalculating flights\u003c/h3\u003e\n\u003cp\u003eThe last step is to filter \u003cstrong\u003eDelta Air Lines\u003c/strong\u003e flights and group them by months.\u003c/p\u003e\n\u003c/div\u003e" - } - ] - }, - "dateCreated": "Jan 9, 2017 12:04:09 PM", - "dateStarted": "Jan 9, 2017 12:04:26 PM", - "dateFinished": "Jan 9, 2017 12:04:26 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "title": "flights grouping", - "text": "%flink\n\ndef countFlightsPerMonth(flights: DataSet[Flight],\n carrier: DataSet[CarrierFlightsCount]) \u003d {\n val carrierFlights \u003d flights\n .join(carrier)\n .where(\"carrierCode\")\n .equalTo(\"carrierCode\")\n .map(_._1)\n \n carrierFlights\n .map(flight \u003d\u003e CountByMonth(flight.month, 1))\n .groupBy(\"month\")\n .sum(\"count\")\n .sortPartition(\"month\", Order.ASCENDING)\n}\n\nval bestCarrierFlights_98 \u003d countFlightsPerMonth(flights98, bestCarrier)\nval bestCarrierFlights_99 \u003d countFlightsPerMonth(flights99, bestCarrier)\nval bestCarrierFlights_00 \u003d countFlightsPerMonth(flights00, bestCarrier)", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:05:06 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "scala", - "editOnDblClick": false - }, - "editorMode": "ace/mode/scala", - "lineNumbers": true, - "title": true, - "tableHide": true - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952665972_667547355", - "id": "20170109-120425_2018337048", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "countFlightsPerMonth: (flights: org.apache.flink.api.scala.DataSet[Flight], carrier: org.apache.flink.api.scala.DataSet[CarrierFlightsCount])org.apache.flink.api.scala.DataSet[CountByMonth]\nbestCarrierFlights_98: org.apache.flink.api.scala.DataSet[CountByMonth] \u003d org.apache.flink.api.scala.PartitionSortedDataSet@2aa64309\nbestCarrierFlights_99: org.apache.flink.api.scala.DataSet[CountByMonth] \u003d org.apache.flink.api.scala.PartitionSortedDataSet@35fe60c4\nbestCarrierFlights_00: org.apache.flink.api.scala.DataSet[CountByMonth] \u003d org.apache.flink.api.scala.PartitionSortedDataSet@4621410f\n" - } - ] - }, - "dateCreated": "Jan 9, 2017 12:04:25 PM", - "dateStarted": "Jan 9, 2017 12:04:50 PM", - "dateFinished": "Jan 9, 2017 12:04:51 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "title": "making a results table", - "text": "%flink\n\ndef monthAsString(month: Int): String \u003d {\n month match {\n case 1 \u003d\u003e \"Jan\"\n case 2 \u003d\u003e \"Feb\"\n case 3 \u003d\u003e \"Mar\"\n case 4 \u003d\u003e \"Apr\"\n case 5 \u003d\u003e \"May\"\n case 6 \u003d\u003e \"Jun\"\n case 7 \u003d\u003e \"Jul\"\n case 8 \u003d\u003e \"Aug\"\n case 9 \u003d\u003e \"Sept\"\n case 10 \u003d\u003e \"Oct\"\n case 11 \u003d\u003e \"Nov\"\n case 12 \u003d\u003e \"Dec\"\n }\n}\n\n// We should put all the results into a common DataFrame\n// to show them in a common picture\nval bestCarrierFlights \u003d bestCarrierFlights_98\n .join(bestCarrierFlights_99)\n .where(\"month\")\n .equalTo(\"month\")\n .map(tuple \u003d\u003e (tuple._1.month, tuple._1.count, tuple._2.count))\n .join(bestCarrierFlights_00)\n .where(0)\n .equalTo(\"month\")\n .map(tuple \u003d\u003e (tuple._1._1, tuple._1._2, tuple._1._3, tuple._2.count))\n .collect\n \nvar flightsByMonthTable \u003d s\"Month\\t1998\\t1999\\t2000\\n\"\nbestCarrierFlights.foreach(data \u003d\u003e flightsByMonthTable +\u003d s\"${monthAsString(data._1)}\\t${data._2}\\t${data._3}\\t${data._4}\\n\")", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:06:03 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "scala", - "editOnDblClick": false - }, - "editorMode": "ace/mode/scala", - "lineNumbers": true, - "title": true, - "tableHide": true - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952690164_-1061667443", - "id": "20170109-120450_1574916350", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "monthAsString: (month: Int)String\nbestCarrierFlights: Seq[(Int, Int, Int, Int)] \u003d Buffer((1,78523,77745,78055), (2,71101,70498,71090), (3,78906,77812,78453), (4,75726,75343,75247), (5,77937,77226,76797), (6,75432,75840,74846), (7,77521,77264,75776), (8,78104,78141,77654), (9,74840,75067,73696), (10,76145,77829,77425), (11,73552,74411,73659), (12,77308,76954,75331))\nflightsByMonthTable: String \u003d \n\"Month\t1998\t1999\t2000\n\"\n" - } - ] - }, - "dateCreated": "Jan 9, 2017 12:04:50 PM", - "dateStarted": "Jan 9, 2017 12:05:24 PM", - "dateFinished": "Jan 9, 2017 12:05:59 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "title": "\"Delta Air Lines\" flights count by months", - "text": "%flink\n\nprintln(s\"\"\"%table\n$flightsByMonthTable\n\"\"\")", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:06:17 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": { - "0": { - "graph": { - "mode": "lineChart", - "height": 300.0, - "optionOpen": false, - "setting": { - "lineChart": {} - }, - "commonSetting": {}, - "keys": [ - { - "name": "Month", - "index": 0.0, - "aggr": "sum" - } - ], - "groups": [], - "values": [ - { - "name": "1998", - "index": 1.0, - "aggr": "sum" - }, - { - "name": "1999", - "index": 2.0, - "aggr": "sum" - }, - { - "name": "2000", - "index": 3.0, - "aggr": "sum" - } - ] - }, - "helium": {} - } - }, - "editorSetting": { - "language": "scala", - "editOnDblClick": false - }, - "editorMode": "ace/mode/scala", - "title": true, - "lineNumbers": true - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952724460_191505697", - "id": "20170109-120524_2037622815", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TABLE", - "data": "Month\t1998\t1999\t2000\nJan\t78523\t77745\t78055\nFeb\t71101\t70498\t71090\nMar\t78906\t77812\t78453\nApr\t75726\t75343\t75247\nMay\t77937\t77226\t76797\nJun\t75432\t75840\t74846\nJul\t77521\t77264\t75776\nAug\t78104\t78141\t77654\nSept\t74840\t75067\t73696\nOct\t76145\t77829\t77425\nNov\t73552\t74411\t73659\nDec\t77308\t76954\t75331\n" - } - ] - }, - "dateCreated": "Jan 9, 2017 12:05:24 PM", - "dateStarted": "Jan 9, 2017 12:06:07 PM", - "dateFinished": "Jan 9, 2017 12:06:08 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%md\n### Results\nLooking at this chart we can say that February is the most unpopular month, but this is only because it has less days (28 or 29) than the other months (30 or 31). To receive more fair picture we should calculate the average flights count per day for each month.", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:06:34 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "markdown", - "editOnDblClick": true - }, - "editorMode": "ace/mode/markdown", - "editorHide": true, - "tableHide": false - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952767719_-1010557136", - "id": "20170109-120607_67673280", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "HTML", - "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch3\u003eResults\u003c/h3\u003e\n\u003cp\u003eLooking at this chart we can say that February is the most unpopular month, but this is only because it has less days (28 or 29) than the other months (30 or 31). To receive more fair picture we should calculate the average flights count per day for each month.\u003c/p\u003e\n\u003c/div\u003e" - } - ] - }, - "dateCreated": "Jan 9, 2017 12:06:07 PM", - "dateStarted": "Jan 9, 2017 12:06:34 PM", - "dateFinished": "Jan 9, 2017 12:06:34 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%flink\n\ndef daysInMonth(month: Int, year: Int): Int \u003d {\n month match {\n case 1 \u003d\u003e 31\n case 2 \u003d\u003e if (year % 4 \u003d\u003d 0) {\n 29\n } else {\n 28\n }\n case 3 \u003d\u003e 31\n case 4 \u003d\u003e 30\n case 5 \u003d\u003e 31\n case 6 \u003d\u003e 30\n case 7 \u003d\u003e 31\n case 8 \u003d\u003e 31\n case 9 \u003d\u003e 30\n case 10 \u003d\u003e 31\n case 11 \u003d\u003e 30\n case 12 \u003d\u003e 31\n }\n}\n\n\nvar flightsByDayTable \u003d s\"Month\\t1998\\t1999\\t2000\\n\"\n\nbestCarrierFlights.foreach(data \u003d\u003e flightsByDayTable +\u003d s\"${monthAsString(data._1)}\\t${data._2/daysInMonth(data._1,1998)}\\t${data._3/daysInMonth(data._1,1999)}\\t${data._4/daysInMonth(data._1,2000)}\\n\")", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:06:58 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": {}, - "editorSetting": { - "language": "scala", - "editOnDblClick": false - }, - "editorMode": "ace/mode/scala", - "lineNumbers": true, - "tableHide": true - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952794097_-785833130", - "id": "20170109-120634_492170963", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TEXT", - "data": "daysInMonth: (month: Int, year: Int)Int\nflightsByDayTable: String \u003d \n\"Month\t1998\t1999\t2000\n\"\n" - } - ] - }, - "dateCreated": "Jan 9, 2017 12:06:34 PM", - "dateStarted": "Jan 9, 2017 12:06:53 PM", - "dateFinished": "Jan 9, 2017 12:06:53 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "title": "\"Delta Air Lines\" flights count by days", - "text": "%flink\n\nprintln(s\"\"\"%table\n$flightsByDayTable\n\"\"\")", - "user": "anonymous", - "dateUpdated": "Jan 9, 2017 12:10:56 PM", - "config": { - "colWidth": 12.0, - "enabled": true, - "results": { - "0": { - "graph": { - "mode": "lineChart", - "height": 300.0, - "optionOpen": false, - "setting": { - "lineChart": {} - }, - "commonSetting": {}, - "keys": [ - { - "name": "Month", - "index": 0.0, - "aggr": "sum" - } - ], - "groups": [], - "values": [ - { - "name": "1998", - "index": 1.0, - "aggr": "sum" - }, - { - "name": "1999", - "index": 2.0, - "aggr": "sum" - }, - { - "name": "2000", - "index": 3.0, - "aggr": "sum" - } - ] - }, - "helium": {} - } - }, - "editorSetting": { - "language": "scala", - "editOnDblClick": false - }, - "editorMode": "ace/mode/scala", - "title": true, - "lineNumbers": true - }, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952813391_1847418990", - "id": "20170109-120653_1870236569", - "results": { - "code": "SUCCESS", - "msg": [ - { - "type": "TABLE", - "data": "Month\t1998\t1999\t2000\nJan\t2533\t2507\t2517\nFeb\t2539\t2517\t2451\nMar\t2545\t2510\t2530\nApr\t2524\t2511\t2508\nMay\t2514\t2491\t2477\nJun\t2514\t2528\t2494\nJul\t2500\t2492\t2444\nAug\t2519\t2520\t2504\nSept\t2494\t2502\t2456\nOct\t2456\t2510\t2497\nNov\t2451\t2480\t2455\nDec\t2493\t2482\t2430\n" - } - ] - }, - "dateCreated": "Jan 9, 2017 12:06:53 PM", - "dateStarted": "Jan 9, 2017 12:07:22 PM", - "dateFinished": "Jan 9, 2017 12:07:23 PM", - "status": "FINISHED", - "progressUpdateIntervalMs": 500 - }, - { - "text": "%flink\n", - "dateUpdated": "Jan 9, 2017 12:07:22 PM", - "config": {}, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483952842919_587228425", - "id": "20170109-120722_939892827", - "dateCreated": "Jan 9, 2017 12:07:22 PM", - "status": "READY", - "progressUpdateIntervalMs": 500 - } - ], - "name": "Zeppelin Tutorial/Using Flink for batch processing", - "id": "2C35YU814", - "angularObjects": { - "2C4PVECE6:shared_process": [], - "2C4US9MUF:shared_process": [], - "2C4FYNB4G:shared_process": [], - "2C4GX28KP:shared_process": [], - "2C648AXXN:shared_process": [], - "2C3MSEJ2F:shared_process": [], - "2C6F2N6BT:shared_process": [], - "2C3US2RTN:shared_process": [], - "2C3TYMD6K:shared_process": [], - "2C3FDPZRX:shared_process": [], - "2C5TEARYX:shared_process": [], - "2C5D6NSNG:shared_process": [], - "2C6FVVEAD:shared_process": [], - "2C582KNWG:shared_process": [], - "2C6ZMVGM7:shared_process": [], - "2C6UYQG8R:shared_process": [], - "2C666VZT2:shared_process": [], - "2C4JRCY3K:shared_process": [], - "2C64W5T9D:shared_process": [] - }, - "config": { - "looknfeel": "default" - }, - "info": {} -} diff --git a/notebook/2C57UKYWR/note.json b/notebook/2C57UKYWR/note.json index 22afb2a5701..79a90288d86 100644 --- a/notebook/2C57UKYWR/note.json +++ b/notebook/2C57UKYWR/note.json @@ -3,7 +3,6 @@ { "text": "%md\n\n\n### [Apache Pig](http://pig.apache.org/) is a platform for analyzing large data sets that consists of a high-level language for expressing data analysis programs, coupled with infrastructure for evaluating these programs. The salient property of Pig programs is that their structure is amenable to substantial parallelization, which in turns enables them to handle very large data sets.\n\nPig\u0027s language layer currently consists of a textual language called Pig Latin, which has the following key properties:\n\n* Ease of programming. It is trivial to achieve parallel execution of simple, \"embarrassingly parallel\" data analysis tasks. Complex tasks comprised of multiple interrelated data transformations are explicitly encoded as data flow sequences, making them easy to write, understand, and maintain.\n* Optimization opportunities. The way in which tasks are encoded permits the system to optimize their execution automatically, allowing the user to focus on semantics rather than efficiency.\n* Extensibility. Users can create their own functions to do special-purpose processing.\n", "user": "anonymous", - "dateUpdated": "Jan 22, 2017 12:48:50 PM", "config": { "colWidth": 12.0, "enabled": true, @@ -32,16 +31,12 @@ "apps": [], "jobName": "paragraph_1483277502513_1156234051", "id": "20170101-213142_1565013608", - "dateCreated": "Jan 1, 2017 9:31:42 PM", - "dateStarted": "Jan 22, 2017 12:48:50 PM", - "dateFinished": "Jan 22, 2017 12:48:51 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%md\n\nThis pig tutorial use pig to do the same thing as spark tutorial. The default mode is mapreduce, you can also use other modes like local/tez_local/tez. For mapreduce mode, you need to have hadoop installed and export `HADOOP_CONF_DIR` in `zeppelin-env.sh`\n\nThe tutorial consists of 3 steps.\n\n* Use shell interpreter to download bank.csv and upload it to hdfs\n* use `%pig` to process the data\n* use `%pig.query` to query the data", "user": "anonymous", - "dateUpdated": "Jan 22, 2017 12:48:55 PM", "config": { "colWidth": 12.0, "enabled": true, @@ -70,16 +65,12 @@ "apps": [], "jobName": "paragraph_1483689316217_-629483391", "id": "20170106-155516_1050601059", - "dateCreated": "Jan 6, 2017 3:55:16 PM", - "dateStarted": "Jan 22, 2017 12:48:55 PM", - "dateFinished": "Jan 22, 2017 12:48:55 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { - "text": "%sh\n\nwget https://s3.amazonaws.com/apache-zeppelin/tutorial/bank/bank.csv\nhadoop fs -put bank.csv .\n", + "text": "%sh\n\ncd $(mktemp -d)\nwget https://s3.amazonaws.com/apache-zeppelin/tutorial/bank/bank.csv\nif ! hadoop fs -test -e bank.csv; then\n hadoop fs -put bank.csv .\nelse\n echo \"bank.csv already in your home directory in DFS\"\nfi", "user": "anonymous", - "dateUpdated": "Jan 22, 2017 12:51:48 PM", "config": { "colWidth": 12.0, "enabled": true, @@ -106,16 +97,12 @@ "apps": [], "jobName": "paragraph_1485058437578_-1906301827", "id": "20170122-121357_640055590", - "dateCreated": "Jan 22, 2017 12:13:57 PM", - "dateStarted": "Jan 22, 2017 12:51:48 PM", - "dateFinished": "Jan 22, 2017 12:51:52 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%pig\n\nbankText \u003d load \u0027bank.csv\u0027 using PigStorage(\u0027;\u0027);\nbank \u003d foreach bankText generate $0 as age, $1 as job, $2 as marital, $3 as education, $5 as balance; \nbank \u003d filter bank by age !\u003d \u0027\"age\"\u0027;\nbank \u003d foreach bank generate (int)age, REPLACE(job,\u0027\"\u0027,\u0027\u0027) as job, REPLACE(marital, \u0027\"\u0027, \u0027\u0027) as marital, (int)(REPLACE(balance, \u0027\"\u0027, \u0027\u0027)) as balance;\n\n-- The following statement is optional, it depends on whether your needs.\n-- store bank into \u0027clean_bank.csv\u0027 using PigStorage(\u0027;\u0027);\n\n\n", "user": "anonymous", - "dateUpdated": "Feb 24, 2017 5:08:08 PM", "config": { "colWidth": 12.0, "editorMode": "ace/mode/pig", @@ -137,16 +124,12 @@ "apps": [], "jobName": "paragraph_1483277250237_-466604517", "id": "20161228-140640_1560978333", - "dateCreated": "Jan 1, 2017 9:27:30 PM", - "dateStarted": "Feb 24, 2017 5:08:08 PM", - "dateFinished": "Feb 24, 2017 5:08:11 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%pig.query\n\nbank_data \u003d filter bank by age \u003c 30;\nb \u003d group bank_data by age;\nforeach b generate group, COUNT($1);\n\n", "user": "anonymous", - "dateUpdated": "Feb 24, 2017 5:08:13 PM", "config": { "colWidth": 4.0, "editorMode": "ace/mode/pig", @@ -182,16 +165,12 @@ "apps": [], "jobName": "paragraph_1483277250238_-465450270", "id": "20161228-140730_1903342877", - "dateCreated": "Jan 1, 2017 9:27:30 PM", - "dateStarted": "Feb 24, 2017 5:08:13 PM", - "dateFinished": "Feb 24, 2017 5:08:26 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%pig.query\n\nbank_data \u003d filter bank by age \u003c ${maxAge\u003d40};\nb \u003d group bank_data by age;\nforeach b generate group, COUNT($1) as count;", "user": "anonymous", - "dateUpdated": "Feb 24, 2017 5:08:14 PM", "config": { "colWidth": 4.0, "editorMode": "ace/mode/pig", @@ -235,16 +214,12 @@ "apps": [], "jobName": "paragraph_1483277250239_-465835019", "id": "20161228-154918_1551591203", - "dateCreated": "Jan 1, 2017 9:27:30 PM", - "dateStarted": "Feb 24, 2017 5:08:14 PM", - "dateFinished": "Feb 24, 2017 5:08:29 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 }, { "text": "%pig.query\n\nbank_data \u003d filter bank by marital\u003d\u003d\u0027${marital\u003dsingle,single|divorced|married}\u0027;\nb \u003d group bank_data by age;\nforeach b generate group, COUNT($1) as count;\n\n\n", "user": "anonymous", - "dateUpdated": "Feb 24, 2017 5:08:15 PM", "config": { "colWidth": 4.0, "editorMode": "ace/mode/pig", @@ -299,27 +274,8 @@ "apps": [], "jobName": "paragraph_1483277250240_-480070728", "id": "20161228-142259_575675591", - "dateCreated": "Jan 1, 2017 9:27:30 PM", - "dateStarted": "Feb 24, 2017 5:08:27 PM", - "dateFinished": "Feb 24, 2017 5:08:31 PM", "status": "FINISHED", "progressUpdateIntervalMs": 500 - }, - { - "text": "%pig\n", - "dateUpdated": "Jan 1, 2017 9:27:30 PM", - "config": {}, - "settings": { - "params": {}, - "forms": {} - }, - "apps": [], - "jobName": "paragraph_1483277250240_-480070728", - "id": "20161228-155036_1854903164", - "dateCreated": "Jan 1, 2017 9:27:30 PM", - "status": "READY", - "errorMessage": "", - "progressUpdateIntervalMs": 500 } ], "name": "Zeppelin Tutorial/Using Pig for querying data", @@ -331,4 +287,4 @@ }, "config": {}, "info": {} -} \ No newline at end of file +} diff --git a/notebook/2D23Y84Q3/note.json b/notebook/2D23Y84Q3/note.json new file mode 100644 index 00000000000..7f56e3af8f8 --- /dev/null +++ b/notebook/2D23Y84Q3/note.json @@ -0,0 +1,257 @@ +{ + "paragraphs": [ + { + "text": "%md\n## Welcome to Zeppelin.\n##### This is a live tutorial, you can run the code yourself. (Shift-Enter to Run)", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch2\u003eWelcome to Zeppelin.\u003c/h2\u003e\n\u003ch5\u003eThis is a live tutorial, you can run the code yourself. (Shift-Enter to Run)\u003c/h5\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512661301573_2061540649", + "id": "20171207-154141_454157179", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Load data into table", + "text": "%livy \nimport org.apache.commons.io.IOUtils\nimport java.net.URL\nimport java.nio.charset.Charset\n\n// Livy creates and injects sc (SparkContext)\n// So you don\u0027t need create it manually\n\n// load bank data\nval bankText \u003d sc.parallelize(\n IOUtils.toString(\n new URL(\"http://s3.amazonaws.com/apache-zeppelin/tutorial/bank/bank.csv\"),\n Charset.forName(\"utf8\")).split(\"\\n\"))\n\ncase class Bank(age: Integer, job: String, marital: String, education: String, balance: Integer)\n\nval bank \u003d bankText.map(s \u003d\u003e s.split(\";\")).filter(s \u003d\u003e s(0) !\u003d \"\\\"age\\\"\").map(\n s \u003d\u003e Bank(s(0).toInt, \n s(1).replaceAll(\"\\\"\", \"\"),\n s(2).replaceAll(\"\\\"\", \"\"),\n s(3).replaceAll(\"\\\"\", \"\"),\n s(5).replaceAll(\"\\\"\", \"\").toInt\n )\n).toDF()\nbank.createOrReplaceTempView(\"bank\")", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "scala", + "editOnDblClick": false + }, + "editorMode": "ace/mode/scala", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [] + }, + "apps": [], + "jobName": "paragraph_1512661314646_-2008317892", + "id": "20171207-154154_1112862106", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%livy.sql \nselect age, count(1) value\nfrom bank \nwhere age \u003c 30 \ngroup by age \norder by age", + "user": "anonymous", + "config": { + "colWidth": 4.0, + "enabled": true, + "results": { + "0": { + "graph": { + "mode": "multiBarChart", + "height": 300.0, + "optionOpen": false + }, + "helium": {} + } + }, + "editorSetting": { + "language": "sql", + "editOnDblClick": false + }, + "editorMode": "ace/mode/sql" + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TABLE", + "data": "age\tvalue\n19\t4\n20\t3\n21\t7\n22\t9\n23\t20\n24\t24\n25\t44\n26\t77\n27\t94\n28\t103\n29\t97" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512661342551_-1771515073", + "id": "20171207-154222_1233613473", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%livy.sql \nselect age, count(1) value \nfrom bank \nwhere age \u003c ${maxAge\u003d30} \ngroup by age \norder by age", + "user": "anonymous", + "config": { + "colWidth": 4.0, + "enabled": true, + "results": { + "0": { + "graph": { + "mode": "multiBarChart", + "height": 300.0, + "optionOpen": false + }, + "helium": {} + } + }, + "editorSetting": { + "language": "sql", + "editOnDblClick": false + }, + "editorMode": "ace/mode/sql" + }, + "settings": { + "params": { + "maxAge": "30" + }, + "forms": { + "maxAge": { + "name": "maxAge", + "defaultValue": "30", + "hidden": false + } + } + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TABLE", + "data": "age\tvalue\n19\t4\n20\t3\n21\t7\n22\t9\n23\t20\n24\t24\n25\t44\n26\t77\n27\t94\n28\t103\n29\t97" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512661399415_-565803965", + "id": "20171207-154319_2077681889", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%livy.sql \nselect age, count(1) value \nfrom bank \nwhere marital\u003d\"${marital\u003dsingle,single|divorced|married}\" \ngroup by age \norder by age", + "user": "anonymous", + "config": { + "colWidth": 4.0, + "enabled": true, + "results": { + "0": { + "graph": { + "mode": "stackedAreaChart", + "height": 300.0, + "optionOpen": false + }, + "helium": {} + } + }, + "editorSetting": { + "language": "sql", + "editOnDblClick": false + }, + "editorMode": "ace/mode/sql", + "runOnSelectionChange": true + }, + "settings": { + "params": { + "marital": "single" + }, + "forms": { + "marital": { + "name": "marital", + "defaultValue": "single", + "options": [ + { + "value": "single" + }, + { + "value": "divorced" + }, + { + "value": "married" + } + ], + "hidden": false + } + } + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TABLE", + "data": "age\tvalue\n19\t4\n20\t3\n21\t7\n22\t9\n23\t17\n24\t13\n25\t33\n26\t56\n27\t64\n28\t78\n29\t56\n30\t92\n31\t86\n32\t105\n33\t61\n34\t75\n35\t46\n36\t50\n37\t43\n38\t44\n39\t30\n40\t25\n41\t19\n42\t23\n43\t21\n44\t20\n45\t15\n46\t14\n47\t12\n48\t12\n49\t11\n50\t8\n51\t6\n52\t9\n53\t4\n55\t3\n56\t3\n57\t2\n58\t7\n59\t2\n60\t5\n66\t2\n69\t1" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512661425230_-541534136", + "id": "20171207-154345_1702378141", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n## Congratulations, it\u0027s done.\n##### You can create your own notebook in \u0027Notebook\u0027 menu. Good luck!", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch2\u003eCongratulations, it\u0026rsquo;s done.\u003c/h2\u003e\n\u003ch5\u003eYou can create your own notebook in \u0026lsquo;Notebook\u0026rsquo; menu. Good luck!\u003c/h5\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512661484870_936209768", + "id": "20171207-154444_191499102", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + } + ], + "name": "Zeppelin Tutorial/Livy • Basic Features (Spark)", + "id": "2D23Y84Q3", + "angularObjects": {}, + "config": {}, + "info": {} +} diff --git a/notebook/2D25QSMZD/note.json b/notebook/2D25QSMZD/note.json new file mode 100644 index 00000000000..7da1c3d9683 --- /dev/null +++ b/notebook/2D25QSMZD/note.json @@ -0,0 +1,632 @@ +{ + "paragraphs": [ + { + "title": "Hello R", + "text": "%livy.sparkr\nfoo \u003c- TRUE\nprint(foo)\nbare \u003c- c(1, 2.5, 4)\nprint(bare)\ndouble \u003c- 15.0\nprint(double)", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "r", + "editOnDblClick": false + }, + "editorMode": "ace/mode/r", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "[1] TRUE\n[1] 1.0 2.5 4.0\n[1] 15" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665091345_-1552032532", + "id": "20171207-164451_2086089298", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Load R Librairies", + "text": "%livy.sparkr\nlibrary(data.table)\ndt \u003c- data.table(1:3)\nprint(dt)\nfor (i in 1:5) {\n print(i*2)\n}\nprint(1:50)", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "r", + "editOnDblClick": false + }, + "editorMode": "ace/mode/r", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "V1\n1: 1\n2: 2\n3: 3\n[1] 2\n[1] 4\n[1] 6\n[1] 8\n[1] 10\n [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25\n[26] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665104140_959623612", + "id": "20171207-164504_1226912823", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n\n## Zeppelin SparkR Tutorial\n\n##### This is a live tutorial, you can run the code yourself. (Shift-Enter to Run)", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch2\u003eZeppelin SparkR Tutorial\u003c/h2\u003e\n\u003ch5\u003eThis is a live tutorial, you can run the code yourself. (Shift-Enter to Run)\u003c/h5\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665144527_1393882527", + "id": "20171207-164544_1919906532", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Load Iris Dataset", + "text": "%livy.sparkr\ncolnames(iris)\niris$Petal.Length\niris$Sepal.Length", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "r", + "editOnDblClick": false + }, + "editorMode": "ace/mode/r", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "[1] 5.1 4.9 4.7 4.6 5.0 5.4 4.6 5.0 4.4 4.9 5.4 4.8 4.8 4.3 5.8 5.7 5.4 5.1\n [19] 5.7 5.1 5.4 5.1 4.6 5.1 4.8 5.0 5.0 5.2 5.2 4.7 4.8 5.4 5.2 5.5 4.9 5.0\n [37] 5.5 4.9 4.4 5.1 5.0 4.5 4.4 5.0 5.1 4.8 5.1 4.6 5.3 5.0 7.0 6.4 6.9 5.5\n [55] 6.5 5.7 6.3 4.9 6.6 5.2 5.0 5.9 6.0 6.1 5.6 6.7 5.6 5.8 6.2 5.6 5.9 6.1\n [73] 6.3 6.1 6.4 6.6 6.8 6.7 6.0 5.7 5.5 5.5 5.8 6.0 5.4 6.0 6.7 6.3 5.6 5.5\n [91] 5.5 6.1 5.8 5.0 5.6 5.7 5.7 6.2 5.1 5.7 6.3 5.8 7.1 6.3 6.5 7.6 4.9 7.3\n[109] 6.7 7.2 6.5 6.4 6.8 5.7 5.8 6.4 6.5 7.7 7.7 6.0 6.9 5.6 7.7 6.3 6.7 7.2\n[127] 6.2 6.1 6.4 7.2 7.4 7.9 6.4 6.3 6.1 7.7 6.3 6.4 6.0 6.9 6.7 6.9 5.8 6.8\n[145] 6.7 6.7 6.3 6.5 6.2 5.9" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665156758_2138402297", + "id": "20171207-164556_1955057552", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "TABLE Display", + "text": "%livy.sparkr cat(\"%table name\\tsize\\nsmall\\t100\\nlarge\\t1000\")", + "user": "anonymous", + "config": { + "colWidth": 6.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "r", + "editOnDblClick": false + }, + "editorMode": "ace/mode/r", + "lineNumbers": false, + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TABLE", + "data": "name\tsize\nsmall\t100\nlarge\t1000" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512669567944_-1475796255", + "id": "20171207-175927_1945271951", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "HTML Display", + "text": "%livy.sparkr \n\ncat(\"%html \u003ch3\u003eHello HTML\u003c/h3\u003e\")\ncat(\"\u003cfont color\u003d\u0027blue\u0027\u003e\u003cspan class\u003d\u0027fa fa-bars\u0027\u003e Easy...\u003c/font\u003e\u003c/span\u003e\")\nfor (i in 1:10) {\n cat(paste0(\"\u003ch4\u003e\", i, \" * 2 \u003d \", i*2, \"\u003c/h4\u003e\"))\n}", + "user": "anonymous", + "config": { + "colWidth": 6.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "r", + "editOnDblClick": false + }, + "editorMode": "ace/mode/r", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003ch3\u003eHello HTML\u003c/h3\u003e\u003cfont color\u003d\u0027blue\u0027\u003e\u003cspan class\u003d\u0027fa fa-bars\u0027\u003e Easy...\u003c/font\u003e\u003c/span\u003e\u003ch4\u003e1 * 2 \u003d 2\u003c/h4\u003e\u003ch4\u003e2 * 2 \u003d 4\u003c/h4\u003e\u003ch4\u003e3 * 2 \u003d 6\u003c/h4\u003e\u003ch4\u003e4 * 2 \u003d 8\u003c/h4\u003e\u003ch4\u003e5 * 2 \u003d 10\u003c/h4\u003e\u003ch4\u003e6 * 2 \u003d 12\u003c/h4\u003e\u003ch4\u003e7 * 2 \u003d 14\u003c/h4\u003e\u003ch4\u003e8 * 2 \u003d 16\u003c/h4\u003e\u003ch4\u003e9 * 2 \u003d 18\u003c/h4\u003e\u003ch4\u003e10 * 2 \u003d 20\u003c/h4\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665208351_-164442464", + "id": "20171207-164648_863691650", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n---", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": false, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003chr/\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512669622682_561741975", + "id": "20171207-180022_247175894", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "GoogleVis: Bar Chart", + "text": "%livy.sparkr\n\n# Workaround for Spark issue with googleVis: SPARK-23780\ndetach(\"package:SparkR\")\nlibrary(googleVis)\nsuppressPackageStartupMessages(library(SparkR))\n\ndf\u003ddata.frame(country\u003dc(\"US\", \"GB\", \"BR\"), \n val1\u003dc(10,13,14), \n val2\u003dc(23,12,32))\nBar \u003c- gvisBarChart(df)\ncat(\"%html \", Bar$html$chart)", + "user": "anonymous", + "config": { + "colWidth": 4.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "r", + "editOnDblClick": false + }, + "editorMode": "ace/mode/r", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": " \u003c!-- BarChart generated in R 3.2.2 by googleVis 0.6.2 package --\u003e\n\u003c!-- Thu Dec 7 18:01:14 2017 --\u003e\n\n\n\u003c!-- jsHeader --\u003e\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataBarChartID7a0230417b00 () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"US\",\n10,\n23\n],\n[\n\"GB\",\n13,\n12\n],\n[\n\"BR\",\n14,\n32\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027country\u0027);\ndata.addColumn(\u0027number\u0027,\u0027val1\u0027);\ndata.addColumn(\u0027number\u0027,\u0027val2\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartBarChartID7a0230417b00() {\nvar data \u003d gvisDataBarChartID7a0230417b00();\nvar options \u003d {};\noptions[\"allowHtml\"] \u003d true;\n\n\n var chart \u003d new google.visualization.BarChart(\n document.getElementById(\u0027BarChartID7a0230417b00\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"corechart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartBarChartID7a0230417b00);\n})();\nfunction displayChartBarChartID7a0230417b00() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\u003c!-- jsChart --\u003e \n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartBarChartID7a0230417b00\"\u003e\u003c/script\u003e\n \n\u003c!-- divChart --\u003e\n \n\u003cdiv id\u003d\"BarChartID7a0230417b00\" \n style\u003d\"width: 500; height: automatic;\"\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665244199_58773195", + "id": "20171207-164724_884681703", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "GoogleVis: Candlestick Chart", + "text": "%livy.sparkr\n\n# Workaround for Spark issue with googleVis: SPARK-23780\ndetach(\"package:SparkR\")\nlibrary(googleVis)\nsuppressPackageStartupMessages(library(SparkR))\n\nCandle \u003c- gvisCandlestickChart(OpenClose, \n options\u003dlist(legend\u003d\u0027none\u0027))\n\ncat(\"%html \", Candle$html$chart)", + "user": "anonymous", + "config": { + "colWidth": 4.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "r", + "editOnDblClick": false + }, + "editorMode": "ace/mode/r", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": " \u003c!-- CandlestickChart generated in R 3.2.2 by googleVis 0.6.2 package --\u003e\n\u003c!-- Thu Dec 7 18:01:17 2017 --\u003e\n\n\n\u003c!-- jsHeader --\u003e\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataCandlestickChartID7a023ea8fb79 () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"Mon\",\n20,\n28,\n38,\n45\n],\n[\n\"Tues\",\n31,\n38,\n55,\n66\n],\n[\n\"Wed\",\n50,\n55,\n77,\n80\n],\n[\n\"Thurs\",\n50,\n77,\n66,\n77\n],\n[\n\"Fri\",\n15,\n66,\n22,\n68\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027Weekday\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Low\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Open\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Close\u0027);\ndata.addColumn(\u0027number\u0027,\u0027High\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartCandlestickChartID7a023ea8fb79() {\nvar data \u003d gvisDataCandlestickChartID7a023ea8fb79();\nvar options \u003d {};\noptions[\"allowHtml\"] \u003d true;\noptions[\"legend\"] \u003d \"none\";\n\n\n var chart \u003d new google.visualization.CandlestickChart(\n document.getElementById(\u0027CandlestickChartID7a023ea8fb79\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"corechart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartCandlestickChartID7a023ea8fb79);\n})();\nfunction displayChartCandlestickChartID7a023ea8fb79() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\u003c!-- jsChart --\u003e \n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartCandlestickChartID7a023ea8fb79\"\u003e\u003c/script\u003e\n \n\u003c!-- divChart --\u003e\n \n\u003cdiv id\u003d\"CandlestickChartID7a023ea8fb79\" \n style\u003d\"width: 500; height: automatic;\"\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665288367_1530838334", + "id": "20171207-164808_865335538", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "GoogleVis: Line chart", + "text": "%livy.sparkr\n\n# Workaround for Spark issue with googleVis: SPARK-23780\ndetach(\"package:SparkR\")\nlibrary(googleVis)\nsuppressPackageStartupMessages(library(SparkR))\n\ndf\u003ddata.frame(country\u003dc(\"US\", \"GB\", \"BR\"), \n val1\u003dc(10,13,14), \n val2\u003dc(23,12,32))\n\nLine \u003c- gvisLineChart(df)\n\ncat(\"%html \", Line$html$chart)", + "user": "anonymous", + "config": { + "colWidth": 4.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "r", + "editOnDblClick": false + }, + "editorMode": "ace/mode/r", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": " \u003c!-- LineChart generated in R 3.2.2 by googleVis 0.6.2 package --\u003e\n\u003c!-- Thu Dec 7 18:01:21 2017 --\u003e\n\n\n\u003c!-- jsHeader --\u003e\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataLineChartID7a0239daae6a () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"US\",\n10,\n23\n],\n[\n\"GB\",\n13,\n12\n],\n[\n\"BR\",\n14,\n32\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027country\u0027);\ndata.addColumn(\u0027number\u0027,\u0027val1\u0027);\ndata.addColumn(\u0027number\u0027,\u0027val2\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartLineChartID7a0239daae6a() {\nvar data \u003d gvisDataLineChartID7a0239daae6a();\nvar options \u003d {};\noptions[\"allowHtml\"] \u003d true;\n\n\n var chart \u003d new google.visualization.LineChart(\n document.getElementById(\u0027LineChartID7a0239daae6a\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"corechart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartLineChartID7a0239daae6a);\n})();\nfunction displayChartLineChartID7a0239daae6a() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\u003c!-- jsChart --\u003e \n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartLineChartID7a0239daae6a\"\u003e\u003c/script\u003e\n \n\u003c!-- divChart --\u003e\n \n\u003cdiv id\u003d\"LineChartID7a0239daae6a\" \n style\u003d\"width: 500; height: automatic;\"\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665330615_-1163696131", + "id": "20171207-164850_808131457", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n---", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": false, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003chr/\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512669697337_694403725", + "id": "20171207-180137_971347907", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%livy.sparkr\npairs(iris)\nplot(iris)", + "user": "anonymous", + "config": { + "colWidth": 4.0, + "enabled": true, + "results": { + "0": { + "graph": { + "mode": "table", + "height": 406.0, + "optionOpen": false + } + } + }, + "editorSetting": { + "language": "r", + "editOnDblClick": false + }, + "editorMode": "ace/mode/r" + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "IMG", + "data": "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\u003d\u003d" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665367190_-1404872394", + "id": "20171207-164927_316245836", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%livy.sparkr \nplot(iris, col \u003d heat.colors(3))", + "user": "anonymous", + "config": { + "colWidth": 4.0, + "enabled": true, + "results": { + "0": { + "graph": { + "mode": "table", + "height": 469.0, + "optionOpen": false + } + } + }, + "editorSetting": { + "language": "r", + "editOnDblClick": false + }, + "editorMode": "ace/mode/r" + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "IMG", + "data": "iVBORw0KGgoAAAANSUhEUgAAAeAAAAHgCAIAAADytinCAAAgAElEQVR4nOyddUCVVxvAfzeAS7ekRVgzMLAndrezawbWrBlz+s1Z28xt1maic3ajQ7FQsMBGRUWRVDHpuMS99/3+QJ1O1KuCF7f39xe87znPec7L5bnnPecJiSAIiIiIiIgUPaS6VkBEREREJH9EAy0iIiJSRBENtIiIiEgRRTTQIiIiIkUU0UCLiIiIFFFEAy0iIiJSRBENtIiIiEgRRTTQIiIiIkUU0UCLiIiIFFFEAy0iIiJSRBENtIiIiEgRRTTQIiIiIkUU0UCLiIiIFFFEAy0iIiJSRBENtIiIiEgRRTTQIiIiIkUU0UCLiIiIFFFEAy0iIiJSRBENtIiIiEgRRTTQIiIiIkUU0UCLiIiIFFFEAy0iIiJSRBENtIiIiEgRRTTQIiIiIkUU0UCLiIiIFFFk06dP17UO/xXCw8OHDRt29OjRc+fONWrUCLhz5463t/eePXuysrIqVar0HjLPnTs3evTogICA+Pj4GjVqFIhMQRD69u1rZGTk5uZWIAJFXuTV5xkSEtKlS5egoKCwsDAvL693FajRaK5evTpixIju3bvnXflAgeT3Wf1wmSLvgVzXCvyHCA4OnjNnjpubW5MmTTQajVQqXbZs2ciRIz///PN69er16tXrPWRevHhx6dKltra2bdq0GTp0KPDhMhcuXKhWq5//+uECRV7k1ecZGhrq5uYmkUhq1ar1HgITEhKOHj2alpb2/MoHCiS/z+qHyxR5D0QD/fEYMGBAWlrarFmzOnToIJVKgbt375YpU0YikRgaGgqCIJFI3lXm0KFDIyMj27Rp06RJk7wrHyjT39/fyMjoxSXShysp8iKvPs+GDRt26dLFzMysbt26TZs2lclk7yTQ1tZ27NixR44ceX7lAwWS32f1w2WKvAfiHvTHIzw8fPLkyb169Ro9enTeFWdn54iICEEQsrKy3s/wbdu2zcHB4eDBg76+voIgfLjMv/766/r169u2bVu+fHliYmKBKCnyIq8+z7CwMLlcbmBgYGpqmvdH/EA+XOCrn9UCV1JEGyTis/5oDB8+PD4+3tTUFBg0aFBERESLFi0mT55sZGTUuHHj99s98Pf39/HxcXBwsLOza9CgQXh4+IfLBJYvX16qVCkjI6OCEijynNjY2OfP09nZOTw8vGrVqvPmzZPJZE2bNh08ePD7iW3btq2fn9/x48cLROCrn9UCUVLkXRENtIiIiEgRRdziEBERESmiiAZaREREpIgiGmgRERGRIopooEVERESKKKKBFhERESmiiAZaNyiVysuXL2vTMjo6+uHDh9q0PHv27IsRgG8gJCREm2ZZWVmhoaHatBR5P0JDQ7OysrRpqeWfTK1Wnz17VpuWDx8+jI6O1qbl5cuXlUqlNi1FChzRQOuGyMjI1atXa9Ny3759J06c0KblggUL8kJL3srkyZO1aRYVFbVq1SptWoq8H6tWrYqKitKmpZZ/ssTExAULFmjT8sSJE/v27dOm5erVqyMjI7VpKVLgiAZaREREpIgiGmgRERGRIopooEVERESKKDoO9d66devOnTt1qEAhMXbs2Lp16+Z766effgoNDU1MTDx79qy5uflbRaWnp8tkMkNDw7e2TExMtLCwyMs99maePHliY2Pz1mYqlUoikdSvX/+tLWUy2c8//+zo6PjqLUEQvL29U1NT3yrk08LMzGzVqlX5Zo+Kj48fP368Nge2J0+eFARBLn97Ukkt/2QajSY5OdnKyuqtLZVKpVqtNjExeWvLlJSUmjVrWllZeXh4TJkyJd82p0+fXrhw4VtFfXJ06dLleZZtnaDjdKMBAQHDhg0rVaqUbtUoWA4dOnTu3LnXGeiDBw+uXbsWiIqK0tPTe6s0tVotkUi0Mbu5ubnaCHxdSzu7g8WKHVUqHWNiBqhUZnkXzc3NzczM3ipwwYIFMTEx+RpolUp18+bNdevWaaNYkUIiybK0XKyvf0Op/DwlZeA/7vbv31+lUuX7wGNiYiwtLSdMmPDWIZKSktLT07VR5gP/uP/A2Di6RIk/JRLV3bu9UlPLv7mxIAglSpQABgwY8DoDfe7cucaNGzdv3lwbDYs45uY+hoYnc3IqHDxYy9vbe/78+XnXZTLZqlWrKleu/DGV0X0+6OLFi7u4uOhai4LEzs4uLi7udXelUmnefIvYrM9CJPwFIU5OvuDzTp0tLS3fcNfQ0LCITVZLpkFV+MnIaJq19WXo9OK9N7/TWFpaFu0p/w9WgoGtbT8IAIU2fd68SrCzsyvaU9aSHZAOu42Mtnp6HnVycjp//rwOtRH3oEXyCIWOYAvtQCvHr/8Al6AXWEF3uKhrZQqQFLCFCuAKFeG1i4n/JKHQDaygj6lphK6VKQIraBFtyUlHzwjJ8+9UNWSD0d8N0tMxMkKLzZD8aASDUVqiCEbi+cG6fuLk5KDRoGgF30JP+AkmkfEIhQUIADIDXav4IZhDBqwHBVyB0k8vp6VhaopaRUoCVnY61VCHtITZ3O+Cw7mEhOoQoFttRAP9KaDJZVtXslPISaf1UpxqgT9MBUMoA6vJVdG1KykppKezdCnvUTUutxTfSvEYRpSElhv4L5edW7mSVauQyWjbhu8awgE04wn5EuMkTJVcsCPZnko9da3lB9IS8naT+4AeCQl06oSBAemR3HuAsQGmRpyMRF+rrY9/FTEKeu9Hvp90QdZ/kPb91Gq1lue974S4xfEpEO5LiXr0P0Zvf4JmAjAPjsMJMIdT+PpSuzaLFrFxIzNnvs8Qe/Zg14q+MXx1jhk/Fqj2nxS5uaxfz5kzhIRw5iwPG8APnA8h2QKHME554paB91lu+RnKtIqqL5LkwG6IgzsIlwk/wdy5TJzI4cOkJtH6C24kUaMKy6bqWk9dMK8dDSuy+BLjx7se0upke+bMmQcOHKhZs2bTpk19fN7t8OatiCvoT4FcJQbmAHpGqLIB0IA+8PR1NTGRdet48oQrV3jw4H2GUCrJ8/kzMiI7u2DU/hRRqTAweLpNZGqKUsnjx6xfSUU5S/vSOhepGkDPSF+qlfdFkUQFCpCQnc2ZUI79ge8JgHbt0AhkawDMLUj7t3lGakVWNpF32LSJG/skeY/ibdy6dSsiIiI4OFhfX7979+6DBr3DuvutiAb6U6B8Zza24uEVHl6mzngAvoQW4Aax8C38gUpFVhYZGe85ROfOtGrF1atcvsy4cQWm+SeHoSF16tCpE0ZGGBhQqhSLF1N3BuXGU1Yf21vEliKiOxalUnJv6lrX98YIakJnEpMxc2CaD32jqFKFnBxMjTnuS7vK3Izl1HVd66kLijXnwFY0S7ijzHIrxt2394iIiHBzc0tJSZHL5VpmK9Me0UB/Cuib0P8oj29g6ohRXrTCAGgJCVABpFhZMWgQrVujUPD11+8zhLExAQHcuIGjI1oERPybmTWLmBhycihTBsDYmNRsKt/nzFrm7GTxMgBLV379pH1+f4Ro7oWw+TweYGdHtWoMGMCsWSTf5/YV6rX9L25AA8VqMrouRjHY1k6avwZi39pj0aJFoaGhCQkJYWFhkyZNKlh1RAP9iSDVw+4FD3lVFqG+pD+gUi+sy9KpE97eHD2KRoN2yczyQU+Pj+uEX0TJSiJ+O+ocHAdi4kCfPnz5Jbt2IQgsXoylq671+wA0Kq6sJymKcp1wqEb1Umw8S9OmqNXMnEmVKgCmphQvo2tFdceQIYxojcE9Mo9d6zw8fuLEbt26Pb85ZcoUDw+Pf/SoXbt27dq1gXLlyq1cudLTsyCdoEQD/WniN5RSDXFtzl5vum7FxIG1a3Wt07+FbV2pPQa5Idu60v8oBgZs3qxrnQqIgMmYOFCmLUcm0WoJNuX49Vdd61TEeHKOdnbU+4Vr24rfv2JnZ7dixYrnN/ONqn38+LGtrW3ez+XKlStYdUQvjk+TlDg8BlC8HuWrc68z9IBrutbpX0F2KvrGlGmHS1XsUkhqBut1rdOHo4HZ0Ir47dQZiVMtqg4k9riutSqSRAVQ2x7HqXipbVLOyWQyyxeQyWSv9vD09Pz555/zdp8bNGhQsOqIBvrTxNCa2OOkRxO5jmJzYCoM07VO/woMzMhK5vE1kobySMBiA+wFXQb7FgTrIRU2YWXPrWFkPibcF4dqutaqSOKYw7UTKJdyJV5WXCt3pvr163t5eQ0ePHjjxo1paWkFq45ooD9N2i4nfDf7h1KvPpZe8BnTHtO8MV27EhMDQBh0gGawOH8Jj8LY0oH1zTjzmgb/KQSB77+nRQu61qHSQwJqsGkv5QchLw6d4JKu9XtHAgJo2ZLmzfH1BeA8REAvWjhxN5i93lTqhWMNrUSdWcT6ZmzpwOP/xitaadh3i2/KsHoXriotO9WoUWPZsmVSqbRv374Fq464B/1pYmRDi18hFxqDL/tPk6lk117i4hg3jl27YCysB3voB5eg6j8lHBhL5/UY2+PbjweXsH+lwX+KffvIzubgRsJa8+UjguPQq8Ifi/FwR7EcPqm6X2o1U6YQGIhcTtOmNGqEeQ5Ew2L0p9HYFXxfap+ZiYEB+b28c/8i8Rfoc4j0+/j2/zjq65hf/LDSY5Iv+8eZzYuEUm/tkZe3UqFQ9OzZs2fPAg4xFVfQnzR6sA2ucTeSYCvataN/f54m0lODA0jAA+7l01VQY+KARIK9B6n5NfhPER+PhwfThnM7jgdpHDoLHhQrTuYBWARlda3fu5CRgb09hobo6eHuzpMnYAKDwP9Z7qdnCAIDB9KiBbVqceBAPqLS4rGvgkSCqSMabZeTnzb300k3YPdQLqZJHmrl1DxixIjCU0dcQX/qODxNHXlvNhO9OX6ca3mvol4wEOzgNPjl06+kF/6jsHLn5l565dfgX0UKxEDZ1+bVbNuW1q2xsiBaziAH1L155EaKA5aLP5FFTCZEgAuYYmaGiQkzZmBgwKNHuLhAL/gGusNOeMFN/uRJLC05cYKMDNq1o2XLf0ot2YDT85HqkXCLUo3g2Mec0mtQwi0oDW9PU/4+2NXAbw82pblwOcfJmjuFMoj2fBIfPpG3oa/PmDE4OjJuHMWKAeAEUXANjJ4Fhb9Mw+lU7otFKXrvR9/046r7kQmCLrAJmsP9/Js4OjJ9OjbFmPgX3//OGStSZ9F99wu5A4syt6ElbIZ2T7fLN2ygfn2qVmXXLiQSqA7rwBaWQNO/+2VkPI3vNzBAld8C2cCM3vuxKEWVfnh9/1Hm8mbioAVsgo4QXCgjaKoyehbFnJjwU0qU7v3BxRX0v4JOnWjTBg8PLl+msSujPqfNAySLefCItjewDoQW+fRyqvmx9dQNC2ETFCN5JdGjMRzB+XuYm9OmDdFHSL+PexvuJfHoEffusWQZ0dF0+Qa3VrpWW3tWwU9QH8KJHU/SF7i2oEmTl9s4g/M/+zVqxNJ5fH2Mu8n0ffaevngxly8zciRVqwLoGVO2faHPQFv+IHEAsQL2DXFYCHUKfoQvv6RjQ/SzyD53u+foxAUL5s6d+/xmt27dSpcu/YbeBc4nsUAQeRvm5hw7xpAhNLVny2ZUuQy+zcqppKTQZh3/yaQ3L2AMKSTcYsc8lBI6duXyAU6coIMn4b5kp7K6EYP7IAhYWlKxIuvWMezT8lk0hmSA4GVciiZXyZaOpL/mXeElsuiYiVdtulfFJRqgY0cWLyYjg88/JzS0UJV+Hx6m4jsHTS6Bc7nxpFCGOLaD+7FYW5Hy2P3cboVCUf0Znp6e1tbWhTLo6xFX0J8mqixC1/4d6g08uUKCL3v92LAdz/aE29PwMqMWYurM8Vza6lphXTIdvuJkGJYy7lSlU2kqXqX3BipY0uoMYVuIyGRgA/oPpWsVAgbhKIOB8PYqvUWG0US2InYsNx8x7AYSJwzMiNhP1belVYs9QSkv5PqYeBK2BeD0cR4uQBLN7FYsXMgffxS+8u/CdTkVFaTMo4IxV5x4SzHF92LNImbUo5mapG42XbcaGbk0bdr07b0KDXEF/WniNxS54mmod/p9Hl/nyGTKtMVSnz+/ARifiJ4tmnXUjaPUJ13+48Nx4/pgktwwbEfqZU7swcieJ09Qw+FvSI6hmCGn/Hl0BIk3DyqCCQzRtc7vwq2TXHGj/FZybDm1HkHg/gXMXtnQeBWZHqF/4NKUnEwSbwPMzCU8GNrS+CB13lRnUjdkZ3NJQrmtRJQjS1koQ9QXcA/GeCgPDzBQKJQh3gXRQBdtQkLo3JkePZ75Zjzj71Dvztw7S+xxrNQEtmN4MaJvUkdCqVx8y9LoOwKcGNmDeiU57vuaMf6tXIXu0AWCMPiRXhpcruN2ng7R+OxhRnlGlSN0DTGH6O+A+T06NKffDS4bktQebbJMFh2iDlPXEofv6FMH0x+It6FkLK55pw6+0BbNIGZPplUrvv+enJy/O+YqsXRhS0fOLMTEFqCTC99v4sv6BCgYehK6sGc+bdsyeDDx8TqZ3EvoKfgiGvuatPbDKD/H7Q/nS0d8VHzTD5/H6lbvsLJRq9WqfA9aPwzRQBdhlErGj2fBAqZO/eeu6NNQ7/tEHqJYJeT3iAihpS/FH9JVRvAlKio4kILfctZeZeES5i5l8H8j0OApAgyD6TAP+iCvyLn6uFtQI4em0+lkx4y69FiLnhHNS7AvnQsGTClGQ2OSz7GqPxS9xeMbcMwi7BxZv6M+jqIiTuGUk8M5uA0rYRXri5G6m02bMDNj8QuBowpzkiLpH0C9iWQ8ATiTxY/j+P0q7qnEdyFyJMt/YtVS+vZl1Chdze9vqh5GP5fcALKK0axwwjt33MAeRq7gM335Vq2yqxdqRRXRQBdh4uKoXBkXFz77DCsrUl847Hsa6j0S59r4DeO2D4IVMzpim4HaADxgItwkbgjOTnj0on47zI14pGuvzo9HEodltPyaRv2584gr0cSFcfMaKgtK98CmFlap2JTDuS5ZF/ELp6IHBuP5sh4ZUbheghVvH6EokBeHzWF8U/i2FqlJ2BcH22fh6WHQAhy4pKSHKZaW9O7NxRfKk2enUr4rQbNIisGiFECSHWU0GI2nfHHuWxGWSovyOOTg5UVioo4m+QKWD/CVM6YlM5Mxe9/aFG/mQRZlHLD/nvZVJdqNcOvWrY0bNwYHBwcGBh46dKhg1RENdBHGxYWwMHx92bSJ9HReTHVoZEPTBTRbQvRReu/D0Bv9ewwbym1j7JUwEeZDB9z3cC2J3yYzdyQZ2RQrrrvJfFxUZky9zO5efJ/FLglNylJKHxcjLME8mqRjXNYjdBMZjyg1mVFuhFwhegb9ziN15XwfsNX1BLTg/gXuX6TPIfzrUuIuC3cjWHP+CsoDsAwaQB3YBkdopWTGY0IOM2UKzZ/VGchJx7YSD0OpMRzz4li5QxrO9QnUI/YbziVQMo06yWwP40gUv/5KqVK6nGwe/qZ0yWDxEGblcK5wtjhc6nP1PltK8sd5ldxYmx4REREajSYlJSUpKanAK6qIBroIo6fHtm1cu8a9e+zY8dItf39q1aJHe67dRyLngQ2+0PtHxqbzwBC2gBfcQr8D+xty+QJ3Yjh6QUfT0AXp6TjUx/ABivvMN+KnEyQFc6cNbEAymXup9Ayi52iSPGAxLbP5Ts4JAeNsvshkVtFx+30jafHYVUEi4ZEdJvbgT3wP9sSzYxgT5KjcwA584AQt3FljjO1Qfgnmy8YAy3tRz55WlYmCuBPI1bSJgM40OoqFCVFHaLkLGw3F7uGznxPBKBQsW6brCcNGK/aYkbKKC/qMLJxIwi/ncERgyVn2qKMbd3t7e1i0aNHnn3+ekJAQEBAgVlT5j+HgwP/+99KVtDSiopg9m+1LSY9n20T8eqE8jZuUTou5uY9tBxnoB6OIWsKDh9QOZOUXUF9HE9ARFhYYSpj5gN3Z2BrQbjQHfya7PO4ykt0IMue6H9c9GbKO4cfQJFNqJiutMdoJj2FA/sHxRY2SXgQvwPgxbrdZrUZWgu++Y/5SqlTl7j727KFLFygHM2AmFjOxcINElLO4PogN+zibikRKm2KM+RbbE2SO5Zo7LomU2UTaKGwrQEPIE1BkPjz2ZZgaynlPjl7Hyb5QhljdjvFlcfLEANd1f6al2W3fvv35zYYNGz7Pzf8csaKKyDMuXGDCBGrVQi+E0e1xdSI8jt7mPEqlki3m5ni3Yow/bCYlhC0dSXPkdAQjvV6XguJfigB92eDM6r/wyMC0DSvXU2kY2XLIIMsYEyUzW5FyEz0V12tzW4qnhm6w6BiuNSFL1/prh4E+/QxJvMyX6bQbzlEFVlZs2sTVq/j5vVya8i7shFYk7eRmGpv1uJ5JWjpmBpimo9xA5FkGbKZub47upEManuY8uET33RgUzir1vbHUY7lA4C2mZxGgVyhDxKbg/gBVNJocyR2JWq1OSkp6fjMzM/N1/dRqtSAIYkWVfzuhoaxZw81nFaOViZweRchEcjMBlizh668pU4YUSJZgqqCClBBXqk5j00OCRzN8NKOawRx8pUxWMLsmbvr4zuPGRFIiCV3736ijcQMMUS/hVzl1rLlyn+wUUpZhlc3Yn3lyAIMgNh8jriwe1ixXY+qMoGCuglU9EZoRW4Ubuz6F5G3HSKyGX3eOT8ZpL/36oa9PbgT396OfgctF2ALPnIWjE1n0FxfuUtOFRZ7UcaBxSXqXxNiIEltZ1YwfE5iTRZcHRHjSfAEVunD0O8K2kHaPy+uIPIige6dgPK+xWcFJOStN6VI4J96VVSyF1TlMQPKFYGFhMeQFSpYs+WqPH3/8cd++ffXq1WvSpMmNGzcKVh3dG+jz58/Xr1/fwcHB3t6+Q4cOt2/fLhCZNWq8lI88OTlZIpGkp6d/uPAXmTlzJhAWFvZqKcn3Yd8+Jk9GpWLwYM6fR6Mi3J3cSLLOcb4sQEICc+agUlFBRS1zPGtgkAtZpCYx1poh37L7CzrOAlDJye6AMILbaQip3DnF8grkKrnkQ8jCAlC1SGMMKXTrhjKTBBmyJEal41qPoYMYfZCweMZJmCSlWXl+rYOphIv3SC9LhBIDY7bKuGvHg1B2F3Dm9YInBVqvJSWFv3z5Lg7A5SGV1LiUoFE8xEAMdACBW4+Ym0hyaW7nkHILlKwWGDGE6ZPw6Q9gbEeyCwwguwJGDgDnlpHxkCfh/PYZymRu/sXR/71elY9FbAqHkrG04loCJwvHi6OkQAA00WMLNNaqR0RExNatW0+ePBkUFBQQEFCw6uh4i0OlUnl7e2/atKlVq1aZmZkrV67s3r37hQufzHHWzJkzv/++QLJ8XQBfNgewejNOJalWja1bsUki3Rav/QDn7Ml4iJ4eCgW7dlFZwbFogldRXYb5OfSvk+1IuWHo9wJvEGhTmhl/wh/YyOl5jUs+GF8lJYKSDbn8J7XHFoTORZaS3HXGfgVrzMgUuBTPga74RTDQiURTTJvTZgNXHPl9Jz/Jqa+hmIrEq2wzpq4M00rU+wZgUxtUSuRFOOD7eC5dnai8kLp6TDIHKKHEyYZSN4lUcN2eZt/CbYjix1i+c4RwnPTxzMDJl64uDHJDOhjGQVNGZdPfgt+/JVdOy0AWlSZXybBNRPhjU5aybbF0ZV0jXU8YNiUzVQ/7cAbLGJlDYXxlyAQiwSKXO+CuVQ+pVJqTk7N//34LC4t/W8krlUqVk5NTsWJFPT09c3PzMWPGDBkyRKPRAFu3bi1fvryrq2vLli1jYmKADRs29O/fv02bNq6urvXq1bv2LLhu1qxZpUuXLlu2bIsWLd71FePVUcLCwho3bjxhwoRq1arVqlXr6NGjgCAIs2fPdnNz8/DwmDVrVt6ZQN++fdVqdf369YGcnJxRo0Z5eHjUqlUrLCzsHR/DdZgMbXFScGE8wPnzODtj4YbZQ7JTyXiIUTqG1ri7M3YsBw6QpU+FtszcRrIa41FYzOFODHpG4AT7wR/7q8xOZtpVrBWoU8lNIjmDcl1IuEH6vz09f24mB88QVxKrkiTZcd+M0idJTyE+m8zHnNyJhzG25TCzpuUSHlbgvAM1IvmiHZZuJNxEnUNOGplPkBXtnXsrPRLuUnodqd6UfQIg19CwHl9sIC0bBzPIhgiwIcmALhpSt3JBQSVbDh3CuxrSEiCBX+EIFifYcxJ/fybVpd4wumxCpeTBZYxtSI7B0IrUu0iLwHmVm5RwgRprCDWlqqRQhoiVcV6C45+EKtBuoTh37txy5crt3Lnzr7/+Wlbgvi6CTvH29p48ebK9vX379u1/+eWXq1ev5l2/evVqmTJlHj16pFarfXx8qlWrJgjC+vXrFQrF5cuXBUFYsWJF5cqVNRrN48eP69Spk5SUpNFoxo0bN27cOEEQzp07V7169RcHytvpT0tLe/FivqNcvXpVJpMdO3ZMEIRffvmlZcuWgiDs27evSpUqKSkpOTk57dq1q1WrVp4EmUyW1wUICgoSBGHlypU1atRYuHDh66bcsGHDV64tE4TNgiAIyclCbzvBy0sYMkTIyhIEQQieKIRaCJcshdgegtBKyB4iDOkqeHkJFSyFBUbCHD3B21DoaCZMLCbMsRbm2gjLKgvJUS/Jvj5RWGshLDES1lYW1jYQNrcXVtfW+u+jLVOmTDl16lS+t3Jycpo1a1bgI+aHRhDmCkJLIX6QcHCMcOiQ0NBCGGsh/FpPqCkRRsiEdnJhu1QIlwgxZsLuusKK6oIgCClRwmJDYZFM+NNWyE4SwrYKaxsI6xoLMYFvHqxZs2Y5OTn53jp16tSUKVMKfHp/8/CKsL2bsLq2MOMzoYGV0MpZOGIg3NAXLuoJ422FsebC9JKCUFYQvARhtyAIwoihQm9LYaie8LWpcM5eEBoIwrT8Jf/x7MN5dIqwpKyw1kvYM1BY20DY0FJ4FJbfR/cpCxcu3LVrV8HOMh8O1BL6SIRBCF0RVlgWyhDfewrHEO4hXCd3vFG5cuUKZRSt0f0e9IABA2JjY7/99lulUtm1a9euXbsKgipRYsUAACAASURBVHDw4MHU1NR27drVrVt3xYoViYmJeXHuTZs2rVy5MuDt7X337t3Hjx/b2Njs2bMnODh43rx5Bw4cyM7WqhBvHq8bpVSpUg0bNgQ8PT2VSiXg7+/fr18/MzMzPT29QYPySRLm6uqaV3G9fPnyOS+mO9CKarAHHmMexIYOBAayYgUGBgC151ElCQ9/SgiwDv02rDAnMBBTJSUG03orQUr6j6FWNzKTGHSCMu1Y/3JdjPLz+DKJzoGYlKfbDqoOolild1TvU2EjPIFNWLkR/xee7myphLOM9EfUMSXdhoqG7KzL/r5EDKPjKaxciTmG9A+sS9EnhL6D0N/LZ934Moh+AZT00vV0XoMg4Dccr2k0HoHiFnv8mWSAqQqrM8iN6WrIrxFMqwUbIBA6AlQUKF2W6Tex9eKIJwTB9PyFW7lx6y8yH5Nwm84b+DKQ9j58GURvf2w/+3hzfB13Yqgo4ZuNNDAmrYDPk55SCaINyT3KxZKU17151PFry71791avXj137tw6derUqVNnwoQJpUuXDgkJUavVTZo02bBhA5CbmxsTEyOX/1NVqVSam5t78eLFrl27Dhs2rEmTJgqFIiIiQvvRXzeKiYnJP1qqVCqJRPJ83FdFvdrlXagJV8AZFLAzvwaXoCPYghcMhZYs0rBFYNUyKphy4Fcs1Rwy5MQIrK1xfZCPAEdPJBJ++wwja7rtyKfBv4ELcAd6YmBOi5IETCZBQ3ISTxIp6UXQVfZlU+wcy57gFwVnaDOVE36c2U6dBVjVAD1YC/10PYtXyYIxEAd2sJhsNcbFsK2A7U2c3fGbims8tsUp5kH8RMp8x0VnBGtuHEUQ8PSm3Gyc9NmYzYpalHRktNubhmrxCyfncNGHqgO0Lfv9MUnLQCawszcGEnIKZ4vDQ84PUoKakqWvmZBfKaKPi46/IoyNjZctW7Z//36NRpOdnX38+PGMjIzixYs3adLk4MGDN2/eVKvVM2fOnDBhQl77I0eO5O0n+Pj42NraOjo6BgUFVatWbeLEiZUqVTp06NCroZY3b958nffi60Z5lWbNmv3555+pqam5ubmrV69+fl2j0RREcOcMMIZs2Az5pjRqAD5wHNpBfTiAgTnjTjK7LmHptJ5Jekv0MvixCTXvcfhZCKygfuqcB9z2x9SJiY/o4cvRqR+scNFECTHwHaRjL6fzBm6EUaUPzuXwPY57LhM09LNh7CPcRoE+htNp/jPdZ1D6KJyC2dBM11PIl9+hJvhDJ5iDwpKsZK7v4FYKWXfpNQHDCqjvEjwRp6mEVuSz+8gfUn4YveMIWUb2HYKVVNbj7Gpa2nPsjV7e+qY0/pEevpTt8LFm9y7INeQIuHcgV4p+4bj97c6mtJp58+lgLV2ozs3NjXqB3NzcQhn09eh4BW1hYeHj4zN//nxvb+/s7OwyZcps3brV2dnZ2dl53rx5bdu2zczMrFSp0po1a/La16tXb/z48bdv37azs9u6datEIunevfv27dsrV65cvHjx1q1bz549u2/fvvr6f3/1ValS5cCBA3lucC4uLs/Xv+vXr2/WrFm+o7xKp06drly5UrFiRTs7uwYNGjzPK9i0adMqVaps2bLlwx5DFOStVpr/7bj6EhVgDviCChYAVNrGk+Hc2E/VhlzN5JHAHBlO0+kLt1wAbvtzdCp6hli5096HtHjsPQCsypCV/GHaFlnM4Us4QGgKZrc4UguNms6rCV3H9hHoZ1HagvvJdJVSOxiuQgkQYABYgT8MhOZvHUMX3Hsa1Ee1py9Y3XZwcTUaFd32IT2MxyBOhKDZQrqUW/rca4uTBHkcUiOs7Mi8jUTgmytIezBMYE5DHc7kQzGRkSshbi8GEvQLZwWda8hXppjMpKO+OkCekJDwYsmrkSNHVqr0UXcIdX8y6+HhcexYPtWCBwwYMGDAgH9cdHZ2/uPlKg+Ojo6nT59+/utXX32V98P58+fzfsjKerpkEPLztH91lIoVK4Y+K/ZTv379wMBAICQkRKFQxMbGSiSSUaNGVatWLa/B8+RVL3aZPn16XFzcG6b8Cl+DFyTCeWj9mjaVQQ8qk9mPO/qUTEezmMp2bJmCXI7XHY7JuP4rVw4zKgDg1DwGBKFnzMFx3DmFe2u2dyM7lXtn+UyrDAMA3IcEqFCgb1oP4XFBywQgux0pY5GWwSgGh2UMaMkoB76vh6kjbhKiKpDSl2tT6ZZF9H0MSuKYCHn/5B2gUBeML5Tcfh96kTuOxAZYnkH/GwBDKyrVRsjGpP7TIP7PhwOsd6BCIkJN3EO4fIqTLclIwcKLLt+w1J4yk7mylIHXeLgD27ZI3+ajEh2NSoW7dr5mTynkkttqO4zScLbkQTLKwvGxGViG5ReRleNhbM44a/tT+itW6DK1oe4N9CdB5cqVlyxZUqFCBQsLi+LFi+fFpxQcHhAK8+FH6J5fgxToDFWJ3k7AHawM2JbFhfGUaowg4OqKlQ2NO3DBnCFjKBYEoFEhMwBQmJObgYkDPfcSsZ/aY58upd/OKtgPJeE27IQCKcvyB+wGFwiHXQVZVureWc5MoNFdboZSUkHgz7jqUcsD6y94HEPvATi1JjyZsZO4O53DD0lUM06Gk/DMRhceETAI6kIILIR3D2hKNGePkuIXuZtMSyvsIaYW0gQEPVIESoT/3VKogHV/kkK4NQOTUGzLU9cXiRQbGQMHcl5Bd0dMr3N5Lg+G0P0qBk6vHXTSJB48wMAAQWDVKm3UNDJ6Ai2gDpyDn6D2O8/07WMY0kBCYhoeEoIKJ9Rbno19NrdvYpstNTAGHW9Df0oGuk+fPn369NHJ0MbGxps2bSrMEUrD7/ldV4IvnCKrGeF2XF5EG1ccIlhuS7dbPKgFLly4QI1OVJpAy2CkSWSOAqjSjw0tsHIjOYb63wIoLKnU+00qxMdz8CCurjRoAMB6OAYymAXHoOU/2wsChw7x4AFtta94uBYCQA5zuf0nx/UoXZo7d7CxoVUrJO9oK1VZhPsilVOuA2eX0Lo6cfWIymDFFqyvcKoHHX/FzIlSlliXYf9Iitfj4BZaSvlfAwSBvYdx0CAtnKyVf7MKZkM9uEHQRKK/oHFjzp5FpaJjRxRaLAMvrqLGaFTZFLfi7FJazcYgEofloOL4WMZWx6Uyc1cjk+E5gpCF2FXh4SF67fs7k4bFjxh1wMsdo8uwmuYDudCL8AFUGQgd8vmazMjg0iXy3g579iQujhIl3qpmiRIBHG7N5ls07USvRYVioMsksE5KEJSFrwonZcqGk/Qzx6o6WRGGK+LBVct+ebk4XvVl+EB070ci8no00A7iUMWSOYtcJVKB5ASArCSSVSQm8ttvPLjH1VuM0kPREtOmT7N8VfOm8wZqjqK3/9Ol9JuJj6djR5RKfHxYmBcLLnuWNigZ8kuMO2UKf/1FSgpt2hho690of7rJfjWC/ktIT6dLFwIDOXyY8eO1k/AMQWBze5JjeHyNHT3QNyFXwuNkpm3nWDpHNRzW4DOd2CAyn3BwHH38qd6R2vr8oQcmZJclUFL41pm/S27/vIy1UWRm4uHB2bPExNC+vVYJLrKSODEbjYoTc1AmIDXCKhUhjOiztH5IUiY7/6JKKYDyXei+m+pD6H/spTxH5k2wiMFkCketMPkcIOso+hkQA+3hFR309FAqn+qWloahVi86Jvsf0GUmOTmMm86317Xp8s4cz2SOGokhWzQsLJxkKVl6+GqgNleMhVitzGOhVlT5lFbQ/z0iybTjVxVyA77Wx3MLbqXYFskJKS0FztqjF0wJBY536atkkCk/XMOpAiN2Ix0DYOKAiYO2Q/n7M3o0ffowfDhNmjB2LHwPjcAAqsDn+XQJCSHv8EClKh0Sot0w06EJKNiRwezfyMllxAhu3GDNGhq9YyRxcjRmzngO5+JqHt+g5kj+GE5iFK1UWEkwM2CjBZfvUr44SUmUqc6j60gCyaqEIoSvdyPRUF37vfgPYTR0hYXsO0/ANVKN8fGhfHkGDCAiguhoXFzeIkCqj54x17cjCMTcY/ZMRlmTuoAl2dST0TON3NKMDQVQqdj6F1FRdOrEs2OSp+jZYN2VBrlsrIqeHKMcJG05oaFqMUyi/rlO1Ndn8GBq1UIqpXt3XkmwmS/Gax4wpxQjHnCrNE3uMecdnpG2bMxklITqaWgkjBJYXghDDG3NT78z+SfUpHd2x//tPW7duhUREREcHKyvr9+9e/d84yTeG3EFXYTJMePWPupUo6Q79zLBj6v9yHal0UZuSenQiDELaazEpQLR8DAOr4EoIgm/8j5jOTo+LYYUHo6FBQCNIBgOwtL8u8jl3L2LRsOlS6mmWh5/fQ7BcADHIVy8hIMDJ07g6MiD/Hy334yRDUmRbO+GmRPAdG9uN4MhnNfnohS9xsgzQI2ZAQ29mLGJh5GcPkI5BY2+ov0w5rSgzwf63miJJRyBXZh5cSsNIyPi47GxITubiAhsbLQQ4EL1wXTfRaQ9GnOad+J+JkmHsOxMuJriHYnNQFABfPstycm0bcukSYSH5yPKoReDkul5HaUeNu44ebDND415Pi379+fkSYKCXk5b+iYyrWw5Ug52s6MzNoWTrNlYQ6BAXHO2gGnhuNmlyKlZkf47aPaFJkOrc5dCragirqCLFDkwC85zz51pB7mbwDBjOvYDKdMM2V0Krxy+2ot7YypN5Qdf1H54muIfyOUnDDGl+VqkNsxSU+ENI+Qwaxbnz+PlxaRJf2/7tmrFmTN4eWFhwaJFz1rLwOjvvn/+yZYtuLgwcyZWVixciLc3mZl07hz/6FFVLaeYlsnUqfj5kZTE4sVYW3PhAv37P9tX0RoDMzxH4jeMJ+mEOBERxKSD1EonWk2AmtN7cYOqEn77nlZONKrHjVN8Pg3H89j5s/kG+qffPkSBoFLRpg0XL1K6NOPGkZ5Oixb88gs//8y4cS+VMXsd1Yfh3ZBL32KSRUBPDGdxpCoWLemczT4pnX5DT0IXKcCFCyxYADBwIMePk29uYokcjREGlSjzO2i4XpukRKzz+57Qf7fzsUsjRjgPHYrUHH19goPfqa+2eEjxg2mHMIIehXO6G6WgtivFF+Fiq1mpD2+PCl60aFFoaGhCQkJYWFiBV1QRV9BFioVgBZsYuZZ+tVixC9f7XPiOnRM4m0nATSp3ZeX/yErCQMbv4zl7mcpyFvzGnFFE5nDPh/WVSHN80wiLFmFpyaZNJCezYcNLt6ZPJyiIPXvyrz539ix+fqxbR4sWTJwI8Nln+PsTFMSYMe8wxalTSUigb19atqROHUaM4PRpDh6kSpV3EJLHZ12xr8IhBd7Q1pwLD5GlUtmGKaacK4fCDIk1k5fhL+NWOp815dp2lEO4OhCHwdpmKvtwhg0jLY2zZylWDLmcoCD++INjxwgMpGNHrSRs3o5jE4JuU82YyXE8+ZnxITyYQvyXVNdwYSm7P6exAODqip8fjx/j6/vPLY4XUViQKeXxUpJ8eJyJ+dsPALWh/KZNuLkREUGHDrw+5uuDkAm0ga3/o4eEAl6qPsPRk2vOKHdxpXGy0RtWOn9Tu3btYcOGlStX7osvvrh0qYBrjYsr6CLFJfgVLEmW0MAGinHegSXLuW7CyLLYGjBoFQPd2dmLKn1JjmP/KJr8wNnFyPTJKse0KiiN+PZg/rIDG3InjD2Z+GzG0pIePVi3jr59ubGL88uRSKn3DaUbk30H/0ZkpGBejFbByJ4t8UJD6dgRW1vateOXX95/iqHnEe5SIxbFFW7nsGE73cZhbABLoOs7S+vgw/w6VFLj7se55qwwplJxlEoOh1PZiebzGD6GJ0/o25caPbiYzq4+2HvgNe399X9Xzp9hWDIGNRmu4CsJwClvTv0JAtXb0STfyP6XuXSJAQOwtGSSC83iuDYaK2faWkMyW0xYPw4TI4aaAIwdS6dOJCfTqBE13hio3cGHY9+jzqHVEuQF41BsfTucelEcdacmzLcsEJn/5LEEcymBP5It5UbhbHGU60TafXb1xtHzulX7O3d2Nmv2d3zp7Nmza7zmwRZSRRXRQBcpmsIM6EMJgWUXKbcHm4cs/Jb7Ksb8QOlQTpzAvju9Xz5/qTGC/fuxPcb8UMLDmTKFXbv+Kfj2LHIy6PuEpHGM7MX0QyxZQr9+ZCVzZjHd/dCo2NyCQac50Y0KPSkzi4v9OdeP2r5PJXz+OaNG4eTEqVN8SNU1x3SelGRDJE9ycCpG98esL8mwTVDvfQy0RWmqt2DnI8os44wRM5NQNAdf6E2jWI5MZ84cli3j0SNu36baYKoNfn/N348GOfyQRNYP/PYDdeWo0glay7g45MYssMEzHLO3/Us3bcqcOYwcyZ9KJlei/0gad2KHEgM7bipZMRv5ebgEMHUq+/ZRpgzjxnHwIC1avFamlTtdNhfkNMGsxh12Q4UFLJtK6aS3d3gPrJ25GM3gJuwKxLZw/KABzxF4jgAUhw8XL1788OHDb24+c+bMmjVr/u9//zM1Ne3bt2+Dp16qBYO4xVGkGAQtwJ8Vf4IDgfsx+hPLVCqomLODw4dxcuKHH/LpFx9PXkmXsmVJzi+SOy0S+xoAoxZQQYq/P4MG0bo1mU8ISKdhExo240wWORmkPcS+PYBDC1Lv/i2hfHnmzePIERwc+Omn959iS1v6DUKTTYqEhCTWSIm7z5t2zd/GihXQluMpDOqEYggEQltYw86dZGcTG8vOnVSpQnz8+w/xIQxQIDVkxgwypIxRkBmHgRyFI3JzTIxJ1OKNuG1bBgzA35/W0+jfGo6wYxdRWVxVsmMdcj+QwlmAlJSngX/VqnH37pulFjgSLw2GBsycSZoBhVQY3bw3zSpy5CTVSyC8Lub2Y3Pr1q2NGzcGBwcHBgY+Dy0uKMQVdFGjI3REAcM7vXS5OlTPc0RLgyhwf+n4rk0bunUjJYUzZ+iWn/eY+1i2e/EkmsRbdG2Faden/8m5Jly7w+Kvyc1i5DIkCqoMxa8tZZtzbR/NnvtvRICcatXetLOpJR79iNiPhytu5+hUkegwQmRQDcq/p0CFguFfwVeQCtEw92nkhZUVHTuSlMSmTZw8yeTJH6r5O6OB62xxZmok1tXIusLqsqyqgEzOOhdkemRlUUI7V7/mzWn+Qp4QK/im4bNf8oKPMuEyXdowYAA1a7J5My/Uov44pJwsZdkmgiZSwjM5YUFhPO9u3RgZSM+J+F9i0D/zQOiKiIgINze3lJQUuVwuenH8x7kAE6AWnIY/4JkLrYMDe/eyfz9ff02+1RH1SiOpyIOHPNFn2w1cNnP6NGvXIpdTtjG25ZEZ4HSa3FxcJmFZkztb6XgQs7ytjMEghWywh7n5CH8nKvdFbsh6b9pJcbqBjQFBChgMIz5M7lmYBDUhGP6EUgA//0xQEI8esW+fVjF7BYdcroK2UJasy8TnEHGTB7kk6gGMTuTkYFRZ9LhQQJEyt2Eg1OWrEC4N5bqGvXuxLJxd4NeTkWZn2eg2dlnYSziptQP+O1HRjM05HLnEvCeU0cJD8aNQqF4cooEu4uSFeutDe9CDJbAS3OH4sxjiZ1ha0jvfSG4l7CH8FPbtuG/HwS00i6HGIzzasno1s2ejUOD9M2o1Ho6EtMK6Kh6LsHweNhILmZAX5t4cMvKPKnwnbh+giyXfKZHro8pmdDoUh52QAx3fmKDjDFyDxk/t70ssAR9wgaPgA7OeXvbSTd59V9dYqAf/Qx3A1kfoy0k2of5lQgdTcjD6xZDnoH6MfCc4vEsWvSCIghbwoq/OKvgJ6kM4VedTtYCD2bTEqv0NZulRXM1dPaZGFc4gf+AwlL4COMBCqFM4o7wbtWvXzquBV+AnhIh70EWbZ6HeXH32JmvyNG6YZNCmRIAG2kMMSbGMm4lSyY3zHI1EyOXRDDIj0Gi4e5eaNalXjLI7UWVybS3+lV+QYAipz0RlQUGczCTf4EEssmzqp+GRQ1oWIYNgAcRBW9C8ptufMB9yoAe8WpbhXZ9M4ZKTo/dUn5xkGuZS1prGj9GkkJtOaF0yTqC5jqoMQjLsQdvqp7/AWlBCR7j/wnXjojB3RW4aI3KoA8OyUb9DYaN3IRXmQC7MhSeFM0TRQlxB64KsLNau5cEDevWibNmXbvn6Po0iadYMIqE05L00tYN0mAw9nyWWe7kwyv0LhPti6UrlPkjlkAVr4TrpBlzSEKpPcwmyyYzK4KgUXz8emtL3LJvHYGfOggUE1CPYmjpHMTHG58XdgGLQGGqDBkZ+cHKvbIS1OIdyQWAIOEoQBPwESquprYBJEAO3IN+VyGbYDQooSfxvLDfD0pL0dAwMGDwYqynQCwxABh97+/VVYmJKQBI0wOw+NhKqXuUR3JeRYo+FPo4Cjg7EmZPoQfExRFYhVoKT52vT5M+ezb59/HkXl2iQgOmzBNZ5jIYvYBHkQAH7ZmiPNDsHcwmCGkvIKBwfOORsy2LxJJoYMb1yoSciLAKIK2hdMHQoCgXNm+Pt/VKUs48PR4/SqRNr1+LvDzYQAdmQDo8gr2j3cdgFh+GFCN3H1zkymTJtUSZwNG85NgwUaFqy7QBOJTCXYaKk3U8YSCmvosd8KiYQr8KjLeePkBCLQQmMUjA2JmYVev/42h4HQXCyIMpBjeLEASIFrCBNQpxAggQrkCaDGnLgJrwu84PDU2eyhIP8cYTmzVmyhNBQKlSga1coDidgNxwsrGTE74IgAL/DASQysgVKtEEDZrm4d6J4NknFUHngmIhFCW6t5cpjynci8hCX1+Uja9o0Vq1i2DBikpjaDQS4AM4vtLB4Gk1O0MtbHx8VQSohUcDZjeTXvwV9IEducz2VofNJNONgIe2iFC3EFbQuiIsjr0pAp06cPUv7Z05JR47w66/Y29O/PxMm8FtJxpQk0RmZFK/Z2D7/Nn0l8UXscazcCJyBaQkubmbfGsZqKL6GpAiKueEyB8cn/GhM303UktFJjf5E3KREZXN5Cf1q0KoNJsUYaIWPAn0ZTTf8U37BJIMGIoiRkmlNmXiOajCDDIHqMozVkAXNYBRYv6bvTzAKHnFfwd36fPcdhoY8fEjbtqxZQ1ISlpb/fDLJMQROQ5lEzZG4fvRqKesmE7oG+2xipfjuwwpKCpxuTxkDDI9x7RSujph259Qj6v6OXXVMHDg8kSqv1Dzbu5fatdm4kaqtabwHGkLj/Lat80uHEu7LxdWY2NNoJqaFa7slGQKG8PA2+s92xQqcWfFMscbxWypY0yf51Qy4BUPwL0Qdxt4jO6PanTt3tAxUKSTEFbQusLbm+HHu3+fwYV6soFO9Ops3k5TEqFF06sTyJRzcS/kdeB3Fb+2bBMoNiNhHy184F0COhklnSYe/OmGuz+NHJC0ldgzOufj54F6cG0aUukg4FKtAkx/Qv8K2/7N33oE1nW0A/92bvXdkSGSQhBAhxEjMEAliNrRG1dYqRVFatVqjVFtFjZq1t4iRIgiiQRArhET2kEgie9/z/ZFQNEi418WX31/nnvOe93nec+597nve84wVnDzJoBRGlfBZIbU/ktnIa1FLn6IcssFGjAEoKfNIhSJtuAhB8BLRJrAbgrjxGQcP8euv5OURH094OA8fPk7w9CyHP6f5WLr9QfBP5FY/H9MbUDc7mrB1jDuOljJWEiasw0CEoEDPe9zXQu8vGoeiaQ1nMJ3LzYsUZnJja+V1WvX0CApiwQIOXGOCDQRB1SIhMyK5vIYef9J4MEfHSXeA/0UQiXgEjl+QJ6p2au8qomvE4GyKjzFGhyLZrLaH7yErlj5b0bFskOFfHqjyhLdsnakx0PJh5Uq2bGHkSCZMwNr63/0TJpCTw4ABCAJz56JahEEDYrIwckRNn6IXT0tKi9hZRuNG7IziiIjBo1jRELNgFMfh/SPHl3HzOip6XGuAXgylNqxxRVkTg/oEzsChD0VvrUrhCjx0EPI4KRAp4SLkF5OZhzCOjh3x9GRPFSqOFxTQpw8zZtC6Nfn5zJ3L2rWVW4TSQsxd0a5NnXY8vC31wbyEunnR5JkydByXVCiCY8PJgWXQ0pEzrpjuhqmwHFRxGoy6IXsHUFpEi/GV9OXkhJ0dnTtjaEi1CuKl3qBuFzRNqdOOggxpDe1FlCWqcAN2/cF1gUzZGOiGDWnUCC8vVFRo8AbBTS8hJQzHfqjp4zRIK/e/76LfNjUGWh4YGbFmDYcOPRN9ACgqMnMmR4/SujUrVxIWS+pNaj0ifDfFec+kYH+O3TdJySY4EAtd0nKZ2Jeym2x2gY2YfEG/vag34LoZLSXUP0jAQ6akYeeDiTOtJpIWjrmrrEf8GAMU17LXgCI1DFXJFRGigo47X/6Enx+HD/PHH6Snv6IPd3du3WLqVBo3ZtAgduzAzq7yluqGhG0g6hhRx6pc6Es6pJQYI7rNeB9ERWSC20K2KGCmzPU0FOpxfDjshcYAIhGtJjHwKG1nIKrMLbpTJ0xMOHCA+vXxqs5Tvbkr4XuJPsmlFehav7r9myEOLCUdWvSgUMxB2SxCe3igo8P+/bi44O0tExE2HpxfQtw5Tn6frusiExHVocZAv5OU16k8e57PDlMcz6MY+r3UMyElh3ZduH8cVQcKlAnZQvN6KJ8CW7ADCUlRNHUFsPPkUQFA91WIRMSewWdNNfL6S4FrZDyinw5qJdQXYSQwOQ9NAS0tlJSwtyct7RUd2NmxZAmBgRgZsWjRy1r22khRDgn/0PsvVN9q4EZRkQoGQ7jhT6YIRxEZM9AsI1EE1Y/D/jfUuytD/rNC/RK0zPFZQ2wQIjHdKi2oJlViwVlEgT9eECObGXTHjowbR0AAbdvyxRtGNr0Aqw60GE9kAJbu8SbViFgvKysrLZV+kZeal4TvJKqqfP754w/ur24/fjzt2lGowJVoFLWhIxsXsHwzNIQhsArvr+jVhrhowiPo0Q5AURWHXuSnY/CC6afUyYwC0JvDsK+Y/icDRAwUoQAAIABJREFUdNmWjmdDhmfQX5FZs1BX5+7dF06Hn8bZufKAyedQ0qh80UD2RFtaEniWQYNodpUfi+luxsME6pQyYwZ//42/f/W6ey7Uu+oYOtB+zuucWH2KJuuoLU7HVZWwQvrLLJNRhw7VLr5TXazaY9UeIPEVaZLKeS5ZknQrqtQY6A8CFxfOnmX5cgYP5tQpQkNZLqHeWmgIsfAQDRtqNSA+EYk6dT4FuPIn946gU4eMSPrvrVLdwjfhyJeUFiAIqFyhrw+dCokq4h8Rv8fSfACf3mR/Y4qKOHQI8YfwVFeoosKRIxw6hKcGA3I5l8KXAhckbL+MqioabxyN+e5R1leF/nC8iJ9F6Mu5GPbbpKbk1f8VYbAeIqp9XpMmrFtHWhpbt3LkCPY6JJ6H41AIjThwAJ8RrLzBulCWLwe4tpl+e/D6DXNXYk5LexTPoKNUTHYCPdbRcz2ZJhR+TqoOFjaY2fKNiGY7EY2jTx8++aSK9UnfDwwMGDKEnGIMlRjcAAURbRQ42p8R3fDzk7dy0kc1OhNDBb6oTV1lsgvkrc7bQ6Ylr2oM9DvFYZgOpTACQl+nAw2NinSjeeYcHwTzYAQYPbU/ryJtkFiB0kKAwkcoyXZCVywRU5wLIAjUycJMBTU96gtEexNTizWdoPer+nhvydUmzAe+JUIbDSAfzxUYlMhbLekjERQpUwUv8k0Q/o8My9KlS9u0aZOenh4YGFiTLOnDZjusBXNoCjuhKk6XybAelKEZnGSONT3Ho6ZFYwlTjhL3NzrN0HGjZyn9+tGhAzk5FTNot6lsaIeSGvr1sHCT6agKyhSp14U/XQE+EjjiQQs/CgSMVzChIatmy1S6nLHbQXErwvfhKjDSlqzd9DFhrIwi7eRJcm3XOnmBpP6JJhg7vfqEd56ioqKkpKR+T+Xv/fbbb53/8/Lj6WRJa9asaf4m5Sz+Q42Bfqcwh8tgDqHPxvK+iGLwhdlwFz6CIxic41w7cmYwbhxH22BvxaA57EzB1JT9+8nNRV29YpG3rje2npQWoaT+CiFSodXXuH4JMN8KwrDrwE/BfGNJ4I9vrzygXLj8K7s0+HwEK/6gtwEf+aPxHUinDOA7hWZEEnuU+HwE63dgm8Lnrz7lHcfe3t7Hx2fZsmVP9mi/tMhvRkZGTcmrD5tvYSz8AvbwexXa34dG0AkiSdAl6HsMGuB5BS0tYmP5bCNAnxtcvEjPngCazwZfiRTeknUuJ/cBJ2dwPJ2j+mjkMtSFIAWaHwOft6fD2yTyKBdXcDyQ8dNpPpM6jflmJEO6gQd0fvXp7xvq/zxghAmOO7EwZErcB2CgAXV1db1XZdYOCAgo31i1atWYMWOkq8D/0VLR+4AObIHTsLpq6S/qwC24T1o6QiJ9fqeRJvFp8OJocjlycAStJtLEnkWFJLmyJ5lmylVbxnn/MBBlcnEFPdbSzIG1C8kMY9simtWHIJgpb+1kQqGJPjvTKdvD9kJsP0A3lRexefPmTZs2xcTEZGdnx8TESLfzGgP9XqMGS2EKsQegP2qTMM8l2AieiiYfPx4bm1f1I3uEMgQJJk1YGMQDdYb+Sfti2ntLI0Peu4iJKBVbTzRN+CIUdTEDWlBUxLgL8tZLhqRpN8ahNsO8KIZm70Qq/bfDli1bXF1dxWKxk5OT1GfQNUsc7ztNYC91wgmYQMcfiDuHsQo8jiZ/dyhfTgnfg7ohrjb8HoCy/NPqy454wYzbezFrxsPbdOjPr/KpcvI2SdVsZGcoZt1Wbu5A31be6rw9RCLRxIkTjx49GhYWJvXO5W+gZTQwOXLx4sXatV/4iq+4uHi3DAp66uFh7rc4V6lWrE4b4a0XDL19+3a3bt1edPTBgwe7d+9WFH9kc/aQYllBjO7AfL+jb1M9WfDgwYOXHL0QnvB3/d4W/r8WKupF63pI3vodkQXFxcUvOep3PbesYQPj/QseqVknPDIg+r0fcnJy8qsbPcbb29tbBulBRIIgo9oHVSIkJOT69etyVEBGtG/f3u4FIcv+/v7VuvHvC76+vi96nbJ+/XpZpCmQL4qKisOGDav0UGZmpiz+g+WOqampj0/lb3QjIiKCgoLesj5vAScnp3IXOnkhZwNdQw011FDDi6h5SVhDDTXU8I5SY6BrqKGGGt5Ragx0DTXUUMM7So2BrqGGGmp4R6kx0DXUUEMN7yg1BrqGGmqo4R2lxkDXUEMNNbyjyDmScNGiRTt27BB/EFWOnlBSUjJnzpxevXpVenTw4MG3b99+yyrJmuLi4r1799arV0niUIlE0rJlS4nkQ8uALBaLQ0JCKv3q3rt3r2/fvsrKH1rZp/r162/evLnSQwcOHJg1a5aSksxKEcoDiUTy8ccfT506VY46yNlAR0ZG7ty5s9If9mty5Ajz5iEIDB3KyJFS67Y67N+/PzY29kVHExISQkNfq1rKO8x3332XlpZW6X0sKyvT1dU9duzYM3sLCxkxgoQENDX5809M32ZZceng6elZVlZWqYFOS0vz8fGZN29e5Wfu38/PPyMIjBnDp+9TrqgOL67WGhsb+8N33/Xw8yMxES0t/vwTE5O3qZssuHfv3uLFi+Wrwwc1dUUQmD2bv//mzBl27eKl2RJqkCdr19KqFadPM2UKc+fKW5u3SGkpCxZw4gRBQaxbV1GH7IPA6tgx3N05fZpJk/jhB3mr84HwYRno4mL09NDURFGRunVJT5e3QjW8gIcPcXQEcHQkLU3e2rxF8vMxMUFNDSUlrK3JzJS3QlJDOTv7//SeyhL5Z7OTJioqWFoyYQI6OsTEIO3yMzVIjQEDGDSIgQPx9+frr+WtzVtEWxsdHaZMQUWFzEysrOStkNRIaNvWYfJkBg7k4EHkum77IfFhGWhgzRqCgigoYMYMPqx3jx8Udnb4+RESwsqVSPENxHvBpk2cOkVpKbNnIxLJWxupkWNuXnFPV6+mbl15q/OB8KEY6MuXuXaNtm2pW5f27eWtTQ2v4vp1QkNp1er/wjrfucP58zRrhpMTgFiMh4e8dZINublkZpKdLW89Phw+iDnmtm388guamowezYeV+//D5NAhZs9GS4tJkzh3Tt7ayJhz55gwAS0tZs/m8GF5ayND9KKi+PxzNDVZtIhdu+StzgeCTGbQgiCI3uaz286d/PUXOjrUqsX+/Tg7vz3RNbwGu3bx229YWmJnx/r1uLvLWyFZsmcPCxfi7EyLFsyYwYvrzrzvmIaEMGcObm54eDBqFP36yVujDwEpz6C3bt3aunXrTp06ubm5bdu2TbqdP8UVGApfQhyArS3BwQBnzmD7f1QM7X2l4n5t4MxwbGPgZYWU3mcCYTC2Nzl/HD78L2eeiQnnl8IAzkzC1lre6nwgSNlABwQEBAcHBwYGnjt37uTJk9Lt/DGP4Cv4BkbAEIDZszl0CC8vSkoYNEg2QmuQHlOnErIV71nEt+DzjvCTvBWSPgYGmbAYfmDMN8Ssw9ubixc/bN+GkoFqZN3HK4UT9/leXd7qfCBIeYmjqKjo7NmzdnZ29+7dKygokG7nj4mCFlDuQqcEJWhr88cfspFVgwxQU2OpM0wHN8iGwfJWSPqYmKRCN7BCyYpFpvDeF8l9JXoG0fy4DFrBIxgqb3U+EKRsoJcsWbJy5cq1a9fWrl17wYIF0u38MQ7wD5yCR6AElYb/r4ZNIIZZ0Fk2atTwGoTBBCgFa/gdVGAPdJS3VtInIcEM9oALXIcY6ADasBEqr6v7AZCe7gCfgAFkQeXldGuoLlI20BYWFvPnzy/fXrNmzahRo55rkJ+f/3SqoKSkpOqn0dGA9bAaNGBjZQ2SwQ/OQj541Rjod4lpsBXMYRQ0gg3g/EH+mLOytOBn2AyRMB1GwSFYDPPlrZqsUFNLgy6gCGqQIm91PhBk5QedkZHhUFkgX0xMzNMV6UNCQu7evWtvb1/N7u3hl4rN0gzSA9FtjYr546NpUBcUQAvUoRg+tLxi7y2FYEZhJlm6GFqhMBUECAc9MJO3blKnOTSHr8EFAEfYW3Hk0RkkReh/UFMHVdVMSj8lvQBdO1RekCiqhmoiZQMdEBBQvrFq1aoxY8b8t0GDBg0WLlz45KOfn5+qqurry8u9zu42GJqRlkS3ddT6CABHuANzIB3q1Vjnd4luxHkSGIVhHhkX+LgTKp+DCqTAIHifUrtVmcEwBgbAHpgHENSR5NsoKKJtTJfL8lZPajxKrMs/vTA0JO0h3b6hlrwV+iCQsoHevHmzRCJp165ddnZ2TEyMdDuvhCsTaTOFujN4dIZTn9G73EArwCE4ARrQVuY61FANpnDuIJ/MQ9WHG37cmEczG1gAEmj/gRpoZ9gDl2ATWFPykLjrDH4IsNuCnKtoNZG3htLBJvMwbSZRtxWPijk5jT41k2gpIGUDvWXLlt9++00sFjs5OVU6g35TSnO5+CnFubiuQ90CsSKluQDFGYgVAEpL8fenoIBevVCXva9PaipHjmBhgYcHXIZrNX8Jr0CsT2l70ORRDClhPMwiToxFM7zSEG2HXqAmbxWf5vE9TdPh8GFq16ZTp2r38TCX+EzMcrjwO7GhSIphD5QiKUIsrwz3JXAQSqBnxQUvj0d3caFx49frURApcOcCFw+gXbfix/ieEhtI/FYMOqLYQt6qSNsPWiQSTZw40cLCorhYNtEH24zJvEtpDlttKM3F5Q9C17LbgqNDcV8BMGAA0dHk5dG9O6WlMtHhCSkp9O6NkhJHj7LXF5aAJozW1b0vW7nvNe1msecT/urEP7+QFMux6zxaRlF/9osgB7qDjO9aNdgGv4AmxUOZ0hklJY4fZ8aM6vURd46ACahosdqTi7+hokPHPE4N5sJIupSh0UA2mr8SX0iELPABCcHBFfHoc+fi7/96PRZnaJBwhpxYko6S994mgbqzjQxfFHTInGEU+aO8tZHNS0Jvb29vb2/p91uYQkkZ3W4C7DQk+k/qTWTQQ4qTUTYFKCwkO5tJkwCuXeP2bRo1kr4aTzh5kiFDGDiQgQM5ZwR3QQ9MzcyWXb/uJkO57zWmTRlyisBpmDUlVUyeEjZXyAphgzF9RsFVuAvyMlvPsRP+Ah2Cw/nyEs0GMnBgtVNxhe+h00JMnMkagVtn+vxA9CFOuvPlChQmwn14+4nfskEC4wG4BFHs3cv8+TRtSqtWTJuGj89rdGoqukydT/BYQtItjntJV+O3R8pK9H+g1ViyJmjdag6VF657a7xXyZJUTSgpY7Ivoz8h8xFhCgwaxPBu7BzP+cVISlBVJTeX9HQKCrh5E3PzV/f5JtjYEBKCRMKtW6TrQjAAZ3Jza96PvBSRCMP6LNrMjCUsW0KROmoC+kA+hL9L7hy2FffULoUrWUgk3LmDllb1+tC1ZtZI2lhxuZDsQiSaSFI4e4/x48m/CnKpC6UJ6fAI8uEOmGBry/nz8Ebx6IUSXTbvoXY9RvdA9AZv/uWLoj2ZhwEi/izJMZa3Nu+XgQbCLKh/kOZ7+UebDQEMdcdSzFERCsoELwZYupQRI+jbl2++QV9ftsq0bEmLFnh7s2ABzQ7CEfCCwvj4F5Zuq6GCFQe5lcUgM+oXs2g/YY78pQi+8B3oylu5J8yGQ+CFuTEKQ/D25ocfWL68en2cuEl0PP0t0DZg/VG+NmGKCutUWRzHtCIK5ZLvVwy/wBDwhdmgxejRxMfj7U1ICN9883qd5hwwI7aUyfloF/KnjH96sqP1KoQcLhtReCDZdJW8tXm/8kEXFBBXn1XRALs8cW8JsQz/jsHTafwpfsMBXFzYv//tqTR6NKNHP/5QEW4uCG9RgfeUkEt8OZbpi7l/Bxdn5p+Vt0KVov3knjIchr9WDeLQS0xfToeP6HyZUR/x/WVGjkRnL0DhKOLj5ZQRuwX4/ftJUZGf3jQjim5EDKO/YeoC7t6gpesb9iY3xIq0r/g2lt67J19deEcN9OXVhG1CJKb9LGw6k5vCwRHkPiA7noQifuhH69Gkp3PqFI69mDuOOlacXYC1bCKGy4rwG0Z2Iipa9FiHhvyfet5Lbu0kZClAq0mYu9A4g/VLiFnDWXWcqxumJGPuHuLcAgSBZqNpPOT1+wmaQ9RxjB/y9ce4qXC5mPa90NcnI4PDh1FWJjwc63c+61vQXKKOIVagyy8vb5jezLH2hoVELuKmBOf3tqJKWTF+w8hOQFlTodF38tbmHTTQucnc8WPYWUry2eKFTWfO/ID7N5xdgEMPmqmzahvxa9mxg9RUduxApwGtVTGwo4lsIoavrsfSnWafE3OKs/PwWioTKR80KgoSLi7ns1MAmzwQyhg+GY2bnDqMlRj/EHkr+BSChLPz+fQECkps8cK+B6qvkz2jtjiZLBFDz6LUhbwsoq1orIRNIsDWrSxbRmkpmzej+O79AJ8m+TKPohl6lrwU9r/iv0qzWSIiXY6JaaBMu/e2WnnYBixa0XwssUH6wStAzmn53r3vR14a+nURKaCshaIakhJykjFqQGkBWmbcP87oNqhoUFaGhQXLllWclZ3N9RvY2/MmcYmVkpOMTScAI0curZRy5/8faCiUoGWOggqApgkJF8koYGQ3OjmQtJeyvHfI97m0ADU9lNQBdK3IT389A61JLkYtEIkoyKCZCWVDaGXB1amk7sOkK8/lESvJJ+MeerYoa0pjDNIjNwWjBohEaJpSVgwvc55TLsmmlzfNsmncjouz3pqOUiYnGav2AMYNFQvToI581Xn3DLSxIw/vEDSH/HQM7BAr0XQ4u/uTk8TBkagqQgkXNMhegYobnTszYQJXrjB+PA0acOcOe/diZCRNfRoNYP+nOA0k4iBuH3I+X9mRUayCpJTA6QgCEdFkPiBjMedAC9I0We+F93xsPeWtJgBKGqgbcexrlNTJS0XvNV0a7kvqEL4XSRmZBQREcuNbjEsYrETIdLJH83EEio9fo2XHs+cTjB1JvUWPtRi+S6XordpzZh6ChNSb2HrC8Ze0zSyzV7+znUI4dxjB4q3pKGUaDWDfIJwGEuGfXfcTQi7IV523baATExO3bNny5GNGRkZJSckzLUQKDDjE/RMoa2DZFqBeNwzsWN2EfjsQhnHGFes8+uRgMZnBsxk3jkWL2L4dCwsOHWLdOqZNk6bGhg584kdCCN1Xo//erqzJHd+dRJ8EOLwBD0tOWqN6DcR0MOWgLTqL3xUDDfTcQMwpyoppN+u1q24Xo8ynx7kfyI40WnzMjDpcWsovKqRGcKk/d2bR8PHD36U/8JhPnbakhfPPL/iskdpA3hwljYpRWHXA3PXlBto05R/UTHFtyYMCYgLemo5SxsDu8e99ZV468H9moFVVVV1cXJ58VFFRqaR6YWEJFzPRKKa2hPsnObudJp0RK6FyDeViHiaQVkZ8HkIIJUVkBpITStwuLL4mP7+SRb2Me8SexcQZ06bcP0FWHCIxCso49Kp4kn0lmqY49H6TUdeASAGbzgAiMQVQkEtuLopFPMhCKRNBl317KSqmVy/UpLfcIUi4d4TCTOx7oKJTSYOoY+Qmo2tFRhQWrStmryKRdF44K2lg34N0VeIvsuYINiWIFdm4EYs0MqNgPA5zUdRFrEhpAUBJHmIp/R4lJdzxQ1KKQ08Uq3k9Y4PIvI+1B0mXKCvBoSf2Pap0oiAiN52D/uiqoiKzSMKMSGLPVPycZURaOBH+lOSh+sGFer8SAwODTk+hoaGh+JxJLS6mWzcKC4mMZJQ7X3yMpg4Lv6GOwI6fWFmKJJb0RMZns2YdvdI58glj3Bg9gy52bNjAiBHP9JYUyqHPUdYkeBG7PyLqGJfXEDSH3BS2dUPy7OS9hreAzxJOx1H8D4X5qJRxrQCLOIoiiFtKTo6UA/QPjeHBNSRlbOtOSd7zR098Q8xpUq6xsy8iCJhA/HmpiX5CdzUC7/N3DnvL+DQXJnH1NEqGFGWw3RahFNcvCV7M7n4c/0Zqa2i7fMlJoiiLbT4I1cm3HryI8D0oqrK6Cam3qtVDibIGusWYlaKdywPZGJbkyxwag7Im5xdza6dMRNzYxm5fVHQInGEUKv9Q73cvUOXWLZo0YfhwJk8m/Taz5tHvV1acwDGHCRncaE/3zgxtRN+RTNyNShrtfsBnExevMSCPo0fRfTbM4fY+OszFsR9dlxNzhg5zUdXFbSra5hg3JO32C5SoQWZ0GIBabZKM8KzHcGccDDigTMtSJogZNQo7O+7elZqs9Lu0+Q7nz7DuSFLo80cTLuAxn7xUWoxDVQ+P+YTvkZroJ1yMZIkXlyJobslh6BuIgTbR/XDZgo4hmUFo1OLTE/Rcx5CT6FpJQWJRFgi0GI/LaHStyIyqxrmRR/H6nXrd0KmDllm1elC5mYWSCZ8ew3MW18peU/mXc3s/7Wfj2I9uf3Bzh0xEhK6kww94/cpnQVpx8i9U9u4ZaHNzbt2isJC0NBSUuXAE4MxaChU4NQKza+ScIzOO8IsYayBWJvEoQOxazCt7L6FvWzEtij2Dqi5ZsRQk8890rk8h/iTaj2PBC3L5ohttrJgz/O2MspyzZ8+6u7ubmpqamJj07NkzMjLyzfsMDQ1t1qzZ03tGjx494vGDRVRUlEgkelJwfePGjY6OjiEhIe7u7s/1ExkZ+aTkwty5c4GbN286Ozu/uYZoWWBdxLU4btwhJRfbMtIkZJSQn094OGbSC/UWK5CTiCSXpJ3o/AKbnzmqrElGJPo2RP7Njg307MTmY8SF4DeMw1/wKEY6OpiqcOUU12ehmYiRIhoNKcjHVpOSVB4moLkKpkI6ytWMIH8JylrkJ1A4jJLPeHgFzZdGk0efZP9gjk+hIB1Aoxap11HSIDcJzVqU5BN4kt6d6edK4iuiNgRjMYkPiBjJkR+RUbSAvi3xS2EAsRPRk40LuYE9W7/HRZOJLqWqhjIRUR3ePQNtbMz48fTpw6hRzDhCRjpeRgQe5W5LdIMYkM1+CfuNaXGXc1/z0REKM9hiRPRR2lYWv+f8GYWZbPEiOpBP/AiaS/otxDoUmVEag8LjlHtje6CsxNYALl1kxfS3M9CCgoJu3bpNnTo1Li4uIiLC3d29f//+shDk6el5+vTp8u3Dhw+bmJj4P85YFhQU5Onp6eTk9Ndff72kh3IDLTXae2KgztUS/ApJK2KNAvr2DLuLry/ffff8M9Cb0HU5ARPY7oyzG7pr4SwceeroMk7NJP48EYkEnmZiLwyt+cIbtyk0/5z9UkpO/d08JMXM2M4jCU3s8e6O5kckf8XuBnSojfJi8IbRr+6n6ohEeJZw4D674+hQhPKLM5pmRhH8Ex3mUq8b/qMBvH7ln1/Y1pVGA7iylt86ciKd3/+izwD6viKBwX1fL5QF5sYSX8ZYR2mO6AmNtSmKZksKkVG0l42H8tV0MnLoo4h2nvhUlkxEVAtBrjg4OBw7dqxKTTe2F4R8QfAWQlcLN7YJwnhBuFZteSXpwhajiu2jzkLKroptFxMhNUEQBGHfSmGgW7W7fZZ9+/b99ttvLzravn378o20tDRlZeWoqKjyj0VFRatWrSorKxMEYceOHQ4ODjY2Nl26dImOjhYEYfPmzZ9++mnXrl1tbGxat2598+bN8rPmzp1rZWVlZ2fn6ekZHh4uCMKlS5dcXFyelpiZmSkWi+Pi4gRB8PT03LRpk56eXnFxsSAI1tbWR44cuXTpkpubmyAIZWVl06dPt7S0dHR0nDx5sr29vSAIgwYNAtzc3G7cuFG/fv0vv/yycePGrq6uJ0+efCLi22+/DQ4OrnS8xcXFnTt3fvYCDRKy4gVBEB6sFI60fnJhXnlhXxcvQSgQBEEQTgrCnEqOf9xS8PtTEAThxlHBSaNi59ZuQnHuSzrt3Llz+TX8L8HBwd9++23Fh6PjhZRrgiAI2QnC3oFPtdorCL8/3pbu2DMEoffj7VGCEPHChrf3CyGPv6gbK9Nh+TRh4uOuHHSffHX/y2+//Za+y16IXyMIglAQI+wwrb7aVeFbQSj/mmUJQg+ZSOheW1g3WRAEIfmupL7yyJEjZSKlyrwzM+jr12nfnmbNmDEDVkMz0AI10KtIGqBvwGVnEs5wcwa1JRAKdq/o88Q3/NmcTR1IvVmxR1EfQULEt6xyIfQamwZywwI60KYB4z/i+Dw2TsA3go3mNLNirRW0gsMwEFqAD6RKccSGhoYLFixwc3Pr2bPnr7/+evfu3dGjR4vF4ps3b86cOfPMmTP37t3r169f3759y9vv2rVrwYIFUVFRQ4YMGTBggCAIDx8+PHr06NWrV+/cudOwYcO1a9dWKkhXV7dFixZBQUG5ubmhoaH9+vWzs7M7d+5cfHx8YmJi27b/VhjYtm3bkSNHrl69GhYWlpiYWL5z8+bNCgoK586dA27fvu3r6xsWFjZixIhffnlF7G9lhBHrzOFt7LLksirnp2B9BVzgZ5nkdTt3Djc3fr9GcG126bDWi50nKP0JmnHfkXWOrGlG6Co8vPlmAi1tmdmbQQVI1Cmug0kSShpS0MHUioONCRaxtza1nn5kdoU9cB7Wg9WbyciHftCSCA882tLck8hLCP5wHG5VlHFJsCBVi0RTCssXlA9CC8xnE76S+GDCNqBTB+DaWi4ocV2BQ8ZIyujcj8Mn2b+KOcMxNXi5EulqDTg0inMidlthXPvNRvQCCpuwph3fKrBEnweWMhHRsQ3+PxMsYpGdpLn8Uz69MwZ62jS2bCE0lJI4MjfCALCGpuBf8QDolUNhX270x0Mf3UOwEV4aNBgbREk+Iy/RayOBT61a9D7N3QAyr/PFQr4YxPos2MhiVerWY/tCvIbzUz0GduBCLfbYk7oVxoM7XIBJFTXlpMekSZNiY2OnTZtWUFDg6+vr6+srCMLff/+dnZ3t4+PTunXr1atXZ2RklJaWAp06dXJycgJGjhyZkJCQlpZmaGjo5+f3zz//LFq0KCAgoKio6EWCunTpcvr06cDAQDc3N1VVVR9RyltFAAAgAElEQVQfH39//zNnzri7u2to/GuGTpw4MWbMGH19fUVFxfHjx/+3H1tb23KDXr9+/YKCguqPeBrfZTCvJ80cOayKSB2H0dAQ1oMMMofNmIG/P196c0GEtRMjBuFixaNNcJHTmgzSY+QF7vqTrUir1piJaalD11qcHUyeiPYKIEhBh0czMdAl6wt0zCl+OhK1NvwMO+AB/P5mMlZCNwhhWj5rHLl0iR0+RG+BY/AXKJE0DMEL4xxEo0kZBGWwEE6jdYkuWtzcTE4yXZcBPBqHxjqcypAocmIYdk34bRkbV5EYz+5XJLSyUjuOsSrZDdE2Rnz1zUb0Av5YhIol88fg2pal22UiouMx2inxrT4ibYWZaTIRUR1k4gctCEIl3s0vp6CgIn2zsznpDym7jroD6ukUNCClAJVodNJoMQtFFVgOxvCqHGA5yRg5AmhbUJTz736NhujPIHsO+urQEPwo0EMtjx+WwBWKfmR/fx5IQMDGltR8jIXHU3XHf3ObSYPjx4+HhYVNmTKlVatWrVq1mjx5srW1dUhISFlZmYeHR3k4T0lJSUxMzPOeiCAWi0tKSq5cueLr6ztmzBgPDw9VVdV7L06+5enpOXjwYLFY3K1bN8DHx6dv3765ubmens+EhzwtqNI7qKn5hrHIhaTlY6hGrd5EHCAynYdpqK9CvQdU5qf8hhgWoV8GqRSZktebsrqoryFTiaz7lKihIpCfjp4BsVcZMwJXLfIPEHyCzquhLahD4RvFoEskxMRAIZaeZLSlnjapi55tUV72+81JhhYQTR6YpJB7gdrNOSXB5vEbbyEZtZ4QjVZHMjdDPuhXDM20IWofo+FS8ZZStRRjD+LjwQ7CIQ3vwXgProoSCqIiTJpwfxCtxKSNk8a4/kN2KnadyHHHqjeFr1NV4NWo5OLuTIeBGGij+lZdBipFygZ669atK1asUFNTKywsHDt27IABA6p6po8Pn3yCiwv7AvkzgfPatL1Lmg6DzKinhWI9uupQRxvr8eiegSq4v9T1YksXinNJvPC8p32L7ogHEj4H3Ux6qaE2HDSgISijYkBvZdKLGQXZV1imAE1hOgyEgzCx2lfkxVhYWPj6+jo6Onp5eZWUlJw9ezYvL8/CwsLDw2Px4sURERF169adO3fu9evX/fz8gBMnTty4caNRo0br1q0zMjIyMzPbtWtX06ZNp0yZUlRUNGvWLEvL5x/6IiIiLCws1NXVmzdvnp6evnv37u+//x5o1KhRcXHx7t27T548+XT7Tp06LVy4sH///tra2r///u+0TiKRlJVJxXGqG0OTmb6P8SXcKGMIbNjOKT8mDEP6gYRzmJvO5ZaoCPSIQ2EmyQUc1ONkFgpdiU7mkgpNnbF6yAgRwkGSzcl+SGttcAYVsH4T66wgkdC3Lzo62GjR+wgPj2IskGYjxeE9RWfoCo3YfpkMCRnBNM9E99q/x3WGozmaXEtU4lH7CrRAHyYjUWH3UVQk5C6gyTAc+3OjLmmWhOvQNRPTFjAWLOHnqiiRmtDYxOUioktkCsQY0FIGA+3hi+nPpG9EuYwuTjIQANld0DzIpVAcBUmmDCYN1UTKSxwBAQHBwcGBgYHnzp177sf/CiZNYsoU7OxYvpLlXvT4Bd2dDBH4bgJ66nyyh71dSRnCsUA4AVVYG1LVZfBxDOrhPp1Wk545pBiNaz/CvuTKPNpZwlQ4DXshn5JORBaRfYjZfmgrc1YbdoMfWMJq6F6tq/FyHBwcAgICFi9ebGFhYW5uPnPmzJ07d9auXdvFxWXRokXdu3e3tLS8dOnSypUVz8Vubm5ff/21jY3NunXrdu7cKRKJ+vfvn5iY6OTk1KdPn65du/r7+58//0yoRePGjS9evAgoKip6eHhYWlpaWFgAIpGoR48eSkpKjZ+tENqvX7+uXbs2btzYycnp6YDPTp06NX7dWqLPMoWPt/LZZGZooGdNyEw6TcOmjMVSd0jPg/M0uEvhX9TNpt4OUrqR7kmTntT6lC8NuHCEoEIGmeHdn9jRJA0jrxG1PkZjOQyBhbDxTcTbxMTQtCkbNxKlxjIdbOsR6c2+DCmN7jmCYD1MRqKKRlei16DSA8lThQX04ihdQVY/itZhVO6ZsAm6EqOJyWh6bWLA4YpEYDtrozePxu04b03AONgF8ZBYFSX0d95hpwN16xHryX7Z+D9Y76fQg6AepI2nTXX8u6vO7mCu2tGoHiVe4uW5MhFRHaQ8gy4qKjp79qydnd29e/eqsUaZFMqD69gr4iIh2pTsQuiKRML9rwlR5XIGBX+RncodDfIl5Bei/pTT6L17nD2LszNNm7JnEal38HXGyBB6URaHmj8PjDgahmYSKvdxGoi1ByiiUMyAWTw4xcb56PxCJwlRGxH8UI3jb5jVBG1TpkOeDRs3oaBASQnO2TwdXJqXx4EDlJZSVkadOnh4vMblatmy5alTp/67f+jQoUOHDn1uZ+3atTdu3Pj0HjMzs6ct8tixY8s3QkMrgjIKCwufHN2z55kojGXLli17nAuwWbNm5e8AxWLx/Pnz58+fX75/0qSKP7Zjx46Vb4SFhZVvuLu7nzhxokqDfIJQxt1DFOVQvwOWS9n9AOsrFOtTJHrtlBeVkB1P5FbUz2J5F/8d7NrNqFxUgokz5VwcqpnkZ6NXyv5FqJcg0qcokpx0TJtiokFECKcKsfelKJ2SHf/GncfGEhhIgwa0rOq0UCIWk5nIipGU5CESUDYmq5QHEjZupEsXTE3fbJAl4AelFLTh7gk0Y3moysUY2pZx6i6nf2FKETcL0dlBzzaonYBYouKJukDteFy0AUpKOZhJUj4695nQHiNLTHNhPaJc4iFDh5xidMqNQwFUsUS3mIR0thShUIIgo1BvRcJvoppJuCF1ZSNCUczdOCKKIF5QkH/pWynPoJcsWRIQEDB16tSjR48ueC6n4gswfXiSkF+pfZiYmWQlYT2RBkZ07UrnzrRvz/z5JCoRsJ/SEJIDaerJjl7kPfamCA3l88/R1GTRIsbWJ2wvXc5wfirpkZS1J7c1idncXIHRFBYu5GIqu325tR1swZqUFvTxoMwQ/31MkRB5gPyNnI+ggwFNzHFQxkSP+etJSGDGDAICWLSIXbsq5JbHo6ekMHs2a9dy9Chz5kj3Sn6AHBjKwwj0jrGqG7sKGJDPxUMc+IurKvSRUlWRzCgO9sF0Lg+jOZFE+gDEh9gj4dRSFq8g7hJ/X2b8HTZEknKMn9VxPcGxSzS8j80uInaxIpWrpxg/gOXLycnBx4eyMu7c4dNP0dDgzz9ZU9VMRlkm2hSs59gJ7PKZks35f7h2HBVNVFTw9SUh4c3G6QtJlCSR2RCRiBPJHJiLejAbCrC+y+SLTLzIjVpkJeHjiETEmVAKtqD7kKLtHAsF8PUlMZGHpczchsYt8ndy9zpo4XSXfXM4dpbEFNr8DF7QvIoONvkOBtxOoyCH8Gj0lN9sgC/AvwTHB5iW4JzE4eJXt38NhCI8C0HAtYCmsomHrA5SNtAWFhbz58//66+/5s+fHxBQSUar69evd36KhIQEw5RAuq3EKB2tTVwtgoHMcGPfPgICuHePFSv4thUHQ4lUYGwgamo4DSLm8axz3z7mzqVfP5YvpySKH4OwrUtUb/ZnUFBAWXv862ITgFMme4OJUKX9HC6WrxjMJ1CTz1yJ6s+IU5wpxu0qf+vS6Xf6NGDzEjYuY/AiPvuMggK2bSM1lRUr2Pk4/D88HGdnTE2ZPh1NTebNo7KJsBQZNGjQc9Pn9wyhjJxE3KZikcyyMn5cQLexzG7ATQMuPEQtXDpS7h2hUR2KOtPgDGe0aQTdWlA6kFVGdLHB14u1g4i15bQqo1rSuTFaIhqcw+gRy+05YYz9RP64Tk4RwKhR2NgQGcmhQ0yZQv/+rFzJ7t1VVMTl0XWsurH9Fn11CVDD5zaXG/JTNp98wqhRPH4ieS2yQIDx3LdF0R6n1sQ+wsSO4aHsUOBsHexvkGhI7B1G22LlQFQrshOQmNMuFuMNiMLIzkYiYfx4JFGo6jEokB+/IFMCvpSVsq0PW65RfyR7HMAPvq+iWjopsfRpxtdXmLeR+//JfCIVzO4TaEnLCLKW0VA2Btoym91qfHWV/CkiV2m48bwZsspmJwjCk0Dhp2nYsOGuJ/NQaNmyZbGmBfHB1FUndTJ6SqAKiyry7tvZ4e/PWHuWLsBQg9v7MGnK/eO0nFBxfnk14tatOXMGiTJ/DqH5JbYWYNyU1jloh2Prw44pjBIxqSuJmhwLQkuD7T6oizGOY0ccbr0IHIuuiIDPEAy4vR0TR6Kv0GI8Qilr19K6Nfv3Y6TDmYnY3oYNMBRzc8LD6dOHVavIzSUyUprxbx8kIgUkpeQ9QN0cezGHV9G5hJOpWNuTeu31C4llxRK8iJICWozDpAl6tsQf584FDnZEI50bAtG3OXOJjGKuZWGjTkYqTgXklpKrgEYBaaBylZB/0I4lSIGiUG4bgQiRiPxUbgdgmoWbLf7n6d6d8+exsqqiXkmqJqReRlmRLG1s0rGchXYMN7VxEDh3joEDX3O8AFrwEB6hZ0LAZS7bkQVFagwYjbmEmCzGzyQtA4cG5JlwJwoTI25qE5+CWSOuZaKggaYm6ek8eoStC5l7MTIhKKNiqiZW4p9i3NSJCaLVSFCpulpl2iqKd8Oov4jvDmMkG//ddGVuxzPMDnUR/aCBDEQkibAoJL8Xl+J4B1JzS9lAr1q1av/+/R4eHn5+fl27dn06CKIcsVisp6f39Mcoi8GWEQe5EUprNWrVgRR47Jn755906MDA82gKTHIg+iQPb9PAF7PHuSY++4yZM/HywtYWxxbcOMWCLGYr0KiAYapsdsLtawyLGGlIURo9H1CoiYoSeUk0zeOsOs2s2PktWiKc7Ci8iyVEZ/FQgwb9MGuOGVy7xs6dJCdTK5cTpiw8AAtBF6PefPUVCxeSnIyaGgsW/FvbpYYX4b2MQ5+jkM3q2kyNoTVoKPB5MRd+x+u31+xz32A85qOmx8ERDPqbel0JO8OSYwxMRazOikLaPKSpCHcFIlX56R51xcxX4pQal4NZrcXC7ymYhVkuRq2IjOafQ8w6gmtTihXwdWHmALS/pNXn3K5Dly5YWrJwYRX1uqHr8LGpHlPMURYYpEzMHr5TZLURa73o3p127V5zvABiWAJDiL5JaCkmtkgSeZSP0d/kK3IsB8le1NW4eI/+PzBnGFof01iVGwJxcSiLsG2PWMwvvzBkCKWluDXkGwvKFBnpDd580ZlVF9hnjGlbelXPZ+nKgNGua5bSdjvGIhZ89gYDfDEB6qg/YhgcEVgnppMMRAway8nlRMXiTKFINuE21UHKBvrq1atHjhzp2rVrcHDwuHFV8oUsVdDAayXcgfJVgtVwDeoDKCsTHPyykxUUmPc4eGRTB8bEEtkXx14oadEihILhbLelZ0+M5rNiBb83pcdOToyiozclV6ldjyZD6d+bgk2Yt6HwEX5D6f9sQo8xYxgzBoCesBm0YQQcht5060a3btW7Ov/n1HKi/z4ABrPlNNSGm7AaXve/rbQQZQ0s3QFqtyTzPibOKLXk437UD8czlJl6NM/Dvx11WtGmGEEgWYMWMwHMoPy17r57dF6MlhkTr9F+w1N/FR2gvND1AIblMWxFtdX7ci2sha9gODhhlYj7N7Dl1Se+mpbgx1l7tHT4NpIJE9BcytJ0WrfGx5qtW9m+nZwcRo2qaK7QBPdFGPiSF0ZGL4AWLfDzq6RjW1j8mjrp1opnczC0gkfw/Ctu6VCYhX4DOt5CYw8rfWUiQvUqH63EeAzFyUopTpyWiZCqI2UDnZqampqa+scff+Tn5ycnJ1fn1HqwBhrBjtf0cFIzYK0z9WKYE0CQAoUQEkKvXixdiqMuI20xKmNoZ4wUyf8JbYHLR0lYwRdiLn/Ex79TeBlDBza2pziXul3o+FzQYAdYQGpjjoynWA9bAY9yV4dpEAiasAwavo7a/490gEUwBJZBz9fvRlEVQcKdA6hok3gRj++R9Gf/SfY/wkzCwO40LOFBKcmnuBCINuQJ6CgiWUZOHhJFOsyjxziMGuJbj0wBYxHzVgPcPcSZHxBicR1E43GwAZbBDAgATfgdquWB2x5+gXGwCaRUeD7iIGfnoVDIySxsxQgCTWBpI5TvoxnKV/tIVmLWP/+2L/FgyDBSpmP+gC4aKDVHT0zbW4hAuT46AvmqDFUnNptatdiwAX19Dh5k3ryKperBVQpUSUtztLNbCkqwW2ojfQ5tY/aF85cIRWhZRd+SapLryvzPUfmcEiQtrWQiojpIealo+vTpBw8etLW1PXTo0PDh1YrD+RUyYRsseM1CjcU51O/NPoF6dahviJUmG//g0CF8fblxGEtnzpgzwpQeYu6bcVIBL32+s2edMv3nsnMaBnakhNFnM6NCKcgk7tyzvY8DG05Mo898RkVQ+Ii4c3AGcuESbAKp1tn6wBkKTWE9eMOblarp/RdJodw7TM8NKG7GzxCrLzj5Cy1qs+skQxTwN0ddj1oKmKoyyoGOTUnIom1/vrnGqe8BDh+knyezhtKxBSd2V1T1HnKK4fe4doHCdTAD0uEhhMKW6t/o3uAN66GpdOaVQhnnFvLZaRyHUirCR0x7RaLE3G1LYxGShpQNx8ScG089C/qZ4tGavWq4qxPfgxHBKIWRuQGDv1G+TtE+VnXCO4mQEEaPZtEiyspYuJDTpzl/ng0byKqSU3NqqjP0hw1QD76Qwkj/S3YuxiI6QwMRqdUpRFB1Vi5DUczkhhioKwXHyEREdZCygXZ1dR09ejTQr18/b2/v6pyqAd/AMqoagRQLmc/sKCum8Wh0NXH/GHdn3NSIP4aaGt09GKPE0o3Y2OI7Hg01GrljZkxWA6K6oqaM6yfcrEPTEZQWolUbwNiRnOec8xVgJCXWaA1/qkEylKdVrA3y92l/fxDBZ7AC3ji9qkYtOv6I5xIM7Ci+x/1HONhQtymLGmLUAsUmqNZlZAt0dXGxQas+CiVkKfAoDL0CBIHkZJLT8JhC1xW4DyEx8d+q3mItdNuSPwXaQcrjG20Or+Gf0B9WwGcvr4r9MkryeRRdUQCoOA91AwoyiL1OPW0mBOI7BkURKyajqIB3A5Yvx96drOvwOJVEcjLNv0dyEG1b4hK4vR0dVfKzET+iSIvCKJLzcFQGcHQkKYn8fPT1UVNDSYk6dXj4sMqK9oYVMKLKrtPVJLMQbzu+/57ZP5EtGxeLklJ0DLk9AvdJ70KmonevqneVGAU5kAed4HFOHz1rNrXEqYBzi8kqQx9OfEddVZS708ac7MY0t+bj72iuS/JeNIp5kMS18yirMXg4PXoA2Pdg78eYNSd8NwMriya371nR4NYuBgWAGLpAHlwE2WQGqKGKpB7g0EaMSpm2HX8xN8U0NUfVCa90Lt3ALpvt6RSFIxEzqox715ndkEIxkyahX4tPu9LXg23H+GP1M1W9c5IeV/XuDJ5QCJeh69se2pOy3w8j6L4KQwey49nYjhKB4Cym9SA6F2OBC4OwKODKIe72RBLAmNb/Bmp37kzXrjRqhMElGpax9RTqhfgsIVYX02yULjD4NqPSGPA7e/fy/fdoaaGvz+TJqKnx4AE2MopQrz6+LfjhPPd+4DL4yCYftE0Tcq9yYgIqCGpK5MtESNV5Hw30HZBAeS6rdjC24u86M5pP/OiewpGp1IvCdA+6+txrT8lX6HyEsJkZP+G9/n/snXVAlFkXh58pYOgOixRFMRFsRUVXV+xYuzt2XV1d61PXWNtd3XWNtRVs10bFFhW7E5VSQumOie8PsMFkGNF5/hrmvXEuM3Pmzn3POT+Gz+aP39m9mZQY+jZh5n5WjcHdHaDGz0ReIukxPQ7nrXBRYwSR9UgKp8dhtA0B8Idj0BBUJmGp4UM48wut/+PJNWatJPAZa/dxazIZiXSZza1LJIBsP1nZtBGRdp5tNqTI6BZMrw0MHMyUASTcYdtYSrjDG6reOZsoQ/CHo9AQ3N5phwq4sIRGM7Ctnyv73fRPdC1oMp8n5xixGmlZBsfxIAGjX+ipx9M5nC9N61gMcm65/wBPOHGCVasQiTjSgbimjBjAvt+4loqXN+I9CE5TsSHbinHhAqtX57rjtWs5dozsbCZ/urR5wWNwhd9suChgdAmEF1Qyxf0wLEqgTEDfTvD4zsfEGaqEouigxeR+rykgK/dn46VLREaSooezN1enkA7eCQhk3AORAsBYG4yp54GhIfYNiDmK3JQagzAI4JWiE9i4YfPOT6BN1dfkhFMzCIrHSB/V6O9oeBeyDO7uRCimbCuEIrITEcuwzMRDizIO3FDy7Cmnw2k9AB0dYoQYGlLnKUcDkZQnPQxFMAIRaWk4mGDrDM+/kvNW9TaAVsizuLsFoOxn3Nv8WIRiYu8T9xCJDkIxAiHZ6UT7kfAIoZzuK9BeSOw2ysZDOibOlJnOy9JTaSBCLEZfn6ZNOSpArk/Z1uycQKYTNuMBaAlQAkqUgOeq3o7f0VA1N/o+B4EQbTlezgiyUVFdE6EQfV16NuZKkvLOXdXM8REURQftBLbwPWTCQBDi68u+fXh1YGk9zCRYlaQErOuDUkAFfYQrYT6EwjhEQxjtxaBBBAcjENC/P/37I/zUo6aUKLa0w30IQX6EBVB/coEuU8M7USrw9aZ8B2SZbOlAk38IbI1bNkFyhBJWOhIspEQnEhNp0YKDB+nZk06d2C3FOAOv3SQq8Ddi+/d00sV2LXSCETAJar1r0k2tcG4BsKl14awSoLg7O3tj5kzcA7xmIdbB+QZBQShF6D1ld3+Mg2kaBSvgAcwCHegIjUEEdcCaYcPo2pVVq4i3odwORkoRCPgpr/rOp+eQFE7JWmztSPuNGOal86lGTNtwbwPypyjB0k4lUwwbSMB0lgVhqsxsXYk8sqELlaLooIGZkAEikABs3sy6dRgZcbQuSwaxdQWsY3c8bn0pPg32QBs4BEZQhTaH8fYjp3imSIQkf8W29xJ8lEo9qdCVCl1Z46lx0IVKQjDGtrgNBAg+iq4b3w8iuyEtm5H9gKxf6A4LlgIEBhISgoMD+/aRnIyBAbIIhIbItZHL0ekHs6AEOMKGdznolEh0jHEfAhB2ykwrolDWCcFH6eGPqRMZCRwZj2NjtHTpEgpKzlbEZhCOh6AnOEECjIc+MAwGgDI3FdDSEn//3LXLs0h6jEk+x8oP/OhxFIEAhZwHB6nar5DW+IGkHKf8SCq1JUuPOzVVMoXxcTr8TWZtLE0k0e5qd9BfwH3K9yLP4vRstnfh6hoIhaHQB+7memfA0Y7TP5PVnuXDMXhK+EjYjOgGZlaQAlpQBnISXk4Sr+RsVS5WJHTXS++clcWcOXTpwurVH2GYiQOPA1EqeHYLHU2qt6rJgBnQBXwB9KyIe0B2KhkJpEQh0uVwMJcG8bA6kt/QdSH6GRH32TMUrX3IggF8fDhYhwRThO0QxiORoKMDjrnvjRuLWXOKyZNJyScgR2pGYjhZyWQlkxCSmP0ZX+0fhYkj4WfQMuDBbkxPcrMu5nGkRJKRiNFjrFfDTfAHAzIOcSyMrl3ZvRu0QDtXtPvQaP6az8CBLJ6FcCom/4NlMBj6wc3X5spR9VYqCTuFqWMhLfAjsEfnb1I9CatBsqFKZhCWIe1XtNx5Uk6W8R6Vr0KgKDjogJkATeYReoJ7zaALjIRB8FwnZYoee+9Q+hSWmSw2Zcc1AvRodg+dUZCTADYF9kJTyOLxIkyGYj0HWT9SnqfSzJqFUsm8eZw6xc6dH2pYiRqUqI5PM07NzFUM0qBCJoMBzIU94I+WPvUnsfUHdvak6Z8s+INkcDNE/pjYQAT6zJvH/LpsukTLeZyewt5NPFtG28cc6kGgDF4kW/8Kp0moSdBJGm3Bzo5ffsl7fpEWXjPZ3oXtXWg8R1Zoep7VBpEQik8zZBMRNcZuM6k6hFTidjlkxdCbDUNgJTTj9AKiejBrFv/+y8WLxD3g1Aw8f+OqjH1LmD+fmls4+AhmwTioDj9CX3hZkzZX1dunKRblsHuPjLcaMErHJotIMMiieMb7238CMTcwTEMpRT9LrPigQtgqpbCPOC5evDhu3EuFwPDw8LS090WyRFyinQ9aBlTpxsNTlKkN5Gr85OR0Gd7kn0M0aMmiiTAQ6SzCDRAG5qbYAhjmqlWlx5G5lGqDAE44IQnAuQPApUusX4+hIX374udH6w8+YXQbmPsrW4PKuQLTQQK94SI0xr4R9s/LcF9awI9WSMaRpGTjaiZfp+YE7pdj/jGAEyEEHKaXEcJWtJ5DixYcfPB8WF1YhO8/WFhQ0p7e9qxbl68JpepSqq5qV/k2QjGN5wBcNcFjBUBId+RJ1G0D4eAADvAX+DG9Acf6AnTpwuXL6FtQphUmDkQoqWOCjQ1mUnpBUwNwhCyoCJUh7KX+sr4NrdcW9gI/HLN7hHShng+RFzGprpIp9O8S3onKvsRfFVkWesTOWxS2g65WrZq/v/+LP11cXHR13xfPaF6NH6vyQEqVRAabw14wgovPSyVAchkOWGOayZnmVLIn5G9+/N9rDV4gNUWg4MJktC0xfUjJ5x/vBg2YOZNu3Vi48HnxDQ1fGvVhJrSFxfylxzp3DAxYvJjUVEYOJuEmG6HbfraVZ5AWfAcgNePWFoxteXiIOhNZOp0pJ9mdzB9RUPy1sevWZfRoypTh8GGqVctzerURE0OVKiQkMC8DC3PMjXF6QspWqAJ/Qm24kZt8W7kyixbh6cnq1SxcSHFDAv+kVG1sFeyJp+1tnqbzoxLC4BEI4ESu7HdR4Zk7rr78tIn+ClLN+NQaiO8ipTYltnDbDslfshTNEceHsCsKz9aMrIvNd5zpAoGw8zVV7/F+VG7Fpj7ctGZSNkPdsA7MV/a71HFSjxC/GoPtSJ9LZw0fjoMD//xDl7t7CegAACAASURBVC5fYnSRBoBfwRyWcLU6wdZcuMDKlYwdy7hxbLHmykVWVuWYjBER2HlDf4CW/xJzhxsb8V5C/abUGMfWyjQ4i0sJ3qiCU6ECY8awfDlCIdOnq2N1+dOnD02bkpyMgQO+Ms7pQi0cJFAc5sFaiMgtODVzJjIZy5czYQIuLhgUp8k8rq6lmjV9J7J4MeGDqV4NlsNyCIa9ubLfRYV9yQTqMUxCnBHzVTNF1f08dkdvAamWwTr+72+vYr7AKI4IEPPql2NEFI3/xcyMzP84dYpuC97s8SQe09FITHFyJmIvxd95o8/CFectiMVYvjKFSET//gW3BA3vJgGSoMRH7g/EuRUebqzHxYJHj7C0JCkJpRJrJTjTsiWyp1gbQ7fckWPTKDcEC4vcAZo3h/wLEDZsqPrv5jhIg+Ifl/MdGUmrVgQHU0mPSWaMvg5rcnUCFVUIScPCGYMcTW4dRr4uv1nMnWLPhcO79np93HafuIh3kQ45v05Uo6gS85SwThg1wtiMZyoLRa96FsAWgoJUNcUH86U56DlwCvTAEp6rSvftyw8/YGvLtm00akSbNmzejNYr74C+3vzgQYsS+ISz9n0naHPncuIE+vpYWGiKOKuDABgPZSAUtsLHCyc3bEj58jg7ExpKmzaUKEEPP8pXYmcyQ+KhNTSBxfy2jStX0NLCzo45c1SwkI/lAMwCR4iGrR8hGd63LwMHUqoUkaHMcYAFsBH2IsuggwOGukQl0Lc7Hf9QpfHvR1s7AZqAC9yHhVAgKsOvU68DA+ZhtIYkOV1Uo+r9hfFFOehs8HteFbonPARHgObNcXambVtu3KBUKebM4cCB3OoZOTRPxHktN6+zqxg24e+cIZt9+zh+HKBXLx48wMlJVavRkDezYDcYgw/4wuCPHiAggPHjKVMGGxvGj2fpUs43Ieocw46j1x1aw31Sp3MmmoMHAdq3JzLys3VaP5954AdSWAw7ofOH9gsLY/FigoNxceH0bnCAQ2DC0ZlUKcuko8gy8LJRu4O2s/OH8dAMQmBiAVW+fp1N/zGwEbaGGJRizoqCH//L44ty0ALIAiUIIO0120qXxtwcMzOAtDTEb5gtpnQ1SneBLRD21rD+sBG8oAuCo2Q9QRmNwCqvcTQUAiJIB+M3X+IPRyxGqaRVq5dlMD08wAPiIRpWgS5CMRkZoID9pD9A/CJSKBr8oJSqCha/CwFkghTS+KjbW2IxtkIGOnNBwjkreB5iJNYiLR0gOw3Fi9Jui+AaDIMqBWn7B6BU5ryyQKqqHItEjF46v5ZiXxIqlNx+ACehEqgm1Ppj+KI8lBh6QWOQQLU3q0KPH0/z5hgbY2DA/94QspwAA8EcUp4XUXrBehgBPWAcLEJcjz6eNC6DxI1qtT5cYk5DwTEZOoM5ZMGmTxmgZUu6daNNG+LjmTr1lQse0A2c4QHSqbSvQuNSiHSpVxGLXnAAkqEdDIV9cAoKOfNzErQGUxDDTx/Rb5g7XXuzypmYB6x6RXPL8yc2LKOVDUkZjM2Rp/oeIqAueMIhUE0gWj4EBzcpX34ZbIQYWKaSORb3wGMCm86TImOjKgSvgMswGgbCfAMDD9VM8RGoxEErlUrBJ1bA6g+9QJ5HAEbjxjRsSEYGenpv9XKBY5D8st7NS5bA7zAQBkNFCKQf9KyE3Bidbp9koYbPpCocg5S8XqwPQyJh82ZSU5FKX6+jcgT8oCykwEiGb2TgNpSH0NaGyXARHkMv6AJdwLPQHXRdOArpLyU3PxDLo/j7k+yEwYtMbgCEYlbdJy0GbUNEObdkzkIsCEEG/xSyg87O1oeD+XwMCwj7/TxbTGRjbMRQWzVz/AdToC40MTDooPYYxAIOs/Px8alVq5aXl1ft2rV9fX0/aQxJ3uFxgEiUl3d+QZ5vC2fYC8A60IV7oEByAZ0yn2SbhgJBUACfYT29t6pcOcIZMIAzuXcvtMRox0A2XAFbcIBAUMAdMM1rUFUj/GjvDLnrMnhlXa+ia/7cOwMGsAMUEACVP8/UT0Zl3hmgDOzDpjSseTOSvcDIeRcBp7Oy1F8rqoAd9IEDB06fPn3kyJGAgICjR48W7OCfxHKIAyPYCYdgCtSHOuD+3p4aihr9IArqwDHIyVb9G4aBF3QHO6gJ7lAPpsFC9dr6MQyCUKgDATDmnS13wxgwAWf4OE3uIsIySAQj2Aa7VTNFD0iBOrAnLu6DZK9VSgEfcWRmZp46dcrZ2TkoKCg9Pb1gB/8ktJ6XScphY74NNRR5xDDz9Wdc4HWZdgZBkcsUFcMHhglWhkeqtUXNiCHg/a0+CyFMy3mkUKg/DrqAd9Dz588/cODAmDFj/Pz8Zs6c+f4OH87163h6Uq0aEycW5LAaii7r1+PuTvXq7FbRZurLIC2Njh2pUYNWrYhTUZn6IkJmJt26Ub063t5ER6vbmsKggHfQJUuW/P3333MeL1++fMCAAW80SEtLu3Pnzos/MzIylMoPE38cO5YNGyhRgsGDOX2a2iq6RaChiJCUxKpVnDmDTEajRjRvjkg1QqVqZ8kSmjenZ0/27WPuXAp231O0WL2aWrXYsIHjx5k+/VtINCv4KA6FQiEUCoHSpUu/fTUkJGTr1q0v/hQIBO8vlpRDejrFiwOUL88T9ZcB1KBm4uKwtUUiQSLBzIyUFIw+PimxSBAZSf36AOXLs3mzuq1RK5GRuen4rq7fgnemwB305MmTAwICWrduPXz48JUrVzZo8GZJ2XLlys2a9TKWMy4uzsrK6oOGbtGCzp1xc2PbNvzyktzW8E1ha0tcHBMnkpGBvv5X652B7t0ZOJAuXdi+nUmT1G2NWunShe7dcxUJRoxQtzWFQQE76Ojo6CNHjkybNu3hw4cf2GXevHkmJiYf0rK4jo5RYODD2rUzv4jSCvly//59T0/P/K5mZGSMHTu2EM0pDE6ePOnt7Z3nJYFAEBQUpIolC8uWdQoKUgiFD0uVUhb6vzQoKCi/YH+BQHDs2LECXLKRm1uJ48cjXVzi/P3xV1uJtYyMfGvkCwSC9evXnzt3TtU2GHh4lDpxIsrJKfb0aU6ffn+HzyA+Pv5T8zkKDMGHHgF/GO3bt581a5aVldX48ePDwsJ27dr17vYRERGRkZHvblMUcXBwyO9bJygoKCkpqZDtKQRcXV21tfMWqb927ZpMJitke1SNWCyuVCnvekCZmZk3b97M81KRxtDQMM9zSyA+Pv7Ro68wgMTGxqZYsWJqNKCAHfTdu3fPnj3bu3fvu3fvLly4cMmSJQU4uAYNGjR8UxSwg9agQYMGDQVFUVBU0aBBg4ZvEo2D1qBBg4YvFI2D1qBBg4YvFI2D1qBBg4YvFI2D1qBBg4YvFDUrqshksuTkZPXaoAqMjY3zC3HPzMxMS0vL81LRRSQSGRrmqw+UmpqalZVVmPYUAlpaWnr5VydPSkqSy+WFaU8hoKurm1+ou1KpTEhIKGR7CgEDAwPx+4Txnj17ZvFCOb6gUXOY3U8//RQUFKSvr69GGz4HAZhK0lPkkkzFy1fxyZMnvXr16t+/f55dPD09LS0/RpLu5URpKXLtTMWXWBLo5s2bPj4+VarkoYMnl8sdHR09PNSvHlSwnD9//uHDh6K8KjRduXKla9eurq6uhW/V56MtlOuLMuOydd/2C0+fPj2eI7j8Fv/++++aNWuKF1dREf1CRShQmorTE2Q6CclppUuXXrjwPaXD7ezshg8fPmLEiDzfDJ+JmnfQ6enpCxcuzC896UtHnsnmtmgbEh9MvQk4t8h5+r///gsLe1u7NheBQLBly5aPmyg7lU2t0bMk7gENp+PQ+HOsVgUTJkzIr/y3QqFwdnb+6CV/8TRp0kShUOT5mUxPT2/Tps2MGTMK36rP5eEhjv0PUydSn9FpJ5LXCpm9XVrnBWlpab/88kubNm1Ub6KKSX3KptaYOhFz94bdsDaDp0ql0hcX+/Tp4+zs/EaPOnXq1K9fv1+/fl5eXi1btjQwKEhNmS9KNLaoEbSfUnWoM47sVHy9XzjogufODsq0xGM4mYlsaf8FOmgNXwmn59DdH21Dzi3k7n9U6KpugwqdyyuoPYayrYl/aO7TVy6Xd+jQ4cVFa2vrPDtVq1ZtyZIl//33X/fu3Xfu3FmA5mgc9GcgFJO1msvzkWmhlfcrV2ATvTiJEuRs2W7CBEiDllBAwjxTpnDmDIaGzJun0Tv/2jkCc0EBg6ENmZn89BOhoVS6Q3Iy2oYolQi/TeegIHYEl/qTpSOWlNTR0XFzc3t3h1q1agE6OjqdO3fu3LlzwVqjieL4VI4eZe5IhMFcq849E8oGq3Cusm24v4cdXVnfhDq/AjAClsIhOAdXAJYto2ZNevb8RNENPz9SUjh0iBkzGDmyAG3X8OUhh/GwA/bxbALf1aB6dZyc8POjwiD+cmdHV4L2U7a1uu1UB4qrxD5jGdxI1LPN96DyVYYMGaI6czQO+pOIi2P6dLo2Ir05m+L4IVC1/0ixDt0O4TWLnsewyzkHlIMNCKAiPOHUKc6d49Qp+vXj509SC42IoHJlACcn4uML0HYNXx5pYAW63H3I9VR2r6JSJfbuBajTgxBPvGbR7SCivAM2vnKe3uZmHeZcRGeSNuoXGPs2f8V8NqGhVK5MxcE8rkZdKYFuKOvCNrgKXiqZUSDA8FUR+HowHErDXhjMbV+aNEEspm7dT6zp/v33TPyesj5cSaJTlwIyWsOXiQHoQBNMY9DVQduFH3+kcWOWL2fnTkaOfP2d9o1x25XOe0n2pmZ45kIzdVujcdAfwSW4BvXACRcXLlzgWG3iJpG0HMvJlI2DI9AVZlpY1AgLyzcouID4Dc5DFOyAvbSS0W0JxsYEBJC/VsC7sElgmZTAOrR8jFVoARurQf3chTPgBjk1rJ9BbfQyiFnOvl3cfYi3NxYW/P03Dg5qtlS9tK9H9iFi5WjrJjVy5kiEes3ROOgPZCPsgdYwEOaiU5WdO9m4ET1Tpl5DRwdaw1owggkWFgugrupN8gA5NIEfsJaxU5vlt3F3p2XLTxrtJOLh1Mm5xZFvNJWGokkATIe+MBV6QV0whunogW0UFy5QvAIjRny1qrsfRfmrpBXjaDU8wnSdwkHN/xONg/4AZOnELsd4HdolSdUhdRkW/2BmxrBhyOXcvo2VFZZVwBd+gM0JCYWyB0mJJO0yFg4IBgBo++NVk2Kl+XCRnpRI0mKxKIdACFVgPnhBIHzbe6ivkO0wE6pATdJ+JtkWq6dwG7TRC6bnCtB52TYmhogIypUjJ4NOqeDZbXTN0LdRl/WFSwIKB+ROUEws9oV8k0ULh2/SQSuyUcgR67y/JZAcwabWlEjjSWPK9uTBckxKkNiErvvJVvL999ja8ugRwwbQ4SZ0hcYREfbwQfd/P52rq7m2HiNrUg/SeT5pSTQ7ibMl94P49Vdy5QHlkAm6yDNBgEgrrxFKkvqUznsQekBz6AmOMFe1xmsobBzhDDhx70/OnMdsIYoMWk5CqIQFr3nn3buZOxdnZ4KC8PNDqoVvC3TNSI6gci+1mV+YXLen43yqHmSCXDb0zZyUdyCXy5VK5Xvzwj+Wb89BX17B5RWItLCtR8Pp729/YzHdMpGWRH6RRZP58RdE0zkznwcHuK2geXNGjSIzk2bN6HD0eZ//VLkAAK6uoccRhGKO9yCkMadldBvKwJmkpdG6Nd7esA+mgDFRsRzQQy6jYnfch+Q1whRCjuPgBT2gh8ot16AG2kJ1mMr5BLqfRVyVi0u4ZZhHHspff3HoEFIpS5awcyc1LEiNRiknK5kz88BcHcYXLtO2MkTIQCG30O/7CBzf22PGjBmVK1eeNm2atrZ2ly5dBg4cWIDmfGMOWiHjykr6nkYgYks7EkIwtnvl8jUYAWKoBjNRKvn1V6qt4KIxDeZQ4iG6XZCPRyQkOw2hGLGCnLJHhV8JSKlAIUMoJtsaYR9EEfgsZaMnWlq5JjER9JBnIA6h1w6U9Vhdl6p9X8ZOKZXPR0j7VlMSvh2WwlJoDtWR/Y14FdlpSE0BfvuNw4fR1WX6dNzdsU1D0gy0qJ5B0BCe3UIkobs/6TEsdPomHLR+DNUE3NFDmSUskZZ8JXnr1q0vLnp6er5dFykoKOjevXsBAQFisbhjx44aB/05KBFp5ybjaRkgWQ8BYEHSBMb+Rb8t+Pdg9DyEo+AYh2XI5bQeRK8VLKhGGWNmV2CdF1ITpGbUm4i9ko0badaM+Hh+/10l9vr78+efiMVMmMCr9YZq/8oaT3SMMCyJbT0u7uDxVZIzQUDFcgCEwD1SsxBVhJsIPJHoolS8HKHOixFKYFtPJcZrUBeBgcyciVzOzz/TqBGKLJZvYNff1DZm/V70miOW0s6XwECePOHUKeKecsgd93LMCaa9JdnF6R1OO21CyyOXseE7MhIw+jZi74bqsD0V33gqopwplDeWx7+SFpBnHUqhUJiVlbV//35jY+MCr835jTlooQT7hmxqhbYhZnHoAX5wm0nehEkZnYTuYUQLMA/g2WlkbXByYkM6FbTpaIFSD98yzF6DLB2JHoAYfH1JTUUqRaiCTJWkJKZO5eDB3COUs2df3gB09qZ0M2SZueVsHmyluiHLb3N7J+MGAsjNCapCiBhpGuN+IeNXhrRG/LLsC6Wb49T05QgavhoUCkaM4MABJBK++w4PD/7Tpc1pBrUldg+b29J+Hlr6AHFxOCjw9cYggadC8MOoHHZa3JFRqiHiBOy9eSTk6B305UwZB9vVvTbVcz+LEAHmIFIKniiNjY0HDBjw7h6zZ89esmTJ9u3bLS0tlyxZUrDmfCMOWg4r4Ro0oVxb7u8h6hoBGfgbMPwulq6cCce7IkMqMvYWof9jSGlifOg8iMxM9PXRdsSuBk0GsWEUAmGud35B/kWBP5eoKMqVQ1cXXV0aCZENQlIWBoMOSU/4uzdRT2nfg4wI0q4jy2TRX1y6gJcChhKYQRknzIrz/SbG/I6ROcNGUGwYpo5UG5x7g1Qg0njnr4XNcBLcoSfJydjYYGwMULYsUVFcicdwAOY3yOzPmZsMeV7dt3Ipbm3FyRFpMNu1GDoUuwzGPsPAi5jF3DnOsz+5ImXVWp7E8/dGNS6v8PgziypKfGEvynEf1MPCwmLSp2WHfQDfSKr3IgiBIeCDb21sPQmMokksbZ4yoCq0RKKkbDIXO9NASYSM7I2UqoC7O4sW0aIFiRmIqjJ0LD/8UKhWOzry6BHLl7NhHB0eIhkKApgIMKweJezpPoIxYzB34bv+hMYj+gvvw7Q3BT3E6Zi3JNwZsQVaLlSphZYMu5Yg4OiEQl2FBpWzE/bDULgCqzAyAli0iGXLCAnB0ZFqllxdQKlb3JhPNZOX/dLO08+IiFIcK05iLEOdWBTFmcboeSCSEVSaRx7UvUb56jRtS0yMupZXqBTLphkIBDRBoKPOWvk5fCMO+hiMg/KE1oNMzEozW4yOFY4Sfs0mS0JLbTaI8FvLIgEVpEz+Dt/xXL5A1aqMGsX69aSkMGYMHTsWqtUiEbt3Y2BA+Xgq/AEVYQRcAgiPo8cyHGriWYrwh7hWYrkAsTktszHK4K4EAwjZSFJdwmJ5fIHFYxCKEaZh35DIy4W6Cg0q5ziMhHLwKxwF2LKFYsUwMmLPHoRCqu1joow4B4Yo8fZ/2c88mQtPUeoTkMBgMeWMaNGCRVfYfZ5oOc6zaPIb+4T4L2PVKuzt1bS6wqWtkrWwTMx8+LSUrwLlG3HQNeAfTk/k+l9kKDkzhfOZnInmmCtiIVobGW1FK3MehDKlEk2qIDXk6j56SNCWAJQuTZ8+VK2qBsOlUjp3psoARJshHNaSbc+1a1jos+d/xIUSEE4JJyLmEm3OLzcR2BKVRJYxQiFpMezdxpotXDtP1DM6iXl2i32DkcvUsBANKqQG/AuRsASqAUgktG9Pp07kFJu3fcKNWjgt43pbikdz/ToyGUC0kFAB2eFUFbJPTmQzzJOoksaZU1yQIfVD+wwLlAREkZnJP/+oc4mFhouA6vBMiT3UVrcx38wZ9C/wB0H/0nMWVcqwvR1mcg4KiT9FOympZ9GrQi83aj3EqhF7z+FWgTrjuPwvz+5g+SUIF1WFPjCWSF36PqDiRhKt8d/NFh9+HcGT0+ilUlqHW6e4nIK3DiZXeGzC0QS8tHm2iiV7uL6B6Bs8u419Q0JOqns5GgqWTpAAo8Adfszjuk5tHK9wcBRlz3NXhM86rl5l5UrCMtG2pkNx0u2ZsZVRoyh9lSqumDoQGc79OZQxpKwvtb6luqOX9fBIprIMSwFH1G3MN+OgteBXtE6S7EWJkhhU4f5lljjT+io3Uohvy0+jSPkPvco8XoY8k0azsHQl7iF6Hy0eqDJaQSvG9eLvNTg4cPo0e/eyaGbuxZQEPKzQrc9TiHPgp0dci6H0eGqO4tQMQo5jUIzY+7T1IS6IZ7fVuhANqmAQDMr/6goM69NuD4+g9APmWdG+PU2bYmGEnozsUKTGOLkxw5fZJqQ+RduA+BRse1BqQeGt4AvhsRa7IA5QKuurvzjJV+qgp5ch7iEdFdgbY6UEezDiu8nsG0rsPdISSEngD0dqphJ7jamO1FnAsUBMnbizg/3L6DQextG2Jd2+HAcNgFEc+5wxEBCqIFXEz4uROvH7ZZZOYL01brakxtL+LktiEKRxfD3sIDUNSX1K1eHxOVbVRs+K7769T903QuRl9g9DKMZESasMBELoBsMhA0yhGqkX2exIhphLKcyzo50STwVJ2UgFeC8ByFRwMwztELKyuL2R/52ma1d+zGtXrh5SoDMkgwBWg13Bz3BDzmzQBwWCEYr3t3+OilK9v74z6JPcrEjsA+aPwm0YsxNQukIoSDFbTaed2IkYXY7ehkSv49A1KoxAvA15MsKR0BjBSi7d5u4zHiZyN4Hw8MIyezJ4Qgd4o9TnGDABC5gPUCeA0yXZ1Ih4JQ2t+SMJeRZ7/8ExiNh0EDI9iXEC7iYwzIahCayTIlBSCgDPKfQ5zQ87Xk+e1PAVcXQkHfXprYXJde6OhrPwN9SCGtAEjhGlpLEMVyEmclyywBDzVDquoMteLMoDxAko7UIld6IV1G7H2bPs2cOzZ+pe2Av+gXQQgjQ3nKnA8UokHIpBLPT9oCiOqVOnHjhwwMPDw8vLa+XKlQVrzle0g06N5tgkkrYgbY7WYwR+SDpjDrIyiIUcERO9HXtX3EKpkEKShKM63LbmcASruzBGlx2PMKlKwmliROzphSwdQyF5JQ6pgP2QAsfhLox8JR0gGPkSejZAKmbZJIQ/IZRRugoX00FIZCy+3uiLSUvA25GZxxh3nthMBgjxaYZTNo+702M2zIDUQlmFBnWTfYNfzbgSz9wssi6AI8jAF+Vs1v7Gtd8ZloZ5M6w30tiYHk8hG7dMir/yQ/6JAxUESM0x10arMkIh1tbko9euBpSnOP2QoSn0NWXYPZVsL6vDvzBGgIESX7Kzsx89evTiYsmSJSUSyRs97t+/HxQUdPbsWS0trR9++KFv374FaM5X5KD3DaXOz1iFsC0NeSqTH9B7Is0EPF2Dj5RYbcZVZdE2WqfRoyJ2ZQleh48OaeaYxKDngHw/8XGEzcB/C+ueULIc97ZSqnCOOCIgJ0TEGRJePp1+k2dy/lpLejoJdpjEcLsK0l2MLcVBOZcziDuMo4zaK4lbx0AJlWxwiGJRBsY1mHWGxSu5vw9bLbR/KZRVaFAfV1fz0B/rRBwsaNuPy3PosB1ugxHYsT6Smkk8k2CjZO0hHJpSR8lwZ5QV8d9OtpIXPqftaNb8S3Ur/tMl5QQXriKTUaqUOpf2KpdDcXzMMj2KhxKkTRkVTJEpZJqcQ0pqQzaxsbGzZ89+cXHIkCGVKlV6o0dQUJCTk1NiYqJYLJbL5QVrzlfkoNNjKVYbzHBKwqUN4afYZ4CLB5v+wxgU2VS7QOu6xItwuItHOtFSjo6lvS0Uh8uIvJFHUR46VuCOHLdKNL9K0gykk0DV8ijNoA0kQuBrsZdhDpRQoNcVkzRixCTrcrsYv29DcIR9yzEwpXYJdt1EORFFMrcE/FuPg7u4rOC3B1R2IDmdiqXZFk6LSRiaQz8wyd8GDUWH9Dgur0Aho0of9K25vZVzy3mcjZOSLulkH6WCISdFdLCBSFjEej8WSfhrDKv+pEwarpYIJZhVBgMszEiS8ELaqXNnatYkNJSxM7l3j+xs3N3VudI32BnDj3JKJiMRsEnBaBVMsV7CEDm2EuJlyi1YW1svW7bs3T0WLlx49erV2NjYmzdv/vrrrwVrzld0Bm1TldOzCerM9UfY18V2KrXGcKgYbVdjI0dPTqIc6XFGwmUpWxJpkUE1D9Y8Yv8ZwveQYUwsaDthoEc5exRTiUrA3Bm6q9704rATpNAPfnr5tGMZOrvzQMltPfrXw9CQli2ZvpcHrXkgo6oZ1sWwy+LZYx7K0YY7EejpEyenTAXK3cDNGeNeON4iMhWsoYAF4TWojW2dMLDB1InNbbi2moOTOXufsvakCQh+wr04niVSyQuagDYYECzEScoTA64asESIRStO1uW6MbckxNpj6vTa4HZ21K+Pri5VquDh8RH6D4WAbTxhSo4KuS7HSTXh/GfNWCJCacp+LeXqD4riqFGjxqBBg8qWLdu+fXv3gv4+KyIOOiWSkONkJuZxKSGB48eJiqLxXDJ1OX+QGrNIdMOqFre2UMme3+ZxPIvzejgZEKVLXQlr4agLmzwZ1Zz0h5weTdph9oi44ERwA2qU5ckZpLZ0Oo1oAGRAIVQTNQYHeP23pFjM2t3sr8AJN3x2AHTuTI8eXLuGtZCIZILucUeJuQEVzLiZzv5QDsZi70iIiPv6ZKdy/D53tbCpAt3JlhC6n4QQ1a9FgyrJSEBLj4rdcfAi7iFhp7kbgmMqDjUQC5it4KdIIrSQavHEDpkrYdp41OSsjKObCUgn+2qmTAAAIABJREFUtTL0xnsPsoqkl6HTntzijkUCqyz8YL+CowJ0VZOH3WEgfkb8C/+Zxjm4qWSKj6EoOOhH/uzszeNAfL2Jf/Tapfv3adGCwEC6d6f/AJYFcPASPwzmyhaOTqDeBBL3I7jMdSXpcQyqTlw617NZZYBNCtaXybIh/BD9t3FahHc9zEXoHCAojJZLqS9BmgE3IRW08rGsoIiBphAAI8laQ1hYbqKXUsnhgZQTYp/IsV8IDyctjfr1GTWKllpkpnErBgXcFBHxGJRsTiRaTq0IarbESkLALU5uxCkVSQnSAtlwgrDLHBrF9Q0qXo4GVaJtROozHlwg4F+ykknXw0iLUDkhJzCRUVqJvgL9TA7v5Na//LmWR7doX4yKpdgfgktF9gcCSHSp2o9qg9ExVvd6PobbSsrCUyW2SlQUzT+0M666nAVdQfhPLVQzx0dQFM6gLy6lzTr0LCnmxvUN1H+lcNSGDUydipERu3bh40ON6rSX4DaGzOLopqBrTmQ8/Wcjus+pA5w5gJsLwhL8fRbCmOaBtT13A/DuglSO9hSaamFmg81/YAXaMAT0oIDjZvJiN/SBXty6ytAGlOnIvXv4+KCbgUQXr1mkpjLRjp2pPHrM2LE0boxYi06NiIrg3zNkJxAUTTkw8SLpMrLb0Ijiybi1QZDMOiucphIlo8oYKk9Ekc3676jYTfWL0qAaBAIeVGVZG2SpPJbTLIosPRITWb+fRBhVjLnl2XAY3yj+uU2IA5X7YWRHYn32FP0EpRtKDsBjJX9DDRVN4cNfq3HwIjW6+OoOqpnjIygKO2ipGUnhAIlh6L6u6WBmRng4c+YwZgwWFvTqxbEwwkIwMyMlEh0T9MyJuIvXdEpZYCNAEk10IN2sGGmO4DZ9/Hgcim0Gvulk1mXY9zAPVgFQD3aCDyq5VfwGZrkahgtmsbgKy5YxYQJLl6JtREoUSiU7tmJuyPJV7NzJnDkADmXxjcE3DTcB7W9TNwZTAXXu4hXLQjGEscCFzOKwl0B7wv5AOoFEOZCbJ6ah6JKWRuBNzj1m5CS+k1NtO06RlBDQtholYUEsteI5BwZO6MwhLQ1tIzIT3lSkLKKcEFAdekOb3KpQBc9LbxOeJVb/J6UoOGjPKRwey4rqPDhI1ddjDAcMYPduDh9m/nz+/h/nhmD5mON/MsaLsBgC/8AokutrGVeKh4+wKc8QB87JsdBHrMM+GYMsaWpKv6WU9qb6SLR1IUsdvypaQAS4Iz5B1nCArCzEYnTNKdee5W5ETEVSH4GI5GjKXWONJ/r6tElmeDrFBYyW8ocWpZSUu4hrNDVtAPr3ZMMm3NwwNcXDA2dvkiP5151tnfCa/W5rVIe5ubmxsbG5ubmhoaGTk9OmTZve0Xjq1Kn5Xbp48WK1atVefSYhIUEgEKSkpBSYra/YcPPmzcqVKxfsyJ+OUEj1aKhNlTlIlcxV4gMS6BeIeQ3OZ3L9IjfgO232D8eyIusb4/M9XjPfP/KXj4eYx7AbLkId1UxRtR9B+1lRnSPjH5VSv0RnUTjiMChGd/+8L0mlbNvGnTsMGcKJCVzXJz4WsQnaZmy4gDicrnvoZMr+YXTewxpPpP70Wcu66WSnMGwY06YRV5vOS/EVEtONZdVhJbzLa6gGISwF+PURfftisZH4eDZuBKjaj6r9yMyka1fatsX4AmXKIBAQI2d3JllaSAXEx/NwEotmscsMqYxlkdAR53icr9DvuX6aQIj30kJfVx4EBAS4urrKZLJdu3Z169atQYMGVlZWebacOnWq6uqgfyBfgg0oZBwZT8QFLF3xmo2ONj+l422NnoJnIBShgPhsOpYkIppLm6jwA/5jsG+IU1M1W17glBWwD+6BzYeouX4SYh065IoQZvrn43byIS4uztTUtGDNKQo76PfiUpJjUhxj2VGTJ0K6FqNvOrFKXHQ50Q9Df2qeg+ZUzWSzOyVv06g0SxsyLQa6YvodBw+xaivH4nHeCgdALbdNdkJXHFZxrAkrlfh3wDznMOcG9EV7BNsas0VCgywatqDnMXwf0CGGFaUIV7K8LJcXkS5i5DX67yFbCCvBH95Ut/xyEIvF7dq1MzIyun//PrB582YXFxdHR8emTZuGhIQA3bt3l8vlderUAaZNm2Zvb1+mTJnvvvvuzp07HzXR2yPfvHmzYcOGv/zyS9WqVatXr3706FFAqVTOnDnTyckpR565Ro0ab9iQlZU1fPjwypUrV69e/dixYwX733gPF5eg/4ieNhSL5fRsSMXant2OOOgQrWShnMFyDsHK8/Svg2t7AMvyJEcUqpGFw2kZlrADvOCguo15zoHn9OnT58CBAwU7eBF10MEwARZAEgAtwBBKsP0QUjlX7xEchVjA9Xjq3uT+LLQrgS4VvWhZhSq2dDuAwRb4CX6HSQAGOYdN6jpyOg++8Cc5v98MlsN98IV0svqwQMreZJTjEf+NmRVXp7DegcQIylag7FLEOpwyx9+BNMgcgrAjD03Vt5APJTMz09fXNzExsXTp0jdv3pw0adLJkyeDgoI6duzYrl07YP369SKRKCAgICYmxs/P78qVK3fv3nV1dV2xYsWHz5LnyMDJkye9vb0vX77cqVOnuXPnAn5+fps3b758+fKFCxcuXLiQ0+yFDcCdO3c6dOhw9erVfv36LVhQuNWm4vdhrw+LKWmLz2omzSX7Mk8vUCKRZDhsj78UMRgswtWFLe05u4DziynzBVSbL3AsIQwOCrkN+u9vXjisX79+7dq1ISEhSUlJOZuAAqQoOuhU6AleYA850Qh3oA3IMTLFUYs4JU+UGFrh2JhsGWbjsJHCfLiFXl8snz0P/CwLtupcx0suQzuwgEwQgAl0Ah/kUzgeRsXWlDIkSAbaKM2om4WBC3ZKNkSz4Qj6prT3pM04vq9IAtypgni4upfzLurXr29tbW1jYzNt2jQfHx9ra+uDBw8mJSW1aNGiVq1ay5Yti4uLk8le5iCYm5vv2rXr7Nmzc+bMOXDgQGZm5ofPld/IdnZ2np6egLu7e3p6OuDn59ejRw9DQ0OJRJJnLQVHR8d69eoBLi4u6YVcm6KsAYfvc/swvTbjmc2AGO6nEuVJE0tMIEILjGgFtMdNjKcbJg50P/jm7fSvgwFKRPBERIqAYSqfLT09PTQ0tNpzqlevfv369bebbdiwwcPDQygUVqxYcdCgdxR9/RSKwhn0m9wFD2gAwBLIhpLEDeEx/JBE2dr8AWUvMMwbSwliA0wOgTN0gIbwJ4Xwwn409WAU2IECsuESdOW8HfciaRePbgq4kpxB6kVKXsXiO+IbsiSSFeHEXmS7NSVGgA2x1bi/l5KOOH/RW6cTJ064ur6mgSCXyxs1arRhwwYgOzs7JCTk1ZqNly9f7tChw6BBgxo1aqSjoxMUFPThc+U3sr7+m7svmUwmeJ4yJ8xLoP3tLoWHbU+e/cHuddSIomtdMtwQ/UtQPJV7sXMCi1NxSGSgHriBDVajsVJNmbcvAX0xK7NZJWA6lFF5iqNUKrW1tb148eK7mwkEgp9//tnPz+/q1asFbkNR3EE7wjkIhauQDRJ2CVmsi2MWo/RINyZJixIOVNLHXJsTFRANBB2oCyIYBp7qtv9tysF02AvNYCz8x+RE/rYi1JkGAmJXEv2ICc7oHUPXlvsCqrTloYAu5RhhTIl1xCaTEIyZMzVHUqbVl5Wb+wE0atTo4MGD9+7dk8vlU6dO/eWX3NJOCoVCLpefOHGiatWqo0ePrlChwqFDh94uRnPv3r20fIoO5jfy2zRu3HjdunVJSUnZ2dmvnqLk2PDZS/w8LtkwLALdLHZmM60OSS3ZqEPpe6THcduWGVkMMEPUE+Jhfa7k1ddKUDEeCJgkQw67v6zAwWbNmv2jAlWwIrWDlsvJyEDPGBbCFNCDtQBrYlkxg7vpBI9myhHcnKn/FwfXItGjuQ+UgoZqtvxt5HIyM9HVff63G7zIK23H8X84MRFsUaxlWCzGqYzeRZYl9uN41pl9DbCrS4X1ALv7IhQjy0DfBq9Z6ljJ5+Lm5jZnzhxvb++0tLQKFSqsWpUTh46Xl1elSpUOHTq0devWihUrlixZ8vvvv585c2b37t21tF5+OCtVqnTgwIGcMDgHB4cX+9/169c3btw4z5Hfpk2bNtevX3d1dbWysqpXr96LM5YcG94dDqhyfH35ez0Oxfm+N93WEXSHMVW4ZIIsloRSHKtKQghlkqgyNF/Jq6+GO6WRRhKoxErARXNGqdse1VN0HLS/PzNmYGyMgQFr1iBa/fJSsQbc3k7pk1jWpIYVloEcGknp5jSYpj5z38nevcyfj4EBlpYsX87bv6m1yhE1Eqt0Hlrx699UcGF7Vy4oSI+j0e/UrJXbLCEEWQZtfQDWNyY7FYleoS7kI4mJicnz+d69e/fu3fuNJw8dOpTz4MyZMy+eHDp0aM6DFz88MzIych4olXkUZ3h7ZFdX1xc/RevUqXP8+HEgMDBQR0cnNDRUIBAMHz686nOB4Bc2vNrl8OHD71lnwSIVMcGT8iZExVPTgj+ioAOuP/LAj4hL1JuIUs6aBlT5BqQmkxSkKvhZxAE5qR8hd1J0KTpHHL//zr597NyJoyMHXw+x+d80ZstpICDQmBJBlOtIo1nE3CUxNJ+x1M3s2fj5sXs3JiacOpVHg5l/8kMydTMp3ZTKlbmzg1K1+WEHP+zgxG8vm0mkZCYBKBXIMr+SbDF1ULFixevXr5crV65mzZrR0dEjR45Ut0WvoDhDkj1XnAhxRqEDR3O3yRI9MhIAZBkIi85O63Mwf8JZK/rWYH1ZKiSr25rCoCi8rrGxDBvG5cuUK4etLXp6PN/g5GJtzd69hJ/hwE/EBpFaHofGXFuHQjUFCT8foZCc+2BaWrwSsUBSEkOG8PgxlpZoa5OZibExgEKGRBdAJEH5ysZBzwq7BqyogVJB9eEI39R60PCB6Onp+fr6qtuKfFDKePaU2McYalHtldLkpepycxOr6yHLoMlc9dlXmFgjO8uDaAyFZH0Txc2LgoOePp3u3Xn4kOholEpu3XqexPE6JWvR7xynZ/M4kO2d0bPEREXJRp/N4MF4e2NlRUYGv72yI54zh1at6NABFxe6dGHcOLp2pUEDXNqypQNPzpMQQp2xrw1VcyQeQ0Gg2T5/tVzXpVg87Zy4+ohzr0j8CAQ0/4fsNMTaRalk6Odw1wRtJV0duBXGyS/6NK+gKAoOOjiYWrXQ02PZMg4epEuXfLVcBULqjCMjHnk2el+YIPerdOpE8+YkJVG8+GvPBwczeDCAnh6JiYjFuLsTHEz58nTZR/JjpGa5W+lXEWkXktka1EJUIj+vxK00cVl07/Xm1bffD18xT2LoNp+G7ggMaNhY3dYUBkXhDLprV3r2xMSEXr0w0yVmDt8HQ1y+7XVMvmjvnIOBwZveGejShX79WL+e6GjCwli+HL+NNLwLKxCkY1jy2/o0ashh6FCGDWfhGlq3ZYY7LICPCAb/qvjpJzaN5dbPTKxLx47qtuZN8rxN/ZkUhR10hw7/Z+88A6K4ujD8bIOl946gAoqigIqCJRpjR7EricaGRrEmMYm9xBpLjFETWzQxRv1skdh7x94QEQsgCIJ0pEjd8v3A3o27LJp9fu3M3Ln3PbO7Z2fv3HMOlStz+TIdO9J0PqZfY2QPHeCg+lPply1t2+LgwMWL7N/P7dukJLJXF7EzFEBX2KVpfVo0QVAQtrZs3szRj6leFarDAPgDKmtaWZnT1Z6mNVllwixvXD01reYBS5cuDQkJadas2datW/39/cePH6/Czt8HBw3UqUOdOlAI/3u40vMYXIfy8iapDG9vSjNburvDDTgLpVnDN0OO+svXaimX+Pvj7w9NoTQFawYc+i86aHZiMY9vGsI96AtBah3sUah36aZIJPrtt988PZ/1OZcuXdq1a5e/v/+JEyeGD1dxooX3xEE/QAo5kABGcKncZNJQHw5wBbKgANLKfwokLWrGAY5BA9gDQzQtRiN4wA5oANuhxuubvxtvGOqdmpqampq6ePHi/Pz8u3fvqlbD+zAH/RQrYDT0gx/ARNNi1I0h/Aj9YST8Bu9ZDLcWVbMANkJ7aAH1NS1GI3wGluAP10CVMwnvwtixY7dt2+bi4rJjx44XZtp6F96vO2igGpTX9apqoT5s0bQGLeUEC/hF0xo0iwC+oZyFeNerV69evXpAdzU8t3zv7qC1aNGi5b+C1kFr0aJFSzlF66C1aNGipZyiddBatGjRUk7ROmgtWrRoKadoHbQWLVq0vBMKxYMckyqvv1PuHXRBAZcvk5PzxK7bcENjet5fSkoID+clKfNLW0A4vKKBln9Ffj6XL5P7ZP7i2A8zn0ZBJjc3kZuoaR1lyuTJk1u0aLFo0SKgT58+qu28fK+DTkykUyfq1+f0aRYvpk4dmAoXwBhEsErT+t4f7t+nTRs8Pbl8mVGjCAh4vgW0AU+4DKPg+QZa/hVxcXTrRsOGnDzJ779TowaMhZugC8awVNP6VIZJ4S1ie5HmSkkMwsVU66VpRe9KQUFBcnLyoEGDHu0ZPnz4MyWPgZSUlIMHD06bNi0mJkblGsq3g165kmnTaNWK2FgmTeKv3+EQHAGgD8RAec34XN4ICaFHD4KDyc+nY8cXOegQ6AHBkA8dtA5aZSxbxrx5NG7M1assXMiy+XABSitpdYcksNewQhVRQ7ya/NE0mcidUG73gvfeQUulUgsLi9GjRz/aY2//gjcrPT09Ojr6q6++GjduXG6uiuu8lO8pDl1d8vIAcnPR1QUhpEEzaAYRoAOZUBmswAHCAXbtonlzmjXjt980q73csAqaoTuL3IXQkvwOiIte0Eqpw7a1tGzJZ/7cD4M20APul7XYDw9dAXkToQ3JQRzeTbv2xFxAWfoZPgafQmt4TbaH9wKBUkn29+SISGuC7EWfsfcNgUAgkUgqP4FUKn2+2fTp048fP25kZDR06NAXevB3oTzfQS/g6/X0jiBYCWK2TOYPAyyLSLmBwICPDKgigQHQFFbCUuiJ4jJTpnD8OBIJ/v4EBGBrq2kryooTs4n8G1079usRFUfdKsxPQ5wKBRBBxy2sD+TmTRIEdDVhhR8OdWk1/3Etu50S5DfoVoxhAVMqM2c3rINfYPQrR9XyOgYUMvsY+0AG6wLw+ZtQc5xPIRaAALaiKGKPD0mOWHvg/wtiPU0r/pcYVUggXsGfYA5O/6EnGe7u7u7u7qUvlixZotrOy6WDPnaMXRNoncTFmtQ1ZaWU6Vc4MA6RBVky4uVcySM1l8JqHJOjK8HvHyo50uMeihIkEvr2paAAiYSsrP+Kg449SM4dBpxhRD+k6/FQYnSJpWMZ1hNaQSSSzdyGGUIaCrDK4rIZjW5jsYx6Q5k7jh/mI5LRyZJPfJHcpCAVgEpwCYDTMBckMOGpFGJJSUyaRHIyX3yhCZvLPWfGIvydW2kcF3BDiRNIdxLVlqqFDBZio6SPALe7nD+AlR7+p4lYT+hsPv5e07r/JboHsjkk4JKSytCqhI80LeiDoPw46PvwC9whrz0LR/L9PRRCLh1A2ZBO+1BI8YPibPLkVBZQV8FaSBUy/z4H7xNpQV4k7QwYsoe4OJycsLVl0yZU/XejXBIDS8iM5GAeQ6wQZ+Gvy5CTHK6Hwc+QSZaS852JucN+Bf1MSLnHCSVjbrNVxq5TnPyVqTdYu5oLK/n5MI59iU3C5Q7Mhp2wFArgG1gNRRwJZHMTKldmyBCkUoYPZ8QIqlQhKMi8UiVNX4fyhas4luwdHLXjdCru0A8uwhEFB0NpIKNObSpbc2gP9vPJDMW7E4BTQ6J2alr4v0dwUYkJTIBIOKNpNR8K5WYOWjGYTQeZH05yTxZdQ6c/lYv5+j679uGm5E4BWWAgQwTZSmQCiqCSG7YymgRzKp/rHhTLWbAAe3tGj6ZPH3r25PZtTVv1LzgK8+DcmzUuhJ4QwDlDth5njjPeStYUEx6Fu4B7uazdz6Z4qnzGVV0aQFc3FOABZ0twiCatBFFDKinxcaVFSywFWGTRujb6lpy6TdJkqA4JUBNcCJcxI4WBnyES8d13AJmZNGmCnR2tW9umpanzmrx/1JJc4VQhzSdjDL6QIaA2VIZhZhwB87tUVSAWc8GK6pM4EE7kJnZ/iUegpoW/A0poCIUCfMBC02I+FMqNg/4mhBghVZOZmUmyAMt17NGnGL6vhrmAVtYoQCTgDhQLSIErEHGNkwJmLidHTso1alegoIC7dwkPJzWJM3upkgKqrxKmTv4Hf4AXTIfDb9A+DmpBE/Zf51sJdfwItsFWzv6V6BdzyQipLpkCzKwxdCcVQrO5CUWgV4FCJUIx/eeTCN/24ecdpIC7KQW3kVpg05lji4hdDFchHP7h9EKCrPBsyJdfEhEB4ObG/PkcOMD69Xf+E39W3gLFHSE1wDQJT1gJ4bAQPKB+G76AXensjOGinDqjcfqM1j9TlMvH31OlnaaFvwM1YRNEwTKoomkxHwrlw0Hn3eViEV93xd+eBhYYwK1cCnUp0WWJAmdDzuQgBkMB3YQIldwQ8oOIkc6sN8SjMQYC3GvhVgkzM/z8SE0mfBR/d0N6BAa9dvDyxDb4AZrDZNjxBu0rQjiE08uMhTIuOrLYmRpCiq5yT8DvDenYDhMFhw7ToC3WItbfwwoMJKS4IBGiU0jqBb4wJPo+dzPZsxahBFkhPXdRuTkf23JzOSSAAi5Rz4j/OZGQwOrVVKsGsHAhUinHj/PHHzmGhuq9MO8bF3S9OatL4nL04TNIVvIZCOBkFpsEeIg4L6BmG4pSASzdqRWEzXtev00IbeG6khZgoGkxHwqan4PWTzlN6HKMdVn0LUPE7FNSZQBev1OSj/NBfGcyPZ5KhjgUUWDCLV3s0mlmSJwXljX43o2CTHTqEBHK5nikznTtSqA7JMA0AJpD8ftTW9YDdkMQ7AKPN2gvhaXwM/6G3JYwZjLuAubVoeJZPteltgSzKJJ1MYzF3Aq3gZTsJsMEQ33a72eLBfpy/ulNxynM+vpxl2Ipt/ZTqz9Re7EaDwOgCNzxDmDANsaMoXJlZs8GkEoZPFhNF+J9546hLWbWnEohT0Al+FyJPiwFs730FBIwDcsg1nfE6LnK7u8tSoQCiYKeIBNw8/3651p+0byDNrm5lsC/aZPM8BZsuscIWzy2c96HO5mIP2dVfYgFa/I+Ya8R99Ko507NGTRuAKBUcnw6FY9wrYhcPT5pQPfukAw3QA75kPf+eGfgOxgDftAK+r7ZKTVhJZGr6GzJlzdRurIxnoowy4vBZ0nK46tKdFsBtR40z89helM23URcAWdTTByJP4Hvlwge/pdq8C0HxrLCDxcBtdqAEsKhGUD79rRvr2qTP0xchLfx+4JGY8j7hKDDXAdXAeskGAvJ9OfofuS7aD4Lib6mlaqMvEB7o4WJnFPirmSitn6matC8g1aI9bmfgq0nE+ZSmIVbW45OodNfKJWsaozPXw98h+ESuowBA2gJDR6cLBDQeCKNJz5dQtMO+kBrkMC8MjfoXdCF+f/mPB0Dblejwp/kpyDrB+D4O5vGQz4EPPbOgL4xMy8ArPqYXnsR6XJwHLePUfHjBw1EurT6CYAIGPKwh9rvYNR/kWIk3E8FOFuTBSexM6JAyvFmtFyFNbhrWp8ayN9vaTStECvIF/E//Q/AxpKSknv37i1fvvzRnnbt2j0fitKlS5fc3FwdnQc3gjt2vMnk5JuieQedWXOY8b7vAHQM6LSanMTHd3NKxRNP+apCyBv32gN6qFZnuca9E9u/4K+WKBW0/BGAGrD1lec8vLACAQrZixq8tgctLyVO4YQyj9XNyYpG/wfsvib7EsUfTuaN58nSrWqzTYquCbJ8DD+EZRx2dnbOzs5mZmaP9ggELyjcPGnSpMOHD3/11Vfq0KB5B11iWIHP9zzetqiCjiH/a09xLh6BCESak/b+IBTT4Y+3O6XecFY1xdgRpZyPp6hH1n8XJdB6AUBuEhs6kxxG2jU6rdawLHUSY9HGPWcZBtakX6fN95qWowIMDQ09PT27dev26mY1a9ZUeYT3IzTsoKVSad++ffX1n52JM9MpLlEI8mTbYbtGhL0LqampQ4YMedlRmUzWokWLstTzMvTFUn1RcnqRLr+1eceuoqOju3Tp8sJDQqEwIiKinJisQq5evSoUvngRlFQqXbdu3dmzZ0s3RQJDC51bmSXGso3Dy1Cg6pHJXvhPC0AqlY6b/tNCGytLndjMYiPZxm/LUpiayM/Pr1OnzmubCYVCKysrNWkQKJXa561atGjRUh4pH+ugtWjRokXLc2gdtBYtWrSUU7QOWosWLVrKKVoHrUWLFi3lFK2D1qJFi5ZyiloctFwuf8WKHC1atGjR8iao2EFPnTp1z5499erVa968+cqVK1XbuRYtWrT8p1BxoMrNmzejoqJOnTqlo6MTGBjYv3//V7ePi4uLjo5WrYbygLe3t6Wl5QsPhYeHp6amlrEedSMQCBo1aqSrq/vCo6GhoYWFhWUsSd1IpdJGjRq98FBRUVFoaOiHF2FgbW3t6fninKjp6elhYWFlrKcMcHV1rVixogYFqNhBR0VFubq6Zmdni8ViuVz+2vYzZ840MTF5mS9TC0qlkzJWTEmc0FXB4zhys/R0q5SUJCenPKNnE3EZkmuviE8T2GYJ3ijDwNWrVyMjI0eMGPHCo8HBwR06dPjX8t8WsUJWN+q4XCg+69KQpyPf7BQJBsrc2yLXkndO+Ld3794ZM2bUr1//+UMymSwoKOi1P9WaxTAnxz4hIc3WNtvCtKI82kKZmiGwjhO5KV7+F3PlypWRkZFi8Qu+QRcvXpwxY0arVq3UKVkDbN269eTJky88tG7duvPnz3t4vEmO3PJOxdzoWrGnou2qHxY4b9q0admyZRoUo2IHvWDBgrCwsIyMjIiIiNFHtLreAAAgAElEQVSj36gg9MCBA93c3FQr41X80xepKVJbEg7z+Z4HuT62bmXRIgICWLuWVauoXv1x+9QItgbh2ZMb2/D7+k1qXoSEhMTHx7/sqK6u7hteGRUgl1HbDk93Cgqa7l7OsdjHh458T1Y21jW4/g+99qHzTvkhc3JyXnbDqFQqK1asWHYm/wuuXKF/f3r2ZOs/1MrGtQSRDooEjO7SY+fj1F1Pc/DgwVeYXL9+/XJt8r9iz549LzukVCo7derUqVOnstSjFnYuIvh3mtWosvWfxl2aj1Q6alaOih20n5+fn58f4O7uvnz58rp16z7TICIiYtGiRY82d+/e3bt377Jz0LJCcpPouApg5xDSrmFdA+DPP1m/HktLPD3ZsIEpTyQPurqBlj/i3Jgan7Fr6HtWlCjsKFZm/HUcoLYNGXexsHtwKO4wfY8CICDu6Html2rZsIF58/joI1rUZmAfarvTcxdrWmNgQ1YM5mV496BF4/w2j7GDGfILMecMOn5E/d6alaP6ZEkKhaI0icwL3a6rq+uTdxYHDx7Mz89XuQZu3GDZMgwMGDGCJ/OYiHQpzGL/KBQlpF7BwPrBfns7IufQ+D5XS3DweaorI3vSruLcmLSrGJWbyns5OfzyC8nJ9OnDU/lclLAKzkMT6I6DK0npFOVTmM+9PE5Nw9INn8GIpYh0yUvGwIa0SFw+tExGryIigpUrMTFhxAjMzVEqkB7m+B9U7M7txhjKKcwiP42ibBQl6Jfh5JuW8oCdDWfWUut/HNZRmOppWo2qV3FMnjy5RYsWpffIL1zFIZVKKz+BRCJ5YYrVd+L+fYKC6NKFunXp0+epQwIBSgU3thG9h7y76D78Xz/Bljlb8D3O6b30rfjUKbWCiA9lhS+nfuKj8SqW+q8ZNgxHR/r04euvuXv3iQMr4DIMhR2wHVtnenSjqjXezrSxpW4wCDg0HqDZTDZ157e6mLtgW+slw3xw3LvHoEEEBlKjBgMGACT0I/A+l9xov4SfRvPd1+Sns6AyBRl49UZq9roetXxYdHEn9R6BWWxNUQzSfF0YFd9Bp6SkHDx4cNq0aTExMart+S24cYP69WnYEODXXyku5mGxA2SFGNgwcBfAnq/IuImNF4DteXZcBGM4Abuh5ePeRLp0XlvGFryehAR69wbo3JmzZ3n81PEwzAM7GAl/QgATljFhGReWo2uMjSc2nvzZFMDeh37HNCVfY1y5QvPm+PkBLF6MUongGHZb2FSLjE3kzcT5K5qpJfO6lvcDySEmfUv9uWRFiRNroOmviIoddHp6enR09FdffTVu3Ljc3FzVdv6muLlx+jSRkWRkUFT02Dtn32bXMOJPsO8bTCtyaSXJF2k1H7s6FNViV1MyinCKx0efWT+xVMyXXzJt2mvG+qYLW/Zibsjqf/DwU6NRJ2YT+TfGjrT9FUM7rK3Zvh0PD7ZtY8UKgJn+pB3DRsSNi1wz4JNU+iRTuJA4XzqcxKEuW3oSOguhGKv3vxjRv6ZaNQYO5NRa3JPwLWa2Mfrg3p3TfjT6G10THH3IFTNMwS0xkybRuvVTp8cd4fBEgKav+2BoeU9R1CP0Rw7+iBi5m+brwqh4imP69OnHjx83MjIaOnSo+qoMvAYjIxYuZO5cNmxg9RM1LHZ/ScsfGRzGjW0cm0bvg3Tfwp6vAI7eo4YXA7LRb0BADhNacyeExYuJinrVQNtWcPkK0fdYtJJ+L05XrxpiD5JzhwFnaDKJvSMBfv2VY8eYPJmJE6lcmS0/kpvIvBzuNMPuDqer0jmJbb3wyMPwMhcWUpCFaSWsPLCrjaxIjVLLOfn5WJtRJxeREXmGmLliXo+YdJpvo1ozKlVmQlP6y/jDiAMHmD2bnJzH58qLOTSBnrvouYtDE8QChebM0KI2juxAKaCtAXoi8fUMTatR9R20u7u7u7t76YslS5aotvO3oHZt/niuBFThPSyqAnh0J/kSFp6IRIh1kBeSGUfjVdAR8y8xPYZxS0jD0ZHLl3nFCpNrYfj6UliEXxty1PCo8xGZMTjWRyDAxou8ZABLS2bNoqiI0mI08Zep+BHCfLKkiESwDvlGks0Q65FbkeKTCPSo2YOaPVEqWf3JSwfKy0Nfn5cUCvkQiI+nsQ+1XMi7y6ULmLtiYMWVPCz8aVCP+0e5dh25HZJcJPq4uXH3LsbGD87NSMDY8cF6RBMnE0miBu3Qoi4kxRQ6YnsI70OEBmtajaZLXpUpzh+xfxS2XsQeZkc8i+tSeBs/Mb1aU+Mjdg7B3YILwbjrcH08vzYnPh5//1d12Kk/n/gSeoLMdKq5qFG5ayv+7olAQPyJB+vhduxg3jyMjLC2ZvlyWvbntxYs341JEokGbNpEhgifNWy7RIUbVFiLrjl/90AgJOEkbm1fMERREZ9/jlxOZiYzZjyYwf/wqF2bsWMxzCIhG10Ft49RYkRRMt/d4ucifi9inBVxYsIqcnY5t27h6gogl9OvHzk5OB+nYATu7hRmZRS/OGxSy/uNzAVJDHOqYqlU3pdoWs1/ykE3ncatA+Tcwe9n9s1liROiuwyIRbGVGp2xmEPyZdp14LMiZl9BqSAqiueKJT7FjTt8Hox+Pg4VWXtIjcpNnOm2kZi9eHTHuTHA7Nns349UyqhRHDvGx3cZ+iV7smjthvM2LuTS+CKJP1IQi/0prL2BBz1U7/agh2cICaF+fUaOJCuLwED27VOjORpEX5+dO9m2Bbur2IIAdp0jcBlDw1i8g6VO/OxL7ZVsHYZRCdu3IxIB7NtHpUpMmUJOFgMaUbsRn/7DnwGaNkaLGjgnwcEGuyxyLZXr7z25XEAj/JccNFC5OcCNGxgaUlMPzBDGoxAiVKBrjJ4ZWfqkFvBpaxBgbkJODocPU6ECtWs/21VJCWfPkiPjh98oLGTNQfUqN7LHu9/jTaEQ8SVIQ0eATAYyxHrcjcVcgZMQXTPsTaj+B4BCRsxehBIqNn2qh2eQyR78GkkkKD7o2VVjYz7v++B10jn+PknWQarfppaE8zrstqCBmC5dnzolI4PYWK5do3JlUizx6F7morWUFSUy9O1xFFHgLlCc0rSa/5qDLqWqC9L9fGbK/Vi6GSDuzI2GnBmMjj7xJzCyJz8Nrz4cm82y+3QLZM2aB3eXj5DJaN2aBg3Ys4eqVXF2fupoGTDYinZdsbGl8Dbfj+HsCdbOwNCUCyFcl9I2gdDZtF2MrTf/C8Deh5J8Lq181XrBzp3p1o1z57h9m1GjytASzXFsOlE7qXabA6dZBpnQTYjxTpbVJHfi4wU8sbHMn09JCS1aYG7+VJSplg+Pzo7cPMIxAaZJyr5GJGtYzn/SQRPJr51ImYCOBLMOsJGw/nRdz8audFnHziE0noi+JREX6dWGYd+iUPDJJ0+54MhIqldn2jSmTqVJEzZuxKxsIxo+TaPtdXJycFgLJ1g9E8nHzPiTzd+wPwTfETj6cWUdusYY2j1YE7auHcW5L825oa/Pjh3cuYOFxWsmdj4Ybu1HYsiIT1gXRw0rLAVUiCW6MqP+pvETs0AhIUyaRIcOREcTHMwHkG5CyyuQHKeaB91nIE8URg7VOmjVUXKfs7+Qc5OaChzNIRhetgDDGqKxseDEL6y/hFMvXG1Ji0Siz50zAMlh1PwcYTZRSQAJCRgaPt2BNdHRyGQUH+HLaMx+g8GwHsJAtWHTMbAEdGA42D3erVByLwDFPYqM0W2BVWVO7mHYbFIiMBcCpF3A+Br6c7l3BUUJ8hIKMpG80vMKjlJhM7jAYJCq1IryQdROolZinYCFgCUiriXRxp0CXRSZCARUdOD6fYwrkJr6cO28HFYSsIfLNmCD6QIGZsF9MNCwIVrUh1JKfCTnOmEpoLKqg5zfng/IQe/5GnsfXK6xN48OEzHtCwdf4mhsoRMxHky8xU+jOHWLXaEUJJOTwLkL6BgTtYu7l2gUTGIUdetiaMgTCZ4AbG3p1IkutZgUj/0sEEAbaARDYKqVVfX4eFNVmFQMvWEm5EMvOPD4SOotrOWI9Ci8gUxA59Es34jJEhIFpJuzwg/TZDqOR1wb756srItAl8YTHqTuezGRMBN+hMMwHuapQn95IuEEF37mk0wiEgkq4Ocq1DFj0ilywb0E31gKIrllyfYjCC8wr9T8hZCGyzwK23JzCwu8mNYdvoLfNGyLFvVhbMPBWzSHS0qlSPPusawVnD9/fuzYsY82ExISVJYsKTOKgIXwDzX6ckeGqQ/chBfnF4eBnLrE5w3xnoE3NDXjt+svaPXco8EnOhjIQAMohNJMxxNhJ5jAl1ZWP8FH72wPEAue0ASAZZADD9fkch/dLICMTog3ctKZn/4iMBCgaVMGHIam8AWAVzBeleC1GahDIQg8wROaqkJ8OSPuKPV8sFZS8T6mW/jEEASEuNNvIdYPc2aNgTFPnnMY1iI0omZ/OMuvO4EP8+JoecS520zyofU5MvYL+rRE0+nRytpB+/j47N+//9FmtWrV9FU142niROR2HO4RuYKqzqwMQbyalr9i1+PpdufgOzKTOXSfrSlYNyIvDMc3m0EuDRbPS8GrN/WGQR34CprDaagAS6A3rLx3z1U1FuEMx6A0k5EcjLkXxwwvZHl4gqIyMilVb1PrK+oaMWUKDRpw6NDDyBob2AY1YBuseIOx6sJkaAiHQVX6yxOOvlz6GYerGMVzX87sFHboEiYkMPDZaeU7pzkwGnkJHatgsRh6QTgkQRycB1vN6NdSNng7sOI8NgLWoawmIlvDcj6gmLHWC0i6wEEbahtwYzt9jtDtFHsGP9fuazKW08eRFW4sHkLQEMLP88vRNxpi95e0mMuA0ySdJ+kcuMNXMB7CYC8I4VtolJTkqyKTisEAPKEWSEDJ/Ma4f8R8OUl66CfTuoA7VSiyoHZtgoIYM4aYGH76CYBf4ThMholQ+Q3GqgX9YQxEw08q0l+eqNQMrzok5rAR/qjASRDKOHWKpUtJSXncTKlk3zd8+g+9D7ArFlkRfAsBMBsmwHlYrDkbtKgfi2I8BcwCuYAA7Ry0CpGa0nwWQPL/sM5G3AgxSEXI7lEgwsiI4lzEIpR6xGTiVAUhBPZm7RW+34mOFCA3l+fqXT0kF4wozMDSHcDRj8wY7OtCa3iUT+fR6rQQFZmUDN4ULUIgQKcL5JGXSZMR5OZiYEVEPh2u4rKEzKtYVKRDBxq7YOyKSAq5YIFyBrIcJG+e0bjDG8yEvM+YGFHUhGopVN/NdGcuKvGojmcN7oZh1gAdI/LykCjQM0VqCBJMq5DdHotJD8/XdNCCllJesRjp3dHPouHHtGuPdXVBbuvXt1czH5CDfoRVAHeHcKk3lptpUEyYFRtc2JFCdzfuxdC2gKON6KggR4h4DQvF6LQmvh99VmFpSW4u69dj+uQjviz4FARwBmcL9lfCdhzhfxGoKi/8ClyIPcD2OohLaKNPJSMqfsTUNuTZUCmZLEOGNsL6KoOMuDOEW1kIhBgoEFtTqzFFF8jOoESK3IQKVxF8iG/0m3NsN23bU03OYiWOUGSDfTHRBhwbyORVCH9BLqSZIYb16HIVYS5naiCuSNY5zNNBAmt457KNWlRAYRabP0PXiPx02i19kFpHtdyrRPPDJB3GBOUVzX9rPqApjkeIDOl5A91IimxYN5mEZQyP5bc1LE+g5xyWKqjfijpV8NfjmC/OVWEreWNYupRNm+jfn9+eeUa/DAaBFayhaSNceyHfT2DI42os6iMphVHVGTGewTPop09xMRFONBtGUxeO2OLYjuCO3BcTv5lwR7Lgo0wqtaMoFTZSkIJlMA7JKExJ+UXtUss5A4NoVZG+X3CtK9li1kH6KJrY4ruZUx3JPklKLX60YKwbn3qyzgfjGeiG0uMyghCoB/9o2gAtAFxYTp0v6LaJtos5NkMtQxTeYI+QGBv2GQl0ZWoZ4m14Txx0ZhTXQ8i7+/qWpUisMf4EsScy0DMFMDFDqURqBAKSq5NXAVNDHFwR6IAYpRK9ZAhBvxDZM++KDPRABmYgo5If3t5l4Z0BuRyRIdFmxJkj1kOhQCbDdzDDjyOzQlYRj1boSJDJUJagFKDchrTwwblKeDCBpoeyuCzUlk8STnBzBzI5OiKkRuiZck3ANQOqzsDcGqkOAgliPYql6AhRliA0RKakWldq6CIuXTOjD5r/omoBUMiQFkAIumko1POmCCFThPMSSmqWB++o+Xv413NjK2cWUTWA4z/Q6U8sq73RWXYTSK7Kp1dIv8NfLqxpS79q/BFMrDnf/YK5mNEiav4DztARvXb06YSbO9FXWb/z6Y6+gE/BHNrCJ/AjrFeDkS/C0YGcM3xxheJiKhsilTJyJMHBVK1KSQknTzI0kUwhrkEU55Oq4EBfzOQIDGAQ+qakLadoM+JibEaUkeDyxt6RFOdh6kxHc5ZEc+knMpVsFtJCCR7wNRTSbAK5W6mooK0edhkkn+AvV+gAXaALOEMKbNK0JVoA8GnE/fZc9cD8Op9MVssQkgBqbSO+M1VQnDVRyxBvg1octFwuVyqVYrGKOg9bRbcN6FlgXZOIDXz8/RudJTLGPpH7F6hQAYdiRppTnInQhOxcxCUUFlDRAiSQD2ZU7sXeCJJlOEQh2g+NnujIBg5AIphDJjjAK8I9VEpWDMGf4PcDYjF7elGch4cH+/aRnIy9PQUF5OTg8Cf5kQi34mrLLSXWDTAOhvHo/oppGsVJGNZ5/UAfKncvPKhcXlJA/wYk7cMM3KeinwfdH6wTF/TCMJJcW/ZUICkZS0v0ckEM5pD/8B3X/NN8LQB6B5H+g8QFQzGi4fCl6ocw8Ua3J9lLcBuVL5mv+v7fEhU76KlTp9arV2/8+PFGRka9evXq37+/Cjo1sictEqePSLuKscNbnCgQYliXa6OI3Y1tbWqtRCDGqBCWQsHDWHBzAOzRicapMWyD54YI2caBA9SqRb9+D/JPlg36ltyLw9IMeQkFWUj0QYHOWpzOw8cYdXuw5kS/OsRxeA1/m+MSzWATpE4AOnbo2MEtWAIS4uoQeRhzlwdVvf8LKOQcGIPsPmkH8I6jmgckQTzMh1TIBHMwRdgAEzh0iC1bcHVl8GB0S3M968N/Iy3Je4M9N0ZyKx5bO2o1VsvvppE9ew4SW52YQzJHKzUM8HaoeJbl5s2ba9euPXXq1JEjR/apKqdw44mEzmaFL4ln8erz+vZPEjOTyA14jyLjOudKI1Z6Q13wh75Q8LDdBJgDvnAa+j7Vw759bNhAv35ERbG4bNfASs3w6s3KBvzZlI/GIRDCCgiHINgG2x+3vOrErBP0OoTgb8Y/WUitBHpBS9IqcHwgXr0eV/X+LyAUcz2E6A1kJqMfBOcgGerDRRgIAx63vHKFOXPo1QuFggkTNKdYyyu5dYaIK3jrkRnN2TeLXXhbMpwJ2YvZUU6FyK9o3kGr+A46KirK1dU1OztbLBbL5XLVdGpoS48d//LcuO34TsS+F+aN2NqIeoUgerjadzvcBC8AbOElQxw5wpdf4uODqyv9+jF8+L9U8u/w6vP0b9JhmA+28M2Dot2lhJ4gaA6+gfhC0ydjkWOhJrQgPpZalXFwx8H3QVXv/wICIcNugD9HfUgWUHE4rIGRUJoy9JPHz1JDQ+nfH19ffH2fvoBayhNxB/Adif0cLK4T4o2qAsKe5Fgoc9bSsCG5udKOHXFRZ6WkN0DFDnrBggVhYWEZGRkRERGjR49WbecvQC7nu++4coUqVRg6lLFjKSyka1e++ALgzAKyksgMJmskIiV2HhDIgWPs1KdYhHsJ6w4g06NqVWaPx+EHSIF6JAVwcAJCMb7DcW2Dnx8rV+LszKpV+PiozRJ/OA3msOWp/CF79jBnDkZGjB9PvXrgB0thELcn8ddRMtYgrcuZQhQKLp6l3+cYimki4q4lOY6McUBewso4rMJxkHI4BqccYv/B/OVVFj8MLq7gyloMbYm9iZcAAfy+G8kadqdyq4T6Mi4vwsCNpmKsHv5JrlePadPw8+PAAaqqYXWtFpVgXZeoucTOpRj03zwC623wq8eWjkTKSNEt9mzLfbUM8uaoeIrDz88vODjY3d29a9euly5der6BQqG49QSyZ9e0vSWrVmFvz/79tGpFx47MmcPu3YSGcu4cd06REo5NU4osCZOTYws3YA47BYy0pqsuN8U4V0YqZdw4LrSHQNgLBsR8Ttf/Efg3J+dRkEH79jRowLffIpGoLZP9j5AI6TDj8U0xkJPD7Nns2MGqVXz9NUolDAVj+Ia1R+iwnfFXuXOYiYOoXYmiYjp0pZUU/WLs0rmQwnQBIbuZ60LWFGxPUXsWB8aQGUOrDy5T3ZOkXiH2IL0P8MkMliXiZ8naCiyB0XfRE/B5bUKdqCrDP5VdT0xh1qlDnz6MGUNcHD/+qDn1Wl7J6q2kCkgzIFHAnXS1DOG4DSc7DrdBXFOvyWm1DPE2qHGZXWl572eIiIiYPn36o83U1NSCgoLnm70BhSAiJoZ27QAaNODevQf3Pn5+3LqFVIajHzl/IJ2C/H9UWc6tmuTZgBRBT+IvURJBVXsyC3F1JTMLGkIu8jqYLEfPAsCqCjm30bMgKIigoH8l8g25DB+BELrBE4WEk5Nxc0NfH319zM25n4KhNYyEXNKcqNGQaxHIrIm/TORVrMyZPQ3lKZrnQS7nTGmYjKgIm+aEViEggKpQdaA6rSgP5JIVi31dBCLMKpMH0VWpbkxYKllCPrcnW0yVzgSvJtwZBCiVCAQPTuzUSZuMv7xjoyDUnJF/cP4SZt+rZYisG3QaxJe9KcyU/F1XLUO8DWp00I0bv6A4qaen58aNGx9tVqtWTU9P7+37HguXoYjOHzFmEgMHsmULjRszahReXqxZQ0gI+go2dsHRi4KvMLVHUhUbIemW6Bvy11LOirDL46/dWNlSpQo9xBTZYO+IaQIRJpz2wcea1DNYXYde8PJSfqphMLQBARx9Kk+piwsxMSxfTnEu4y5iOASSoBhc8ShimjliU0wS+GkzWbk0TGNyDayKmCugwIcZ0Wwx4VgjQpLYflXN+ssD0fAFWOKUzokCDKzYNBMZJJ9kkpJPoQQq3UQYy7kzTLImVIxZRQQCiIX+YAlZsA40/1xIy0tJMKNKJn90wASSBbRXwxDVeyMfSs4CihLy3VuhnieRb46Kpzi6dOnSsmXLdg9RbecPiYbbsAv247OXJb+Sl8eoUWzeTMuWlJQQEoK1NYa2BG7B2hfFVDzjufwFFeQk16GDHvY9aNYJn/lMmoKjI0FBzGxBpTlEZmD1A599i4UtGbH0SEB4AH4DdRdRbQBHIB++gG2Pd4tE7NiBkRH17lDva9gC7iCFjQgr8XFtJI2QV6B/IJ96c8+IDB8Mq3JGj7SGyB2p3xrFl2zthtkVNesvD8yCBbAJva/57BOKczmSQGYe/vr8LkDkQA0/dnsxxYz6blTpjfWXBCwHYA7MhY0wEtQYE29paWlqamppaWlsbOzq6rp+/avCnaZOnfqyQ+fPn/d5+lnIoEGDBgx4sCIlJiZGIBCsW7eudHPVqlUeHh6nT59u1KjRM/1ER0c/+o9bOlxERIS3t/dbmlW26OdwS0SxkHgpEqVahrDJwngsMY1gClXUMsJboWIHPWnSJH9//x0PUW3nD5E9XJ0qBB3cXAkKonYGjEE/BL2dzBvJV73Jy8bABu+++H1Hjg6OdSCEfH3yBDi2waUdvezoWwlvL2q6kBNHnh3npAhcESiwq46TIxJ9EIJE/Q4aqAUrYTi3jhIyhsQLD3brSfjMjHpSRKU1t5SUFHE9BKUSZQnSHCRi7q/n/iX09Zk+gp6uFOrg9DtGprhUpE8fLCw+5EhlpZK4I0TvRlEMerCbu1NZtom1O7EuYc9oLO2oIkBqg7AOphWp4k5JMzxnUiUAQemHvzSUnzII6Q4NDU1PT8/MzJw9e3a/fv1Snkxz+jSvcNDP07JlyyNHjpS+3rlzp62t7fbtD5ZgHj16tGXLlp6enqtXr35FD281nCZRgkDITSUSXbWNIcOwHrV+x+qz8vDFUbGDrlmzZs+ePVXb53O4gw58Bu2hC4hgHHzKxl+5tZhhf7N6LQcOYGdF0cPZbUknJIO5HIjrEQ7ocTSI08Px/xr5ZnqdYexg+ucQ3BM/M9b1Y2s4CVG4uj4conPZBcTv+4GgjiRE0eVjLv0PFBAAJ+A+TIP+FJzl8E2yf2TTdUafIvQYW25x+xbKVCxS2DCMP/ZSTQyDoAROQh+4Bh+Xkf6yZ1sQ10O4c4a1UShbEteWxheYHs32Xdwu5u9fiYvGTcnZS5xczOndmBbi90z42UgYAoNgMjyfPVz1iMXiLl26mJiY3Lx5E9iwYUO1atVcXFxat24dFxcH9OrVSy6Xl97zTps2rVKlSlWrVm3VqtW1a9de2GGzZs1iY2MTEhKAnTt3zp49e+/evSUlJTx00JGRkb179wYUCsW4ceOcnZ1r1KixbNmy0tOfHK64uHj48OHe3t6+vr6HDx8ug6vxduTpsa6EZCUbsrmqpvDOATAPBkLvrKwv1DPEW6BiBy0UCq2symAWbzH8DH9CaZaJVXCa64W0mkgJfF+LsbUwN2Tjw0oiHvcxj+JyFcy3ch7a9aN7NdxacW0QJpHs2cK87eyMQlKZwFiaziUwBNGSh0OoIZz0Zaxezl97GPE3Cxawbj7EgD1Mg4XgDd+xzwu/y/j+j3Qhk4fy+WdUEeDdgc/mowstZ/BtIroeMB7CYTvMhA3vR8aVf4G8iJxEWi/g4++xqI0sj5Uu1KnH5y78KCRNwkkRXmJKutCoB+6f0mcG/UMxe2ZlazXYD+PhADiWgeqioqJ169ZlZ2e7ublFRERMmjTp2LFjUVFR3bt379KlC/DXX3+JRKLS2+3du3dfunTp+vXrNWrUWLHixZVxTE1NfX19jx49mpeXd/78+e7du1epUiU0NDQhIZlzvXgAACAASURBVCExMfHJR0Hr1q3btWvXpUuXwsLCEhMTS3c+Gg64du1at27dwsLCBgwY8NNP5a9uw8k8WuswZSgz2xKhnikOrGE/TIC9RUXV1TPEW/D+fnVtHr+Um7NyMqlKdm2iBByv4ZpNzj1sV1DQAj13lHZELiUzl6gN5EFSPN5ZxMix1kciJD0an+bEHkLkio4JOiYvGKJssLfm6n4q+HI1FAcHsIQ4KIESwq6S042MQk7MQwnFSmLOYm5NupKK1dARI4IG5yiJxcAEnAAwArXlNS8PCHUozqM4D7GUzGju6mB3h4i73CrGVomNnHtwR0l4FN5KxEXIPBBKXtSR5OEVUy9NmjSRSCTFxcU2NjZr1661tbVdu3ZtTk5OQEAAoFQqMzMzZTLZoyQ2lpaWW7duPXXqVHh4+J49e5q+PIKmVatWR44cMTIyatiwoVQqDQgI2L59e506dRo1amRg8LgG+YEDB4KDg83NzYERI0b07dv3mX5cXFxKHXq1atU2bNig6gvwzhgLSCzBbSkhCnUG4YvK5vPwJry/DvoJFrYhbTEBQvZeZyB0LUR4i066eLQgqzF6qZyx5f5fNJewewsBFmw5wgwJwclY90SxkPh1hP2BgTXtV2rYkNFr6PcRk2dR0ZxVYWAGvaEB0dnIM7GYRPp8rq9E6spHYhafRSDASsDemSihuoRdodjLaddQw1aUGQIBTSaxujkKGVbuZHryxX4uFLIbTkI7JW6wXcztcO4bUa0DSReIP46TSur5/huOHj1ao0aNJ/fI5fJmzZqtWbMGKCkpiYuLezLF2MWLF7t16xYcHNysWTOpVBoVFfWynlu2bNmrVy+hUNi2bVsgICCgS5cueXl5LVs+VQXmyc4FghdMERgaGv5b48qE71wYGU19OXow9YW/tR8aH4SDPnyDtckYGWF0CMuBjJ9K3nwqNoDe3F2FPI/Y83S5iI4RJieI2s3YxwuxEULntpqT/jQWbmxLfnpXH+jD3c8RW1IniOv7+TiBWhH8Vpf2jWg1n9M/Y1aJqslgCoHAf6vstGtrXFsDbOqOR2UkZ5h7hLH/4GRJDxHjMxh/mKYV6LqXKtWJO0zsYQ066Odp1qzZ3Llzb9y44erqOnXq1PDw8K1btwIKhUIulx89erR27drfffddUVHR5MmTnZyeva27ceNGhQoV9PX169atm5GRsWnTpokTJwI1a9YsLi7etGnToUOHnmzfvHnzWbNmBQYGGhsbL1y48NH+0uHUb+47Uyme31vSYC93jyL5T3zOy0FK6rfjHLQEf9j9eJ9fLRY34ZYf4zqRncKZkxhdR7aH1P0I5IgMcfTj3GJyk7i4Anv1hWurkGhoB62J60qYOcY70V9F0hkEcaRIyLtLQRZCIVm3uLEN65pQF9ZBAvwJH2okdyZ8Bq1gOjwz/5iC/TXO/8HVT9i0HKebSKriFkmWgMuXSVGSvIPcJC79Xt7e/Tp16syZM6ddu3ZOTk7nzp1bsmRJ6f7mzZt7eXkFBgYmJiZ6enp27tzZ399/+/btJ0+efPJ0Ly+vs2fPAmKxuFmzZk5OThUqVAAEAkH79u0lEomXl9eT7bt37+7v7+/l5eXp6VmnzuM8tKXDqd3adyfTlZr7uCdGpykp6lvIUY547+6gv4atoA8B4PsgWei395jvTZP9jK6PT0sOLUfhRno6RX9ivhugwbecns++b3FpiXtHzRrwZoyAX8kVYloF20MU2pPpTtynmDsiGMbeb6g7hLy7HJlMk4mYlRbtDoIx4PJh1uQGGAeD4GOYCH9D1ycOfUNYHSS18dlH83tEV6P6HUY3ZFoJ2YvYsJ3sPez7Frc2uPlrSn16+otDk/v169ev37ORUI8yQT7pkYcOHVr64vz586UvCgsLHx3dvHnzkz0sWrRo0aJFpa99fHxKnwEKhcKZM2fOnDmzdP/IkSOfGS4sLKz0RaNGjQ4cOPDGxpUV1hZkitGRPygD/R/g/XHQubnoKkEHndJ0mu4Pk+iDThyjV7OjHcPGwiFuDiPNlK5dEYoePBQSFdFQ/Zmb3gq5nKIi9J980pEH+iAkNxejfIoduLUVY1MqZSFtSJQZ1fZjWhmB8CVVjT/0mtzEQn0AGkA4qamYm1Ocj0CITiJHzPllMTbtiAvldyFzf8JUwOOkI7U0JVqLKtGJRTaCu02o5oSivqbVlAXvg4O+do0hQ0hP5/p1togJt2D0dMSR8LD21bVGDKlKiZgmrenXnGvHqWjKku8xccazE97bwRjSYRnlITYI2LGDefMwMsLamuXLEZZAT1BwK4n+xVi5EhGG3BhdXdbkcnI1JSswz2bfKHKTUBRj5kphFl3Wof+fikvuDsHQGsWvNI/i+gKK02isi0LIbSUWenSswT/5DJbxkQF/nqTFIhzqaVqzFpVy1QeDn1j9E22g2IYemtajfsp6DjotLW3TE+Tm5r4+od2MGSxbxp07LFlC1BQK7dh2EHY8/nWZcZllSzg9leb92HSaoXtwboBtLdqv4MJclINgEyyCH9Rt3Zsyeza7d7NtG2ZmHD8OW6AhbGG2Oz9J2biR2yV06UD4QobX4HIycjm53xO4BUt3RFK6bcTvK87+qmkzypj+EAz3meWOtBYn99HNlKO6ZHjRSMT21XjqMdWVAWMZd5DuVTk6RdOCtaiakdv5XZ8xjUisyvCXxmF+SJT1HXRxcXFWVtajTYVCoVS+bsG5TEZpQiVTU9LTueiM0p3O+k838IEKeJsTvgcLUyT6KEpQKhAJUeoiAAzKQ+DmA4RCShc86esjkz0OXpcp0RMACAQoK0Mf+J37AVjaUJIPwMPsaxIDdVU1Ltf4gi+p4RgYICtBrAOFCIUIxRT7YFsJO2tcKoIBYiXKMgjQ11K2KCDdCLfjiIahuKFpNWVBWTtoBweHgQMfJ72cP3++RPK69YyjRtG7N87O9OiBoyOFhTyzhL60gZsb0dF814cDo0mNRChkz9dUC0L4I2yFm7DwJQOUOYMH064dNjYUFjJxIhRBNzjLyJsE51N1EKamLF/Opk0UFzNsGCI5G7uReJb0m8jy2TGI9Bt0WatpMzTEpElUr45/INmp1NdHGc4uEfe+odCE0XfZMpF4GfecaTxL00K1qJopU+g/AWsBuTCggabVlAXvwxx07drs3UtyMmIxiYnUfS5J66MGDg6IRBRmIS8BJSIdpGYwDhLBFnQ0of5FfPopbduSk4NDaYFaMeyEO3hYsE9McjK//kpaGklJPFoL1WMnuXfQs0AoJi8ZI3uE78N7pw7MzUlK4sIFKv2/vfsMjKJaGzj+n23pvYcEQggh9CotNCGAUqQJKAYEAQUsgHp9FTsqCBYuojQVFQEvooDU0IISkCBFCC0QEkJISEJ63Wx2d+b9gCKSQsCFLHh+n5Y57Tk7s0+G2Z0z9dFno1Lj7Pvnm2nisVSKZOz90NzCMraCdXv0Vfo/yfK3GfQsgf+KB9/cJR9ynY4rv9L3979BBcDW7e9lVnTj5l+cnP7+OyEJAgF0/DERPz/8/K4pl3AO/OO1i/VN5w5Tqf78O/3nc4/+eDM1SEE410pMwh3h7MUzt3FVWGtz192oIgiC8G8hErQgCIKVEglaEATBSokELQiCYKVuS4I2m803vv3kFihmjCWW7/ZfqLyotiP4U3mx+MHyPcV6Dq17goV/xTFz5sz27du/+uqrTk5Oo0ePHj9+vMW6TtrBnvewdcXGicFfI6kt1vO/Sl4SGyZg71n7N4ubDayNRDajz6XXewT+a5axvkdpzcWs6IuNM6XZDFiCh3Usq3CXs/AZ9NmzZ1euXLl///6ff/756hJZlhEzi8c288h63Bpwbpsle/5X2TeXPh8y/Hs6Tq/lm8VPryOgEyPX8sg6fha3Zd/16udsp+1TDF9Dv0+JmVXb4dwjLJygExISZFkuKCjIy8uz/BLgKi0oaDOQD4DRwp3fs0ywDaL/WENZNqG1A9Da1/LN4rIJrT3oUe+C/NqMRLAESTGj1cM6tNn/ynUIbgsLX+KYP3/+0aNHc3JyTpw48X//Z9EVPjtMZdUAnM9TptDxEegPW0Fc6KjeleeCt4NS+BJW0ul5Nk3Gs1Ht3yzeeChrhnLpTfKdCG8AM0Ccdt3FcoKasG8yp5uRc5oHZ9Z2OPcICyfojh07duzYEQgLC1u6dOl9FW7LPnHixNV1xIGMjIxrFx2vTthg6nemeCgeewGYDPHQ1EKB36uSwA/eAWAAFOHVhNHbyE3EI7SWbxbX2vPoC+Rswvk9dI7Q7cZNBCvmXT+OvhvI9sbNCc3z8GxtR3QvuC0fUUVRJEkKCwurWBQSEnLtmXV2dnZoaA2/TCjBZhnaMxxrToGO+yTsvAF2DOZcDH6NeOhnVDoyM1mwAL2eSZNoeK8+/KnmrnkuePJJTnXGvQHtvsbrmufJJyWxaBFaLS+0xWM3SjDH40g9TL0Ims6rru+iIhYsICODMWNuIiLZyKElZJ9m9zEcjtHDSJfD0BwuwIswCUJubapC7dLr3fitP3alJOhoP/ZuWUXCyln4GvTKlSs7d+4cERERHh6emppasYKtrW3wNTw8PFSqGsYwHdy5pKdZAu0yyTlJmZa94zi5g85TyUrmx44AY8Zw333068fYsdTw3Pxe5gpjoTtZbYnJoeV0kIiO+KvcaGT0aPr0oW8gyU/CaNL+h/MWWv8fZzdw5rXq+n7mGfz8GDmSF15wLi6uaUR73sNQyMFLlO9nsBMnjfz8G3wB7vAAPA6GW56tUIvCEn/ApxRHJ7zK2f9VbYdzj7Bwgo6Kitq3b9+uXbv27t173ROF/7EE9A+TYY/6E+w/ILERKTs5s51uU2n5BiO2kHIavR61mkGD6NWLdu04e9aiAdylxsCvpLSl9VjqPEHHdaQn/lWYlETz5vTuTXcVvzQgvxEHLhNYD79IOr/P+c3VdZySwrhxhIczdGjApUs1DSclhi4vk3SIri2wD6PfdH5QgQu4QgS0hnO3Pleh9tjWzeNkU4ILkZdTR3yHbxkW/n+IwWCIiYkJDQ1NSEjQ6/U338EheJfLaSyAEwG8/jo//sjhwzRowKt2xA6hVQ77p5BuT4Ny/MMJbMuBpfj1YPc0vAOwsyMzkx49kGVycnj3XcvO7vYrgZchAVrBuze3d/77X7Zuxc+P99/H1/f6Ur/eRE3lg99ok4umlJVe+DSg58/UrUtcHCdPoranyTHmB1OvlBQ9Xkc5Ohv/jtWN6O7O1q00bsyWLRkNGtSvYZx+9Tjui5TF3FSaSOh+wVlNfhGFJdQ9Bb9D0E3MWrAaxkv22t0nmSsRBGNVBNd2QPcEC59Bf/TRR1FRUS+99NLWrVtnz76FR0w9S/Ycxjryth2fz2HECJydiYqiVy8mxdMtFJMKnQ2+zhzQoC6lzwb8G7F6KBI8eghAUWjQgPr1sbPDeNf9GX8fwiEKvOHzm2i3axdnzhAVxbPPMm1aJRXiAylqwKBMHNIx1uGxLBx9OTwWOzuWLGHePKYtIsmDNx6ge18+K2LbYDyb0GJRdYMuXUp0NK+/zv/9X66bW3U1rxW6l/kl2DrzgQYFihTGu6AfwLE0+AgWgcNNTFywGuptRlQSn0Mbic9v9JgkoWYsfAYdGBh49aHuN82cS5kNF0tp1ApzAc45SOV07kB6At26MasQx9fYtoagN1A74LWAnIO4+TIwBlMmOj/S03GzwdubL78EeGYyaRdwd7fg7G6/RJgEQDf4X41bGUg8Q9eulJ6nVTMyM0lJoG5DzAUYkrFvidnAuTN4Pk+rAI6MYbc7CQnUe5jj8yjNpnlzPvmYRZNQtcI4l7q2mL0YlgyAGQx/PI7riuJi7O258rWBhwcffPDH9l27bhym2QCgy+GSRN8O2Bhoc5rlRZxsx8Or+bAXvT/D1rbGsxasi6rASEAjdrrSNpJNz9R2OPcIq/mqdccE3vsGVxnnTkw3U25G/oolEgPvx1uFWaavxKdBlMDZF8iUKFJ45XF0XjhepqsdS4pw8KAcOnTg449x2sGpgzQ+A6NhXG3PreZGwlQYCV/AWzVr8jbE0iuXkQfJ1HDEhD283pL2ZYQrlEvEg9QLcx7fHeZN8ILgFCKb0NSEjy1xzdheRIYdBiP1iynYRmkmXi0B2AwfghN4w1LKTTz2GGYzubm89x7hN3ln9q8fkLQTvyQu5yPJyDv4CuyhCDZEEWfPZWeGDKFDB96q4cQF62Ly02oy4kmHM7GESrUdzj3Calazm/UNmxNYb6ClDjuZI/242AJ3hVHdiFAYM5wmCt7dKYC6KkJVZOjoNJSeGXw5hU/K+fIN9rsSGQlQ3xaHQjaloNkJn8NdtBbPIHgNiuAT6FSD+pfgCGylyJUFtrh+RpE9H6n4ppRwhcX+NEmjSKJ5Gb9dIEzFq0s5a0scLB5NtprvvBgcRZqRpa/x4xLOaNB2ptXrBF65yDAHtsIG8IBfWLeOjh1Zu5Z163j75u7MdtSYSNxB5DZ6ZfOZwsMQB4ckfCXGwgbIN3IqjK1biYvj4sWbf9+E2qfWG5ElsENRIYlLHJZhHQk6NwGzgsEAGrQOFKnp2pgm9SkHN1c6ga1MOai9KZVQ2aBRI4FdNkhonQDc/ZDMODtjMjGkJ6MaY28PKtDeVQkaaAVPQA2ft2b+8/qDEWcNqrN4yADr1gEYJWQzEsjlGM0gMToSWUMOtF6GWY17Geei0aowlyObsNEycT4Dnv3jpnCkP/+DZQuxmOKwtwOwseGGD2L/O5WkoLEFkE3ICmVwWcIVJNBeHU0FYG+PxVcIEO6k5oFoxemzxdT+JQ7H1J3EruWRZgxpTD17isy0d+DYQoL1OGnYsoHTEqE/4i5xYQ1ecE5PETSQiNuNpw3j32eMM0Mm4+1P9gJ+/RXCQAePQgkMtYY53jaB4AcjcdCjFNFsPh2MRMH+x+gNk9M4Xg8HmXO+RNix7hdaO2CrUCDh5kZbIw/kc3wWDuW8MB+zTCM7dv+H/AuEvwTAZOgPPrADnmBIMcPf49BhLlzgpZduKspCoxaf5nzSgMYaxqjYJ9NH4Qhkw0YYBs62DC7BZiR+fgQF3YY3SrjtzI4aTZmRuLOoEDnaUmo/eTknrWXUauzc6f4Np9fy8E8ACWsp8CRqIf8dzPpJPPg5C+fzUDOSNvPQW5jWYTcDexcME/Bbj+k33mvB/jj69kV35dHdC+EC2IF3rU7uDvgYLrLlBTQtGNCWdzdj8zMPz8a3KdsW8Fh9wj+g8CIqLZHurBpPlwk4NWHNGmxWMWwleWfp/DtFGhpGENycnDM4B6BzBOAReAAOgQbewx42JXJmMgFhODredJj3zyR5Nx1j6G0mLwxZRxcnfnFkQBqjNLROINOIwfDXk3+Fu40qCJQmxJ+heTeUn2s7nHtE7V/iMNl5k3UKoKSQ4AF/bG04lIBu6LoSX4BNIIdi8GtHeg7O99OyH21lmnQgqC6NVDj74D4Qz3oMHPhndr6i3r8gO18RiFdTMuKo9yRaW2QNw6dwLgn7cII+AHAOxNEXjY4x3xLcHS8vpkxBZ4/GhqAIsk7R6n4atECS8Az7Mztf4Qrt/rxZvBQpl7DWt5KdAUkNKnQOKP4YbHDxJawT4V3xCKAoANzx8RHZ+a5mNtqgyuATE9pysYqZpdT+GXRu86dd9r5HeQkudXnoi7+VTZjAuHGk2xK0BGdbMjx4Jw4cYRB0BVWNf+pwr3vkdV5dx3RXtBKJYfTogY8PX39dXZNes/hhJLKZkL74tq663p83iyPBK//og3f/TFb2R5HpOp3Nn5H3A47Q3Z2Q3bfep2A1oj0/6Fs8hUUSKom2y2s7nHuEpNzkFz6W9fHHH69ataoWA7gdJEl68803BwwYUGnphAkTjh49eodDut20Wu3y5csbVrY6lSzL3bp1q+mahXcPR0fH6OjoSleSSUhIGDVqVO1+sm6Hdu3aLV68uNKin3766Z133rnD8dwBY8aMee6552oxgFpO0IIgCEJVav8atCAIglApkaAFQRCslEjQgiAIVkokaEEQBCslErQgCIKVEglaEAThn7pNP4cTCVoQBOHWXfsgVovf1VHLdxLu2bNn//79tRvD7fDQQw81bty40qLVq1cnJyff2XBuO0mSJk6c6FbFc1Xmz59/792oYmtrO3Xq1EqL8vLyPv/883vvDoP69euPGDGi0qJTp05t3LjxDsdzB3Tq1Klbt27V17nyIFZJkhRFmThx4qhRoywYQC0n6BUrVoSHh9epU6e6SorikxvjWJyY69omz6VlVbVUiskvc5u96RJNFZuSHNftJ2R/TW6ftrr0Qt3aLFWgSemhlmLM6lSD3X2pko+iGFScRknRlAe7Hwj5uMPP02312aYGDmXtvXWL8+UEdfyjzxhsPH1iYly9j9sXpBnC3HMHtZFOmOU8m0zP7sYSB/9t29y1x5xLz5Y7uxwaMN8l8LiTU2JubptNm4q3b99eVYJesGDBWxXWpHd2TvDy2qfX+6en91KUv26n9in/JazOfEVWp+X3d3M7VloW6LDqvENuSqlPPSf9WclsLqgXZn7AXla0rt+c0ISVKJekYud6ducvF7cIcbjvvNq+LOf39o7fJcsadUb/CI/i30rtAuODn5FVOpVc7n95m648N9OrZ4ldIOCbHx2g2VhaHhjv+oys0nEzli1bdvr06c6dO1csMhqNy5cvnzNnzk11aBFqucw/Y6vWXJzuFeG3Zmed2C3aboXS42Z0MrGYo+yVFmpVXaPqPoN0Uma9Sk5VK1M1ckONapHJmOuU+VQPu/bpRqNj0YEQ953HHNqfL3GoX1xSX0ZzyffB6a/OmjJlilarrTju6dOnjx49+sQTT9yxmXp4HHZ1jSsqbOCZdtDekJouRWg2F6tkOa97c3fT4XKde2jpYpV3uZKjNno7a5yKizJDnE6fw6SUBAU5kiSbdTnH2zofOFvYvL5XwBFJZ9KX15Hbq1WSOetkR+/omHI316OPvjtj7vyqEvSOHTvKysrCb/YxDtYtLS1txYoVN0zQ//hBrNWp/bU4OnfuXOktwn858AmSga7T6++dTcN21OteebXNkwlrie9+tuVx6hwPeNKs3P7Hzdi7ME3LrBJSZPpCuJ4dEq0UqUjmW3jaZFem7vHWI3TU0tlOuz9fu6eAtzVE9Ws9+w1CO/KwF5tP8YC/zck05++y6BfEfePrb1/Od3qGdGD5MYL97HSarqsfY/k0mF6//hy9vsXhw1XORqvVRkRE/H3bKZgL78DeJk12w/t/bNZnYO7D2QdQp9TvtJKiL11/eI4gPS2fdE5YjFHCJ9AlNZ74LsQd5+ES1jpJDYudHJNpO8lVWkyuDqWL12e7GRZOkbrBoWVM/tI1fr3/xZmMj2X9WJp1xyM0eOcrDF/N5SPkL8Vmrmv2T/68TfcDN7MP2b27uvU0PDw8Kkz5jlgznDZDcaoT/OSjnNHTr5AnzPjCq/AImu4l/B/0l9ip8BtMlFWbZbaY1P4Kk7Tq4zn1Xl7DweHIJ2mwn2bl/NjLITTau2EyPktCYmZ5enhUM3L9+vXv4JQ3wQl4nov9cAzAdrrrB5PoOwNdI16cxLYN7OpFiJnz3lLfy7rsfPL7uSzYzCNO2Ng4zT3Hay3Vp055y/uYOtM2+Q2MElKIne05znvj0abuDz8y8VnbuN+7zZ6gtW1TTRAtWrSonb182yQkJOzbt++G1T766KNFixZ98cUXAQEBt/Qg1urcDdegk3bS7TX829FxOkk7q6yWHU+7xwlwwzYMeyd6t8LvGZIVQt7CP593d1FmIHQ6PqCpywEVGyHCnuYQuY9L8MwOeqio1w2TgvMjjAzA057G/pw+yLMvcF8JM3dSUEC7Twm4hHt32npzcTfhweQ0Y+5Zko3wKrSD6V5ex25yhnvgCegAL8A1mfHCUnJ1tNmCawv0EqoWxJYxwI7xiyiFAjueW4wiceQY9oVEScw5yQ41odBrEc1gtYb8UQRI3J9Dy0BS1Hh25JENZJ8BKLhAq3EEhtN4KGm/kfYdZY/S9Am6/4TL2VvYS9aoNJtmj1KvG2eMdFQz3JtiHb/A7xI9wRmKQFY4qaaemhCJqXABfgA7Bx5X00pDTDi7W1DsgW19Jn9KtBMOhYQ8gEtdDxtDbU/vqp3wIrQltowGYdgPoSiQ+omE2tK0GUY//MycsmF8JgrkK4Q9gR00tEHR0QiatWG7GyEQ+ToSfGPPxLW4SmTncs6ejmo6t2JBDJdLa3uad1pKSsqaNWt6X+PQoUMVq115EOvy5ctnzZoVFRVl2RjuhgTt35bjq9DncnI1/u2qrObkT9I+SgoxpVNaTOI5cr7HWSJ+ASXOLB2BpOXiVxigNJ0mMt3gNz35EDseF/hiKKlaLu6lRKJgPacdySklNZ+wZny3mPPufD4SO3uSPqAkhKIjHM8ipDtHz6NL59OH8FHBKsiF1fn5ITc5wzbwE2TBBgj9a3OdSNzLOTef4iJsFewdCbblVwNn9mADbkZ+3oii0LAVeXZ0VFj+Mm3NpEHGHlLhfok6uaQqpISRr8LHjJ2K6Ndw8AWw8+DCHorTSdqBd3M8eiNtICee316nyOdWdpMV0jqQ9huFqdTREK8iOhcbI+3BVeF3MIAk4SgRYibXTLHCOvCHtiCXc0TmhImO2XRMwaYYfSrrV9JKT5kDOWfJT84tv7mrQLdTW/gf5NHEhqxsdBnYplHWlBIXTp/GXUe2RKCBmJcBbCRKDRRCvi1u7pyFIlfa6cmE80eRYIieg2spUrDzINSH42ZKnFj8HE6VXMy5t8my7Ovru+Ma7dpVnX9AUZSwsDDLxnA3JOgur1CYytrH8GpCo0FVVuv3KafX8pOOTvb0DeHTVDYk8XwrOsGTMtkFvOjAyVKW2dGpHE/wh94KsyQ2xrDxRwwlfJuFu8RCJxYamDKf195g/BSi1Nhp+eI82QX8px2nc/jpIVgbIQAAHA5JREFUB9o/xZufsimdAA8KT/LrL8yOhnR4DBpdutThJmfYHgbD4xANH/612SmY5KdxeQHPrVzoQl43IoP51YG591PiSImRbYtxdqR/Is978qtEg1UEKuyzY9T9RLnSXY/vy0zy540oFvxIhz4s60bCZsZGAwxYRNwKNkykw3O4BdPkcRjK+W7oN9H6Xln/c+BSDi1i8xSWr8C/LYtl1oIKVkIGTFXRRcUu6AtNYQp8C0/DbBWv6pmk4vWOOH+HZwmmviz2IuhdGnlysQXRrzFwqaxYz0NDIsELRhE2jiMZrO/O0FEsOMqsz/jwXbY/g/NE9BK6ORyRUKkwPcZUJ35MZeUJnnPgzDzaFxMfQPsO7PXFS+boW5Tr6FtEk68Ib8vYSL79lu/vlaPC0hYvXty3b9+5c+d26dIlJibGsp3X/jXoG1PruL8GKxnautF/4R+vQ6HPn9ubww9/vr7uWncTePXP1/+95mcGi66pM3jw35r0v+b1lUf//eXqxfHrttfESBhZyeZmC2ABwNWT2qVVdPB+FdvHwJjKttt7MfDvfXWaB/NuHOldxMmfQV/98XpLz78VBcK1O7YT/N81/9z69346QsfbEZ+lSPA8PI8a+v75ZWyvq6VTAFjytxahcN1vDSZU0XcLeNdCYd6jfv/99y1btvTr12/fvn3PPvusZTu/G86gBUEQrNXly5cvX768cOHC0tLS9PR0y3YuErQgCMKte+WVVzZs2NCgQYNNmzaNHz/esp3fDZc4BEEQrFX79u3bt28PVPUj8X9CnEELgiBYKZGgBUEQrJRI0IIgCFZKJGhBEAQrJRK0IAiClRIJWhAEwUqJBC0IgmClRIIWBEGwUiJBC4IgWClxJ6EgCEIlZFkuKipas2bN1S09e/b0qPZBDRYnErQgCEIljEaj2WzOy8u7uqW4uFgkaEEQhNpnY2Pj6ur65JNP1mIM4hq0IAiClRIJWhAEwUqJBC0IgmClRIIWBEGwUiJBC4IgWCmRoAVBEKyUSNCCIAhWSiRoQRAEKyUStCAIgpUSCVoQBOGfUhTldnQrErQgCHdaTExMly5d/Pz8fH19Bw0adO7cuX/YYWxsbJcuXSwS281auXJl586dIyIiwsPDV61aZdnORYIWBOGO0uv1/fv3f+mll1JSUs6cOdOlS5eRI0f+wz5btGixfPlyi4R3s6Kiovbt27dr1669e/dGR0dbtvO7YbEkReGHHzh4kKNHURfzQSOaeQLgBxOIPsTuXeR/SUEe3jIlKo4qKBLTnAiy54wP5kzIp0hGVuPhgos3qy5TIDPeE1s3DrUiP43cNP6TSXMDaSHEt0eRsZForeDkBxPA2cIzKivjq6/IyGDUKBo1AmAFLIFGvJJDzCEiulF0luwshj1AwBpkDYUDKdoO9dGmU5CCUwgrk8kro21TEk8CtPOh1SWydIT3xOUEZd1YeZbCLB6aQhsTaEm/j/hduDWgRSSqu2G/X0+G1XASHmR1Kp99hr87jfIhg+w6uGppdpb6qRSb+RyyJDqpOa1wyER3aA5NJDJsSHFlsBet6yH1gKfAobYnVTODAjh2iSZuzG+KLpmi7pjWoVL4nw3b83CS6OlCSQH2rhQX4COT60M/N2QTrV/DMQl8SQokOZY699FoUG1PhpKSEoPB0KxZM61W6+LiMnXqVGdnZ1mWV61atWPHjuzs7Pj4eF9f36VLlzZt2hRYvXr1W2+9VV5e3rBhw8WLFwcFBQHz5s1bunSpXq/v2bPnggULTp8+PW3atL1791Zav6SkZNy4cbGxsWazecKECW+//bYFp2MwGGJiYkJDQxMSEvR6vQV75u44g16wgF9/5YcfOHmS/6Yz73tOroUtUIedffjsM9Lncy4LHyOZJlaX42YkwsjEHBIzuP8wZanklmAowVRMaiqfHCE0nRGZTItj5zm8l7BrO6/G4XaBjSV478P7GxI24fYTa7ZgCoBHLT+jp54C6NOHiRPJyICNMBVGMHYdGzbxxOPMXUXseXr2o9cScuqQ50y7L3EZQvavXEggKILZJ7HV064Jy+KQbHFSk3KJC3XwMZIZhfI4xatomkT7Yfz6Mmlmsh3ZMYzg3uhziH7V8jO6E+ZCHAwk60W+mMjgh0jbwpZYtuvR7sEuGl0yaiPPy5yS0ZnZXI69kUcVLirUVfhdpqWeKZn4nCLmOGyHMbU9o5p50Ic9adzflKG57NmHOZKwFRjt+VnFN3mEaQFW5BPUFJs8nBWSvRiTyelL1GnH9+OQ25IZj+o5QgeQuIOjX9fydMDT03P27Nnh4eGDBg2aN2/e2bNnn3rqKZVKBXz//fezZ89OTEx8/PHHR40apSjKiRMn3njjjT179iQkJIwYMWLYsGHAjh07li1btn///vj4+IsXL167ZHOl9desWXP58uXk5OS4uLjo6Ojc3FwLTuejjz6Kiop66aWXtm7dOnv2bAv2zN2RoHfuZOZM8vJYsxyVmvLuLHaBCGjMrkz+M52zpczbjEahU0eAzva0VOEvcbwuaoWmIzHJDPwCk0yDQTgp9OxAHTdcHNF14qiRd94luBSbbewsQfM0IQV0fBhC8O9NTiMwQbmFZ5SSwuTJhIczZAi//QbfwWPwLLvL+M6OJ97FT0WZmqa+ZEvEhVHiRolEo9GozTRT02kT2RChwrU+fuDlRl8ZAzT9kI72eILru2xT0UlDvyep48auOC6oaFOfei3p9AJpv1l4OndINMyEDix35unG9GhJp2AyZGyDeUZNukJniWU2NNESIfEsnIFz4AopsFjilMQpLeUSac48UwQGyIXb8sWOhcVlMbYrXx3nIRX/kbEJpxRcG7C7hObw4lQ6QDFMOk5rKFX4YDsnILGI4BF425KXze8S/h4EdKTbqyRur+35ADz//PMXLlx4+eWX9Xr98OHDhw8ffuVLtoiIiBYtWgATJ05MTU3Nysratm1bYWHhwIEDO3fuvGTJktzcXJPJtGXLltGjR7u6utra2m7cuPHaKySV1m/Tpk1qaupTTz21c+fObdu2ubu71yTI0tLSixcv9r7GoUOHKlYLDAycNWvW8uXLZ82aFRUVZaF36A93Q4Ju3ZpVq3B05D9vYA+pBxhcDPuhmFYOfL+Wejreexgz7D+ACZJLKZNJVwi4hCQR/yNaia2T0KhI2USOxO9HMRVyuYS8eMLULJxPng2XhxPohOkrLtsTtxuHFNL34VYC5aCz8IxcXdmzh/R0tm2jWTOIgJ8gnmZ2vFHO+aNkKTjbINfHS6GRHTod9grmcgxqkmSyNuMECRL+gWSD1pFDOmxBf4af9RSA4SgdFJJtSDrH5TzadcHXlfgLlJZzZgNuDSw8nTukJXwHOQw2siYRxZn487hIlOeyw4ynxHGFh8o5a+J3hXXgB76gBzsYrhAE/ka04F3Kk25gCzYg1fakaiDQjrV72b6MgzKREjZO2EO+mRYuxMOBvSSADZxcxhkwSaz4ghBwciLvPJfLcGlHQzvyiyjLI24lfm1qez7s2LHjgw8+0Ol0nTp1mjFjxrFjx3799dfY2NjrqqlUqitL5vfq1Ss2NjY2Nnbv3r3bt2/XaDQmk+nKGTdQWlqak5NztVWl9Vu0aBEfHz98+PCDBw82btw4OTm5JnHa29sHBgbuuEa7du0qrSnL8pUXDRs2vNl3o3p3Q4J++WXOnycoiMRERuTxkRe9fMEGPmPEj/j7k9Oe/DJ+Ax+FkbAFXlR4WUVPExsdMSnYgcmEUUENDzuysZzpZubYMTKDoy0wGhlmg0MxLxaSp+bnZihw0I1etug+g8WWn9Gnn7J8ORMnMmUKwcHwBPSBbqz2IceD9h1oUQeHQp6fwLog7ltJq1hi6pJ8P3WcUWn4aSDjtWwx8c5nNNWSHU9aKX4q6ryFq4zWHTrgX4eVhSwYilcPmuyhzjoaTmfd4yTtoPdcy8/oTngT4iCSBg/jMoIHB3LWi+5OtEvgO1d+cydOSxOYqJANP8OD4A6rIBzKoZdCmcRqMKmZYgAnWFTbM6qZ6HRsVQwdz+sSL7qg7c0pPwIP8XQB7SRmxXJSobeKVeO5INFAIeAzdkiEmtjxKn2HoZlBg8skD+HHURgK6fBcbc+HwMDA9957b8uWLbIsGwyGPXv2lJSUBAYGAjt37jx+/Djw5Zdfenl5+fv79+rVa9u2bWfOnDGbzTNnznzxxReB3r17r1ixoqCgoKys7LHHHlu9evXVziut/84770RGRnbp0mXWrFk+Pj4HDhyw4HTefPPN3r17L1iw4ErYFuyZu+NLQjs73n+/8iIJXmzEiy9W2bZJZRunXvO699+LfGDsTYZ3C+rU4Ysv/r7pc/gcR4ipool/hS2TK6sW+ucLP5jz96KW0PLmwrQyjvDhHy8XwsKFldeaXMU7c/eyd+aM6W9bWvz54ocqmjxQYUt7aG/RqP6BsLCwqKioV155ZeLEiQaDITQ0dPXq1QEBAUB4ePgLL7xw7tw5Hx+f1atXS5LUtm3buXPnDhgwoLS0tHnz5suWLQMGDhx44sSJtm3blpaWRkREPP300ydOnLjSeaX1J0+ePGrUqLp169ra2vbr12/w4MEWnE5mZuauXbveeeedxMREC3Z7xd2QoAVBuLd07Nhx9+7dFbcHBAR8/fXX120cN27cuHHjrt0iSdKMGTNmzJhxdUu7du2u/ISj0vqenp7bt9+ui+/Z2dnnzp2bNm3ajBkzioqKLNv53XCJQxAEwVq9++67MTExTk5OTz/9tL9/xf/q/iPiDFoQBKsQGRkZGRlZ21HctLCwsLCwsCsvFi2y8Bcb4gxaEATBSokELQiCYKVEghYEQbBSIkELgiBYKZGgBUEQrJRI0IIgCFZKJGhBEAQrJRK0IAiClRIJWhAEwUqJOwkFQRAqIctycXHxtU8D6NOnj4uLy52MQSRoQRCEShiNRpPJlJeXd3VLfn6+SNCCIAi1z8bGxtXV9cknn6zFGMQ1aEEQBCslErQgCIKVEglaEATBSokELQiCYKVEghYEQbBSIkELgiBYKZGgBUEQrJRI0IIgCFZKJGhBEAQrJRK0IAjCP6Uoyu3oViRoQRCEW7dy5crOnTtHRESEh4evWrXKsp2LBC0IgnDroqKi9u3bt2vXrr1790ZHR1u2cytYLElROLWGS7Ec30peESPa4qriRDQqiQ71eSmV/xXQVqKjFvcSJAV7CTP4OFLgzZk+sJ3mqay343ITHh7OsGE8/TSlecxpRtuGMAGca3uGN2U9HILu0Ltm9dNhGej4dBMH9uHuSGEI+Qk0aMVDJ5BL8BpCbg5qHXtOEH0BHw0eHpizsQ8ixI3SLDyG8slGtFqebkH5VnR+tHiQywcJeoBgD8iACIgBLUfuY90ugoMZPRqNhrIyvvqKjAweffTGYZZmcXAhGfvpkMLXZ8GMCY6DGtpIPOhDQWNUahrPxaf1P3sDrcZPz5DxBbIDJ/JAQZKQbMkvo3V92l9ABU6TaO4BfiQGcmE/de6j0aDaDlq4aQaDISYmJjQ0NCEhQa/XW7bz2j+Ddj27nORf+O5zLqTQxcDUjWjW0cFIm0Lif2dVFiNNXDZQXIxeQQP5Cq4Kx4v4OINmi0lMYoaKg/k0PcasWTRujL8XHZPo8TX5dvBIbc/vpnwJG+FBWAhba1C/HIZCU75Zy7d7aNyM9AKKDtOoPw/uIbMAp+ZE/w+tmt8OknyBtp5IJqRM3FujSyQ+joDuvDyPpr7UMfDWarwi0J8n9hMaRnJwFgk/QSfoB76chmlDiIjgwgVmzACYPJmyMrp2Zdw45+LiaqKUUPjfYM5s5P4DvB+PxowKdkIXKIdfFBIzUH7B7j6yumEotMx7Was6mQ+R9hnaVpzKQ6UgqUBB1hMazI9JJEmUOOH9GfrGnD3I4Wdo+CCn13Hsm9oOXLhpH330UVRU1EsvvbR169bZs2dbtvPaT9D2Gb8S8T5Jel74ibbOjPTktBq3YDwHUgIP9yFY5p0FyODREzXYqyiRuACdveisMD2CLDNbl9O+lP/8h/JyFk/h3Udp0IidXmCG8tqeYs3thPcgHN6AHTWonwStYDAHjtNDx4xfUGlIhfdX4Q7/syHoHWxVSMkcyMZN4q0jeMMv8PZBZBVmE+auuKrp58sgGVQ0fAtXP0wKgZPoUZ/ETKgPLcCdGBcmhdC9JW+9xcGDAMnJTJ9ORASPPBJw6VI1UXraGHALxtYVbzih4AxbIUzCIHE/HIZs6KDQYRa59bm42zLvZa3q5HwIlQtPxKIo6OBTMxdBgmEv8Rgc0ND7Swogdi1JjnT3JDCcXrNI3F7bgQt/MZlMaWlpva9x6NChitUCAwNnzZq1fPnyWbNmRUVFWTaG2k/QBrcmHF+Ft46fplJsZlcewTIl2WTuwAE270SWmDMVCQp2o4BexgP84JSZPIlN0TjbMWUCJTpWrEClYu0p9uzgfCLhrmAEXW1PseZaw3eQB/+DNjWoXw/iIIlGocSVs+9/2Mp4wME9lEBXCcMp9DJO99HUgxKFXxZQLNEK9q9GkUFLiBMFZnJ9uOCPWUZKprAQjUTZeY5n4BcIOjgFLrRyYX0yWeVs3EhwMIC7O7/8Qno6UVEZXl7VRJlbbkNuIsZS9GqCwCjRGY4ruCmchAbgAokSF3/B7QJ+nSzyVtauU0WhUEjsFyBRBnNCCAAk0vPYCQ1sif0GF2gxAV87ThRRlsfxlfjVZKcLd0hISMgjjzyy4xrt2rWrvklYWJhlY6j9a9D7VN3uO73FqUuwXWyCtMVsfMzNVG6yS82UJEyBNuqgculthaYKbhKyQgk4QKaitJFkp4yi8XWcfdNV0wr4SjK8Zp8Vqj377LMdnnpeI5eVvuRWYHgzbsOQwsIVBoPBxsam0tGrKSovL9doNCpVJX/DZFk2m81arbbShvHx8V5VJyyz2Xz48OFKiySpm5/fFw4OA4qK2mZkhEHl1a7l4DDR13eCMsbLOdFePWMSXpLsYKNa0MPsbK8eqSftBWOwJ79HKcFqbbJKevYDfKClxE+PKCVq2Uur2jTa8GCQ7uUvUEnmSe7a6EGyRlvuEKxZ3Ubv3cDgr9IWjCkuHuPg8K7SUFPaeqTj4CHlAQGXJk0yHT6snTjRf948bU5O9pAhJ0+cqCat5hUUna7/rO+pBbZfmHQv6NTzy2kMD8AvcAkikH3Uxmw7OW/IkfhB549VdxZZzf6yVAWj0ahWqyvd71coiuLr61tYWN2lmK8yG9/nl257dCLNVMoJpLREJLis8M7LSkeVNDAf1QbDqTplbd4x1vEwnets/9WAEpcm6aETlCqODWtgNpurKU1MTDx8+HBKSkpJSUnN+5Rl2WQy6XQ3cRZ1wz34z4fQaDQNGza8ePFizZtc0a1bt5ttUj3pNv18r4Z+/PHH7dv/9oGMj48/c+aMu7t7xcpGozE7O9vPz6/SrlJSUurWrXuzRenp6Z6enpWm2tzcXBsbGwcHh4pFxcXF5eXlVQWp1+vXrl3bvn37Skf86KOPzp49W2nR3Uur1c6YMcPf379ikaIo06ZNKysr+/3339PS0qp5YpDBYMjPz/fx8almoGp2ZQ0rVLPHr8jNzdXpdI6OjlVVKC0ttbOz69ev34cffihJUsUKly5dmjlzZu1+sm6HJk2aTJ06tdKi/fv3f/3118D3339f1Se0UiUlJQaDodKPUlVuuIsrDlFWVubh4VHzJqmpqY8++ijQr1+/QYNu8M3tsGHDioqKrv4B2LRpU80HujHFymzYsOHDDz+stCgxMfGJJ56oqmGPHj1uoejxxx9PTk6utGjOnDmbN2+utGjTpk1z586ttOj8+fNjx46tarh/s2XLln399dfVVDhy5Mj06dOrqSDL8v3331/9KNXs6yuq2eNXfPzxx+vXr6+mwu7du998883qR/nXuuH7f53NmzfPmTPntg6xZcuW999///YNcfTo0Xnz5t1U/zVX+5c4BEEQ7l7Nmzev9P+OFlH7XxIKgiDcvVQqVTXfOf3Tzm9Tv4IgCMI/dZsundyyrKys8+fPV1pkMBiOHTtWVcODBw/eQtHRo0fLy8srLUpKSsrOzq60KDs7Oykpqaogjx49WtVw/2ZpaWlpaWnVVCguLj558mT1nVSzK2tYoZo9fsX58+ezsrKqqZCXl3f27NnqR/nXuuH7f51qPkoWHCIxMfG2DnH71PKvOARBEISqiEscgiAIVkokaEEQBCslErQgCIKVEglaEATBSokELQiCYKWsJUErihIZGXl1sb7Y2NgOHTpERkbOnDmzqiYV69SkFZCVlTV+/PixY8fOmjWr5g0XLlwYGRkZGRkZFBSUlZVV8+EEQJbluLi4YcOGVVUhPj7+4YcfnjRp0ptvvllphYMHDw4bNuzJJ59csmRJNQNddyBd54a7rOKxcZ2Kh4FQcedevHhxxIgRkZGRlnoEVMXDw+JDVDzALD7ELbCWW73/+9//XrtW1tGjR0NCQiRJ6tChQ1VNKtapSStg0aJFer1eo9G0bNmy5g2nTJkyZcqUQ4cOtWrV6sqNQzUcTgBycnKio6OLioqqqrB///73338/JCSkV69esixXXEzuyJEjn376qZeXV//+/Z966qmq+rnuQLrODXdZxWPjOhUPA6Hizl20aNEzzzzTtWvX8PDwUaNG/fMhKh4eFh+i4gFm8SFuRS3/DltRFEXZsmXL4sWLFy1atHXr1itbTp8+ffny5bKysjZt2phMpkpbVaxTk1aKoowZM2b9+vVGo7FDhw5ms7nmDfV6/bhx465WqGEr4ar+/ftXU1pYWDhz5sz58+dXVeHcuXN9+vSpZm2digfSdW64yyoeGxVddxgIV1y7c0ePHp2enq4oSs+ePWVZtkj/1x0et2OI6w6w2zHEzbKKSxwbN248derU999/v3jx4tzcXODEiRMajcbGxsbJyUmp4laainVq0grw9vZ2c3O7UlOW5Zo3XLduXa9evdRqdVUBCLcsPj7+lVdeGTVq1HPPPVdphSvrWG7btu3KUnOV1ql4IF3nhrus4rFR0XWHgVBRQEBAQkKCoihlZWWVrsh6syoeHhYfouIBZvEhboEV3Um4ePHioKAge3v7+Pj41q1bz507V61WR0RETJgwodL6Bw8evFonNDS0hq2A8+fPT5482cnJqVevXk2aNKl5w8GDB3/77bdOTk579uypeSvhqgEDBlS1Wu7kyZMvXbrk5OQEfPPNNxXT39atW7/88ks/Pz8fH5/XXnutmlGuHEgPPPBAxaJrj5lKd9m1x8akSZMq7f/qYVBNDP9CV3bulY9G3759X3nlFXt7+549e1rk4sC1h8f48eMTEhIsPsS1B1i3bt1uxyxugRUlaEEQBOFaVnGJQxAEQahIJGhBEAQrJRK0IAiClRIJWhAEwUqJBC0IgmClRIIWBEGwUiJBC4IgWCmRoAVBEKyUSNCCIAhWSiRoQRAEKyUStCAIgpUSCVoQBMFKiQQtCIJgpUSCFgRBsFIiQQuCIFgpkaAFQRCslEjQgiAIVkokaEEQBCslErQgCIKVEglaEATBSokELQiCYKVEghYEQbBSIkELgiBYKZGgBUEQrJRI0IIgCFbq/wFwmhAF+IuFjQAAAABJRU5ErkJggg\u003d\u003d" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665402663_-1615852709", + "id": "20171207-165002_1426161441", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%livy.sparkr\nlibrary(ggplot2)\npres_rating \u003c- data.frame(\n rating \u003d as.numeric(presidents),\n year \u003d as.numeric(floor(time(presidents))),\n quarter \u003d as.numeric(cycle(presidents))\n)\np \u003c- ggplot(pres_rating, aes(x\u003dyear, y\u003dquarter, fill\u003drating))\nplot(p + geom_raster())", + "user": "anonymous", + "config": { + "colWidth": 4.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "r", + "editOnDblClick": false + }, + "editorMode": "ace/mode/r" + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "IMG", + "data": "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\u003d\u003d" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665417816_957763332", + "id": "20171207-165017_1061799115", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n---", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": false, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003chr/\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512669724124_-314225646", + "id": "20171207-180204_1784004277", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "GoogleViz: Bubble Chart", + "text": "%livy.sparkr\n\n# Workaround for Spark issue with googleVis: SPARK-23780\ndetach(\"package:SparkR\")\nlibrary(googleVis)\nsuppressPackageStartupMessages(library(SparkR))\n\nbubble \u003c- gvisBubbleChart(Fruits, idvar\u003d\"Fruit\", \n xvar\u003d\"Sales\", yvar\u003d\"Expenses\",\n colorvar\u003d\"Year\", sizevar\u003d\"Profit\",\n options\u003dlist(\n hAxis\u003d\u0027{minValue:75, maxValue:125}\u0027))\ncat(\"%html \", bubble$html$chart)", + "user": "anonymous", + "config": { + "colWidth": 6.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "r", + "editOnDblClick": false + }, + "editorMode": "ace/mode/r", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": " \u003c!-- BubbleChart generated in R 3.2.2 by googleVis 0.6.2 package --\u003e\n\u003c!-- Thu Dec 7 18:02:19 2017 --\u003e\n\n\n\u003c!-- jsHeader --\u003e\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataBubbleChartID7a02ab90674 () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"Apples\",\n98,\n78,\n\"2008\",\n20\n],\n[\n\"Apples\",\n111,\n79,\n\"2009\",\n32\n],\n[\n\"Apples\",\n89,\n76,\n\"2010\",\n13\n],\n[\n\"Oranges\",\n96,\n81,\n\"2008\",\n15\n],\n[\n\"Bananas\",\n85,\n76,\n\"2008\",\n9\n],\n[\n\"Oranges\",\n93,\n80,\n\"2009\",\n13\n],\n[\n\"Bananas\",\n94,\n78,\n\"2009\",\n16\n],\n[\n\"Oranges\",\n98,\n91,\n\"2010\",\n7\n],\n[\n\"Bananas\",\n81,\n71,\n\"2010\",\n10\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027Fruit\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Sales\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Expenses\u0027);\ndata.addColumn(\u0027string\u0027,\u0027Year\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Profit\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartBubbleChartID7a02ab90674() {\nvar data \u003d gvisDataBubbleChartID7a02ab90674();\nvar options \u003d {};\noptions[\"hAxis\"] \u003d {minValue:75, maxValue:125};\n\n\n var chart \u003d new google.visualization.BubbleChart(\n document.getElementById(\u0027BubbleChartID7a02ab90674\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"corechart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartBubbleChartID7a02ab90674);\n})();\nfunction displayChartBubbleChartID7a02ab90674() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\u003c!-- jsChart --\u003e \n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartBubbleChartID7a02ab90674\"\u003e\u003c/script\u003e\n \n\u003c!-- divChart --\u003e\n \n\u003cdiv id\u003d\"BubbleChartID7a02ab90674\" \n style\u003d\"width: 500; height: automatic;\"\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665433847_1903891526", + "id": "20171207-165033_556330875", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "GoogleViz: Geo Chart", + "text": "%livy.sparkr\n\n# Workaround for Spark issue with googleVis: SPARK-23780\ndetach(\"package:SparkR\")\nlibrary(googleVis)\nsuppressPackageStartupMessages(library(SparkR))\n\ngeo \u003d gvisGeoChart(Exports, locationvar \u003d \"Country\", colorvar\u003d\"Profit\", options\u003dlist(Projection \u003d \"kavrayskiy-vii\"))\ncat(\"%html \", geo$html$chart)", + "user": "anonymous", + "config": { + "colWidth": 6.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "r", + "editOnDblClick": false + }, + "editorMode": "ace/mode/r", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": " \u003c!-- GeoChart generated in R 3.2.2 by googleVis 0.6.2 package --\u003e\n\u003c!-- Thu Dec 7 18:02:23 2017 --\u003e\n\n\n\u003c!-- jsHeader --\u003e\n\u003cscript type\u003d\"text/javascript\"\u003e\n \n// jsData \nfunction gvisDataGeoChartID7a022c4718d0 () {\nvar data \u003d new google.visualization.DataTable();\nvar datajson \u003d\n[\n [\n\"Germany\",\n3\n],\n[\n\"Brazil\",\n4\n],\n[\n\"United States\",\n5\n],\n[\n\"France\",\n4\n],\n[\n\"Hungary\",\n3\n],\n[\n\"India\",\n2\n],\n[\n\"Iceland\",\n1\n],\n[\n\"Norway\",\n4\n],\n[\n\"Spain\",\n5\n],\n[\n\"Turkey\",\n1\n] \n];\ndata.addColumn(\u0027string\u0027,\u0027Country\u0027);\ndata.addColumn(\u0027number\u0027,\u0027Profit\u0027);\ndata.addRows(datajson);\nreturn(data);\n}\n \n// jsDrawChart\nfunction drawChartGeoChartID7a022c4718d0() {\nvar data \u003d gvisDataGeoChartID7a022c4718d0();\nvar options \u003d {};\noptions[\"width\"] \u003d 556;\noptions[\"height\"] \u003d 347;\noptions[\"Projection\"] \u003d \"kavrayskiy-vii\";\n\n\n var chart \u003d new google.visualization.GeoChart(\n document.getElementById(\u0027GeoChartID7a022c4718d0\u0027)\n );\n chart.draw(data,options);\n \n\n}\n \n \n// jsDisplayChart\n(function() {\nvar pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\nvar callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\nvar chartid \u003d \"geochart\";\n \n// Manually see if chartid is in pkgs (not all browsers support Array.indexOf)\nvar i, newPackage \u003d true;\nfor (i \u003d 0; newPackage \u0026\u0026 i \u003c pkgs.length; i++) {\nif (pkgs[i] \u003d\u003d\u003d chartid)\nnewPackage \u003d false;\n}\nif (newPackage)\n pkgs.push(chartid);\n \n// Add the drawChart function to the global list of callbacks\ncallbacks.push(drawChartGeoChartID7a022c4718d0);\n})();\nfunction displayChartGeoChartID7a022c4718d0() {\n var pkgs \u003d window.__gvisPackages \u003d window.__gvisPackages || [];\n var callbacks \u003d window.__gvisCallbacks \u003d window.__gvisCallbacks || [];\n window.clearTimeout(window.__gvisLoad);\n // The timeout is set to 100 because otherwise the container div we are\n // targeting might not be part of the document yet\n window.__gvisLoad \u003d setTimeout(function() {\n var pkgCount \u003d pkgs.length;\n google.load(\"visualization\", \"1\", { packages:pkgs, callback: function() {\n if (pkgCount !\u003d pkgs.length) {\n // Race condition where another setTimeout call snuck in after us; if\n // that call added a package, we must not shift its callback\n return;\n}\nwhile (callbacks.length \u003e 0)\ncallbacks.shift()();\n} });\n}, 100);\n}\n \n// jsFooter\n\u003c/script\u003e\n \n\u003c!-- jsChart --\u003e \n\u003cscript type\u003d\"text/javascript\" src\u003d\"https://www.google.com/jsapi?callback\u003ddisplayChartGeoChartID7a022c4718d0\"\u003e\u003c/script\u003e\n \n\u003c!-- divChart --\u003e\n \n\u003cdiv id\u003d\"GeoChartID7a022c4718d0\" \n style\u003d\"width: 556; height: 347;\"\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665500598_-1640522611", + "id": "20171207-165140_73691836", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n\n## Congratulations, it\u0027s done.\n### You can create your own notebook in \u0027Notebook\u0027 menu. Good luck!", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch2\u003eCongratulations, it\u0026rsquo;s done.\u003c/h2\u003e\n\u003ch3\u003eYou can create your own notebook in \u0026lsquo;Notebook\u0026rsquo; menu. Good luck!\u003c/h3\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512665635926_-1387206238", + "id": "20171207-165355_1943378000", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + } + ], + "name": "Zeppelin Tutorial/Livy • R (SparkR)", + "id": "2D25QSMZD", + "angularObjects": {}, + "config": {}, + "info": {} +} diff --git a/notebook/2D3979PMW/note.json b/notebook/2D3979PMW/note.json new file mode 100644 index 00000000000..a4d0ba3d8ef --- /dev/null +++ b/notebook/2D3979PMW/note.json @@ -0,0 +1,263 @@ +{ + "paragraphs": [ + { + "title": "First line", + "text": "%livy.pyspark\ntry:\n from StringIO import StringIO\nexcept ImportError:\n from io import StringIO\n\nimport matplotlib\nmatplotlib.use(\u0027svg\u0027)\nimport matplotlib.pyplot as plt\nplt.close() # Added here to reset the plot when rerunning the paragraph\nplt.switch_backend(\u0027svg\u0027)\n\ndef show(plt):\n img \u003d StringIO()\n plt.savefig(img, format\u003d\u0027svg\u0027)\n img.seek(0)\n print(r\u0027%html \u0027 + img.getvalue())\n\nplt.plot([1, 2, 3], label\u003dr\u0027$y\u003dx$\u0027)\nshow(plt)", + "user": "anonymous", + "config": { + "colWidth": 6.0, + "enabled": true, + "results": { + "0": { + "graph": { + "mode": "table", + "height": 454.0, + "optionOpen": false + } + } + }, + "editorSetting": { + "language": "python", + "editOnDblClick": false + }, + "editorMode": "ace/mode/python", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003c?xml version\u003d\"1.0\" encoding\u003d\"utf-8\" standalone\u003d\"no\"?\u003e\n\u003c!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\"\u003e\n\u003c!-- Created with matplotlib (http://matplotlib.org/) --\u003e\n\u003csvg height\u003d\"345pt\" version\u003d\"1.1\" viewBox\u003d\"0 0 460 345\" width\u003d\"460pt\" xmlns\u003d\"http://www.w3.org/2000/svg\" xmlns:xlink\u003d\"http://www.w3.org/1999/xlink\"\u003e\n \u003cdefs\u003e\n \u003cstyle type\u003d\"text/css\"\u003e\n*{stroke-linecap:butt;stroke-linejoin:round;}\n \u003c/style\u003e\n \u003c/defs\u003e\n \u003cg id\u003d\"figure_1\"\u003e\n \u003cg id\u003d\"patch_1\"\u003e\n \u003cpath d\u003d\"M 0 345.6 \nL 460.8 345.6 \nL 460.8 0 \nL 0 0 \nz\n\" style\u003d\"fill:#ffffff;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"axes_1\"\u003e\n \u003cg id\u003d\"patch_2\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 414.72 307.584 \nL 414.72 41.472 \nL 57.6 41.472 \nz\n\" style\u003d\"fill:#ffffff;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"matplotlib.axis_1\"\u003e\n \u003cg id\u003d\"xtick_1\"\u003e\n \u003cg id\u003d\"line2d_1\"\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 0 0 \nL 0 3.5 \n\" id\u003d\"m4f38bcb3c2\" style\u003d\"stroke:#000000;stroke-width:0.8;\"/\u003e\n \u003c/defs\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"73.832727\" xlink:href\u003d\"#m4f38bcb3c2\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_1\"\u003e\n \u003c!-- 0.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 31.78125 66.40625 \nQ 24.171875 66.40625 20.328125 58.90625 \nQ 16.5 51.421875 16.5 36.375 \nQ 16.5 21.390625 20.328125 13.890625 \nQ 24.171875 6.390625 31.78125 6.390625 \nQ 39.453125 6.390625 43.28125 13.890625 \nQ 47.125 21.390625 47.125 36.375 \nQ 47.125 51.421875 43.28125 58.90625 \nQ 39.453125 66.40625 31.78125 66.40625 \nz\nM 31.78125 74.21875 \nQ 44.046875 74.21875 50.515625 64.515625 \nQ 56.984375 54.828125 56.984375 36.375 \nQ 56.984375 17.96875 50.515625 8.265625 \nQ 44.046875 -1.421875 31.78125 -1.421875 \nQ 19.53125 -1.421875 13.0625 8.265625 \nQ 6.59375 17.96875 6.59375 36.375 \nQ 6.59375 54.828125 13.0625 64.515625 \nQ 19.53125 74.21875 31.78125 74.21875 \nz\n\" id\u003d\"DejaVuSans-30\"/\u003e\n \u003cpath d\u003d\"M 10.6875 12.40625 \nL 21 12.40625 \nL 21 0 \nL 10.6875 0 \nz\n\" id\u003d\"DejaVuSans-2e\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(62.699915 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_2\"\u003e\n \u003cg id\u003d\"line2d_2\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"114.414545\" xlink:href\u003d\"#m4f38bcb3c2\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_2\"\u003e\n \u003c!-- 0.25 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 19.1875 8.296875 \nL 53.609375 8.296875 \nL 53.609375 0 \nL 7.328125 0 \nL 7.328125 8.296875 \nQ 12.9375 14.109375 22.625 23.890625 \nQ 32.328125 33.6875 34.8125 36.53125 \nQ 39.546875 41.84375 41.421875 45.53125 \nQ 43.3125 49.21875 43.3125 52.78125 \nQ 43.3125 58.59375 39.234375 62.25 \nQ 35.15625 65.921875 28.609375 65.921875 \nQ 23.96875 65.921875 18.8125 64.3125 \nQ 13.671875 62.703125 7.8125 59.421875 \nL 7.8125 69.390625 \nQ 13.765625 71.78125 18.9375 73 \nQ 24.125 74.21875 28.421875 74.21875 \nQ 39.75 74.21875 46.484375 68.546875 \nQ 53.21875 62.890625 53.21875 53.421875 \nQ 53.21875 48.921875 51.53125 44.890625 \nQ 49.859375 40.875 45.40625 35.40625 \nQ 44.1875 33.984375 37.640625 27.21875 \nQ 31.109375 20.453125 19.1875 8.296875 \nz\n\" id\u003d\"DejaVuSans-32\"/\u003e\n \u003cpath d\u003d\"M 10.796875 72.90625 \nL 49.515625 72.90625 \nL 49.515625 64.59375 \nL 19.828125 64.59375 \nL 19.828125 46.734375 \nQ 21.96875 47.46875 24.109375 47.828125 \nQ 26.265625 48.1875 28.421875 48.1875 \nQ 40.625 48.1875 47.75 41.5 \nQ 54.890625 34.8125 54.890625 23.390625 \nQ 54.890625 11.625 47.5625 5.09375 \nQ 40.234375 -1.421875 26.90625 -1.421875 \nQ 22.3125 -1.421875 17.546875 -0.640625 \nQ 12.796875 0.140625 7.71875 1.703125 \nL 7.71875 11.625 \nQ 12.109375 9.234375 16.796875 8.0625 \nQ 21.484375 6.890625 26.703125 6.890625 \nQ 35.15625 6.890625 40.078125 11.328125 \nQ 45.015625 15.765625 45.015625 23.390625 \nQ 45.015625 31 40.078125 35.4375 \nQ 35.15625 39.890625 26.703125 39.890625 \nQ 22.75 39.890625 18.8125 39.015625 \nQ 14.890625 38.140625 10.796875 36.28125 \nz\n\" id\u003d\"DejaVuSans-35\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(103.281733 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_3\"\u003e\n \u003cg id\u003d\"line2d_3\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"154.996364\" xlink:href\u003d\"#m4f38bcb3c2\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_3\"\u003e\n \u003c!-- 0.50 --\u003e\n \u003cg transform\u003d\"translate(143.863551 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_4\"\u003e\n \u003cg id\u003d\"line2d_4\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"195.578182\" xlink:href\u003d\"#m4f38bcb3c2\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_4\"\u003e\n \u003c!-- 0.75 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 8.203125 72.90625 \nL 55.078125 72.90625 \nL 55.078125 68.703125 \nL 28.609375 0 \nL 18.3125 0 \nL 43.21875 64.59375 \nL 8.203125 64.59375 \nz\n\" id\u003d\"DejaVuSans-37\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(184.445369 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_5\"\u003e\n \u003cg id\u003d\"line2d_5\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"236.16\" xlink:href\u003d\"#m4f38bcb3c2\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_5\"\u003e\n \u003c!-- 1.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 12.40625 8.296875 \nL 28.515625 8.296875 \nL 28.515625 63.921875 \nL 10.984375 60.40625 \nL 10.984375 69.390625 \nL 28.421875 72.90625 \nL 38.28125 72.90625 \nL 38.28125 8.296875 \nL 54.390625 8.296875 \nL 54.390625 0 \nL 12.40625 0 \nz\n\" id\u003d\"DejaVuSans-31\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(225.027187 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_6\"\u003e\n \u003cg id\u003d\"line2d_6\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"276.741818\" xlink:href\u003d\"#m4f38bcb3c2\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_6\"\u003e\n \u003c!-- 1.25 --\u003e\n \u003cg transform\u003d\"translate(265.609006 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_7\"\u003e\n \u003cg id\u003d\"line2d_7\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"317.323636\" xlink:href\u003d\"#m4f38bcb3c2\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_7\"\u003e\n \u003c!-- 1.50 --\u003e\n \u003cg transform\u003d\"translate(306.190824 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_8\"\u003e\n \u003cg id\u003d\"line2d_8\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"357.905455\" xlink:href\u003d\"#m4f38bcb3c2\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_8\"\u003e\n \u003c!-- 1.75 --\u003e\n \u003cg transform\u003d\"translate(346.772642 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_9\"\u003e\n \u003cg id\u003d\"line2d_9\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"398.487273\" xlink:href\u003d\"#m4f38bcb3c2\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_9\"\u003e\n \u003c!-- 2.00 --\u003e\n \u003cg transform\u003d\"translate(387.35446 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"matplotlib.axis_2\"\u003e\n \u003cg id\u003d\"ytick_1\"\u003e\n \u003cg id\u003d\"line2d_10\"\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 0 0 \nL -3.5 0 \n\" id\u003d\"mb7ed64b91a\" style\u003d\"stroke:#000000;stroke-width:0.8;\"/\u003e\n \u003c/defs\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mb7ed64b91a\" y\u003d\"295.488\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_10\"\u003e\n \u003c!-- 1.00 --\u003e\n \u003cg transform\u003d\"translate(28.334375 299.287219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_2\"\u003e\n \u003cg id\u003d\"line2d_11\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mb7ed64b91a\" y\u003d\"265.248\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_11\"\u003e\n \u003c!-- 1.25 --\u003e\n \u003cg transform\u003d\"translate(28.334375 269.047219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_3\"\u003e\n \u003cg id\u003d\"line2d_12\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mb7ed64b91a\" y\u003d\"235.008\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_12\"\u003e\n \u003c!-- 1.50 --\u003e\n \u003cg transform\u003d\"translate(28.334375 238.807219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_4\"\u003e\n \u003cg id\u003d\"line2d_13\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mb7ed64b91a\" y\u003d\"204.768\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_13\"\u003e\n \u003c!-- 1.75 --\u003e\n \u003cg transform\u003d\"translate(28.334375 208.567219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_5\"\u003e\n \u003cg id\u003d\"line2d_14\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mb7ed64b91a\" y\u003d\"174.528\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_14\"\u003e\n \u003c!-- 2.00 --\u003e\n \u003cg transform\u003d\"translate(28.334375 178.327219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_6\"\u003e\n \u003cg id\u003d\"line2d_15\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mb7ed64b91a\" y\u003d\"144.288\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_15\"\u003e\n \u003c!-- 2.25 --\u003e\n \u003cg transform\u003d\"translate(28.334375 148.087219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_7\"\u003e\n \u003cg id\u003d\"line2d_16\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mb7ed64b91a\" y\u003d\"114.048\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_16\"\u003e\n \u003c!-- 2.50 --\u003e\n \u003cg transform\u003d\"translate(28.334375 117.847219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_8\"\u003e\n \u003cg id\u003d\"line2d_17\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mb7ed64b91a\" y\u003d\"83.808\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_17\"\u003e\n \u003c!-- 2.75 --\u003e\n \u003cg transform\u003d\"translate(28.334375 87.607219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_9\"\u003e\n \u003cg id\u003d\"line2d_18\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mb7ed64b91a\" y\u003d\"53.568\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_18\"\u003e\n \u003c!-- 3.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 40.578125 39.3125 \nQ 47.65625 37.796875 51.625 33 \nQ 55.609375 28.21875 55.609375 21.1875 \nQ 55.609375 10.40625 48.1875 4.484375 \nQ 40.765625 -1.421875 27.09375 -1.421875 \nQ 22.515625 -1.421875 17.65625 -0.515625 \nQ 12.796875 0.390625 7.625 2.203125 \nL 7.625 11.71875 \nQ 11.71875 9.328125 16.59375 8.109375 \nQ 21.484375 6.890625 26.8125 6.890625 \nQ 36.078125 6.890625 40.9375 10.546875 \nQ 45.796875 14.203125 45.796875 21.1875 \nQ 45.796875 27.640625 41.28125 31.265625 \nQ 36.765625 34.90625 28.71875 34.90625 \nL 20.21875 34.90625 \nL 20.21875 43.015625 \nL 29.109375 43.015625 \nQ 36.375 43.015625 40.234375 45.921875 \nQ 44.09375 48.828125 44.09375 54.296875 \nQ 44.09375 59.90625 40.109375 62.90625 \nQ 36.140625 65.921875 28.71875 65.921875 \nQ 24.65625 65.921875 20.015625 65.03125 \nQ 15.375 64.15625 9.8125 62.3125 \nL 9.8125 71.09375 \nQ 15.4375 72.65625 20.34375 73.4375 \nQ 25.25 74.21875 29.59375 74.21875 \nQ 40.828125 74.21875 47.359375 69.109375 \nQ 53.90625 64.015625 53.90625 55.328125 \nQ 53.90625 49.265625 50.4375 45.09375 \nQ 46.96875 40.921875 40.578125 39.3125 \nz\n\" id\u003d\"DejaVuSans-33\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(28.334375 57.367219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-33\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_19\"\u003e\n \u003cpath clip-path\u003d\"url(#pc26c722a56)\" d\u003d\"M 73.832727 295.488 \nL 236.16 174.528 \nL 398.487273 53.568 \n\" style\u003d\"fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_3\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 57.6 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_4\"\u003e\n \u003cpath d\u003d\"M 414.72 307.584 \nL 414.72 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_5\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 414.72 307.584 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_6\"\u003e\n \u003cpath d\u003d\"M 57.6 41.472 \nL 414.72 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cdefs\u003e\n \u003cclipPath id\u003d\"pc26c722a56\"\u003e\n \u003crect height\u003d\"266.112\" width\u003d\"357.12\" x\u003d\"57.6\" y\u003d\"41.472\"/\u003e\n \u003c/clipPath\u003e\n \u003c/defs\u003e\n\u003c/svg\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512664292648_-352334556", + "id": "20171207-163132_1423616707", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Second line", + "text": "%livy.pyspark\nplt.plot([3, 2, 1], label\u003dr\u0027$y\u003d3-x$\u0027)\nshow(plt)", + "user": "anonymous", + "config": { + "colWidth": 6.0, + "enabled": true, + "results": { + "0": { + "graph": { + "mode": "table", + "height": 454.0, + "optionOpen": false + } + } + }, + "editorSetting": { + "language": "python", + "editOnDblClick": false + }, + "title": true, + "editorMode": "ace/mode/python" + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003c?xml version\u003d\"1.0\" encoding\u003d\"utf-8\" standalone\u003d\"no\"?\u003e\n\u003c!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\"\u003e\n\u003c!-- Created with matplotlib (http://matplotlib.org/) --\u003e\n\u003csvg height\u003d\"345pt\" version\u003d\"1.1\" viewBox\u003d\"0 0 460 345\" width\u003d\"460pt\" xmlns\u003d\"http://www.w3.org/2000/svg\" xmlns:xlink\u003d\"http://www.w3.org/1999/xlink\"\u003e\n \u003cdefs\u003e\n \u003cstyle type\u003d\"text/css\"\u003e\n*{stroke-linecap:butt;stroke-linejoin:round;}\n \u003c/style\u003e\n \u003c/defs\u003e\n \u003cg id\u003d\"figure_1\"\u003e\n \u003cg id\u003d\"patch_1\"\u003e\n \u003cpath d\u003d\"M 0 345.6 \nL 460.8 345.6 \nL 460.8 0 \nL 0 0 \nz\n\" style\u003d\"fill:#ffffff;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"axes_1\"\u003e\n \u003cg id\u003d\"patch_2\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 414.72 307.584 \nL 414.72 41.472 \nL 57.6 41.472 \nz\n\" style\u003d\"fill:#ffffff;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"matplotlib.axis_1\"\u003e\n \u003cg id\u003d\"xtick_1\"\u003e\n \u003cg id\u003d\"line2d_1\"\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 0 0 \nL 0 3.5 \n\" id\u003d\"m14b8b11d47\" style\u003d\"stroke:#000000;stroke-width:0.8;\"/\u003e\n \u003c/defs\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"73.832727\" xlink:href\u003d\"#m14b8b11d47\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_1\"\u003e\n \u003c!-- 0.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 31.78125 66.40625 \nQ 24.171875 66.40625 20.328125 58.90625 \nQ 16.5 51.421875 16.5 36.375 \nQ 16.5 21.390625 20.328125 13.890625 \nQ 24.171875 6.390625 31.78125 6.390625 \nQ 39.453125 6.390625 43.28125 13.890625 \nQ 47.125 21.390625 47.125 36.375 \nQ 47.125 51.421875 43.28125 58.90625 \nQ 39.453125 66.40625 31.78125 66.40625 \nz\nM 31.78125 74.21875 \nQ 44.046875 74.21875 50.515625 64.515625 \nQ 56.984375 54.828125 56.984375 36.375 \nQ 56.984375 17.96875 50.515625 8.265625 \nQ 44.046875 -1.421875 31.78125 -1.421875 \nQ 19.53125 -1.421875 13.0625 8.265625 \nQ 6.59375 17.96875 6.59375 36.375 \nQ 6.59375 54.828125 13.0625 64.515625 \nQ 19.53125 74.21875 31.78125 74.21875 \nz\n\" id\u003d\"DejaVuSans-30\"/\u003e\n \u003cpath d\u003d\"M 10.6875 12.40625 \nL 21 12.40625 \nL 21 0 \nL 10.6875 0 \nz\n\" id\u003d\"DejaVuSans-2e\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(62.699915 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_2\"\u003e\n \u003cg id\u003d\"line2d_2\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"114.414545\" xlink:href\u003d\"#m14b8b11d47\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_2\"\u003e\n \u003c!-- 0.25 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 19.1875 8.296875 \nL 53.609375 8.296875 \nL 53.609375 0 \nL 7.328125 0 \nL 7.328125 8.296875 \nQ 12.9375 14.109375 22.625 23.890625 \nQ 32.328125 33.6875 34.8125 36.53125 \nQ 39.546875 41.84375 41.421875 45.53125 \nQ 43.3125 49.21875 43.3125 52.78125 \nQ 43.3125 58.59375 39.234375 62.25 \nQ 35.15625 65.921875 28.609375 65.921875 \nQ 23.96875 65.921875 18.8125 64.3125 \nQ 13.671875 62.703125 7.8125 59.421875 \nL 7.8125 69.390625 \nQ 13.765625 71.78125 18.9375 73 \nQ 24.125 74.21875 28.421875 74.21875 \nQ 39.75 74.21875 46.484375 68.546875 \nQ 53.21875 62.890625 53.21875 53.421875 \nQ 53.21875 48.921875 51.53125 44.890625 \nQ 49.859375 40.875 45.40625 35.40625 \nQ 44.1875 33.984375 37.640625 27.21875 \nQ 31.109375 20.453125 19.1875 8.296875 \nz\n\" id\u003d\"DejaVuSans-32\"/\u003e\n \u003cpath d\u003d\"M 10.796875 72.90625 \nL 49.515625 72.90625 \nL 49.515625 64.59375 \nL 19.828125 64.59375 \nL 19.828125 46.734375 \nQ 21.96875 47.46875 24.109375 47.828125 \nQ 26.265625 48.1875 28.421875 48.1875 \nQ 40.625 48.1875 47.75 41.5 \nQ 54.890625 34.8125 54.890625 23.390625 \nQ 54.890625 11.625 47.5625 5.09375 \nQ 40.234375 -1.421875 26.90625 -1.421875 \nQ 22.3125 -1.421875 17.546875 -0.640625 \nQ 12.796875 0.140625 7.71875 1.703125 \nL 7.71875 11.625 \nQ 12.109375 9.234375 16.796875 8.0625 \nQ 21.484375 6.890625 26.703125 6.890625 \nQ 35.15625 6.890625 40.078125 11.328125 \nQ 45.015625 15.765625 45.015625 23.390625 \nQ 45.015625 31 40.078125 35.4375 \nQ 35.15625 39.890625 26.703125 39.890625 \nQ 22.75 39.890625 18.8125 39.015625 \nQ 14.890625 38.140625 10.796875 36.28125 \nz\n\" id\u003d\"DejaVuSans-35\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(103.281733 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_3\"\u003e\n \u003cg id\u003d\"line2d_3\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"154.996364\" xlink:href\u003d\"#m14b8b11d47\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_3\"\u003e\n \u003c!-- 0.50 --\u003e\n \u003cg transform\u003d\"translate(143.863551 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_4\"\u003e\n \u003cg id\u003d\"line2d_4\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"195.578182\" xlink:href\u003d\"#m14b8b11d47\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_4\"\u003e\n \u003c!-- 0.75 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 8.203125 72.90625 \nL 55.078125 72.90625 \nL 55.078125 68.703125 \nL 28.609375 0 \nL 18.3125 0 \nL 43.21875 64.59375 \nL 8.203125 64.59375 \nz\n\" id\u003d\"DejaVuSans-37\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(184.445369 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_5\"\u003e\n \u003cg id\u003d\"line2d_5\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"236.16\" xlink:href\u003d\"#m14b8b11d47\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_5\"\u003e\n \u003c!-- 1.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 12.40625 8.296875 \nL 28.515625 8.296875 \nL 28.515625 63.921875 \nL 10.984375 60.40625 \nL 10.984375 69.390625 \nL 28.421875 72.90625 \nL 38.28125 72.90625 \nL 38.28125 8.296875 \nL 54.390625 8.296875 \nL 54.390625 0 \nL 12.40625 0 \nz\n\" id\u003d\"DejaVuSans-31\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(225.027187 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_6\"\u003e\n \u003cg id\u003d\"line2d_6\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"276.741818\" xlink:href\u003d\"#m14b8b11d47\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_6\"\u003e\n \u003c!-- 1.25 --\u003e\n \u003cg transform\u003d\"translate(265.609006 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_7\"\u003e\n \u003cg id\u003d\"line2d_7\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"317.323636\" xlink:href\u003d\"#m14b8b11d47\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_7\"\u003e\n \u003c!-- 1.50 --\u003e\n \u003cg transform\u003d\"translate(306.190824 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_8\"\u003e\n \u003cg id\u003d\"line2d_8\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"357.905455\" xlink:href\u003d\"#m14b8b11d47\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_8\"\u003e\n \u003c!-- 1.75 --\u003e\n \u003cg transform\u003d\"translate(346.772642 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_9\"\u003e\n \u003cg id\u003d\"line2d_9\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"398.487273\" xlink:href\u003d\"#m14b8b11d47\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_9\"\u003e\n \u003c!-- 2.00 --\u003e\n \u003cg transform\u003d\"translate(387.35446 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"matplotlib.axis_2\"\u003e\n \u003cg id\u003d\"ytick_1\"\u003e\n \u003cg id\u003d\"line2d_10\"\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 0 0 \nL -3.5 0 \n\" id\u003d\"m4669d210ce\" style\u003d\"stroke:#000000;stroke-width:0.8;\"/\u003e\n \u003c/defs\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m4669d210ce\" y\u003d\"295.488\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_10\"\u003e\n \u003c!-- 1.00 --\u003e\n \u003cg transform\u003d\"translate(28.334375 299.287219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_2\"\u003e\n \u003cg id\u003d\"line2d_11\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m4669d210ce\" y\u003d\"265.248\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_11\"\u003e\n \u003c!-- 1.25 --\u003e\n \u003cg transform\u003d\"translate(28.334375 269.047219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_3\"\u003e\n \u003cg id\u003d\"line2d_12\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m4669d210ce\" y\u003d\"235.008\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_12\"\u003e\n \u003c!-- 1.50 --\u003e\n \u003cg transform\u003d\"translate(28.334375 238.807219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_4\"\u003e\n \u003cg id\u003d\"line2d_13\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m4669d210ce\" y\u003d\"204.768\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_13\"\u003e\n \u003c!-- 1.75 --\u003e\n \u003cg transform\u003d\"translate(28.334375 208.567219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_5\"\u003e\n \u003cg id\u003d\"line2d_14\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m4669d210ce\" y\u003d\"174.528\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_14\"\u003e\n \u003c!-- 2.00 --\u003e\n \u003cg transform\u003d\"translate(28.334375 178.327219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_6\"\u003e\n \u003cg id\u003d\"line2d_15\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m4669d210ce\" y\u003d\"144.288\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_15\"\u003e\n \u003c!-- 2.25 --\u003e\n \u003cg transform\u003d\"translate(28.334375 148.087219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_7\"\u003e\n \u003cg id\u003d\"line2d_16\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m4669d210ce\" y\u003d\"114.048\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_16\"\u003e\n \u003c!-- 2.50 --\u003e\n \u003cg transform\u003d\"translate(28.334375 117.847219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_8\"\u003e\n \u003cg id\u003d\"line2d_17\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m4669d210ce\" y\u003d\"83.808\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_17\"\u003e\n \u003c!-- 2.75 --\u003e\n \u003cg transform\u003d\"translate(28.334375 87.607219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_9\"\u003e\n \u003cg id\u003d\"line2d_18\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m4669d210ce\" y\u003d\"53.568\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_18\"\u003e\n \u003c!-- 3.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 40.578125 39.3125 \nQ 47.65625 37.796875 51.625 33 \nQ 55.609375 28.21875 55.609375 21.1875 \nQ 55.609375 10.40625 48.1875 4.484375 \nQ 40.765625 -1.421875 27.09375 -1.421875 \nQ 22.515625 -1.421875 17.65625 -0.515625 \nQ 12.796875 0.390625 7.625 2.203125 \nL 7.625 11.71875 \nQ 11.71875 9.328125 16.59375 8.109375 \nQ 21.484375 6.890625 26.8125 6.890625 \nQ 36.078125 6.890625 40.9375 10.546875 \nQ 45.796875 14.203125 45.796875 21.1875 \nQ 45.796875 27.640625 41.28125 31.265625 \nQ 36.765625 34.90625 28.71875 34.90625 \nL 20.21875 34.90625 \nL 20.21875 43.015625 \nL 29.109375 43.015625 \nQ 36.375 43.015625 40.234375 45.921875 \nQ 44.09375 48.828125 44.09375 54.296875 \nQ 44.09375 59.90625 40.109375 62.90625 \nQ 36.140625 65.921875 28.71875 65.921875 \nQ 24.65625 65.921875 20.015625 65.03125 \nQ 15.375 64.15625 9.8125 62.3125 \nL 9.8125 71.09375 \nQ 15.4375 72.65625 20.34375 73.4375 \nQ 25.25 74.21875 29.59375 74.21875 \nQ 40.828125 74.21875 47.359375 69.109375 \nQ 53.90625 64.015625 53.90625 55.328125 \nQ 53.90625 49.265625 50.4375 45.09375 \nQ 46.96875 40.921875 40.578125 39.3125 \nz\n\" id\u003d\"DejaVuSans-33\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(28.334375 57.367219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-33\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_19\"\u003e\n \u003cpath clip-path\u003d\"url(#pcad25c7aa2)\" d\u003d\"M 73.832727 295.488 \nL 236.16 174.528 \nL 398.487273 53.568 \n\" style\u003d\"fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_20\"\u003e\n \u003cpath clip-path\u003d\"url(#pcad25c7aa2)\" d\u003d\"M 73.832727 53.568 \nL 236.16 174.528 \nL 398.487273 295.488 \n\" style\u003d\"fill:none;stroke:#ff7f0e;stroke-linecap:square;stroke-width:1.5;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_3\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 57.6 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_4\"\u003e\n \u003cpath d\u003d\"M 414.72 307.584 \nL 414.72 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_5\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 414.72 307.584 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_6\"\u003e\n \u003cpath d\u003d\"M 57.6 41.472 \nL 414.72 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cdefs\u003e\n \u003cclipPath id\u003d\"pcad25c7aa2\"\u003e\n \u003crect height\u003d\"266.112\" width\u003d\"357.12\" x\u003d\"57.6\" y\u003d\"41.472\"/\u003e\n \u003c/clipPath\u003e\n \u003c/defs\u003e\n\u003c/svg\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512664496179_-424592373", + "id": "20171207-163456_418708714", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n\n---", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": false, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003chr/\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512667852186_467566627", + "id": "20171207-173052_973221116", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Label axes", + "text": "%livy.pyspark\nplt.xlabel(r\u0027$x$\u0027, fontsize\u003d20)\nplt.ylabel(r\u0027$y$\u0027, fontsize\u003d20)\nshow(plt)", + "user": "anonymous", + "config": { + "colWidth": 6.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "python", + "editOnDblClick": false + }, + "editorMode": "ace/mode/python", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003c?xml version\u003d\"1.0\" encoding\u003d\"utf-8\" standalone\u003d\"no\"?\u003e\n\u003c!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\"\u003e\n\u003c!-- Created with matplotlib (http://matplotlib.org/) --\u003e\n\u003csvg height\u003d\"345pt\" version\u003d\"1.1\" viewBox\u003d\"0 0 460 345\" width\u003d\"460pt\" xmlns\u003d\"http://www.w3.org/2000/svg\" xmlns:xlink\u003d\"http://www.w3.org/1999/xlink\"\u003e\n \u003cdefs\u003e\n \u003cstyle type\u003d\"text/css\"\u003e\n*{stroke-linecap:butt;stroke-linejoin:round;}\n \u003c/style\u003e\n \u003c/defs\u003e\n \u003cg id\u003d\"figure_1\"\u003e\n \u003cg id\u003d\"patch_1\"\u003e\n \u003cpath d\u003d\"M 0 345.6 \nL 460.8 345.6 \nL 460.8 0 \nL 0 0 \nz\n\" style\u003d\"fill:#ffffff;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"axes_1\"\u003e\n \u003cg id\u003d\"patch_2\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 414.72 307.584 \nL 414.72 41.472 \nL 57.6 41.472 \nz\n\" style\u003d\"fill:#ffffff;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"matplotlib.axis_1\"\u003e\n \u003cg id\u003d\"xtick_1\"\u003e\n \u003cg id\u003d\"line2d_1\"\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 0 0 \nL 0 3.5 \n\" id\u003d\"m02b82f756d\" style\u003d\"stroke:#000000;stroke-width:0.8;\"/\u003e\n \u003c/defs\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"73.832727\" xlink:href\u003d\"#m02b82f756d\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_1\"\u003e\n \u003c!-- 0.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 31.78125 66.40625 \nQ 24.171875 66.40625 20.328125 58.90625 \nQ 16.5 51.421875 16.5 36.375 \nQ 16.5 21.390625 20.328125 13.890625 \nQ 24.171875 6.390625 31.78125 6.390625 \nQ 39.453125 6.390625 43.28125 13.890625 \nQ 47.125 21.390625 47.125 36.375 \nQ 47.125 51.421875 43.28125 58.90625 \nQ 39.453125 66.40625 31.78125 66.40625 \nz\nM 31.78125 74.21875 \nQ 44.046875 74.21875 50.515625 64.515625 \nQ 56.984375 54.828125 56.984375 36.375 \nQ 56.984375 17.96875 50.515625 8.265625 \nQ 44.046875 -1.421875 31.78125 -1.421875 \nQ 19.53125 -1.421875 13.0625 8.265625 \nQ 6.59375 17.96875 6.59375 36.375 \nQ 6.59375 54.828125 13.0625 64.515625 \nQ 19.53125 74.21875 31.78125 74.21875 \nz\n\" id\u003d\"DejaVuSans-30\"/\u003e\n \u003cpath d\u003d\"M 10.6875 12.40625 \nL 21 12.40625 \nL 21 0 \nL 10.6875 0 \nz\n\" id\u003d\"DejaVuSans-2e\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(62.699915 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_2\"\u003e\n \u003cg id\u003d\"line2d_2\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"114.414545\" xlink:href\u003d\"#m02b82f756d\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_2\"\u003e\n \u003c!-- 0.25 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 19.1875 8.296875 \nL 53.609375 8.296875 \nL 53.609375 0 \nL 7.328125 0 \nL 7.328125 8.296875 \nQ 12.9375 14.109375 22.625 23.890625 \nQ 32.328125 33.6875 34.8125 36.53125 \nQ 39.546875 41.84375 41.421875 45.53125 \nQ 43.3125 49.21875 43.3125 52.78125 \nQ 43.3125 58.59375 39.234375 62.25 \nQ 35.15625 65.921875 28.609375 65.921875 \nQ 23.96875 65.921875 18.8125 64.3125 \nQ 13.671875 62.703125 7.8125 59.421875 \nL 7.8125 69.390625 \nQ 13.765625 71.78125 18.9375 73 \nQ 24.125 74.21875 28.421875 74.21875 \nQ 39.75 74.21875 46.484375 68.546875 \nQ 53.21875 62.890625 53.21875 53.421875 \nQ 53.21875 48.921875 51.53125 44.890625 \nQ 49.859375 40.875 45.40625 35.40625 \nQ 44.1875 33.984375 37.640625 27.21875 \nQ 31.109375 20.453125 19.1875 8.296875 \nz\n\" id\u003d\"DejaVuSans-32\"/\u003e\n \u003cpath d\u003d\"M 10.796875 72.90625 \nL 49.515625 72.90625 \nL 49.515625 64.59375 \nL 19.828125 64.59375 \nL 19.828125 46.734375 \nQ 21.96875 47.46875 24.109375 47.828125 \nQ 26.265625 48.1875 28.421875 48.1875 \nQ 40.625 48.1875 47.75 41.5 \nQ 54.890625 34.8125 54.890625 23.390625 \nQ 54.890625 11.625 47.5625 5.09375 \nQ 40.234375 -1.421875 26.90625 -1.421875 \nQ 22.3125 -1.421875 17.546875 -0.640625 \nQ 12.796875 0.140625 7.71875 1.703125 \nL 7.71875 11.625 \nQ 12.109375 9.234375 16.796875 8.0625 \nQ 21.484375 6.890625 26.703125 6.890625 \nQ 35.15625 6.890625 40.078125 11.328125 \nQ 45.015625 15.765625 45.015625 23.390625 \nQ 45.015625 31 40.078125 35.4375 \nQ 35.15625 39.890625 26.703125 39.890625 \nQ 22.75 39.890625 18.8125 39.015625 \nQ 14.890625 38.140625 10.796875 36.28125 \nz\n\" id\u003d\"DejaVuSans-35\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(103.281733 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_3\"\u003e\n \u003cg id\u003d\"line2d_3\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"154.996364\" xlink:href\u003d\"#m02b82f756d\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_3\"\u003e\n \u003c!-- 0.50 --\u003e\n \u003cg transform\u003d\"translate(143.863551 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_4\"\u003e\n \u003cg id\u003d\"line2d_4\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"195.578182\" xlink:href\u003d\"#m02b82f756d\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_4\"\u003e\n \u003c!-- 0.75 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 8.203125 72.90625 \nL 55.078125 72.90625 \nL 55.078125 68.703125 \nL 28.609375 0 \nL 18.3125 0 \nL 43.21875 64.59375 \nL 8.203125 64.59375 \nz\n\" id\u003d\"DejaVuSans-37\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(184.445369 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_5\"\u003e\n \u003cg id\u003d\"line2d_5\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"236.16\" xlink:href\u003d\"#m02b82f756d\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_5\"\u003e\n \u003c!-- 1.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 12.40625 8.296875 \nL 28.515625 8.296875 \nL 28.515625 63.921875 \nL 10.984375 60.40625 \nL 10.984375 69.390625 \nL 28.421875 72.90625 \nL 38.28125 72.90625 \nL 38.28125 8.296875 \nL 54.390625 8.296875 \nL 54.390625 0 \nL 12.40625 0 \nz\n\" id\u003d\"DejaVuSans-31\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(225.027187 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_6\"\u003e\n \u003cg id\u003d\"line2d_6\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"276.741818\" xlink:href\u003d\"#m02b82f756d\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_6\"\u003e\n \u003c!-- 1.25 --\u003e\n \u003cg transform\u003d\"translate(265.609006 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_7\"\u003e\n \u003cg id\u003d\"line2d_7\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"317.323636\" xlink:href\u003d\"#m02b82f756d\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_7\"\u003e\n \u003c!-- 1.50 --\u003e\n \u003cg transform\u003d\"translate(306.190824 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_8\"\u003e\n \u003cg id\u003d\"line2d_8\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"357.905455\" xlink:href\u003d\"#m02b82f756d\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_8\"\u003e\n \u003c!-- 1.75 --\u003e\n \u003cg transform\u003d\"translate(346.772642 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_9\"\u003e\n \u003cg id\u003d\"line2d_9\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"398.487273\" xlink:href\u003d\"#m02b82f756d\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_9\"\u003e\n \u003c!-- 2.00 --\u003e\n \u003cg transform\u003d\"translate(387.35446 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_10\"\u003e\n \u003c!-- $x$ --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 60.015625 54.6875 \nL 34.90625 27.875 \nL 50.296875 0 \nL 39.984375 0 \nL 28.421875 21.6875 \nL 8.296875 0 \nL -2.59375 0 \nL 24.3125 28.8125 \nL 10.015625 54.6875 \nL 20.3125 54.6875 \nL 30.8125 34.90625 \nL 49.125 54.6875 \nz\n\" id\u003d\"DejaVuSans-Oblique-78\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(230.16 343.459)scale(0.2 -0.2)\"\u003e\n \u003cuse transform\u003d\"translate(0 0.3125)\" xlink:href\u003d\"#DejaVuSans-Oblique-78\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"matplotlib.axis_2\"\u003e\n \u003cg id\u003d\"ytick_1\"\u003e\n \u003cg id\u003d\"line2d_10\"\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 0 0 \nL -3.5 0 \n\" id\u003d\"mcfc74bcb66\" style\u003d\"stroke:#000000;stroke-width:0.8;\"/\u003e\n \u003c/defs\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mcfc74bcb66\" y\u003d\"295.488\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_11\"\u003e\n \u003c!-- 1.00 --\u003e\n \u003cg transform\u003d\"translate(28.334375 299.287219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_2\"\u003e\n \u003cg id\u003d\"line2d_11\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mcfc74bcb66\" y\u003d\"265.248\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_12\"\u003e\n \u003c!-- 1.25 --\u003e\n \u003cg transform\u003d\"translate(28.334375 269.047219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_3\"\u003e\n \u003cg id\u003d\"line2d_12\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mcfc74bcb66\" y\u003d\"235.008\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_13\"\u003e\n \u003c!-- 1.50 --\u003e\n \u003cg transform\u003d\"translate(28.334375 238.807219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_4\"\u003e\n \u003cg id\u003d\"line2d_13\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mcfc74bcb66\" y\u003d\"204.768\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_14\"\u003e\n \u003c!-- 1.75 --\u003e\n \u003cg transform\u003d\"translate(28.334375 208.567219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_5\"\u003e\n \u003cg id\u003d\"line2d_14\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mcfc74bcb66\" y\u003d\"174.528\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_15\"\u003e\n \u003c!-- 2.00 --\u003e\n \u003cg transform\u003d\"translate(28.334375 178.327219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_6\"\u003e\n \u003cg id\u003d\"line2d_15\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mcfc74bcb66\" y\u003d\"144.288\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_16\"\u003e\n \u003c!-- 2.25 --\u003e\n \u003cg transform\u003d\"translate(28.334375 148.087219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_7\"\u003e\n \u003cg id\u003d\"line2d_16\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mcfc74bcb66\" y\u003d\"114.048\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_17\"\u003e\n \u003c!-- 2.50 --\u003e\n \u003cg transform\u003d\"translate(28.334375 117.847219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_8\"\u003e\n \u003cg id\u003d\"line2d_17\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mcfc74bcb66\" y\u003d\"83.808\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_18\"\u003e\n \u003c!-- 2.75 --\u003e\n \u003cg transform\u003d\"translate(28.334375 87.607219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_9\"\u003e\n \u003cg id\u003d\"line2d_18\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mcfc74bcb66\" y\u003d\"53.568\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_19\"\u003e\n \u003c!-- 3.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 40.578125 39.3125 \nQ 47.65625 37.796875 51.625 33 \nQ 55.609375 28.21875 55.609375 21.1875 \nQ 55.609375 10.40625 48.1875 4.484375 \nQ 40.765625 -1.421875 27.09375 -1.421875 \nQ 22.515625 -1.421875 17.65625 -0.515625 \nQ 12.796875 0.390625 7.625 2.203125 \nL 7.625 11.71875 \nQ 11.71875 9.328125 16.59375 8.109375 \nQ 21.484375 6.890625 26.8125 6.890625 \nQ 36.078125 6.890625 40.9375 10.546875 \nQ 45.796875 14.203125 45.796875 21.1875 \nQ 45.796875 27.640625 41.28125 31.265625 \nQ 36.765625 34.90625 28.71875 34.90625 \nL 20.21875 34.90625 \nL 20.21875 43.015625 \nL 29.109375 43.015625 \nQ 36.375 43.015625 40.234375 45.921875 \nQ 44.09375 48.828125 44.09375 54.296875 \nQ 44.09375 59.90625 40.109375 62.90625 \nQ 36.140625 65.921875 28.71875 65.921875 \nQ 24.65625 65.921875 20.015625 65.03125 \nQ 15.375 64.15625 9.8125 62.3125 \nL 9.8125 71.09375 \nQ 15.4375 72.65625 20.34375 73.4375 \nQ 25.25 74.21875 29.59375 74.21875 \nQ 40.828125 74.21875 47.359375 69.109375 \nQ 53.90625 64.015625 53.90625 55.328125 \nQ 53.90625 49.265625 50.4375 45.09375 \nQ 46.96875 40.921875 40.578125 39.3125 \nz\n\" id\u003d\"DejaVuSans-33\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(28.334375 57.367219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-33\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_20\"\u003e\n \u003c!-- $y$ --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 24.8125 -5.078125 \nQ 18.5625 -15.578125 14.625 -18.1875 \nQ 10.6875 -20.796875 4.59375 -20.796875 \nL -2.484375 -20.796875 \nL -0.984375 -13.28125 \nL 4.203125 -13.28125 \nQ 7.953125 -13.28125 10.59375 -11.234375 \nQ 13.234375 -9.1875 16.5 -3.21875 \nL 19.28125 2 \nL 7.171875 54.6875 \nL 16.703125 54.6875 \nL 25.78125 12.796875 \nL 50.875 54.6875 \nL 60.296875 54.6875 \nz\n\" id\u003d\"DejaVuSans-Oblique-79\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(20.134375 180.528)rotate(-90)scale(0.2 -0.2)\"\u003e\n \u003cuse transform\u003d\"translate(0 0.3125)\" xlink:href\u003d\"#DejaVuSans-Oblique-79\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_19\"\u003e\n \u003cpath clip-path\u003d\"url(#p652d672f8e)\" d\u003d\"M 73.832727 295.488 \nL 236.16 174.528 \nL 398.487273 53.568 \n\" style\u003d\"fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_20\"\u003e\n \u003cpath clip-path\u003d\"url(#p652d672f8e)\" d\u003d\"M 73.832727 53.568 \nL 236.16 174.528 \nL 398.487273 295.488 \n\" style\u003d\"fill:none;stroke:#ff7f0e;stroke-linecap:square;stroke-width:1.5;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_3\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 57.6 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_4\"\u003e\n \u003cpath d\u003d\"M 414.72 307.584 \nL 414.72 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_5\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 414.72 307.584 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_6\"\u003e\n \u003cpath d\u003d\"M 57.6 41.472 \nL 414.72 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cdefs\u003e\n \u003cclipPath id\u003d\"p652d672f8e\"\u003e\n \u003crect height\u003d\"266.112\" width\u003d\"357.12\" x\u003d\"57.6\" y\u003d\"41.472\"/\u003e\n \u003c/clipPath\u003e\n \u003c/defs\u003e\n\u003c/svg\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512664552534_976955925", + "id": "20171207-163552_1926892526", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Add legend", + "text": "%livy.pyspark\nplt.legend(loc\u003d\u0027upper center\u0027, fontsize\u003d20)\nshow(plt)", + "user": "anonymous", + "config": { + "colWidth": 6.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "python", + "editOnDblClick": false + }, + "editorMode": "ace/mode/python", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003c?xml version\u003d\"1.0\" encoding\u003d\"utf-8\" standalone\u003d\"no\"?\u003e\n\u003c!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\"\u003e\n\u003c!-- Created with matplotlib (http://matplotlib.org/) --\u003e\n\u003csvg height\u003d\"345pt\" version\u003d\"1.1\" viewBox\u003d\"0 0 460 345\" width\u003d\"460pt\" xmlns\u003d\"http://www.w3.org/2000/svg\" xmlns:xlink\u003d\"http://www.w3.org/1999/xlink\"\u003e\n \u003cdefs\u003e\n \u003cstyle type\u003d\"text/css\"\u003e\n*{stroke-linecap:butt;stroke-linejoin:round;}\n \u003c/style\u003e\n \u003c/defs\u003e\n \u003cg id\u003d\"figure_1\"\u003e\n \u003cg id\u003d\"patch_1\"\u003e\n \u003cpath d\u003d\"M 0 345.6 \nL 460.8 345.6 \nL 460.8 0 \nL 0 0 \nz\n\" style\u003d\"fill:#ffffff;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"axes_1\"\u003e\n \u003cg id\u003d\"patch_2\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 414.72 307.584 \nL 414.72 41.472 \nL 57.6 41.472 \nz\n\" style\u003d\"fill:#ffffff;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"matplotlib.axis_1\"\u003e\n \u003cg id\u003d\"xtick_1\"\u003e\n \u003cg id\u003d\"line2d_1\"\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 0 0 \nL 0 3.5 \n\" id\u003d\"m4edc89429a\" style\u003d\"stroke:#000000;stroke-width:0.8;\"/\u003e\n \u003c/defs\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"73.832727\" xlink:href\u003d\"#m4edc89429a\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_1\"\u003e\n \u003c!-- 0.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 31.78125 66.40625 \nQ 24.171875 66.40625 20.328125 58.90625 \nQ 16.5 51.421875 16.5 36.375 \nQ 16.5 21.390625 20.328125 13.890625 \nQ 24.171875 6.390625 31.78125 6.390625 \nQ 39.453125 6.390625 43.28125 13.890625 \nQ 47.125 21.390625 47.125 36.375 \nQ 47.125 51.421875 43.28125 58.90625 \nQ 39.453125 66.40625 31.78125 66.40625 \nz\nM 31.78125 74.21875 \nQ 44.046875 74.21875 50.515625 64.515625 \nQ 56.984375 54.828125 56.984375 36.375 \nQ 56.984375 17.96875 50.515625 8.265625 \nQ 44.046875 -1.421875 31.78125 -1.421875 \nQ 19.53125 -1.421875 13.0625 8.265625 \nQ 6.59375 17.96875 6.59375 36.375 \nQ 6.59375 54.828125 13.0625 64.515625 \nQ 19.53125 74.21875 31.78125 74.21875 \nz\n\" id\u003d\"DejaVuSans-30\"/\u003e\n \u003cpath d\u003d\"M 10.6875 12.40625 \nL 21 12.40625 \nL 21 0 \nL 10.6875 0 \nz\n\" id\u003d\"DejaVuSans-2e\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(62.699915 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_2\"\u003e\n \u003cg id\u003d\"line2d_2\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"114.414545\" xlink:href\u003d\"#m4edc89429a\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_2\"\u003e\n \u003c!-- 0.25 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 19.1875 8.296875 \nL 53.609375 8.296875 \nL 53.609375 0 \nL 7.328125 0 \nL 7.328125 8.296875 \nQ 12.9375 14.109375 22.625 23.890625 \nQ 32.328125 33.6875 34.8125 36.53125 \nQ 39.546875 41.84375 41.421875 45.53125 \nQ 43.3125 49.21875 43.3125 52.78125 \nQ 43.3125 58.59375 39.234375 62.25 \nQ 35.15625 65.921875 28.609375 65.921875 \nQ 23.96875 65.921875 18.8125 64.3125 \nQ 13.671875 62.703125 7.8125 59.421875 \nL 7.8125 69.390625 \nQ 13.765625 71.78125 18.9375 73 \nQ 24.125 74.21875 28.421875 74.21875 \nQ 39.75 74.21875 46.484375 68.546875 \nQ 53.21875 62.890625 53.21875 53.421875 \nQ 53.21875 48.921875 51.53125 44.890625 \nQ 49.859375 40.875 45.40625 35.40625 \nQ 44.1875 33.984375 37.640625 27.21875 \nQ 31.109375 20.453125 19.1875 8.296875 \nz\n\" id\u003d\"DejaVuSans-32\"/\u003e\n \u003cpath d\u003d\"M 10.796875 72.90625 \nL 49.515625 72.90625 \nL 49.515625 64.59375 \nL 19.828125 64.59375 \nL 19.828125 46.734375 \nQ 21.96875 47.46875 24.109375 47.828125 \nQ 26.265625 48.1875 28.421875 48.1875 \nQ 40.625 48.1875 47.75 41.5 \nQ 54.890625 34.8125 54.890625 23.390625 \nQ 54.890625 11.625 47.5625 5.09375 \nQ 40.234375 -1.421875 26.90625 -1.421875 \nQ 22.3125 -1.421875 17.546875 -0.640625 \nQ 12.796875 0.140625 7.71875 1.703125 \nL 7.71875 11.625 \nQ 12.109375 9.234375 16.796875 8.0625 \nQ 21.484375 6.890625 26.703125 6.890625 \nQ 35.15625 6.890625 40.078125 11.328125 \nQ 45.015625 15.765625 45.015625 23.390625 \nQ 45.015625 31 40.078125 35.4375 \nQ 35.15625 39.890625 26.703125 39.890625 \nQ 22.75 39.890625 18.8125 39.015625 \nQ 14.890625 38.140625 10.796875 36.28125 \nz\n\" id\u003d\"DejaVuSans-35\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(103.281733 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_3\"\u003e\n \u003cg id\u003d\"line2d_3\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"154.996364\" xlink:href\u003d\"#m4edc89429a\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_3\"\u003e\n \u003c!-- 0.50 --\u003e\n \u003cg transform\u003d\"translate(143.863551 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_4\"\u003e\n \u003cg id\u003d\"line2d_4\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"195.578182\" xlink:href\u003d\"#m4edc89429a\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_4\"\u003e\n \u003c!-- 0.75 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 8.203125 72.90625 \nL 55.078125 72.90625 \nL 55.078125 68.703125 \nL 28.609375 0 \nL 18.3125 0 \nL 43.21875 64.59375 \nL 8.203125 64.59375 \nz\n\" id\u003d\"DejaVuSans-37\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(184.445369 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_5\"\u003e\n \u003cg id\u003d\"line2d_5\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"236.16\" xlink:href\u003d\"#m4edc89429a\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_5\"\u003e\n \u003c!-- 1.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 12.40625 8.296875 \nL 28.515625 8.296875 \nL 28.515625 63.921875 \nL 10.984375 60.40625 \nL 10.984375 69.390625 \nL 28.421875 72.90625 \nL 38.28125 72.90625 \nL 38.28125 8.296875 \nL 54.390625 8.296875 \nL 54.390625 0 \nL 12.40625 0 \nz\n\" id\u003d\"DejaVuSans-31\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(225.027187 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_6\"\u003e\n \u003cg id\u003d\"line2d_6\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"276.741818\" xlink:href\u003d\"#m4edc89429a\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_6\"\u003e\n \u003c!-- 1.25 --\u003e\n \u003cg transform\u003d\"translate(265.609006 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_7\"\u003e\n \u003cg id\u003d\"line2d_7\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"317.323636\" xlink:href\u003d\"#m4edc89429a\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_7\"\u003e\n \u003c!-- 1.50 --\u003e\n \u003cg transform\u003d\"translate(306.190824 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_8\"\u003e\n \u003cg id\u003d\"line2d_8\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"357.905455\" xlink:href\u003d\"#m4edc89429a\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_8\"\u003e\n \u003c!-- 1.75 --\u003e\n \u003cg transform\u003d\"translate(346.772642 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_9\"\u003e\n \u003cg id\u003d\"line2d_9\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"398.487273\" xlink:href\u003d\"#m4edc89429a\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_9\"\u003e\n \u003c!-- 2.00 --\u003e\n \u003cg transform\u003d\"translate(387.35446 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_10\"\u003e\n \u003c!-- $x$ --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 60.015625 54.6875 \nL 34.90625 27.875 \nL 50.296875 0 \nL 39.984375 0 \nL 28.421875 21.6875 \nL 8.296875 0 \nL -2.59375 0 \nL 24.3125 28.8125 \nL 10.015625 54.6875 \nL 20.3125 54.6875 \nL 30.8125 34.90625 \nL 49.125 54.6875 \nz\n\" id\u003d\"DejaVuSans-Oblique-78\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(230.16 343.459)scale(0.2 -0.2)\"\u003e\n \u003cuse transform\u003d\"translate(0 0.3125)\" xlink:href\u003d\"#DejaVuSans-Oblique-78\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"matplotlib.axis_2\"\u003e\n \u003cg id\u003d\"ytick_1\"\u003e\n \u003cg id\u003d\"line2d_10\"\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 0 0 \nL -3.5 0 \n\" id\u003d\"mc67d6807a0\" style\u003d\"stroke:#000000;stroke-width:0.8;\"/\u003e\n \u003c/defs\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mc67d6807a0\" y\u003d\"295.488\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_11\"\u003e\n \u003c!-- 1.00 --\u003e\n \u003cg transform\u003d\"translate(28.334375 299.287219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_2\"\u003e\n \u003cg id\u003d\"line2d_11\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mc67d6807a0\" y\u003d\"265.248\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_12\"\u003e\n \u003c!-- 1.25 --\u003e\n \u003cg transform\u003d\"translate(28.334375 269.047219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_3\"\u003e\n \u003cg id\u003d\"line2d_12\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mc67d6807a0\" y\u003d\"235.008\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_13\"\u003e\n \u003c!-- 1.50 --\u003e\n \u003cg transform\u003d\"translate(28.334375 238.807219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_4\"\u003e\n \u003cg id\u003d\"line2d_13\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mc67d6807a0\" y\u003d\"204.768\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_14\"\u003e\n \u003c!-- 1.75 --\u003e\n \u003cg transform\u003d\"translate(28.334375 208.567219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_5\"\u003e\n \u003cg id\u003d\"line2d_14\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mc67d6807a0\" y\u003d\"174.528\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_15\"\u003e\n \u003c!-- 2.00 --\u003e\n \u003cg transform\u003d\"translate(28.334375 178.327219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_6\"\u003e\n \u003cg id\u003d\"line2d_15\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mc67d6807a0\" y\u003d\"144.288\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_16\"\u003e\n \u003c!-- 2.25 --\u003e\n \u003cg transform\u003d\"translate(28.334375 148.087219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_7\"\u003e\n \u003cg id\u003d\"line2d_16\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mc67d6807a0\" y\u003d\"114.048\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_17\"\u003e\n \u003c!-- 2.50 --\u003e\n \u003cg transform\u003d\"translate(28.334375 117.847219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_8\"\u003e\n \u003cg id\u003d\"line2d_17\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mc67d6807a0\" y\u003d\"83.808\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_18\"\u003e\n \u003c!-- 2.75 --\u003e\n \u003cg transform\u003d\"translate(28.334375 87.607219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_9\"\u003e\n \u003cg id\u003d\"line2d_18\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#mc67d6807a0\" y\u003d\"53.568\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_19\"\u003e\n \u003c!-- 3.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 40.578125 39.3125 \nQ 47.65625 37.796875 51.625 33 \nQ 55.609375 28.21875 55.609375 21.1875 \nQ 55.609375 10.40625 48.1875 4.484375 \nQ 40.765625 -1.421875 27.09375 -1.421875 \nQ 22.515625 -1.421875 17.65625 -0.515625 \nQ 12.796875 0.390625 7.625 2.203125 \nL 7.625 11.71875 \nQ 11.71875 9.328125 16.59375 8.109375 \nQ 21.484375 6.890625 26.8125 6.890625 \nQ 36.078125 6.890625 40.9375 10.546875 \nQ 45.796875 14.203125 45.796875 21.1875 \nQ 45.796875 27.640625 41.28125 31.265625 \nQ 36.765625 34.90625 28.71875 34.90625 \nL 20.21875 34.90625 \nL 20.21875 43.015625 \nL 29.109375 43.015625 \nQ 36.375 43.015625 40.234375 45.921875 \nQ 44.09375 48.828125 44.09375 54.296875 \nQ 44.09375 59.90625 40.109375 62.90625 \nQ 36.140625 65.921875 28.71875 65.921875 \nQ 24.65625 65.921875 20.015625 65.03125 \nQ 15.375 64.15625 9.8125 62.3125 \nL 9.8125 71.09375 \nQ 15.4375 72.65625 20.34375 73.4375 \nQ 25.25 74.21875 29.59375 74.21875 \nQ 40.828125 74.21875 47.359375 69.109375 \nQ 53.90625 64.015625 53.90625 55.328125 \nQ 53.90625 49.265625 50.4375 45.09375 \nQ 46.96875 40.921875 40.578125 39.3125 \nz\n\" id\u003d\"DejaVuSans-33\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(28.334375 57.367219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-33\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_20\"\u003e\n \u003c!-- $y$ --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 24.8125 -5.078125 \nQ 18.5625 -15.578125 14.625 -18.1875 \nQ 10.6875 -20.796875 4.59375 -20.796875 \nL -2.484375 -20.796875 \nL -0.984375 -13.28125 \nL 4.203125 -13.28125 \nQ 7.953125 -13.28125 10.59375 -11.234375 \nQ 13.234375 -9.1875 16.5 -3.21875 \nL 19.28125 2 \nL 7.171875 54.6875 \nL 16.703125 54.6875 \nL 25.78125 12.796875 \nL 50.875 54.6875 \nL 60.296875 54.6875 \nz\n\" id\u003d\"DejaVuSans-Oblique-79\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(20.134375 180.528)rotate(-90)scale(0.2 -0.2)\"\u003e\n \u003cuse transform\u003d\"translate(0 0.3125)\" xlink:href\u003d\"#DejaVuSans-Oblique-79\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_19\"\u003e\n \u003cpath clip-path\u003d\"url(#p5d2419fcee)\" d\u003d\"M 73.832727 295.488 \nL 236.16 174.528 \nL 398.487273 53.568 \n\" style\u003d\"fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_20\"\u003e\n \u003cpath clip-path\u003d\"url(#p5d2419fcee)\" d\u003d\"M 73.832727 53.568 \nL 236.16 174.528 \nL 398.487273 295.488 \n\" style\u003d\"fill:none;stroke:#ff7f0e;stroke-linecap:square;stroke-width:1.5;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_3\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 57.6 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_4\"\u003e\n \u003cpath d\u003d\"M 414.72 307.584 \nL 414.72 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_5\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 414.72 307.584 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_6\"\u003e\n \u003cpath d\u003d\"M 57.6 41.472 \nL 414.72 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"legend_1\"\u003e\n \u003cg id\u003d\"patch_7\"\u003e\n \u003cpath d\u003d\"M 161.36 116.26575 \nL 310.96 116.26575 \nQ 314.96 116.26575 314.96 112.26575 \nL 314.96 55.472 \nQ 314.96 51.472 310.96 51.472 \nL 161.36 51.472 \nQ 157.36 51.472 157.36 55.472 \nL 157.36 112.26575 \nQ 157.36 116.26575 161.36 116.26575 \nz\n\" style\u003d\"fill:#ffffff;opacity:0.8;stroke:#cccccc;stroke-linejoin:miter;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_21\"\u003e\n \u003cpath d\u003d\"M 165.36 67.668875 \nL 205.36 67.668875 \n\" style\u003d\"fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_22\"/\u003e\n \u003cg id\u003d\"text_21\"\u003e\n \u003c!-- $y\u003dx$ --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 10.59375 45.40625 \nL 73.1875 45.40625 \nL 73.1875 37.203125 \nL 10.59375 37.203125 \nz\nM 10.59375 25.484375 \nL 73.1875 25.484375 \nL 73.1875 17.1875 \nL 10.59375 17.1875 \nz\n\" id\u003d\"DejaVuSans-3d\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(221.36 74.668875)scale(0.2 -0.2)\"\u003e\n \u003cuse transform\u003d\"translate(0 0.3125)\" xlink:href\u003d\"#DejaVuSans-Oblique-79\"/\u003e\n \u003cuse transform\u003d\"translate(78.662109 0.3125)\" xlink:href\u003d\"#DejaVuSans-3d\"/\u003e\n \u003cuse transform\u003d\"translate(181.933594 0.3125)\" xlink:href\u003d\"#DejaVuSans-Oblique-78\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_23\"\u003e\n \u003cpath d\u003d\"M 165.36 97.06575 \nL 205.36 97.06575 \n\" style\u003d\"fill:none;stroke:#ff7f0e;stroke-linecap:square;stroke-width:1.5;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_24\"/\u003e\n \u003cg id\u003d\"text_22\"\u003e\n \u003c!-- $y\u003d3-x$ --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 10.59375 35.5 \nL 73.1875 35.5 \nL 73.1875 27.203125 \nL 10.59375 27.203125 \nz\n\" id\u003d\"DejaVuSans-2212\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(221.36 104.06575)scale(0.2 -0.2)\"\u003e\n \u003cuse transform\u003d\"translate(0 0.78125)\" xlink:href\u003d\"#DejaVuSans-Oblique-79\"/\u003e\n \u003cuse transform\u003d\"translate(78.662109 0.78125)\" xlink:href\u003d\"#DejaVuSans-3d\"/\u003e\n \u003cuse transform\u003d\"translate(181.933594 0.78125)\" xlink:href\u003d\"#DejaVuSans-33\"/\u003e\n \u003cuse transform\u003d\"translate(265.039062 0.78125)\" xlink:href\u003d\"#DejaVuSans-2212\"/\u003e\n \u003cuse transform\u003d\"translate(368.310547 0.78125)\" xlink:href\u003d\"#DejaVuSans-Oblique-78\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cdefs\u003e\n \u003cclipPath id\u003d\"p5d2419fcee\"\u003e\n \u003crect height\u003d\"266.112\" width\u003d\"357.12\" x\u003d\"57.6\" y\u003d\"41.472\"/\u003e\n \u003c/clipPath\u003e\n \u003c/defs\u003e\n\u003c/svg\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512664593478_-1903613117", + "id": "20171207-163633_133565100", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n---\n", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": false, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003chr/\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512667873807_-2110453507", + "id": "20171207-173113_1748353665", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Add title", + "text": "%livy.pyspark\nplt.title(\u0027Inline plotting example\u0027, fontsize\u003d20)\nshow(plt)", + "user": "anonymous", + "config": { + "colWidth": 6.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "python", + "editOnDblClick": false + }, + "editorMode": "ace/mode/python", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003c?xml version\u003d\"1.0\" encoding\u003d\"utf-8\" standalone\u003d\"no\"?\u003e\n\u003c!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\"\u003e\n\u003c!-- Created with matplotlib (http://matplotlib.org/) --\u003e\n\u003csvg height\u003d\"345pt\" version\u003d\"1.1\" viewBox\u003d\"0 0 460 345\" width\u003d\"460pt\" xmlns\u003d\"http://www.w3.org/2000/svg\" xmlns:xlink\u003d\"http://www.w3.org/1999/xlink\"\u003e\n \u003cdefs\u003e\n \u003cstyle type\u003d\"text/css\"\u003e\n*{stroke-linecap:butt;stroke-linejoin:round;}\n \u003c/style\u003e\n \u003c/defs\u003e\n \u003cg id\u003d\"figure_1\"\u003e\n \u003cg id\u003d\"patch_1\"\u003e\n \u003cpath d\u003d\"M 0 345.6 \nL 460.8 345.6 \nL 460.8 0 \nL 0 0 \nz\n\" style\u003d\"fill:#ffffff;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"axes_1\"\u003e\n \u003cg id\u003d\"patch_2\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 414.72 307.584 \nL 414.72 41.472 \nL 57.6 41.472 \nz\n\" style\u003d\"fill:#ffffff;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"matplotlib.axis_1\"\u003e\n \u003cg id\u003d\"xtick_1\"\u003e\n \u003cg id\u003d\"line2d_1\"\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 0 0 \nL 0 3.5 \n\" id\u003d\"md9ef6e772b\" style\u003d\"stroke:#000000;stroke-width:0.8;\"/\u003e\n \u003c/defs\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"73.832727\" xlink:href\u003d\"#md9ef6e772b\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_1\"\u003e\n \u003c!-- 0.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 31.78125 66.40625 \nQ 24.171875 66.40625 20.328125 58.90625 \nQ 16.5 51.421875 16.5 36.375 \nQ 16.5 21.390625 20.328125 13.890625 \nQ 24.171875 6.390625 31.78125 6.390625 \nQ 39.453125 6.390625 43.28125 13.890625 \nQ 47.125 21.390625 47.125 36.375 \nQ 47.125 51.421875 43.28125 58.90625 \nQ 39.453125 66.40625 31.78125 66.40625 \nz\nM 31.78125 74.21875 \nQ 44.046875 74.21875 50.515625 64.515625 \nQ 56.984375 54.828125 56.984375 36.375 \nQ 56.984375 17.96875 50.515625 8.265625 \nQ 44.046875 -1.421875 31.78125 -1.421875 \nQ 19.53125 -1.421875 13.0625 8.265625 \nQ 6.59375 17.96875 6.59375 36.375 \nQ 6.59375 54.828125 13.0625 64.515625 \nQ 19.53125 74.21875 31.78125 74.21875 \nz\n\" id\u003d\"DejaVuSans-30\"/\u003e\n \u003cpath d\u003d\"M 10.6875 12.40625 \nL 21 12.40625 \nL 21 0 \nL 10.6875 0 \nz\n\" id\u003d\"DejaVuSans-2e\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(62.699915 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_2\"\u003e\n \u003cg id\u003d\"line2d_2\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"114.414545\" xlink:href\u003d\"#md9ef6e772b\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_2\"\u003e\n \u003c!-- 0.25 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 19.1875 8.296875 \nL 53.609375 8.296875 \nL 53.609375 0 \nL 7.328125 0 \nL 7.328125 8.296875 \nQ 12.9375 14.109375 22.625 23.890625 \nQ 32.328125 33.6875 34.8125 36.53125 \nQ 39.546875 41.84375 41.421875 45.53125 \nQ 43.3125 49.21875 43.3125 52.78125 \nQ 43.3125 58.59375 39.234375 62.25 \nQ 35.15625 65.921875 28.609375 65.921875 \nQ 23.96875 65.921875 18.8125 64.3125 \nQ 13.671875 62.703125 7.8125 59.421875 \nL 7.8125 69.390625 \nQ 13.765625 71.78125 18.9375 73 \nQ 24.125 74.21875 28.421875 74.21875 \nQ 39.75 74.21875 46.484375 68.546875 \nQ 53.21875 62.890625 53.21875 53.421875 \nQ 53.21875 48.921875 51.53125 44.890625 \nQ 49.859375 40.875 45.40625 35.40625 \nQ 44.1875 33.984375 37.640625 27.21875 \nQ 31.109375 20.453125 19.1875 8.296875 \nz\n\" id\u003d\"DejaVuSans-32\"/\u003e\n \u003cpath d\u003d\"M 10.796875 72.90625 \nL 49.515625 72.90625 \nL 49.515625 64.59375 \nL 19.828125 64.59375 \nL 19.828125 46.734375 \nQ 21.96875 47.46875 24.109375 47.828125 \nQ 26.265625 48.1875 28.421875 48.1875 \nQ 40.625 48.1875 47.75 41.5 \nQ 54.890625 34.8125 54.890625 23.390625 \nQ 54.890625 11.625 47.5625 5.09375 \nQ 40.234375 -1.421875 26.90625 -1.421875 \nQ 22.3125 -1.421875 17.546875 -0.640625 \nQ 12.796875 0.140625 7.71875 1.703125 \nL 7.71875 11.625 \nQ 12.109375 9.234375 16.796875 8.0625 \nQ 21.484375 6.890625 26.703125 6.890625 \nQ 35.15625 6.890625 40.078125 11.328125 \nQ 45.015625 15.765625 45.015625 23.390625 \nQ 45.015625 31 40.078125 35.4375 \nQ 35.15625 39.890625 26.703125 39.890625 \nQ 22.75 39.890625 18.8125 39.015625 \nQ 14.890625 38.140625 10.796875 36.28125 \nz\n\" id\u003d\"DejaVuSans-35\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(103.281733 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_3\"\u003e\n \u003cg id\u003d\"line2d_3\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"154.996364\" xlink:href\u003d\"#md9ef6e772b\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_3\"\u003e\n \u003c!-- 0.50 --\u003e\n \u003cg transform\u003d\"translate(143.863551 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_4\"\u003e\n \u003cg id\u003d\"line2d_4\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"195.578182\" xlink:href\u003d\"#md9ef6e772b\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_4\"\u003e\n \u003c!-- 0.75 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 8.203125 72.90625 \nL 55.078125 72.90625 \nL 55.078125 68.703125 \nL 28.609375 0 \nL 18.3125 0 \nL 43.21875 64.59375 \nL 8.203125 64.59375 \nz\n\" id\u003d\"DejaVuSans-37\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(184.445369 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_5\"\u003e\n \u003cg id\u003d\"line2d_5\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"236.16\" xlink:href\u003d\"#md9ef6e772b\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_5\"\u003e\n \u003c!-- 1.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 12.40625 8.296875 \nL 28.515625 8.296875 \nL 28.515625 63.921875 \nL 10.984375 60.40625 \nL 10.984375 69.390625 \nL 28.421875 72.90625 \nL 38.28125 72.90625 \nL 38.28125 8.296875 \nL 54.390625 8.296875 \nL 54.390625 0 \nL 12.40625 0 \nz\n\" id\u003d\"DejaVuSans-31\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(225.027187 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_6\"\u003e\n \u003cg id\u003d\"line2d_6\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"276.741818\" xlink:href\u003d\"#md9ef6e772b\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_6\"\u003e\n \u003c!-- 1.25 --\u003e\n \u003cg transform\u003d\"translate(265.609006 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_7\"\u003e\n \u003cg id\u003d\"line2d_7\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"317.323636\" xlink:href\u003d\"#md9ef6e772b\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_7\"\u003e\n \u003c!-- 1.50 --\u003e\n \u003cg transform\u003d\"translate(306.190824 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_8\"\u003e\n \u003cg id\u003d\"line2d_8\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"357.905455\" xlink:href\u003d\"#md9ef6e772b\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_8\"\u003e\n \u003c!-- 1.75 --\u003e\n \u003cg transform\u003d\"translate(346.772642 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"xtick_9\"\u003e\n \u003cg id\u003d\"line2d_9\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"398.487273\" xlink:href\u003d\"#md9ef6e772b\" y\u003d\"307.584\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_9\"\u003e\n \u003c!-- 2.00 --\u003e\n \u003cg transform\u003d\"translate(387.35446 322.182437)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_10\"\u003e\n \u003c!-- $x$ --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 60.015625 54.6875 \nL 34.90625 27.875 \nL 50.296875 0 \nL 39.984375 0 \nL 28.421875 21.6875 \nL 8.296875 0 \nL -2.59375 0 \nL 24.3125 28.8125 \nL 10.015625 54.6875 \nL 20.3125 54.6875 \nL 30.8125 34.90625 \nL 49.125 54.6875 \nz\n\" id\u003d\"DejaVuSans-Oblique-78\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(230.16 343.459)scale(0.2 -0.2)\"\u003e\n \u003cuse transform\u003d\"translate(0 0.3125)\" xlink:href\u003d\"#DejaVuSans-Oblique-78\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"matplotlib.axis_2\"\u003e\n \u003cg id\u003d\"ytick_1\"\u003e\n \u003cg id\u003d\"line2d_10\"\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 0 0 \nL -3.5 0 \n\" id\u003d\"m09ab649f93\" style\u003d\"stroke:#000000;stroke-width:0.8;\"/\u003e\n \u003c/defs\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m09ab649f93\" y\u003d\"295.488\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_11\"\u003e\n \u003c!-- 1.00 --\u003e\n \u003cg transform\u003d\"translate(28.334375 299.287219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_2\"\u003e\n \u003cg id\u003d\"line2d_11\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m09ab649f93\" y\u003d\"265.248\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_12\"\u003e\n \u003c!-- 1.25 --\u003e\n \u003cg transform\u003d\"translate(28.334375 269.047219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_3\"\u003e\n \u003cg id\u003d\"line2d_12\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m09ab649f93\" y\u003d\"235.008\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_13\"\u003e\n \u003c!-- 1.50 --\u003e\n \u003cg transform\u003d\"translate(28.334375 238.807219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_4\"\u003e\n \u003cg id\u003d\"line2d_13\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m09ab649f93\" y\u003d\"204.768\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_14\"\u003e\n \u003c!-- 1.75 --\u003e\n \u003cg transform\u003d\"translate(28.334375 208.567219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-31\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_5\"\u003e\n \u003cg id\u003d\"line2d_14\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m09ab649f93\" y\u003d\"174.528\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_15\"\u003e\n \u003c!-- 2.00 --\u003e\n \u003cg transform\u003d\"translate(28.334375 178.327219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_6\"\u003e\n \u003cg id\u003d\"line2d_15\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m09ab649f93\" y\u003d\"144.288\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_16\"\u003e\n \u003c!-- 2.25 --\u003e\n \u003cg transform\u003d\"translate(28.334375 148.087219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_7\"\u003e\n \u003cg id\u003d\"line2d_16\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m09ab649f93\" y\u003d\"114.048\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_17\"\u003e\n \u003c!-- 2.50 --\u003e\n \u003cg transform\u003d\"translate(28.334375 117.847219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_8\"\u003e\n \u003cg id\u003d\"line2d_17\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m09ab649f93\" y\u003d\"83.808\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_18\"\u003e\n \u003c!-- 2.75 --\u003e\n \u003cg transform\u003d\"translate(28.334375 87.607219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-32\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-37\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-35\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"ytick_9\"\u003e\n \u003cg id\u003d\"line2d_18\"\u003e\n \u003cg\u003e\n \u003cuse style\u003d\"stroke:#000000;stroke-width:0.8;\" x\u003d\"57.6\" xlink:href\u003d\"#m09ab649f93\" y\u003d\"53.568\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_19\"\u003e\n \u003c!-- 3.00 --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 40.578125 39.3125 \nQ 47.65625 37.796875 51.625 33 \nQ 55.609375 28.21875 55.609375 21.1875 \nQ 55.609375 10.40625 48.1875 4.484375 \nQ 40.765625 -1.421875 27.09375 -1.421875 \nQ 22.515625 -1.421875 17.65625 -0.515625 \nQ 12.796875 0.390625 7.625 2.203125 \nL 7.625 11.71875 \nQ 11.71875 9.328125 16.59375 8.109375 \nQ 21.484375 6.890625 26.8125 6.890625 \nQ 36.078125 6.890625 40.9375 10.546875 \nQ 45.796875 14.203125 45.796875 21.1875 \nQ 45.796875 27.640625 41.28125 31.265625 \nQ 36.765625 34.90625 28.71875 34.90625 \nL 20.21875 34.90625 \nL 20.21875 43.015625 \nL 29.109375 43.015625 \nQ 36.375 43.015625 40.234375 45.921875 \nQ 44.09375 48.828125 44.09375 54.296875 \nQ 44.09375 59.90625 40.109375 62.90625 \nQ 36.140625 65.921875 28.71875 65.921875 \nQ 24.65625 65.921875 20.015625 65.03125 \nQ 15.375 64.15625 9.8125 62.3125 \nL 9.8125 71.09375 \nQ 15.4375 72.65625 20.34375 73.4375 \nQ 25.25 74.21875 29.59375 74.21875 \nQ 40.828125 74.21875 47.359375 69.109375 \nQ 53.90625 64.015625 53.90625 55.328125 \nQ 53.90625 49.265625 50.4375 45.09375 \nQ 46.96875 40.921875 40.578125 39.3125 \nz\n\" id\u003d\"DejaVuSans-33\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(28.334375 57.367219)scale(0.1 -0.1)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-33\"/\u003e\n \u003cuse x\u003d\"63.623047\" xlink:href\u003d\"#DejaVuSans-2e\"/\u003e\n \u003cuse x\u003d\"95.410156\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003cuse x\u003d\"159.033203\" xlink:href\u003d\"#DejaVuSans-30\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_20\"\u003e\n \u003c!-- $y$ --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 24.8125 -5.078125 \nQ 18.5625 -15.578125 14.625 -18.1875 \nQ 10.6875 -20.796875 4.59375 -20.796875 \nL -2.484375 -20.796875 \nL -0.984375 -13.28125 \nL 4.203125 -13.28125 \nQ 7.953125 -13.28125 10.59375 -11.234375 \nQ 13.234375 -9.1875 16.5 -3.21875 \nL 19.28125 2 \nL 7.171875 54.6875 \nL 16.703125 54.6875 \nL 25.78125 12.796875 \nL 50.875 54.6875 \nL 60.296875 54.6875 \nz\n\" id\u003d\"DejaVuSans-Oblique-79\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(20.134375 180.528)rotate(-90)scale(0.2 -0.2)\"\u003e\n \u003cuse transform\u003d\"translate(0 0.3125)\" xlink:href\u003d\"#DejaVuSans-Oblique-79\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_19\"\u003e\n \u003cpath clip-path\u003d\"url(#pd960d6d74d)\" d\u003d\"M 73.832727 295.488 \nL 236.16 174.528 \nL 398.487273 53.568 \n\" style\u003d\"fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_20\"\u003e\n \u003cpath clip-path\u003d\"url(#pd960d6d74d)\" d\u003d\"M 73.832727 53.568 \nL 236.16 174.528 \nL 398.487273 295.488 \n\" style\u003d\"fill:none;stroke:#ff7f0e;stroke-linecap:square;stroke-width:1.5;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_3\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 57.6 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_4\"\u003e\n \u003cpath d\u003d\"M 414.72 307.584 \nL 414.72 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_5\"\u003e\n \u003cpath d\u003d\"M 57.6 307.584 \nL 414.72 307.584 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"patch_6\"\u003e\n \u003cpath d\u003d\"M 57.6 41.472 \nL 414.72 41.472 \n\" style\u003d\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"text_21\"\u003e\n \u003c!-- Inline plotting example --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 9.8125 72.90625 \nL 19.671875 72.90625 \nL 19.671875 0 \nL 9.8125 0 \nz\n\" id\u003d\"DejaVuSans-49\"/\u003e\n \u003cpath d\u003d\"M 54.890625 33.015625 \nL 54.890625 0 \nL 45.90625 0 \nL 45.90625 32.71875 \nQ 45.90625 40.484375 42.875 44.328125 \nQ 39.84375 48.1875 33.796875 48.1875 \nQ 26.515625 48.1875 22.3125 43.546875 \nQ 18.109375 38.921875 18.109375 30.90625 \nL 18.109375 0 \nL 9.078125 0 \nL 9.078125 54.6875 \nL 18.109375 54.6875 \nL 18.109375 46.1875 \nQ 21.34375 51.125 25.703125 53.5625 \nQ 30.078125 56 35.796875 56 \nQ 45.21875 56 50.046875 50.171875 \nQ 54.890625 44.34375 54.890625 33.015625 \nz\n\" id\u003d\"DejaVuSans-6e\"/\u003e\n \u003cpath d\u003d\"M 9.421875 75.984375 \nL 18.40625 75.984375 \nL 18.40625 0 \nL 9.421875 0 \nz\n\" id\u003d\"DejaVuSans-6c\"/\u003e\n \u003cpath d\u003d\"M 9.421875 54.6875 \nL 18.40625 54.6875 \nL 18.40625 0 \nL 9.421875 0 \nz\nM 9.421875 75.984375 \nL 18.40625 75.984375 \nL 18.40625 64.59375 \nL 9.421875 64.59375 \nz\n\" id\u003d\"DejaVuSans-69\"/\u003e\n \u003cpath d\u003d\"M 56.203125 29.59375 \nL 56.203125 25.203125 \nL 14.890625 25.203125 \nQ 15.484375 15.921875 20.484375 11.0625 \nQ 25.484375 6.203125 34.421875 6.203125 \nQ 39.59375 6.203125 44.453125 7.46875 \nQ 49.3125 8.734375 54.109375 11.28125 \nL 54.109375 2.78125 \nQ 49.265625 0.734375 44.1875 -0.34375 \nQ 39.109375 -1.421875 33.890625 -1.421875 \nQ 20.796875 -1.421875 13.15625 6.1875 \nQ 5.515625 13.8125 5.515625 26.8125 \nQ 5.515625 40.234375 12.765625 48.109375 \nQ 20.015625 56 32.328125 56 \nQ 43.359375 56 49.78125 48.890625 \nQ 56.203125 41.796875 56.203125 29.59375 \nz\nM 47.21875 32.234375 \nQ 47.125 39.59375 43.09375 43.984375 \nQ 39.0625 48.390625 32.421875 48.390625 \nQ 24.90625 48.390625 20.390625 44.140625 \nQ 15.875 39.890625 15.1875 32.171875 \nz\n\" id\u003d\"DejaVuSans-65\"/\u003e\n \u003cpath id\u003d\"DejaVuSans-20\"/\u003e\n \u003cpath d\u003d\"M 18.109375 8.203125 \nL 18.109375 -20.796875 \nL 9.078125 -20.796875 \nL 9.078125 54.6875 \nL 18.109375 54.6875 \nL 18.109375 46.390625 \nQ 20.953125 51.265625 25.265625 53.625 \nQ 29.59375 56 35.59375 56 \nQ 45.5625 56 51.78125 48.09375 \nQ 58.015625 40.1875 58.015625 27.296875 \nQ 58.015625 14.40625 51.78125 6.484375 \nQ 45.5625 -1.421875 35.59375 -1.421875 \nQ 29.59375 -1.421875 25.265625 0.953125 \nQ 20.953125 3.328125 18.109375 8.203125 \nz\nM 48.6875 27.296875 \nQ 48.6875 37.203125 44.609375 42.84375 \nQ 40.53125 48.484375 33.40625 48.484375 \nQ 26.265625 48.484375 22.1875 42.84375 \nQ 18.109375 37.203125 18.109375 27.296875 \nQ 18.109375 17.390625 22.1875 11.75 \nQ 26.265625 6.109375 33.40625 6.109375 \nQ 40.53125 6.109375 44.609375 11.75 \nQ 48.6875 17.390625 48.6875 27.296875 \nz\n\" id\u003d\"DejaVuSans-70\"/\u003e\n \u003cpath d\u003d\"M 30.609375 48.390625 \nQ 23.390625 48.390625 19.1875 42.75 \nQ 14.984375 37.109375 14.984375 27.296875 \nQ 14.984375 17.484375 19.15625 11.84375 \nQ 23.34375 6.203125 30.609375 6.203125 \nQ 37.796875 6.203125 41.984375 11.859375 \nQ 46.1875 17.53125 46.1875 27.296875 \nQ 46.1875 37.015625 41.984375 42.703125 \nQ 37.796875 48.390625 30.609375 48.390625 \nz\nM 30.609375 56 \nQ 42.328125 56 49.015625 48.375 \nQ 55.71875 40.765625 55.71875 27.296875 \nQ 55.71875 13.875 49.015625 6.21875 \nQ 42.328125 -1.421875 30.609375 -1.421875 \nQ 18.84375 -1.421875 12.171875 6.21875 \nQ 5.515625 13.875 5.515625 27.296875 \nQ 5.515625 40.765625 12.171875 48.375 \nQ 18.84375 56 30.609375 56 \nz\n\" id\u003d\"DejaVuSans-6f\"/\u003e\n \u003cpath d\u003d\"M 18.3125 70.21875 \nL 18.3125 54.6875 \nL 36.8125 54.6875 \nL 36.8125 47.703125 \nL 18.3125 47.703125 \nL 18.3125 18.015625 \nQ 18.3125 11.328125 20.140625 9.421875 \nQ 21.96875 7.515625 27.59375 7.515625 \nL 36.8125 7.515625 \nL 36.8125 0 \nL 27.59375 0 \nQ 17.1875 0 13.234375 3.875 \nQ 9.28125 7.765625 9.28125 18.015625 \nL 9.28125 47.703125 \nL 2.6875 47.703125 \nL 2.6875 54.6875 \nL 9.28125 54.6875 \nL 9.28125 70.21875 \nz\n\" id\u003d\"DejaVuSans-74\"/\u003e\n \u003cpath d\u003d\"M 45.40625 27.984375 \nQ 45.40625 37.75 41.375 43.109375 \nQ 37.359375 48.484375 30.078125 48.484375 \nQ 22.859375 48.484375 18.828125 43.109375 \nQ 14.796875 37.75 14.796875 27.984375 \nQ 14.796875 18.265625 18.828125 12.890625 \nQ 22.859375 7.515625 30.078125 7.515625 \nQ 37.359375 7.515625 41.375 12.890625 \nQ 45.40625 18.265625 45.40625 27.984375 \nz\nM 54.390625 6.78125 \nQ 54.390625 -7.171875 48.1875 -13.984375 \nQ 42 -20.796875 29.203125 -20.796875 \nQ 24.46875 -20.796875 20.265625 -20.09375 \nQ 16.0625 -19.390625 12.109375 -17.921875 \nL 12.109375 -9.1875 \nQ 16.0625 -11.328125 19.921875 -12.34375 \nQ 23.78125 -13.375 27.78125 -13.375 \nQ 36.625 -13.375 41.015625 -8.765625 \nQ 45.40625 -4.15625 45.40625 5.171875 \nL 45.40625 9.625 \nQ 42.625 4.78125 38.28125 2.390625 \nQ 33.9375 0 27.875 0 \nQ 17.828125 0 11.671875 7.65625 \nQ 5.515625 15.328125 5.515625 27.984375 \nQ 5.515625 40.671875 11.671875 48.328125 \nQ 17.828125 56 27.875 56 \nQ 33.9375 56 38.28125 53.609375 \nQ 42.625 51.21875 45.40625 46.390625 \nL 45.40625 54.6875 \nL 54.390625 54.6875 \nz\n\" id\u003d\"DejaVuSans-67\"/\u003e\n \u003cpath d\u003d\"M 54.890625 54.6875 \nL 35.109375 28.078125 \nL 55.90625 0 \nL 45.3125 0 \nL 29.390625 21.484375 \nL 13.484375 0 \nL 2.875 0 \nL 24.125 28.609375 \nL 4.6875 54.6875 \nL 15.28125 54.6875 \nL 29.78125 35.203125 \nL 44.28125 54.6875 \nz\n\" id\u003d\"DejaVuSans-78\"/\u003e\n \u003cpath d\u003d\"M 34.28125 27.484375 \nQ 23.390625 27.484375 19.1875 25 \nQ 14.984375 22.515625 14.984375 16.5 \nQ 14.984375 11.71875 18.140625 8.90625 \nQ 21.296875 6.109375 26.703125 6.109375 \nQ 34.1875 6.109375 38.703125 11.40625 \nQ 43.21875 16.703125 43.21875 25.484375 \nL 43.21875 27.484375 \nz\nM 52.203125 31.203125 \nL 52.203125 0 \nL 43.21875 0 \nL 43.21875 8.296875 \nQ 40.140625 3.328125 35.546875 0.953125 \nQ 30.953125 -1.421875 24.3125 -1.421875 \nQ 15.921875 -1.421875 10.953125 3.296875 \nQ 6 8.015625 6 15.921875 \nQ 6 25.140625 12.171875 29.828125 \nQ 18.359375 34.515625 30.609375 34.515625 \nL 43.21875 34.515625 \nL 43.21875 35.40625 \nQ 43.21875 41.609375 39.140625 45 \nQ 35.0625 48.390625 27.6875 48.390625 \nQ 23 48.390625 18.546875 47.265625 \nQ 14.109375 46.140625 10.015625 43.890625 \nL 10.015625 52.203125 \nQ 14.9375 54.109375 19.578125 55.046875 \nQ 24.21875 56 28.609375 56 \nQ 40.484375 56 46.34375 49.84375 \nQ 52.203125 43.703125 52.203125 31.203125 \nz\n\" id\u003d\"DejaVuSans-61\"/\u003e\n \u003cpath d\u003d\"M 52 44.1875 \nQ 55.375 50.25 60.0625 53.125 \nQ 64.75 56 71.09375 56 \nQ 79.640625 56 84.28125 50.015625 \nQ 88.921875 44.046875 88.921875 33.015625 \nL 88.921875 0 \nL 79.890625 0 \nL 79.890625 32.71875 \nQ 79.890625 40.578125 77.09375 44.375 \nQ 74.3125 48.1875 68.609375 48.1875 \nQ 61.625 48.1875 57.5625 43.546875 \nQ 53.515625 38.921875 53.515625 30.90625 \nL 53.515625 0 \nL 44.484375 0 \nL 44.484375 32.71875 \nQ 44.484375 40.625 41.703125 44.40625 \nQ 38.921875 48.1875 33.109375 48.1875 \nQ 26.21875 48.1875 22.15625 43.53125 \nQ 18.109375 38.875 18.109375 30.90625 \nL 18.109375 0 \nL 9.078125 0 \nL 9.078125 54.6875 \nL 18.109375 54.6875 \nL 18.109375 46.1875 \nQ 21.1875 51.21875 25.484375 53.609375 \nQ 29.78125 56 35.6875 56 \nQ 41.65625 56 45.828125 52.96875 \nQ 50 49.953125 52 44.1875 \nz\n\" id\u003d\"DejaVuSans-6d\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(120.700625 35.472)scale(0.2 -0.2)\"\u003e\n \u003cuse xlink:href\u003d\"#DejaVuSans-49\"/\u003e\n \u003cuse x\u003d\"29.492188\" xlink:href\u003d\"#DejaVuSans-6e\"/\u003e\n \u003cuse x\u003d\"92.871094\" xlink:href\u003d\"#DejaVuSans-6c\"/\u003e\n \u003cuse x\u003d\"120.654297\" xlink:href\u003d\"#DejaVuSans-69\"/\u003e\n \u003cuse x\u003d\"148.4375\" xlink:href\u003d\"#DejaVuSans-6e\"/\u003e\n \u003cuse x\u003d\"211.816406\" xlink:href\u003d\"#DejaVuSans-65\"/\u003e\n \u003cuse x\u003d\"273.339844\" xlink:href\u003d\"#DejaVuSans-20\"/\u003e\n \u003cuse x\u003d\"305.126953\" xlink:href\u003d\"#DejaVuSans-70\"/\u003e\n \u003cuse x\u003d\"368.603516\" xlink:href\u003d\"#DejaVuSans-6c\"/\u003e\n \u003cuse x\u003d\"396.386719\" xlink:href\u003d\"#DejaVuSans-6f\"/\u003e\n \u003cuse x\u003d\"457.568359\" xlink:href\u003d\"#DejaVuSans-74\"/\u003e\n \u003cuse x\u003d\"496.777344\" xlink:href\u003d\"#DejaVuSans-74\"/\u003e\n \u003cuse x\u003d\"535.986328\" xlink:href\u003d\"#DejaVuSans-69\"/\u003e\n \u003cuse x\u003d\"563.769531\" xlink:href\u003d\"#DejaVuSans-6e\"/\u003e\n \u003cuse x\u003d\"627.148438\" xlink:href\u003d\"#DejaVuSans-67\"/\u003e\n \u003cuse x\u003d\"690.625\" xlink:href\u003d\"#DejaVuSans-20\"/\u003e\n \u003cuse x\u003d\"722.412109\" xlink:href\u003d\"#DejaVuSans-65\"/\u003e\n \u003cuse x\u003d\"783.919922\" xlink:href\u003d\"#DejaVuSans-78\"/\u003e\n \u003cuse x\u003d\"843.099609\" xlink:href\u003d\"#DejaVuSans-61\"/\u003e\n \u003cuse x\u003d\"904.378906\" xlink:href\u003d\"#DejaVuSans-6d\"/\u003e\n \u003cuse x\u003d\"1001.791016\" xlink:href\u003d\"#DejaVuSans-70\"/\u003e\n \u003cuse x\u003d\"1065.267578\" xlink:href\u003d\"#DejaVuSans-6c\"/\u003e\n \u003cuse x\u003d\"1093.050781\" xlink:href\u003d\"#DejaVuSans-65\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"legend_1\"\u003e\n \u003cg id\u003d\"patch_7\"\u003e\n \u003cpath d\u003d\"M 161.36 116.26575 \nL 310.96 116.26575 \nQ 314.96 116.26575 314.96 112.26575 \nL 314.96 55.472 \nQ 314.96 51.472 310.96 51.472 \nL 161.36 51.472 \nQ 157.36 51.472 157.36 55.472 \nL 157.36 112.26575 \nQ 157.36 116.26575 161.36 116.26575 \nz\n\" style\u003d\"fill:#ffffff;opacity:0.8;stroke:#cccccc;stroke-linejoin:miter;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_21\"\u003e\n \u003cpath d\u003d\"M 165.36 67.668875 \nL 205.36 67.668875 \n\" style\u003d\"fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_22\"/\u003e\n \u003cg id\u003d\"text_22\"\u003e\n \u003c!-- $y\u003dx$ --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 10.59375 45.40625 \nL 73.1875 45.40625 \nL 73.1875 37.203125 \nL 10.59375 37.203125 \nz\nM 10.59375 25.484375 \nL 73.1875 25.484375 \nL 73.1875 17.1875 \nL 10.59375 17.1875 \nz\n\" id\u003d\"DejaVuSans-3d\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(221.36 74.668875)scale(0.2 -0.2)\"\u003e\n \u003cuse transform\u003d\"translate(0 0.3125)\" xlink:href\u003d\"#DejaVuSans-Oblique-79\"/\u003e\n \u003cuse transform\u003d\"translate(78.662109 0.3125)\" xlink:href\u003d\"#DejaVuSans-3d\"/\u003e\n \u003cuse transform\u003d\"translate(181.933594 0.3125)\" xlink:href\u003d\"#DejaVuSans-Oblique-78\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_23\"\u003e\n \u003cpath d\u003d\"M 165.36 97.06575 \nL 205.36 97.06575 \n\" style\u003d\"fill:none;stroke:#ff7f0e;stroke-linecap:square;stroke-width:1.5;\"/\u003e\n \u003c/g\u003e\n \u003cg id\u003d\"line2d_24\"/\u003e\n \u003cg id\u003d\"text_23\"\u003e\n \u003c!-- $y\u003d3-x$ --\u003e\n \u003cdefs\u003e\n \u003cpath d\u003d\"M 10.59375 35.5 \nL 73.1875 35.5 \nL 73.1875 27.203125 \nL 10.59375 27.203125 \nz\n\" id\u003d\"DejaVuSans-2212\"/\u003e\n \u003c/defs\u003e\n \u003cg transform\u003d\"translate(221.36 104.06575)scale(0.2 -0.2)\"\u003e\n \u003cuse transform\u003d\"translate(0 0.78125)\" xlink:href\u003d\"#DejaVuSans-Oblique-79\"/\u003e\n \u003cuse transform\u003d\"translate(78.662109 0.78125)\" xlink:href\u003d\"#DejaVuSans-3d\"/\u003e\n \u003cuse transform\u003d\"translate(181.933594 0.78125)\" xlink:href\u003d\"#DejaVuSans-33\"/\u003e\n \u003cuse transform\u003d\"translate(265.039062 0.78125)\" xlink:href\u003d\"#DejaVuSans-2212\"/\u003e\n \u003cuse transform\u003d\"translate(368.310547 0.78125)\" xlink:href\u003d\"#DejaVuSans-Oblique-78\"/\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003c/g\u003e\n \u003cdefs\u003e\n \u003cclipPath id\u003d\"pd960d6d74d\"\u003e\n \u003crect height\u003d\"266.112\" width\u003d\"357.12\" x\u003d\"57.6\" y\u003d\"41.472\"/\u003e\n \u003c/clipPath\u003e\n \u003c/defs\u003e\n\u003c/svg\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1512664642157_-269307059", + "id": "20171207-163722_1147326893", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + } + ], + "name": "Zeppelin Tutorial/Livy • Matplotlib (PySpark)", + "id": "2D3979PMW", + "angularObjects": {}, + "config": {}, + "info": {} +} diff --git a/notebook/2D4T6X5AR/note.json b/notebook/2D4T6X5AR/note.json new file mode 100644 index 00000000000..9f959ec070f --- /dev/null +++ b/notebook/2D4T6X5AR/note.json @@ -0,0 +1,265 @@ +{ + "paragraphs": [ + { + "text": "%md\n\n## Accessing MapR-DB in Zeppelin Using the MapR-DB OJAI Connector with Scala\n\nThis section contains examples of Apache Spark Scala jobs that use the MapR-DB OJAI Connector for Apache Spark to read and write MapR-DB JSON tables.", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch2\u003eAccessing MapR-DB in Zeppelin Using the MapR-DB OJAI Connector with Scala\u003c/h2\u003e\n\u003cp\u003eThis section contains examples of Apache Spark Scala jobs that use the MapR-DB OJAI Connector for Apache Spark to read and write MapR-DB JSON tables.\u003c/p\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1516899563631_-1405042137", + "id": "20180125-165923_1084237878", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Load source data to MapR-FS", + "text": "%sh\nTEMPDIR\u003d$(mktemp -d)\ncd $TEMPDIR\n\n# Spark can not parse JSON file with newlines, so we need to remove newlines from data file using \"tr -d \u0027\\n\u0027\"\ntr -d \u0027\\n\u0027 \u003e ./zeppelin_maprdb_sample.json \u003c\u003cEOF\n[ { \"_id\": \"rsmith\",\n \"address\": {\"city\": \"San Francisco\", \"line\": \"100 Main Street\", \"zip\": 94105},\n \"dob\": \"1982-02-03\",\n \"first_name\": \"Robert\",\n \"interests\": [\"electronics\", \"music\", \"sports\"],\n \"last_name\": \"Smith\"\n },\n { \"_id\": \"mdupont\",\n \"address\": {\"city\": \"San Jose\", \"line\": \"1223 Broadway\", \"zip\": 95109},\n \"dob\": \"1982-02-03\",\n \"first_name\": \"Maxime\",\n \"interests\": [\"sports\", \"movies\", \"electronics\"],\n \"last_name\": \"Dupont\"\n },\n { \"_id\": \"jdoe\",\n \"address\": {\"city\": \"San Francisco\", \"line\": \"12 Main Street\", \"zip\": 94005},\n \"dob\": \"1970-06-23\",\n \"first_name\": \"John\",\n \"interests\": null,\n \"last_name\": \"Doe\"\n },\n { \"_id\": \"dsimon\",\n \"address\": null,\n \"dob\": \"1980-10-13\",\n \"first_name\": \"David\",\n \"interests\": null,\n \"last_name\": \"Simon\"\n },\n { \"_id\": \"alehmann\",\n \"address\": null,\n \"dob\": \"1980-10-13\",\n \"first_name\": \"Andrew\",\n \"interests\": [\"html\", \"css\", \"js\"],\n \"last_name\": \"Lehmann\"\n }\n]\nEOF\n\nhadoop fs -mkdir -p ./zeppelin/samples/\nif ! hadoop fs -test -e \"./zeppelin/samples/zeppelin_maprdb_sample.json\"; then\n hadoop fs -put ./zeppelin_maprdb_sample.json ./zeppelin/samples/\nfi", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "sh", + "editOnDblClick": false + }, + "editorMode": "ace/mode/sh", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "apps": [], + "jobName": "paragraph_1516361570822_-1350983083", + "id": "20180119-113250_1762409024", + "status": "FINISHED", + "errorMessage": "", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Inserting a Spark DataFrame into a MapR-DB JSON Table", + "text": "%livy\nimport com.mapr.db.spark.sql._\nimport com.mapr.db.MapRDB\n\nval workingDir \u003d \"/user/\" + sc.sparkUser + \"/zeppelin/samples\"\nval samplePath \u003d workingDir + \"/zeppelin_maprdb_sample.json\"\nval tablePath \u003d workingDir + \"/sample_table_json\"\n\nval df \u003d spark.read.json(\"maprfs://\" + samplePath)\n\nif (MapRDB.tableExists(tablePath))\n MapRDB.deleteTable(tablePath)\n\ndf.saveToMapRDB(tablePath,\n idFieldPath \u003d \"last_name\", // Using idFieldPath argument you can specify non-default field to use as identifier\n createTable \u003d true)\n\nval result \u003d spark.loadFromMapRDB(tablePath)\nresult.show()", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "scala", + "editOnDblClick": false + }, + "editorMode": "ace/mode/scala", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "+-------+--------------------+----------+----------+--------------------+---------+\n| _id| address| dob|first_name| interests|last_name|\n+-------+--------------------+----------+----------+--------------------+---------+\n| Doe|[San Francisco,12...|1970-06-23| John| null| Doe|\n| Dupont|[San Jose,1223 Br...|1982-02-03| Maxime|[sports, movies, ...| Dupont|\n|Lehmann| null|1980-10-13| Andrew| [html, css, js]| Lehmann|\n| Simon| null|1980-10-13| David| null| Simon|\n| Smith|[San Francisco,10...|1982-02-03| Robert|[electronics, mus...| Smith|\n+-------+--------------------+----------+----------+--------------------+---------+" + } + ] + }, + "apps": [], + "jobName": "paragraph_1516361902738_-62167857", + "id": "20180119-113822_1630286286", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Inserting a Spark DataFrame into a MapR-DB JSON Table Using Bulk Insert", + "text": "%livy\nimport com.mapr.db.spark.sql._\nimport com.mapr.db.MapRDB\n\nval workingDir \u003d \"/user/\" + sc.sparkUser + \"/zeppelin/samples\"\nval samplePath \u003d workingDir + \"/zeppelin_maprdb_sample.json\"\nval tablePath \u003d workingDir + \"/sample_table_json\"\n\nval df \u003d spark.read.json(\"maprfs://\" + samplePath).orderBy(\"_id\")\n\nif (MapRDB.tableExists(tablePath))\n MapRDB.deleteTable(tablePath)\n\ndf.saveToMapRDB(tablePath, createTable \u003d true, bulkInsert \u003d true)\n\nval result \u003d spark.loadFromMapRDB(tablePath)\nresult.show()", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "scala", + "editOnDblClick": false + }, + "editorMode": "ace/mode/scala", + "title": true, + "editorHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "+--------+--------------------+----------+----------+--------------------+---------+\n| _id| address| dob|first_name| interests|last_name|\n+--------+--------------------+----------+----------+--------------------+---------+\n|alehmann| null|1980-10-13| Andrew| [html, css, js]| Lehmann|\n| dsimon| null|1980-10-13| David| null| Simon|\n| jdoe|[San Francisco,12...|1970-06-23| John| null| Doe|\n| mdupont|[San Jose,1223 Br...|1982-02-03| Maxime|[sports, movies, ...| Dupont|\n| rsmith|[San Francisco,10...|1982-02-03| Robert|[electronics, mus...| Smith|\n+--------+--------------------+----------+----------+--------------------+---------+" + } + ] + }, + "apps": [], + "jobName": "paragraph_1516899766453_1806931981", + "id": "20180125-170246_1999217881", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Selecting and Filtering Data when Loading a Spark DataFrame", + "text": "%livy\nimport com.mapr.db.spark.sql._\nimport com.mapr.db.MapRDB\n\nval workingDir \u003d \"/user/\" + sc.sparkUser + \"/zeppelin/samples\"\nval samplePath \u003d workingDir + \"/zeppelin_maprdb_sample.json\"\nval tablePath \u003d workingDir + \"/sample_table_json\"\n\nval df \u003d spark.read.json(\"maprfs://\" + samplePath)\n\nif (MapRDB.tableExists(tablePath))\n MapRDB.deleteTable(tablePath)\n\ndf.saveToMapRDB(tablePath, createTable \u003d true)\n\nval result \u003d spark.loadFromMapRDB(tablePath).filter($\"first_name\" \u003d\u003d\u003d \"Maxime\" || $\"address.city\" \u003d\u003d\u003d \"San Francisco\")\nresult.show()", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "scala", + "editOnDblClick": false + }, + "editorMode": "ace/mode/scala", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "+-------+--------------------+----------+----------+--------------------+---------+\n| _id| address| dob|first_name| interests|last_name|\n+-------+--------------------+----------+----------+--------------------+---------+\n| jdoe|[San Francisco,12...|1970-06-23| John| null| Doe|\n|mdupont|[San Jose,1223 Br...|1982-02-03| Maxime|[sports, movies, ...| Dupont|\n| rsmith|[San Francisco,10...|1982-02-03| Robert|[electronics, mus...| Smith|\n+-------+--------------------+----------+----------+--------------------+---------+" + } + ] + }, + "apps": [], + "jobName": "paragraph_1516363043460_162168634", + "id": "20180119-115723_900686396", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Joining DataFrames when Loading a Spark DataFrame", + "text": "%livy\nimport com.mapr.db.spark.sql._\nimport com.mapr.db.MapRDB\n\nval workingDir \u003d \"/user/\" + sc.sparkUser + \"/zeppelin/samples\"\nval samplePath \u003d workingDir + \"/zeppelin_maprdb_sample.json\"\nval tablePath \u003d workingDir + \"/sample_table_json\"\n\nval df \u003d spark.read.json(\"maprfs://\" + samplePath)\n\nif (MapRDB.tableExists(tablePath))\n MapRDB.deleteTable(tablePath)\n\ndf.saveToMapRDB(tablePath, createTable \u003d true)\n\nval professions \u003d Seq(\n (\"rsmith\", \"Engineer\"),\n (\"alehmann\", \"Accountant\"),\n (\"alehmann\", \"Doctor\"),\n (\"fake\", \"Software developer\")\n )\nval dfProfessions \u003d sc.parallelize(professions).toDF(\"_id\", \"profession\")\n\nval result \u003d spark.loadFromMapRDB(tablePath).join(dfProfessions, \"_id\")\n\nresult.createOrReplaceTempView(\"zeppelin_sample_table\") // Creating TempView to access this data from SparkSQL\n\nresult.show()", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "scala", + "editOnDblClick": false + }, + "editorMode": "ace/mode/scala", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "+--------+--------------------+----------+----------+--------------------+---------+----------+\n| _id| address| dob|first_name| interests|last_name|profession|\n+--------+--------------------+----------+----------+--------------------+---------+----------+\n|alehmann| null|1980-10-13| Andrew| [html, css, js]| Lehmann|Accountant|\n|alehmann| null|1980-10-13| Andrew| [html, css, js]| Lehmann| Doctor|\n| rsmith|[San Francisco,10...|1982-02-03| Robert|[electronics, mus...| Smith| Engineer|\n+--------+--------------------+----------+----------+--------------------+---------+----------+" + } + ] + }, + "apps": [], + "jobName": "paragraph_1516900036717_791562665", + "id": "20180125-170716_853131331", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Accessing data with SparkSQL", + "text": "%livy.sql\n\nSELECT CONCAT(`first_name`, \" \", `last_name`) as name, `profession`, `interests` FROM zeppelin_sample_table", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": { + "0": { + "graph": { + "mode": "table", + "height": 300.0, + "optionOpen": true, + "setting": { + "multiBarChart": {} + }, + "commonSetting": {}, + "keys": [], + "groups": [], + "values": [ + { + "name": "address", + "index": 1.0, + "aggr": "sum" + } + ] + }, + "helium": {} + } + }, + "editorSetting": { + "language": "sql", + "editOnDblClick": false + }, + "editorMode": "ace/mode/sql", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TABLE", + "data": "name\tprofession\tinterests\nAndrew Lehmann\tAccountant\t[html, css, js]\nAndrew Lehmann\tDoctor\t[html, css, js]\nRobert Smith\tEngineer\t[electronics, mus..." + } + ] + }, + "apps": [], + "jobName": "paragraph_1516906271259_-226212620", + "id": "20180125-185111_200638054", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + } + ], + "name": "MapR Tutorial/Spark MapR-DB OJAI Connector (Scala)", + "id": "2D4T6X5AR", + "angularObjects": {}, + "config": { + "looknfeel": "default", + "personalizedMode": "false" + }, + "info": {} +} diff --git a/notebook/2D4WBY923/note.json b/notebook/2D4WBY923/note.json new file mode 100644 index 00000000000..66a665ff138 --- /dev/null +++ b/notebook/2D4WBY923/note.json @@ -0,0 +1,309 @@ +{ + "paragraphs": [ + { + "text": "%md\n\n## Accessing MapR-DB in Zeppelin Using the MapR-DB OJAI Connector with Python\n\nThis section contains examples of Apache Spark Python jobs that use the MapR-DB OJAI Connector for Apache Spark to read and write MapR-DB JSON tables.", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch2\u003eAccessing MapR-DB in Zeppelin Using the MapR-DB OJAI Connector with Python\u003c/h2\u003e\n\u003cp\u003eThis section contains examples of Apache Spark Python jobs that use the MapR-DB OJAI Connector for Apache Spark to read and write MapR-DB JSON tables.\u003c/p\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1517250946760_1002545274", + "id": "20180129-183546_204768414", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Load source data to MapR-FS", + "text": "%sh\nTEMPDIR\u003d$(mktemp -d)\ncd $TEMPDIR\n\n# Spark can not parse JSON file with newlines, so we need to remove newlines from data file using \"tr -d \u0027\\n\u0027\"\ntr -d \u0027\\n\u0027 \u003e ./zeppelin_maprdb_sample.json \u003c\u003cEOF\n[ { \"_id\": \"rsmith\",\n \"address\": {\"city\": \"San Francisco\", \"line\": \"100 Main Street\", \"zip\": 94105},\n \"dob\": \"1982-02-03\",\n \"first_name\": \"Robert\",\n \"interests\": [\"electronics\", \"music\", \"sports\"],\n \"last_name\": \"Smith\"\n },\n { \"_id\": \"mdupont\",\n \"address\": {\"city\": \"San Jose\", \"line\": \"1223 Broadway\", \"zip\": 95109},\n \"dob\": \"1982-02-03\",\n \"first_name\": \"Maxime\",\n \"interests\": [\"sports\", \"movies\", \"electronics\"],\n \"last_name\": \"Dupont\"\n },\n { \"_id\": \"jdoe\",\n \"address\": {\"city\": \"San Francisco\", \"line\": \"12 Main Street\", \"zip\": 94005},\n \"dob\": \"1970-06-23\",\n \"first_name\": \"John\",\n \"interests\": null,\n \"last_name\": \"Doe\"\n },\n { \"_id\": \"dsimon\",\n \"address\": null,\n \"dob\": \"1980-10-13\",\n \"first_name\": \"David\",\n \"interests\": null,\n \"last_name\": \"Simon\"\n },\n { \"_id\": \"alehmann\",\n \"address\": null,\n \"dob\": \"1980-10-13\",\n \"first_name\": \"Andrew\",\n \"interests\": [\"html\", \"css\", \"js\"],\n \"last_name\": \"Lehmann\"\n }\n]\nEOF\n\nhadoop fs -mkdir -p ./zeppelin/samples/\nif ! hadoop fs -test -e \"./zeppelin/samples/zeppelin_maprdb_sample.json\"; then\n hadoop fs -put ./zeppelin_maprdb_sample.json ./zeppelin/samples/\nfi", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "sh", + "editOnDblClick": false + }, + "editorMode": "ace/mode/sh", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "apps": [], + "jobName": "paragraph_1517250966597_718615910", + "id": "20180129-183606_1283148760", + "status": "FINISHED", + "errorMessage": "", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Ensure that table for next example does not exist", + "text": "%livy\nimport com.mapr.db.spark.sql._\nimport com.mapr.db.MapRDB\n\nval workingDir \u003d \"/user/\" + sc.sparkUser + \"/zeppelin/samples\"\nval tablePath \u003d workingDir + \"/sample_table_json\"\n\nif (MapRDB.tableExists(tablePath))\n MapRDB.deleteTable(tablePath)", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "scala", + "editOnDblClick": false + }, + "editorMode": "ace/mode/scala", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "apps": [], + "jobName": "paragraph_1517251041166_1280272683", + "id": "20180129-183721_804958126", + "status": "FINISHED", + "errorMessage": "", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Inserting a Spark DataFrame into a MapR-DB JSON Table", + "text": "%livy.pyspark\nimport json\nimport sys\nfrom pyspark.sql.functions import col\n\nworking_dir \u003d \"/user/\" + sc.sparkUser() + \"/zeppelin/samples\"\nsample_path \u003d working_dir + \"/zeppelin_maprdb_sample.json\"\ntable_path \u003d working_dir + \"/sample_table_json\"\n\ndf \u003d spark.read.json(\"maprfs://\" + sample_path)\n\nspark.saveToMapRDB(df,\n table_path,\n id_field_path\u003d\"last_name\", # Using id_field_path argument you can specify non-default field to use as identifier\n create_table\u003dTrue)\n\nresult \u003d spark.loadFromMapRDB(table_path)\nresult.show()", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "python", + "editOnDblClick": false + }, + "editorMode": "ace/mode/python", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "+-------+--------------------+----------+----------+--------------------+---------+\n| _id| address| dob|first_name| interests|last_name|\n+-------+--------------------+----------+----------+--------------------+---------+\n| Doe|[San Francisco,12...|1970-06-23| John| null| Doe|\n| Dupont|[San Jose,1223 Br...|1982-02-03| Maxime|[sports, movies, ...| Dupont|\n|Lehmann| null|1980-10-13| Andrew| [html, css, js]| Lehmann|\n| Simon| null|1980-10-13| David| null| Simon|\n| Smith|[San Francisco,10...|1982-02-03| Robert|[electronics, mus...| Smith|\n+-------+--------------------+----------+----------+--------------------+---------+" + } + ] + }, + "apps": [], + "jobName": "paragraph_1517251069415_-965259816", + "id": "20180129-183749_2089498841", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Ensure that table for next example does not exist", + "text": "%livy\nimport com.mapr.db.spark.sql._\nimport com.mapr.db.MapRDB\n\nval workingDir \u003d \"/user/\" + sc.sparkUser + \"/zeppelin/samples\"\nval tablePath \u003d workingDir + \"/sample_table_json\"\n\nif (MapRDB.tableExists(tablePath))\n MapRDB.deleteTable(tablePath)", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "scala", + "editOnDblClick": false + }, + "editorMode": "ace/mode/scala", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "apps": [], + "jobName": "paragraph_1517251092516_-1560435716", + "id": "20180129-183812_1378463398", + "status": "FINISHED", + "errorMessage": "", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Inserting a Spark DataFrame into a MapR-DB JSON Table Using Bulk Insert", + "text": "%livy.pyspark\nimport json\nimport sys\nfrom pyspark.sql.functions import col\n\nworking_dir \u003d \"/user/\" + sc.sparkUser() + \"/zeppelin/samples\"\nsample_path \u003d working_dir + \"/zeppelin_maprdb_sample.json\"\ntable_path \u003d working_dir + \"/sample_table_json\"\n\ndf \u003d spark.read.json(\"maprfs://\" + sample_path).orderBy(\"_id\")\n\nspark.saveToMapRDB(df, table_path, create_table\u003dTrue, bulk_insert\u003dTrue)\n\nresult \u003d spark.loadFromMapRDB(table_path)\nresult.show()", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "python", + "editOnDblClick": false + }, + "editorMode": "ace/mode/python", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "+--------+--------------------+----------+----------+--------------------+---------+\n| _id| address| dob|first_name| interests|last_name|\n+--------+--------------------+----------+----------+--------------------+---------+\n|alehmann| null|1980-10-13| Andrew| [html, css, js]| Lehmann|\n| dsimon| null|1980-10-13| David| null| Simon|\n| jdoe|[San Francisco,12...|1970-06-23| John| null| Doe|\n| mdupont|[San Jose,1223 Br...|1982-02-03| Maxime|[sports, movies, ...| Dupont|\n| rsmith|[San Francisco,10...|1982-02-03| Robert|[electronics, mus...| Smith|\n+--------+--------------------+----------+----------+--------------------+---------+" + } + ] + }, + "apps": [], + "jobName": "paragraph_1517251120515_-1870127932", + "id": "20180129-183840_926142364", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Ensure that table for next example does not exist", + "text": "%livy\nimport com.mapr.db.spark.sql._\nimport com.mapr.db.MapRDB\n\nval workingDir \u003d \"/user/\" + sc.sparkUser + \"/zeppelin/samples\"\nval tablePath \u003d workingDir + \"/sample_table_json\"\n\nif (MapRDB.tableExists(tablePath))\n MapRDB.deleteTable(tablePath)", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "scala", + "editOnDblClick": false + }, + "editorMode": "ace/mode/scala", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "apps": [], + "jobName": "paragraph_1517251137478_-916488650", + "id": "20180129-183857_1399073887", + "status": "FINISHED", + "errorMessage": "", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Selecting and Filtering Data when Loading a Spark DataFrame", + "text": "%livy.pyspark\nimport json\nimport sys\nfrom pyspark.sql.functions import col\n\nworking_dir \u003d \"/user/\" + sc.sparkUser() + \"/zeppelin/samples\"\nsample_path \u003d working_dir + \"/zeppelin_maprdb_sample.json\"\ntable_path \u003d working_dir + \"/sample_table_json\"\n\ndf \u003d spark.read.json(\"maprfs://\" + sample_path)\n\nspark.saveToMapRDB(df, table_path, create_table\u003dTrue)\n\nresult \u003d spark.loadFromMapRDB(table_path).filter((col(\"first_name\") \u003d\u003d \"Maxime\") | (col(\"address.city\") \u003d\u003d \"San Francisco\"))\nresult.show()", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "python", + "editOnDblClick": false + }, + "editorMode": "ace/mode/python", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "+-------+--------------------+----------+----------+--------------------+---------+\n| _id| address| dob|first_name| interests|last_name|\n+-------+--------------------+----------+----------+--------------------+---------+\n| jdoe|[San Francisco,12...|1970-06-23| John| null| Doe|\n|mdupont|[San Jose,1223 Br...|1982-02-03| Maxime|[sports, movies, ...| Dupont|\n| rsmith|[San Francisco,10...|1982-02-03| Robert|[electronics, mus...| Smith|\n+-------+--------------------+----------+----------+--------------------+---------+" + } + ] + }, + "apps": [], + "jobName": "paragraph_1517251148821_1881420675", + "id": "20180129-183908_2093196848", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Ensure that table for next example does not exist", + "text": "%livy\nimport com.mapr.db.spark.sql._\nimport com.mapr.db.MapRDB\n\nval workingDir \u003d \"/user/\" + sc.sparkUser + \"/zeppelin/samples\"\nval tablePath \u003d workingDir + \"/sample_table_json\"\n\nif (MapRDB.tableExists(tablePath))\n MapRDB.deleteTable(tablePath)", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "scala", + "editOnDblClick": false + }, + "editorMode": "ace/mode/scala", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "apps": [], + "jobName": "paragraph_1517251168676_-36521943", + "id": "20180129-183928_253842792", + "status": "FINISHED", + "errorMessage": "", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Joining DataFrames when Loading a Spark DataFrame", + "text": "%livy.pyspark\nimport json\nimport sys\nfrom pyspark.sql.functions import col\nfrom pyspark.sql import Row\n\nworking_dir \u003d \"/user/\" + sc.sparkUser() + \"/zeppelin/samples\"\nsample_path \u003d working_dir + \"/zeppelin_maprdb_sample.json\"\ntable_path \u003d working_dir + \"/sample_table_json\"\n\ndf \u003d spark.read.json(\"maprfs://\" + sample_path)\n\nspark.saveToMapRDB(df, table_path, create_table\u003dTrue)\n\nprofessions \u003d [\n Row(_id\u003d\"rsmith\", profession\u003d\"Engineer\"),\n Row(_id\u003d\"alehmann\", profession\u003d\"Doctor\"),\n Row(_id\u003d\"alehmann\", profession\u003d\"Accountant\"),\n Row(_id\u003d\"fake\", profession\u003d\"Software developer\"),\n ]\ndf_professions \u003d sc.parallelize(professions).toDF()\n\nresult \u003d spark.loadFromMapRDB(table_path).join(df_professions, \"_id\")\nresult.show()", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "python", + "editOnDblClick": false + }, + "editorMode": "ace/mode/python", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "+--------+--------------------+----------+----------+--------------------+---------+----------+\n| _id| address| dob|first_name| interests|last_name|profession|\n+--------+--------------------+----------+----------+--------------------+---------+----------+\n|alehmann| null|1980-10-13| Andrew| [html, css, js]| Lehmann|Accountant|\n|alehmann| null|1980-10-13| Andrew| [html, css, js]| Lehmann| Doctor|\n| rsmith|[San Francisco,10...|1982-02-03| Robert|[electronics, mus...| Smith| Engineer|\n+--------+--------------------+----------+----------+--------------------+---------+----------+" + } + ] + }, + "apps": [], + "jobName": "paragraph_1517251180998_-1623995905", + "id": "20180129-183940_916661658", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + } + ], + "name": "MapR Tutorial/Spark MapR-DB OJAI Connector (Python)", + "id": "2D4WBY923", + "angularObjects": {}, + "config": {}, + "info": {} +} diff --git a/notebook/2D6JT1W6P/note.json b/notebook/2D6JT1W6P/note.json new file mode 100644 index 00000000000..28f666e9eeb --- /dev/null +++ b/notebook/2D6JT1W6P/note.json @@ -0,0 +1,112 @@ +{ + "paragraphs": [ + { + "text": "%md\n\n## Accessing MapR-DB Binary tables in Zeppelin\nThis section contains an example of an Apache Spark job that uses the MapR-DB Binary Connector for Apache Spark to write and read a MapR-DB Binary table.", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true + }, + "editorMode": "ace/mode/markdown", + "editorHide": true, + "tableHide": false + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "HTML", + "data": "\u003cdiv class\u003d\"markdown-body\"\u003e\n\u003ch2\u003eAccessing MapR-DB Binary tables in Zeppelin\u003c/h2\u003e\n\u003cp\u003eThis section contains an example of an Apache Spark job that uses the MapR-DB Binary Connector for Apache Spark to write and read a MapR-DB Binary table.\u003c/p\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1517256223648_300104267", + "id": "20180129-200343_1997599312", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Reading and writing data using DataFrame", + "text": "%livy\nimport org.apache.hadoop.fs.{FileSystem, Path}\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.datasources.hbase.HBaseTableCatalog\n\nval workingDir \u003d \"/user/\" + sc.sparkUser + \"/zeppelin/samples\"\nval tablePath \u003d workingDir + \"/sample_table_binary_df\"\n\n// Create samples directory in MapR-FS\ndef ensureWorkingDir \u003d {\n val fs \u003d FileSystem.get(sc.hadoopConfiguration)\n val workingPath \u003d new Path(workingDir)\n if(!fs.exists(workingPath)) fs.mkdirs(workingPath)\n}\nensureWorkingDir\n\ncase class HBaseRecord(\n col0: String,\n col1: Boolean,\n col2: Double,\n col3: Float,\n col4: Int,\n col5: Long,\n col6: Short,\n col7: String,\n col8: Byte)\n\nval data \u003d (0 to 255).map { i \u003d\u003e\n val s \u003d \"row\" + \"%03d\".format(i)\n new HBaseRecord(s,\n i % 2 \u003d\u003d 0,\n i.toDouble,\n i.toFloat,\n i,\n i.toLong,\n i.toShort,\n s\"String $i extra\",\n i.toByte)\n}\n\nval catalog \u003d s\"\"\"{\n |\"table\": {\"namespace\": \"default\", \"name\": \"$tablePath\"},\n |\"rowkey\": \"key\",\n |\"columns\": {\n |\"col0\": {\"cf\": \"rowkey\", \"col\": \"key\", \"type\": \"string\"},\n |\"col1\": {\"cf\": \"cf1\", \"col\": \"col1\", \"type\": \"boolean\"},\n |\"col2\": {\"cf\": \"cf2\", \"col\": \"col2\", \"type\": \"double\"},\n |\"col3\": {\"cf\": \"cf3\", \"col\": \"col3\", \"type\": \"float\"},\n |\"col4\": {\"cf\": \"cf4\", \"col\": \"col4\", \"type\": \"int\"},\n |\"col5\": {\"cf\": \"cf5\", \"col\": \"col5\", \"type\": \"bigint\"},\n |\"col6\": {\"cf\": \"cf6\", \"col\": \"col6\", \"type\": \"smallint\"},\n |\"col7\": {\"cf\": \"cf7\", \"col\": \"col7\", \"type\": \"string\"},\n |\"col8\": {\"cf\": \"cf8\", \"col\": \"col8\", \"type\": \"tinyint\"}\n |}\n |}\"\"\".stripMargin\n\n\nval rdd \u003d sc.parallelize(data).toDF.write.options(\n Map(HBaseTableCatalog.tableCatalog -\u003e catalog, HBaseTableCatalog.newTable -\u003e \"5\")).format(\"org.apache.hadoop.hbase.spark\").save()\n\nval df \u003d {\n spark\n .read\n .options(Map(HBaseTableCatalog.tableCatalog -\u003e catalog))\n .format(\"org.apache.hadoop.hbase.spark\")\n .load()\n}\n\ndf.show", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "scala", + "editOnDblClick": false + }, + "editorMode": "ace/mode/scala", + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "import org.apache.hadoop.fs.{FileSystem, Path}\nimport org.apache.spark.sql.DataFrame\nimport org.apache.spark.sql.datasources.hbase.HBaseTableCatalog\nworkingDir: String \u003d /user/mapr/zeppelin/samples\ntablePath: String \u003d /user/mapr/zeppelin/samples/sample_table_binary_df\nensureWorkingDir: AnyVal\nres45: AnyVal \u003d ()\ndefined class HBaseRecord\ndata: scala.collection.immutable.IndexedSeq[HBaseRecord] \u003d Vector(HBaseRecord(row000,true,0.0,0.0,0,0,0,String 0 extra,0), HBaseRecord(row001,false,1.0,1.0,1,1,1,String 1 extra,1), HBaseRecord(row002,true,2.0,2.0,2,2,2,String 2 extra,2), HBaseRecord(row003,false,3.0,3.0,3,3,3,String 3 extra,3), HBaseRecord(row004,true,4.0,4.0,4,4,4,String 4 extra,4), HBaseRecord(row005,false,5.0,5.0,5,5,5,String 5 extra,5), HBaseRecord(row006,true,6.0,6.0,6,6,6,String 6 extra,6), HBaseRecord(row007,false,7.0,7.0,7,7,7,String 7 extra,7), HBaseRecord(row008,true,8.0,8.0,8,8,8,String 8 extra,8), HBaseRecord(row009,false,9.0,9.0,9,9,9,String 9 extra,9), HBaseRecord(row010,true,10.0,10.0,10,10,10,String 10 extra,10), HBaseRecord(row011,false,11.0,11.0,11,11,11,String 11 extra,11), HBaseRecord(row012,true,12....catalog: String \u003d\n{\n\"table\": {\"namespace\": \"default\", \"name\": \"/user/mapr/zeppelin/samples/sample_table_binary_df\"},\n\"rowkey\": \"key\",\n\"columns\": {\n\"col0\": {\"cf\": \"rowkey\", \"col\": \"key\", \"type\": \"string\"},\n\"col1\": {\"cf\": \"cf1\", \"col\": \"col1\", \"type\": \"boolean\"},\n\"col2\": {\"cf\": \"cf2\", \"col\": \"col2\", \"type\": \"double\"},\n\"col3\": {\"cf\": \"cf3\", \"col\": \"col3\", \"type\": \"float\"},\n\"col4\": {\"cf\": \"cf4\", \"col\": \"col4\", \"type\": \"int\"},\n\"col5\": {\"cf\": \"cf5\", \"col\": \"col5\", \"type\": \"bigint\"},\n\"col6\": {\"cf\": \"cf6\", \"col\": \"col6\", \"type\": \"smallint\"},\n\"col7\": {\"cf\": \"cf7\", \"col\": \"col7\", \"type\": \"string\"},\n\"col8\": {\"cf\": \"cf8\", \"col\": \"col8\", \"type\": \"tinyint\"}\n}\n}\nrdd: Unit \u003d ()\ndf: org.apache.spark.sql.DataFrame \u003d [col4: int, col7: string ... 7 more fields]\n+----+---------------+-----+----+----+------+----+----+----+\n|col4| col7| col1|col3|col6| col0|col8|col2|col5|\n+----+---------------+-----+----+----+------+----+----+----+\n| 0| String 0 extra| true| 0.0| 0|row000| 0| 0.0| 0|\n| 1| String 1 extra|false| 1.0| 1|row001| 1| 1.0| 1|\n| 2| String 2 extra| true| 2.0| 2|row002| 2| 2.0| 2|\n| 3| String 3 extra|false| 3.0| 3|row003| 3| 3.0| 3|\n| 4| String 4 extra| true| 4.0| 4|row004| 4| 4.0| 4|\n| 5| String 5 extra|false| 5.0| 5|row005| 5| 5.0| 5|\n| 6| String 6 extra| true| 6.0| 6|row006| 6| 6.0| 6|\n| 7| String 7 extra|false| 7.0| 7|row007| 7| 7.0| 7|\n| 8| String 8 extra| true| 8.0| 8|row008| 8| 8.0| 8|\n| 9| String 9 extra|false| 9.0| 9|row009| 9| 9.0| 9|\n| 10|String 10 extra| true|10.0| 10|row010| 10|10.0| 10|\n| 11|String 11 extra|false|11.0| 11|row011| 11|11.0| 11|\n| 12|String 12 extra| true|12.0| 12|row012| 12|12.0| 12|\n| 13|String 13 extra|false|13.0| 13|row013| 13|13.0| 13|\n| 14|String 14 extra| true|14.0| 14|row014| 14|14.0| 14|\n| 15|String 15 extra|false|15.0| 15|row015| 15|15.0| 15|\n| 16|String 16 extra| true|16.0| 16|row016| 16|16.0| 16|\n| 17|String 17 extra|false|17.0| 17|row017| 17|17.0| 17|\n| 18|String 18 extra| true|18.0| 18|row018| 18|18.0| 18|\n| 19|String 19 extra|false|19.0| 19|row019| 19|19.0| 19|\n+----+---------------+-----+----+----+------+----+----+----+\nonly showing top 20 rows" + } + ] + }, + "apps": [], + "jobName": "paragraph_1517255317573_1078963044", + "id": "20180129-194837_1856731574", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Reading and writing data using RDD", + "text": "%livy\nimport org.apache.hadoop.fs.{FileSystem, Path}\nimport org.apache.hadoop.hbase.client.{HBaseAdmin, Put, Get, Result}\nimport org.apache.hadoop.hbase.spark.HBaseContext\nimport org.apache.hadoop.hbase.spark.HBaseRDDFunctions._\nimport org.apache.hadoop.hbase.util.{Bytes \u003d\u003e By}\nimport org.apache.hadoop.hbase.{CellUtil, HBaseConfiguration, HColumnDescriptor, HTableDescriptor, TableName}\n\nval workingDir \u003d \"/user/\" + sc.sparkUser + \"/zeppelin/samples\"\nval tablePath \u003d workingDir + \"/sample_table_binary_rdd\"\nval columnFamily \u003d \"sample_cf\"\n\n// Create samples directory in MapR-FS\ndef ensureWorkingDir \u003d {\n val fs \u003d FileSystem.get(sc.hadoopConfiguration)\n val workingPath \u003d new Path(workingDir)\n if(!fs.exists(workingPath)) fs.mkdirs(workingPath)\n}\nensureWorkingDir\n\n// Initialize HBaseContext\ndef createHBaseContext \u003d {\n val hbaseConf \u003d HBaseConfiguration.create()\n val hbaseContext \u003d new HBaseContext(sc, hbaseConf)\n hbaseContext\n}\nval hbaseContext \u003d createHBaseContext\n\n// Create empty table\ndef ensureTable \u003d {\n val hbaseConf \u003d HBaseConfiguration.create()\n val hbaseAdmin \u003d new HBaseAdmin(hbaseConf)\n if (hbaseAdmin.tableExists(tablePath)) {\n hbaseAdmin.disableTable(tablePath)\n hbaseAdmin.deleteTable(tablePath)\n }\n val tableDesc \u003d new HTableDescriptor(tablePath)\n tableDesc.addFamily(new HColumnDescriptor(By.toBytes(columnFamily)))\n hbaseAdmin.createTable(tableDesc)\n}\nensureTable\n\n\n// Put data into table\nval putRDD \u003d sc.parallelize(Array(\n (By.toBytes(\"1\"), (By.toBytes(columnFamily), By.toBytes(\"1\"), By.toBytes(\"1\"))),\n (By.toBytes(\"2\"), (By.toBytes(columnFamily), By.toBytes(\"1\"), By.toBytes(\"2\"))),\n (By.toBytes(\"3\"), (By.toBytes(columnFamily), By.toBytes(\"1\"), By.toBytes(\"3\"))),\n (By.toBytes(\"4\"), (By.toBytes(columnFamily), By.toBytes(\"1\"), By.toBytes(\"4\"))),\n (By.toBytes(\"5\"), (By.toBytes(columnFamily), By.toBytes(\"1\"), By.toBytes(\"5\")))\n))\n\nputRDD.hbaseBulkPut(hbaseContext, TableName.valueOf(tablePath),\n (putRecord) \u003d\u003e {\n val put \u003d new Put(putRecord._1)\n val (family, qualifier, value) \u003d putRecord._2\n put.addColumn(family, qualifier, value)\n put\n })\n\n// Get data from table\nval getRDD \u003d sc.parallelize(Array(\n By.toBytes(\"5\"),\n By.toBytes(\"4\"),\n By.toBytes(\"3\"),\n By.toBytes(\"2\"),\n By.toBytes(\"1\")\n))\n\nval resRDD \u003d getRDD.hbaseBulkGet[String](hbaseContext, TableName.valueOf(tablePath), 2,\n (record) \u003d\u003e { new Get(record) },\n (result: Result) \u003d\u003e {\n val it \u003d result.listCells().iterator()\n val sb \u003d new StringBuilder\n\n sb.append(By.toString(result.getRow) + \": \")\n while (it.hasNext) {\n val cell \u003d it.next()\n val q \u003d By.toString(CellUtil.cloneQualifier(cell))\n if (q.equals(\"counter\")) {\n sb.append(\"(\" + q + \",\" + By.toLong(CellUtil.cloneValue(cell)) + \")\")\n } else {\n sb.append(\"(\" + q + \",\" + By.toString(CellUtil.cloneValue(cell)) + \")\")\n }\n }\n sb.toString()\n })\n\nresRDD.collect().foreach(v \u003d\u003e println(v))", + "user": "anonymous", + "config": { + "colWidth": 12.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "scala", + "editOnDblClick": false + }, + "editorMode": "ace/mode/scala", + "lineNumbers": false, + "title": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "import org.apache.hadoop.fs.{FileSystem, Path}\nimport org.apache.hadoop.hbase.client.{HBaseAdmin, Put, Get, Result}\nimport org.apache.hadoop.hbase.spark.HBaseContext\nimport org.apache.hadoop.hbase.spark.HBaseRDDFunctions._\nimport org.apache.hadoop.hbase.util.{Bytes\u003d\u003eBy}\nimport org.apache.hadoop.hbase.{CellUtil, HBaseConfiguration, HColumnDescriptor, HTableDescriptor, TableName}\nworkingDir: String \u003d /user/mapr/zeppelin/samples\ntablePath: String \u003d /user/mapr/zeppelin/samples/sample_table_binary_rdd\ncolumnFamily: String \u003d sample_cf\nensureWorkingDir: AnyVal\nres57: AnyVal \u003d ()\ncreateHBaseContext: org.apache.hadoop.hbase.spark.HBaseContext\nhbaseContext: org.apache.hadoop.hbase.spark.HBaseContext \u003d org.apache.hadoop.hbase.spark.HBaseContext@7eda3374\nwarning: there were two deprecation warnings; re-run with -deprecation for details\nensureTable: Unit\nputRDD: org.apache.spark.rdd.RDD[(Array[Byte], (Array[Byte], Array[Byte], Array[Byte]))] \u003d ParallelCollectionRDD[42] at parallelize at \u003cconsole\u003e:58\ngetRDD: org.apache.spark.rdd.RDD[Array[Byte]] \u003d ParallelCollectionRDD[43] at parallelize at \u003cconsole\u003e:55\nresRDD: org.apache.spark.rdd.RDD[String] \u003d MapPartitionsRDD[44] at mapPartitions at HBaseContext.scala:388\n5: (1,5)\n4: (1,4)\n3: (1,3)\n2: (1,2)\n1: (1,1)" + } + ] + }, + "apps": [], + "jobName": "paragraph_1517566433507_1621939549", + "id": "20180202-101353_1150659090", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + } + ], + "name": "MapR Tutorial/Spark MapR-DB Binary Connector (Scala)", + "id": "2D6JT1W6P", + "angularObjects": {}, + "config": {}, + "info": {} +} From c5f52f274ccea588a90aafab14c1ee51541f1251 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Tue, 22 Oct 2019 18:07:53 +0300 Subject: [PATCH 475/492] Change zeppelin.server.addr back to 0.0.0.0 after ZEPPELIN-4166 --- conf/zeppelin-site.xml.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template index 6e46a33f3fb..c52d5e368ea 100755 --- a/conf/zeppelin-site.xml.template +++ b/conf/zeppelin-site.xml.template @@ -21,7 +21,7 @@ zeppelin.server.addr - 127.0.0.1 + 0.0.0.0 Server binding address From fbba6a592fd4b9ff39cb30eb80f434680c3c13a7 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Mon, 18 Nov 2019 17:32:14 +0200 Subject: [PATCH 476/492] [ZEPPELIN-4214]. Spark Web UI is displayed in the wrong paragraph (#136) (cherry picked from commit 786c4af) --- .../java/org/apache/zeppelin/spark/Utils.java | 14 +--------- .../spark/NewSparkInterpreterTest.java | 25 ++++++++++++++++- .../apache/zeppelin/spark/SparkShimsTest.java | 9 ++++-- .../org/apache/zeppelin/spark/SparkShims.java | 28 +++++++++++++------ .../interpreter/InterpreterContext.java | 4 +++ 5 files changed, 55 insertions(+), 25 deletions(-) diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/Utils.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/Utils.java index ccfc348f4bb..b1dd02dd0c1 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/Utils.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/Utils.java @@ -153,19 +153,7 @@ public static String buildJobGroupId(InterpreterContext context) { if (context.getAuthenticationInfo() != null) { uName = getUserName(context.getAuthenticationInfo()); } - return "zeppelin-" + uName + "-" + context.getNoteId() + "-" + context.getParagraphId(); - } - - public static String getNoteId(String jobgroupId) { - int indexOf = jobgroupId.indexOf("-"); - int secondIndex = jobgroupId.indexOf("-", indexOf + 1); - return jobgroupId.substring(indexOf + 1, secondIndex); - } - - public static String getParagraphId(String jobgroupId) { - int indexOf = jobgroupId.indexOf("-"); - int secondIndex = jobgroupId.indexOf("-", indexOf + 1); - return jobgroupId.substring(secondIndex + 1, jobgroupId.length()); + return "zeppelin|" + uName + "|" + context.getNoteId() + "|" + context.getParagraphId(); } public static String getUserName(AuthenticationInfo info) { diff --git a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java index 312839ad106..38c8e707b91 100644 --- a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java @@ -38,6 +38,7 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import org.mockito.ArgumentCaptor; import java.io.File; import java.io.FileOutputStream; @@ -167,9 +168,31 @@ public void testSparkInterpreter() throws IOException, InterruptedException, Int assertEquals(InterpreterResult.Code.SUCCESS, result.code()); // spark rdd operation + //* result = interpreter.interpret("sc.range(1, 10).sum", getInterpreterContext()); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); assertTrue(output.contains("45")); + /*/ + context = getInterpreterContext(); + context.setParagraphId("pid_1"); + result = interpreter.interpret("sc\n.range(1, 10)\n.sum", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertTrue(output.contains("45")); + ArgumentCaptor captorEvent = ArgumentCaptor.forClass(Map.class); + verify(mockRemoteEventClient).onParaInfosReceived(captorEvent.capture()); + assertEquals("pid_1", captorEvent.getValue().get("paraId")); + + reset(mockRemoteEventClient); + context = getInterpreterContext(); + context.setParagraphId("pid_2"); + result = interpreter.interpret("sc\n.range(1, 10)\n.sum", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertTrue(output.contains("45")); + captorEvent = ArgumentCaptor.forClass(Map.class); + verify(mockRemoteEventClient).onParaInfosReceived(captorEvent.capture()); + assertEquals("pid_2", captorEvent.getValue().get("paraId")); + //*/ + // spark job url is sent verify(mockRemoteEventClient).onParaInfosReceived(any(String.class), any(String.class), any(Map.class)); @@ -320,7 +343,7 @@ public void run() { InterpreterResult result = null; try { result = interpreter.interpret( - "val df = sc.parallelize(1 to 10, 2).foreach(e=>Thread.sleep(1000))", context2); + "val df = sc.parallelize(1 to 10, 5).foreach(e=>Thread.sleep(1000))", context2); } catch (InterpreterException e) { e.printStackTrace(); } diff --git a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkShimsTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkShimsTest.java index fefb8306aff..119acdf9a86 100644 --- a/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkShimsTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkShimsTest.java @@ -128,7 +128,10 @@ public void setUp() { @Test public void runUnerLocalTest() { - sparkShims.buildSparkJobUrl("local", "http://sparkurl", 0, mockProperties); + Properties properties = new Properties(); + properties.setProperty("spark.jobGroup.id", "zeppelin|user1|noteId|paragraphId"); + sparkShims.buildSparkJobUrl("local", "http://sparkurl", 0, properties); + Map mapValue = argumentCaptor.getValue(); assertTrue(mapValue.keySet().contains("jobUrl")); assertTrue(mapValue.get("jobUrl").contains("/jobs/job?id=")); @@ -136,7 +139,9 @@ public void runUnerLocalTest() { @Test public void runUnerYarnTest() { - sparkShims.buildSparkJobUrl("yarn", "http://sparkurl", 0, mockProperties); + Properties properties = new Properties(); + properties.setProperty("spark.jobGroup.id", "zeppelin|user1|noteId|paragraphId"); + sparkShims.buildSparkJobUrl("yarn", "http://sparkurl", 0, properties); Map mapValue = argumentCaptor.getValue(); assertTrue(mapValue.keySet().contains("jobUrl")); diff --git a/spark/spark-shims/src/main/scala/org/apache/zeppelin/spark/SparkShims.java b/spark/spark-shims/src/main/scala/org/apache/zeppelin/spark/SparkShims.java index f8e7d8c59d0..c46da7bbc7f 100644 --- a/spark/spark-shims/src/main/scala/org/apache/zeppelin/spark/SparkShims.java +++ b/spark/spark-shims/src/main/scala/org/apache/zeppelin/spark/SparkShims.java @@ -92,15 +92,6 @@ private static String getSparkMajorVersion(String sparkVersion) { */ public abstract void setupSparkListener(String master, String sparkWebUrl); - protected String getNoteId(String jobgroupId) { - String[] tokens = jobgroupId.split("-"); - return tokens[2]; - } - - protected String getParagraphId(String jobgroupId) { - String[] tokens = jobgroupId.split("-"); - return tokens[3]; - } protected void buildSparkJobUrl( String master, String sparkWebUrl, int jobId, Properties jobProperties) { @@ -119,12 +110,31 @@ protected void buildSparkJobUrl( infos.put("jobUrl", jobUrl); infos.put("label", "SPARK JOB"); infos.put("tooltip", "View in Spark web UI"); + infos.put("noteId", getNoteId(jobGroupId)); + infos.put("paraId", getParagraphId(jobGroupId)); + LOGGER.debug("Send spark job url: " + infos); if (eventClient != null) { eventClient.onParaInfosReceived(noteId, paragraphId, infos); } } + public static String getNoteId(String jobGroupId) { + String[] tokens = jobGroupId.split("\\|"); + if (tokens.length != 4) { + throw new RuntimeException("Invalid jobGroupId: " + jobGroupId); + } + return tokens[2]; + } + + public static String getParagraphId(String jobGroupId) { + String[] tokens = jobGroupId.split("\\|"); + if (tokens.length != 4) { + throw new RuntimeException("Invalid jobGroupId: " + jobGroupId); + } + return tokens[3]; + } + /** * This is temporal patch for support old versions of Yarn which is not adopted YARN-6615 * diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterContext.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterContext.java index cb444645ced..baf7c8410ad 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterContext.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterContext.java @@ -202,6 +202,10 @@ public String getParagraphId() { return paragraphId; } + public void setParagraphId(String paragraphId) { + this.paragraphId = paragraphId; + } + public String getParagraphText() { return paragraphText; } From be3bea61bf4cd1129045c41d3b8bfee5c6a78684 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Tue, 19 Nov 2019 15:53:13 +0000 Subject: [PATCH 477/492] CORE-332 Joda Time Library for Brazil (#137) --- pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pom.xml b/pom.xml index c9ec7b1f5f3..e0bd19c3b8f 100644 --- a/pom.xml +++ b/pom.xml @@ -324,6 +324,13 @@ test + + + joda-time + joda-time + 2.10.3 + + org.apache.zookeeper zookeeper From 3334537779c8233be2bdb3b506d1619730efb9f7 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Tue, 19 Nov 2019 18:04:44 +0200 Subject: [PATCH 478/492] DSR-94 Remove old helium-repo.json --- conf/helium-repo.json | 590 ------------------------------------------ 1 file changed, 590 deletions(-) diff --git a/conf/helium-repo.json b/conf/helium-repo.json index 6a65f61feb8..e69de29bb2d 100644 --- a/conf/helium-repo.json +++ b/conf/helium-repo.json @@ -1,590 +0,0 @@ -[{ -"ultimate-area-chart": { - "latest": { - "type": "VISUALIZATION", - "name": "ultimate-area-chart", - "version": "0.0.1", - "published": "2017-04-12T09:58:18.693Z", - "artifact": "ultimate-area-chart@0.0.1", - "author": "ZEPL", - "description": "The Ultimate Area Chart for Apache Zeppelin using amcharts", - "license": "SEE LICENSE IN ", - "icon": "" - } - }, -"ultimate-column-chart-negative-values": { - "0.0.2": { - "type": "VISUALIZATION", - "name": "ultimate-column-chart-negative-values", - "version": "0.0.2", - "published": "2017-06-27T19:04:41.438Z", - "artifact": "ultimate-column-chart-negative-values@0.0.2", - "author": "ZEPL", - "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - }, - "latest": { - "type": "VISUALIZATION", - "name": "ultimate-column-chart-negative-values", - "version": "0.0.3", - "published": "2017-07-06T18:58:27.137Z", - "artifact": "ultimate-column-chart-negative-values@0.0.3", - "author": "ZEPL", - "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - } - }, -"ultimate-column-chart": { - "0.0.1": { - "type": "VISUALIZATION", - "name": "ultimate-column-chart", - "version": "0.0.1", - "published": "2017-04-12T10:00:25.424Z", - "artifact": "ultimate-column-chart@0.0.1", - "author": "ZEPL", - "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - }, - "latest": { - "type": "VISUALIZATION", - "name": "ultimate-column-chart", - "version": "0.0.2", - "published": "2018-01-23T03:11:59.022Z", - "artifact": "ultimate-column-chart@0.0.2", - "author": "ZEPL", - "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - } - }, -"ultimate-dual-column-chart": { - "0.0.1": { - "type": "VISUALIZATION", - "name": "ultimate-dual-column-chart", - "version": "0.0.1", - "published": "2017-06-09T16:15:22.329Z", - "artifact": "ultimate-dual-column-chart@0.0.1", - "author": "ZEPL", - "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - }, - "0.0.2": { - "type": "VISUALIZATION", - "name": "ultimate-dual-column-chart", - "version": "0.0.2", - "published": "2017-06-09T16:52:33.969Z", - "artifact": "ultimate-dual-column-chart@0.0.2", - "author": "ZEPL", - "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - }, - "0.0.3": { - "type": "VISUALIZATION", - "name": "ultimate-dual-column-chart", - "version": "0.0.3", - "published": "2017-06-16T14:17:57.853Z", - "artifact": "ultimate-dual-column-chart@0.0.3", - "author": "ZEPL", - "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - }, - "0.0.4": { - "type": "VISUALIZATION", - "name": "ultimate-dual-column-chart", - "version": "0.0.4", - "published": "2017-06-27T19:04:04.764Z", - "artifact": "ultimate-dual-column-chart@0.0.4", - "author": "ZEPL", - "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - }, - "latest": { - "type": "VISUALIZATION", - "name": "ultimate-dual-column-chart", - "version": "0.0.5", - "published": "2017-07-06T18:57:35.252Z", - "artifact": "ultimate-dual-column-chart@0.0.5", - "author": "ZEPL", - "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - } - }, -"ultimate-line-chart": { - "latest": { - "type": "VISUALIZATION", - "name": "ultimate-line-chart", - "version": "0.0.1", - "published": "2017-04-12T09:57:45.479Z", - "artifact": "ultimate-line-chart@0.0.1", - "author": "ZEPL", - "description": "The Ultimate Line Chart for Apache Zeppelin using amcharts", - "license": "SEE LICENSE IN ", - "icon": "" - } - }, -"ultimate-pie-chart": { - "0.0.1": { - "type": "VISUALIZATION", - "name": "ultimate-pie-chart", - "version": "0.0.1", - "published": "2017-04-12T09:59:02.580Z", - "artifact": "ultimate-pie-chart@0.0.1", - "author": "ZEPL", - "description": "The Ultimate Pie Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - }, - "latest": { - "type": "VISUALIZATION", - "name": "ultimate-pie-chart", - "version": "0.0.2", - "published": "2018-01-23T02:47:47.769Z", - "artifact": "ultimate-pie-chart@0.0.2", - "author": "ZEPL", - "description": "The Ultimate Pie Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - } - }, -"ultimate-scatter-chart": { - "0.0.1": { - "type": "VISUALIZATION", - "name": "ultimate-scatter-chart", - "version": "0.0.1", - "published": "2017-04-12T10:01:36.918Z", - "artifact": "ultimate-scatter-chart@0.0.1", - "author": "ZEPL", - "description": "The Ultimate Scatter Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - }, - "latest": { - "type": "VISUALIZATION", - "name": "ultimate-scatter-chart", - "version": "0.0.2", - "published": "2018-01-23T03:43:29.010Z", - "artifact": "ultimate-scatter-chart@0.0.2", - "author": "ZEPL", - "description": "The Ultimate Scatter Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - } - }, -"volume-leaflet": { - "1.0.1": { - "type": "VISUALIZATION", - "name": "volume-leaflet", - "version": "1.0.1", - "published": "2017-05-02T18:50:05.500Z", - "artifact": "volume-leaflet@1.0.1", - "author": "Tom Grant", - "description": "Geospatial visualization using the Leaflet map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.2": { - "type": "VISUALIZATION", - "name": "volume-leaflet", - "version": "1.0.2", - "published": "2017-11-03T13:54:18.512Z", - "artifact": "volume-leaflet@1.0.2", - "author": "Tom Grant", - "description": "Geospatial visualization using the Leaflet map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "latest": { - "type": "VISUALIZATION", - "name": "volume-leaflet", - "version": "1.0.3", - "published": "2017-12-11T20:16:56.719Z", - "artifact": "volume-leaflet@1.0.3", - "author": "Tom Grant", - "description": "Geospatial visualization using the Leaflet map library.", - "license": "BSD-2-Clause", - "icon": "" - } - }, -"zeppelin-bubblechart": { - "0.0.1": { - "type": "VISUALIZATION", - "name": "zeppelin-bubblechart", - "version": "0.0.1", - "published": "2017-01-08T09:56:42.707Z", - "artifact": "zeppelin-bubblechart@0.0.1", - "author": "leemoonsoo", - "description": "Animated bubble chart", - "license": "BSD-2-Clause", - "icon": "" - }, - "0.0.2": { - "type": "VISUALIZATION", - "name": "zeppelin-bubblechart", - "version": "0.0.2", - "published": "2017-01-08T09:58:44.775Z", - "artifact": "zeppelin-bubblechart@0.0.2", - "author": "leemoonsoo", - "description": "Animated bubble chart", - "license": "BSD-2-Clause", - "icon": "" - }, - "0.0.3": { - "type": "VISUALIZATION", - "name": "zeppelin-bubblechart", - "version": "0.0.3", - "published": "2017-01-08T10:41:15.275Z", - "artifact": "zeppelin-bubblechart@0.0.3", - "author": "leemoonsoo", - "description": "Animated bubble chart", - "license": "BSD-2-Clause", - "icon": "" - }, - "latest": { - "type": "VISUALIZATION", - "name": "zeppelin-bubblechart", - "version": "0.0.4", - "published": "2017-01-23T20:42:34.373Z", - "artifact": "zeppelin-bubblechart@0.0.4", - "author": "leemoonsoo", - "description": "Animated bubble chart", - "license": "BSD-2-Clause", - "icon": "" - } - }, -"zeppelin-csv-spell": { - "latest": { - "type": "SPELL", - "name": "zeppelin-csv-spell", - "version": "0.0.1", - "published": "2017-02-28T04:57:05.463Z", - "artifact": "zeppelin-csv-spell@0.0.1", - "author": "1ambda", - "description": "Parse CSV to table for Apache Zeppelin", - "license": "MIT", - "icon": "", - "spell": {"magic": "%csv", "usage": "%csv "} - } - }, -"zeppelin-highcharts-bubble": { - "latest": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-bubble", - "version": "0.0.2", - "published": "2017-02-14T12:30:44.199Z", - "artifact": "zeppelin-highcharts-bubble@0.0.2", - "author": "1ambda", - "description": "Bubble Chart for Apache Zeppelin using highcharts.js", - "license": "SEE LICENSE IN ", - "icon": "" - } - }, -"zeppelin-highcharts-heatmap": { - "0.0.4": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-heatmap", - "version": "0.0.4", - "published": "2017-02-11T07:47:56.338Z", - "artifact": "zeppelin-highcharts-heatmap@0.0.4", - "author": "1ambda", - "description": "Heatmap Charts for Apache Zeppelin using highcharts.js", - "license": "SEE LICENSE IN ", - "icon": "" - }, - "latest": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-heatmap", - "version": "0.0.5", - "published": "2017-02-14T12:46:02.732Z", - "artifact": "zeppelin-highcharts-heatmap@0.0.5", - "author": "1ambda", - "description": "Heatmap Charts for Apache Zeppelin using highcharts.js", - "license": "SEE LICENSE IN ", - "icon": "" - } - }, -"zeppelin-highcharts-scatterplot": { - "latest": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-scatterplot", - "version": "0.0.2", - "published": "2017-02-14T12:17:22.411Z", - "artifact": "zeppelin-highcharts-scatterplot@0.0.2", - "author": "1ambda", - "description": "Scatter plot for Apache Zeppelin using highcharts.js", - "license": "SEE LICENSE IN ", - "icon": "" - } - }, -"zeppelin-highmaps": { - "latest": { - "type": "VISUALIZATION", - "name": "zeppelin-highmaps", - "version": "1.0.0", - "published": "2018-01-30T15:46:40.265Z", - "artifact": "zeppelin-highmaps@1.0.0", - "author": "odnoklassniki", - "description": "Zeppelin plugin to visualize data using Highmaps", - "license": "Apache-2.0", - "icon": "" - } - }, -"zeppelin-json-spell": { - "latest": { - "type": "SPELL", - "name": "zeppelin-json-spell", - "version": "0.0.3", - "published": "2017-02-28T04:49:27.897Z", - "artifact": "zeppelin-json-spell@0.0.3", - "author": "1ambda", - "description": "Use JSON editor in paragraphs", - "license": "Apache-2.0", - "icon": "", - "spell": {"magic": "%json", "usage": "%json "} - } - }, -"zeppelin-leaflet": { - "1.0.2": { - "type": "VISUALIZATION", - "name": "zeppelin-leaflet", - "version": "1.0.2", - "published": "2017-10-31T09:31:51.660Z", - "artifact": "zeppelin-leaflet@1.0.2", - "author": "Mitchell Yuwono", - "description": "Geospatial visualization using the Leaflet map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.3": { - "type": "VISUALIZATION", - "name": "zeppelin-leaflet", - "version": "1.0.3", - "published": "2017-11-01T12:47:14.830Z", - "artifact": "zeppelin-leaflet@1.0.3", - "author": "Mitchell Yuwono", - "description": "Geospatial visualization using the Leaflet map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "latest": { - "type": "VISUALIZATION", - "name": "zeppelin-leaflet", - "version": "1.0.4", - "published": "2017-11-05T08:24:52.404Z", - "artifact": "zeppelin-leaflet@1.0.4", - "author": "Mitchell Yuwono", - "description": "Geospatial visualization using the Leaflet map library.", - "license": "BSD-2-Clause", - "icon": "" - } - }, -"zeppelin-markdown-spell": { - "latest": { - "type": "SPELL", - "name": "zeppelin-markdown-spell", - "version": "0.0.3", - "published": "2017-03-07T19:17:35.975Z", - "artifact": "zeppelin-markdown-spell@0.0.3", - "author": "1ambda", - "description": "Parse markdown using https://github.com/evilstreak/markdown-js", - "license": "MIT", - "icon": "", - "spell": {"magic": "%markdown", "usage": "%markdown "} - } - }, -"zeppelin-mathjax-spell": { - "latest": { - "type": "SPELL", - "name": "zeppelin-mathjax-spell", - "version": "0.0.1", - "published": "2017-03-08T10:04:18.787Z", - "artifact": "zeppelin-mathjax-spell@0.0.1", - "author": "1ambda", - "description": "MathJax Spell For Apache Zeppelin", - "license": "Apache-2.0", - "icon": "", - "spell": {"magic": "%mathjax", "usage": "%mathjax "} - } - }, -"zeppelin-number": { - "latest": { - "type": "VISUALIZATION", - "name": "zeppelin-number", - "version": "1.0.0", - "published": "2018-07-22T18:55:33.637Z", - "artifact": "zeppelin-number@1.0.0", - "author": "Saravanan Elumalai", - "description": "Zeppelin plugin to visualize number", - "license": "Apache-2.0", - "icon": "" - } - }, -"zeppelin-sigma-spell": { - "0.0.1": { - "type": "SPELL", - "name": "zeppelin-sigma-spell", - "version": "0.0.1", - "published": "2017-03-08T10:09:23.660Z", - "artifact": "zeppelin-sigma-spell@0.0.1", - "author": "datalayer", - "description": "Sigma.js Network Visualization", - "license": "Apache-2.0", - "icon": "" - }, - "latest": { - "type": "SPELL", - "name": "zeppelin-sigma-spell", - "version": "0.0.2", - "published": "2017-03-10T09:31:56.802Z", - "artifact": "zeppelin-sigma-spell@0.0.2", - "author": "datalayer", - "description": "Sigma.js Network Visualization", - "license": "Apache-2.0", - "icon": "" - } - }, -"zeppelin-toc-spell": { - "0.0.1": { - "type": "SPELL", - "name": "zeppelin-toc-spell", - "version": "0.0.1", - "published": "2017-10-14T01:09:22.968Z", - "artifact": "zeppelin-toc-spell@0.0.1", - "author": "Ryan Munro", - "description": "Table of Contents for Zeppelin Notebooks", - "license": "ISC", - "icon": "", - "spell": {"magic": "%toc", "usage": "%toc"} - }, - "latest": { - "type": "SPELL", - "name": "zeppelin-toc-spell", - "version": "0.0.2", - "published": "2017-10-14T01:22:54.639Z", - "artifact": "zeppelin-toc-spell@0.0.2", - "author": "Ryan Munro", - "description": "Table of Contents for Zeppelin Notebooks", - "license": "ISC", - "icon": "", - "spell": {"magic": "%toc", "usage": "%toc"} - } - }, -"zeppelin-translator-spell": { - "latest": { - "type": "SPELL", - "name": "zeppelin-translator-spell", - "version": "0.0.1", - "published": "2017-03-05T07:52:58.069Z", - "artifact": "zeppelin-translator-spell@0.0.1", - "author": "1ambda", - "description": "Translate text using Google Translator API", - "license": "Apache-2.0", - "icon": "", - "config": { - "access-token": { - "type": "string", - "description": "access token for Google Translation API", - "defaultValue": "EXAMPLE-TOKEN" - } - }, - "spell": { - "magic": "%translator", - "usage": "%translator source= target= " - } - } - }, -"zeppelin-echo-spell": { - "1.0.4": { - "type": "SPELL", - "name": "zeppelin-echo-spell", - "version": "1.0.4", - "published": "2017-02-01T02:38:02.497Z", - "artifact": "zeppelin-echo-spell@1.0.4", - "author": "1ambda", - "description": "Zeppelin Echo Spell (example)", - "license": "Apache-2.0", - "icon": "", - "config": { - "repeat": { - "type": "number", - "description": "How many times to repeat", - "defaultValue": 1 - } - }, - "spell": {"magic": "%echo", "usage": "%echo "} - }, - "1.0.5": { - "type": "SPELL", - "name": "zeppelin-echo-spell", - "version": "1.0.5", - "published": "2017-03-05T03:37:00.185Z", - "artifact": "zeppelin-echo-spell@1.0.5", - "author": "1ambda", - "description": "Zeppelin Echo Spell (example)", - "license": "Apache-2.0", - "icon": "", - "config": { - "repeat": { - "type": "number", - "description": "How many times to repeat", - "defaultValue": 1 - } - }, - "spell": {"magic": "%echo", "usage": "%echo "} - }, - "latest": { - "type": "SPELL", - "name": "zeppelin-echo-spell", - "version": "1.0.7", - "published": "2017-03-30T20:33:47.324Z", - "artifact": "zeppelin-echo-spell@1.0.7", - "author": "1ambda", - "description": "Zeppelin Echo Spell (example)", - "license": "Apache-2.0", - "icon": "", - "config": { - "repeat": { - "type": "number", - "description": "How many times to repeat", - "defaultValue": 1 - }, - "delay": { - "type": "number", - "description": "Time to wait", - "defaultValue": 1000 - } - }, - "spell": {"magic": "%echo", "usage": "%echo "} - } - }, -"ultimate-heatmap-chart": { - "0.0.1": { - "type": "VISUALIZATION", - "name": "ultimate-heatmap-chart", - "version": "0.0.1", - "published": "2017-04-12T10:05:54.251Z", - "artifact": "ultimate-heatmap-chart@0.0.1", - "author": "ZEPL", - "description": "The Ultimate Heatmap Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - }, - "latest": { - "type": "VISUALIZATION", - "name": "ultimate-heatmap-chart", - "version": "0.0.2", - "published": "2018-01-23T03:46:12.597Z", - "artifact": "ultimate-heatmap-chart@0.0.2", - "author": "ZEPL", - "description": "The Ultimate Heatmap Chart for Apache Zeppelin using highcharts", - "license": "SEE LICENSE IN ", - "icon": "" - } - }}] From 4a3cffa0b5747f57d3818cd35d70872df4ff0b65 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Tue, 19 Nov 2019 18:15:10 +0200 Subject: [PATCH 479/492] DSR-94 Get recent version of helium.json --- conf/helium-repo.json | 3052 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3052 insertions(+) diff --git a/conf/helium-repo.json b/conf/helium-repo.json index e69de29bb2d..ff55f653d37 100644 --- a/conf/helium-repo.json +++ b/conf/helium-repo.json @@ -0,0 +1,3052 @@ +[{"sap": { + "latest": { + "type": "INTERPRETER", + "name": "sap", + "version": "0.8.2", + "published": "2019-09-26T04:22:33+00:00", + "artifact": "sap@0.8.2", + "description": "Zeppelin SAP support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "sap" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "sap", + "version": "0.8.1", + "published": "2019-01-17T03:11:29+00:00", + "artifact": "sap@0.8.1", + "description": "Zeppelin SAP support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "sap" + } + }, +"snappydata-zeppelin": { + "latest": { + "type": "INTERPRETER", + "name": "snappydata-zeppelin", + "version": "0.7.3.6", + "published": "2019-09-03T07:49:48+00:00", + "artifact": "snappydata-zeppelin@0.7.3.6", + "description": "SnappyData distributed data store and execution engine", + "license": "Apache-2.0", + "icon": "", + "groupId": "io.snappydata", + "artifactId": "snappydata-zeppelin" + }, + "0.7.3.5": { + "type": "INTERPRETER", + "name": "snappydata-zeppelin", + "version": "0.7.3.5", + "published": "2019-05-24T11:28:01+00:00", + "artifact": "snappydata-zeppelin@0.7.3.5", + "description": "SnappyData distributed data store and execution engine", + "license": "Apache-2.0", + "icon": "", + "groupId": "io.snappydata", + "artifactId": "snappydata-zeppelin" + } + }, +"sogou-map-geo": { + "1.0.0": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.0", + "published": "2018-07-10T08:15:18.760Z", + "artifact": "sogou-map-geo@1.0.0", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.1": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.1", + "published": "2018-07-10T08:56:45.109Z", + "artifact": "sogou-map-geo@1.0.1", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.2": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.2", + "published": "2018-07-10T10:25:47.391Z", + "artifact": "sogou-map-geo@1.0.2", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.3": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.3", + "published": "2018-07-10T12:08:27.865Z", + "artifact": "sogou-map-geo@1.0.3", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.4": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.4", + "published": "2018-07-10T12:16:29.034Z", + "artifact": "sogou-map-geo@1.0.4", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.5": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.5", + "published": "2018-07-10T13:14:21.726Z", + "artifact": "sogou-map-geo@1.0.5", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.6": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.6", + "published": "2018-07-11T01:51:24.623Z", + "artifact": "sogou-map-geo@1.0.6", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.7": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.7", + "published": "2018-07-11T03:30:42.712Z", + "artifact": "sogou-map-geo@1.0.7", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.8": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.8", + "published": "2018-07-11T05:37:58.867Z", + "artifact": "sogou-map-geo@1.0.8", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.9": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.9", + "published": "2018-07-11T06:25:05.749Z", + "artifact": "sogou-map-geo@1.0.9", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.10": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.10", + "published": "2018-07-11T06:56:08.847Z", + "artifact": "sogou-map-geo@1.0.10", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.11": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.11", + "published": "2018-07-11T07:20:35.348Z", + "artifact": "sogou-map-geo@1.0.11", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.13": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.13", + "published": "2018-07-11T08:16:30.781Z", + "artifact": "sogou-map-geo@1.0.13", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.14": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.14", + "published": "2018-07-11T09:07:34.729Z", + "artifact": "sogou-map-geo@1.0.14", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.15": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.15", + "published": "2018-07-11T09:49:49.293Z", + "artifact": "sogou-map-geo@1.0.15", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.16": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.16", + "published": "2018-07-11T11:15:05.062Z", + "artifact": "sogou-map-geo@1.0.16", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.17": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.17", + "published": "2018-07-11T11:39:10.139Z", + "artifact": "sogou-map-geo@1.0.17", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.18": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.18", + "published": "2018-07-11T12:04:48.987Z", + "artifact": "sogou-map-geo@1.0.18", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.19": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.19", + "published": "2018-07-11T12:45:08.942Z", + "artifact": "sogou-map-geo@1.0.19", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.20": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.20", + "published": "2018-07-11T12:58:55.105Z", + "artifact": "sogou-map-geo@1.0.20", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.21": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.21", + "published": "2018-07-12T02:38:26.001Z", + "artifact": "sogou-map-geo@1.0.21", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.22": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.22", + "published": "2018-07-12T02:51:35.057Z", + "artifact": "sogou-map-geo@1.0.22", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.23": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.23", + "published": "2018-07-12T03:18:07.254Z", + "artifact": "sogou-map-geo@1.0.23", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.24": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.24", + "published": "2018-07-12T03:25:09.263Z", + "artifact": "sogou-map-geo@1.0.24", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.25": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.25", + "published": "2018-07-12T06:34:36.735Z", + "artifact": "sogou-map-geo@1.0.25", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.26": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.26", + "published": "2018-07-12T07:24:08.297Z", + "artifact": "sogou-map-geo@1.0.26", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.27": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.27", + "published": "2018-07-12T08:48:13.695Z", + "artifact": "sogou-map-geo@1.0.27", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.28": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.28", + "published": "2018-07-12T08:59:59.555Z", + "artifact": "sogou-map-geo@1.0.28", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.29": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.29", + "published": "2018-07-12T11:52:31.785Z", + "artifact": "sogou-map-geo@1.0.29", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.30": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.30", + "published": "2018-07-12T12:28:33.936Z", + "artifact": "sogou-map-geo@1.0.30", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.31": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.31", + "published": "2018-07-12T12:51:02.920Z", + "artifact": "sogou-map-geo@1.0.31", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "sogou-map-geo", + "version": "1.0.32", + "published": "2018-07-12T13:26:44.816Z", + "artifact": "sogou-map-geo@1.0.32", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + } + }, +"sogou-map-vis": { + "1.0.0": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.0", + "published": "2018-07-06T08:22:24.716Z", + "artifact": "sogou-map-vis@1.0.0", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.1": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.1", + "published": "2018-07-07T08:04:59.051Z", + "artifact": "sogou-map-vis@1.0.1", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.2": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.2", + "published": "2018-07-07T14:11:53.250Z", + "artifact": "sogou-map-vis@1.0.2", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.3": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.3", + "published": "2018-07-07T14:24:02.923Z", + "artifact": "sogou-map-vis@1.0.3", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.4": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.4", + "published": "2018-07-07T14:41:28.937Z", + "artifact": "sogou-map-vis@1.0.4", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.5": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.5", + "published": "2018-07-08T08:10:46.242Z", + "artifact": "sogou-map-vis@1.0.5", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.6": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.6", + "published": "2018-07-13T03:06:41.159Z", + "artifact": "sogou-map-vis@1.0.6", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.7": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.7", + "published": "2018-07-13T03:40:09.231Z", + "artifact": "sogou-map-vis@1.0.7", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.8": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.8", + "published": "2018-07-13T03:54:40.428Z", + "artifact": "sogou-map-vis@1.0.8", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.9": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.9", + "published": "2018-07-13T06:26:18.292Z", + "artifact": "sogou-map-vis@1.0.9", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.10": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.10", + "published": "2018-07-13T09:36:16.680Z", + "artifact": "sogou-map-vis@1.0.10", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.11": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.11", + "published": "2018-07-13T09:57:58.047Z", + "artifact": "sogou-map-vis@1.0.11", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.12": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.12", + "published": "2018-07-13T10:06:51.172Z", + "artifact": "sogou-map-vis@1.0.12", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.13": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.13", + "published": "2018-07-16T07:16:41.972Z", + "artifact": "sogou-map-vis@1.0.13", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.14": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.14", + "published": "2018-07-16T08:28:52.733Z", + "artifact": "sogou-map-vis@1.0.14", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.15": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.15", + "published": "2018-07-16T08:59:03.510Z", + "artifact": "sogou-map-vis@1.0.15", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.16": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.16", + "published": "2018-07-16T12:04:18.774Z", + "artifact": "sogou-map-vis@1.0.16", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.17": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.17", + "published": "2018-07-16T15:28:55.810Z", + "artifact": "sogou-map-vis@1.0.17", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.18": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.18", + "published": "2018-07-16T15:49:44.345Z", + "artifact": "sogou-map-vis@1.0.18", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.19": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.19", + "published": "2018-07-17T01:49:25.665Z", + "artifact": "sogou-map-vis@1.0.19", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.20": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.20", + "published": "2018-07-17T02:25:48.828Z", + "artifact": "sogou-map-vis@1.0.20", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.21": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.21", + "published": "2018-07-17T02:37:33.739Z", + "artifact": "sogou-map-vis@1.0.21", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.31": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.31", + "published": "2018-07-17T08:26:16.990Z", + "artifact": "sogou-map-vis@1.0.31", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.32": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.32", + "published": "2018-07-17T08:34:21.665Z", + "artifact": "sogou-map-vis@1.0.32", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.33": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.33", + "published": "2018-07-17T09:30:44.205Z", + "artifact": "sogou-map-vis@1.0.33", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.34": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.34", + "published": "2018-07-17T09:57:48.095Z", + "artifact": "sogou-map-vis@1.0.34", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.35": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.35", + "published": "2018-07-17T11:33:46.681Z", + "artifact": "sogou-map-vis@1.0.35", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.36": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.36", + "published": "2018-07-19T08:14:28.614Z", + "artifact": "sogou-map-vis@1.0.36", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.37": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.37", + "published": "2018-07-19T08:51:58.942Z", + "artifact": "sogou-map-vis@1.0.37", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.38": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.38", + "published": "2018-07-19T09:09:58.592Z", + "artifact": "sogou-map-vis@1.0.38", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.39": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.39", + "published": "2018-07-19T09:33:22.779Z", + "artifact": "sogou-map-vis@1.0.39", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.40": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.40", + "published": "2018-07-19T09:42:07.830Z", + "artifact": "sogou-map-vis@1.0.40", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.41": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.41", + "published": "2018-07-19T10:59:43.954Z", + "artifact": "sogou-map-vis@1.0.41", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.42": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.42", + "published": "2018-07-19T11:16:02.860Z", + "artifact": "sogou-map-vis@1.0.42", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.43": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.43", + "published": "2018-07-19T11:36:49.856Z", + "artifact": "sogou-map-vis@1.0.43", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.44": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.44", + "published": "2018-07-19T11:54:01.078Z", + "artifact": "sogou-map-vis@1.0.44", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.45": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.45", + "published": "2018-07-19T12:04:34.388Z", + "artifact": "sogou-map-vis@1.0.45", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.46": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.46", + "published": "2018-07-19T12:12:52.155Z", + "artifact": "sogou-map-vis@1.0.46", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.47": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.47", + "published": "2018-07-19T12:17:54.942Z", + "artifact": "sogou-map-vis@1.0.47", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.48": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.48", + "published": "2018-07-20T07:49:33.102Z", + "artifact": "sogou-map-vis@1.0.48", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.49": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.49", + "published": "2018-07-20T08:09:33.198Z", + "artifact": "sogou-map-vis@1.0.49", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.50": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.50", + "published": "2018-07-20T08:29:00.316Z", + "artifact": "sogou-map-vis@1.0.50", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.51": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.51", + "published": "2018-07-20T08:42:49.097Z", + "artifact": "sogou-map-vis@1.0.51", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.52": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.52", + "published": "2018-07-20T08:59:17.601Z", + "artifact": "sogou-map-vis@1.0.52", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.53": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.53", + "published": "2018-07-20T09:01:56.716Z", + "artifact": "sogou-map-vis@1.0.53", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.54": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.54", + "published": "2018-07-23T02:34:43.717Z", + "artifact": "sogou-map-vis@1.0.54", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.55": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.55", + "published": "2018-07-24T08:21:08.920Z", + "artifact": "sogou-map-vis@1.0.55", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.56": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.56", + "published": "2018-07-24T08:28:35.565Z", + "artifact": "sogou-map-vis@1.0.56", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.57": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.57", + "published": "2018-07-24T08:55:16.757Z", + "artifact": "sogou-map-vis@1.0.57", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.58": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.58", + "published": "2018-07-24T09:07:46.054Z", + "artifact": "sogou-map-vis@1.0.58", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.59": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.59", + "published": "2018-07-24T09:15:23.585Z", + "artifact": "sogou-map-vis@1.0.59", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.60": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.60", + "published": "2018-07-24T09:48:12.952Z", + "artifact": "sogou-map-vis@1.0.60", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.61": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.61", + "published": "2018-07-24T11:34:27.864Z", + "artifact": "sogou-map-vis@1.0.61", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.62": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.62", + "published": "2018-07-24T12:40:08.342Z", + "artifact": "sogou-map-vis@1.0.62", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.63": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.63", + "published": "2018-07-24T12:57:00.551Z", + "artifact": "sogou-map-vis@1.0.63", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.64": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.64", + "published": "2018-07-25T09:17:55.533Z", + "artifact": "sogou-map-vis@1.0.64", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.65": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.65", + "published": "2018-07-25T09:35:23.470Z", + "artifact": "sogou-map-vis@1.0.65", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.66": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.66", + "published": "2018-07-25T09:44:50.206Z", + "artifact": "sogou-map-vis@1.0.66", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "sogou-map-vis", + "version": "1.0.67", + "published": "2018-07-25T10:00:29.630Z", + "artifact": "sogou-map-vis@1.0.67", + "author": "Robin Liew", + "description": "Geospatial visualization using the sogou map library.", + "license": "BSD-2-Clause", + "icon": "" + } + }, +"spark-scala-2.10": { + "latest": { + "type": "INTERPRETER", + "name": "spark-scala-2.10", + "version": "0.8.2", + "published": "2019-09-26T04:25:03+00:00", + "artifact": "spark-scala-2.10@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "spark-scala-2.10" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "spark-scala-2.10", + "version": "0.8.1", + "published": "2019-01-17T03:12:05+00:00", + "artifact": "spark-scala-2.10@0.8.1", + "description": "Zeppelin Spark Support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "spark-scala-2.10" + } + }, +"spark-scala-2.11": { + "latest": { + "type": "INTERPRETER", + "name": "spark-scala-2.11", + "version": "0.8.2", + "published": "2019-09-26T04:26:09+00:00", + "artifact": "spark-scala-2.11@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "spark-scala-2.11" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "spark-scala-2.11", + "version": "0.8.1", + "published": "2019-01-17T03:12:18+00:00", + "artifact": "spark-scala-2.11@0.8.1", + "description": "Zeppelin Spark Support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "spark-scala-2.11" + } + }, +"spark-scala-parent": { + "latest": { + "type": "INTERPRETER", + "name": "spark-scala-parent", + "version": "0.8.2", + "published": "2019-09-26T04:24:19+00:00", + "artifact": "spark-scala-parent@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "spark-scala-parent" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "spark-scala-parent", + "version": "0.8.1", + "published": "2019-01-17T03:12:23+00:00", + "artifact": "spark-scala-parent@0.8.1", + "description": "Zeppelin Spark Support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "spark-scala-parent" + } + }, +"spark-shims": { + "latest": { + "type": "INTERPRETER", + "name": "spark-shims", + "version": "0.8.2", + "published": "2019-09-26T04:30:27+00:00", + "artifact": "spark-shims@0.8.2", + "description": "Zeppelin Spark Support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "spark-shims" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "spark-shims", + "version": "0.8.1", + "published": "2019-01-17T03:12:35+00:00", + "artifact": "spark-shims@0.8.1", + "description": "Zeppelin Spark Support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "spark-shims" + } + }, +"spark2-shims": { + "latest": { + "type": "INTERPRETER", + "name": "spark2-shims", + "version": "0.8.2", + "published": "2019-09-26T04:21:31+00:00", + "artifact": "spark2-shims@0.8.2", + "description": "Zeppelin Spark Support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "spark2-shims" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "spark2-shims", + "version": "0.8.1", + "published": "2019-01-17T03:12:58+00:00", + "artifact": "spark2-shims@0.8.1", + "description": "Zeppelin Spark Support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "spark2-shims" + } + }, +"spark1-shims": { + "latest": { + "type": "INTERPRETER", + "name": "spark1-shims", + "version": "0.8.2", + "published": "2019-09-26T04:24:06+00:00", + "artifact": "spark1-shims@0.8.2", + "description": "Zeppelin Spark Support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "spark1-shims" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "spark1-shims", + "version": "0.8.1", + "published": "2019-01-17T03:12:47+00:00", + "artifact": "spark1-shims@0.8.1", + "description": "Zeppelin Spark Support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "spark1-shims" + } + }, +"ultimate-area-chart": { + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-area-chart", + "version": "0.0.1", + "published": "2017-04-12T09:58:18.693Z", + "artifact": "ultimate-area-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Area Chart for Apache Zeppelin using amcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-column-chart-negative-values": { + "0.0.2": { + "type": "VISUALIZATION", + "name": "ultimate-column-chart-negative-values", + "version": "0.0.2", + "published": "2017-06-27T19:04:41.438Z", + "artifact": "ultimate-column-chart-negative-values@0.0.2", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-column-chart-negative-values", + "version": "0.0.3", + "published": "2017-07-06T18:58:27.137Z", + "artifact": "ultimate-column-chart-negative-values@0.0.3", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-column-chart": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "ultimate-column-chart", + "version": "0.0.1", + "published": "2017-04-12T10:00:25.424Z", + "artifact": "ultimate-column-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-column-chart", + "version": "0.0.2", + "published": "2018-01-23T03:11:59.022Z", + "artifact": "ultimate-column-chart@0.0.2", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-dual-column-chart": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "ultimate-dual-column-chart", + "version": "0.0.1", + "published": "2017-06-09T16:15:22.329Z", + "artifact": "ultimate-dual-column-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "0.0.2": { + "type": "VISUALIZATION", + "name": "ultimate-dual-column-chart", + "version": "0.0.2", + "published": "2017-06-09T16:52:33.969Z", + "artifact": "ultimate-dual-column-chart@0.0.2", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "0.0.3": { + "type": "VISUALIZATION", + "name": "ultimate-dual-column-chart", + "version": "0.0.3", + "published": "2017-06-16T14:17:57.853Z", + "artifact": "ultimate-dual-column-chart@0.0.3", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "0.0.4": { + "type": "VISUALIZATION", + "name": "ultimate-dual-column-chart", + "version": "0.0.4", + "published": "2017-06-27T19:04:04.764Z", + "artifact": "ultimate-dual-column-chart@0.0.4", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-dual-column-chart", + "version": "0.0.5", + "published": "2017-07-06T18:57:35.252Z", + "artifact": "ultimate-dual-column-chart@0.0.5", + "author": "ZEPL", + "description": "The Ultimate Column Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-heatmap-chart": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "ultimate-heatmap-chart", + "version": "0.0.1", + "published": "2017-04-12T10:05:54.251Z", + "artifact": "ultimate-heatmap-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Heatmap Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-heatmap-chart", + "version": "0.0.2", + "published": "2018-01-23T03:46:12.597Z", + "artifact": "ultimate-heatmap-chart@0.0.2", + "author": "ZEPL", + "description": "The Ultimate Heatmap Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-line-chart": { + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-line-chart", + "version": "0.0.1", + "published": "2017-04-12T09:57:45.479Z", + "artifact": "ultimate-line-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Line Chart for Apache Zeppelin using amcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-pie-chart": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "ultimate-pie-chart", + "version": "0.0.1", + "published": "2017-04-12T09:59:02.580Z", + "artifact": "ultimate-pie-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Pie Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-pie-chart", + "version": "0.0.2", + "published": "2018-01-23T02:47:47.769Z", + "artifact": "ultimate-pie-chart@0.0.2", + "author": "ZEPL", + "description": "The Ultimate Pie Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-range-chart": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "ultimate-range-chart", + "version": "0.0.1", + "published": "2017-04-12T10:02:44.987Z", + "artifact": "ultimate-range-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Range Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-range-chart", + "version": "0.0.2", + "published": "2018-01-23T03:42:31.049Z", + "artifact": "ultimate-range-chart@0.0.2", + "author": "ZEPL", + "description": "The Ultimate Range Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"ultimate-scatter-chart": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "ultimate-scatter-chart", + "version": "0.0.1", + "published": "2017-04-12T10:01:36.918Z", + "artifact": "ultimate-scatter-chart@0.0.1", + "author": "ZEPL", + "description": "The Ultimate Scatter Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "ultimate-scatter-chart", + "version": "0.0.2", + "published": "2018-01-23T03:43:29.010Z", + "artifact": "ultimate-scatter-chart@0.0.2", + "author": "ZEPL", + "description": "The Ultimate Scatter Chart for Apache Zeppelin using highcharts", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"volume-leaflet": { + "1.0.1": { + "type": "VISUALIZATION", + "name": "volume-leaflet", + "version": "1.0.1", + "published": "2017-05-02T18:50:05.500Z", + "artifact": "volume-leaflet@1.0.1", + "author": "Tom Grant", + "description": "Geospatial visualization using the Leaflet map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.2": { + "type": "VISUALIZATION", + "name": "volume-leaflet", + "version": "1.0.2", + "published": "2017-11-03T13:54:18.512Z", + "artifact": "volume-leaflet@1.0.2", + "author": "Tom Grant", + "description": "Geospatial visualization using the Leaflet map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "volume-leaflet", + "version": "1.0.3", + "published": "2017-12-11T20:16:56.719Z", + "artifact": "volume-leaflet@1.0.3", + "author": "Tom Grant", + "description": "Geospatial visualization using the Leaflet map library.", + "license": "BSD-2-Clause", + "icon": "" + } + }, +"zeppelin-beam": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-beam", + "version": "0.8.2", + "published": "2019-09-26T04:28:02+00:00", + "artifact": "zeppelin-beam@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-beam" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-beam", + "version": "0.8.1", + "published": "2019-01-17T03:13:39+00:00", + "artifact": "zeppelin-beam@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-beam" + } + }, +"zeppelin-angular": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-angular", + "version": "0.8.2", + "published": "2019-09-26T04:26:55+00:00", + "artifact": "zeppelin-angular@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-angular" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-angular", + "version": "0.8.1", + "published": "2019-01-17T03:13:27+00:00", + "artifact": "zeppelin-angular@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-angular" + } + }, +"zeppelin-alluxio": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-alluxio", + "version": "0.8.2", + "published": "2019-09-26T04:28:40+00:00", + "artifact": "zeppelin-alluxio@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-alluxio" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-alluxio", + "version": "0.8.1", + "published": "2019-01-17T03:13:15+00:00", + "artifact": "zeppelin-alluxio@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-alluxio" + } + }, +"zeppelin-bubblechart": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "zeppelin-bubblechart", + "version": "0.0.1", + "published": "2017-01-08T09:56:42.707Z", + "artifact": "zeppelin-bubblechart@0.0.1", + "author": "leemoonsoo", + "description": "Animated bubble chart", + "license": "BSD-2-Clause", + "icon": "" + }, + "0.0.2": { + "type": "VISUALIZATION", + "name": "zeppelin-bubblechart", + "version": "0.0.2", + "published": "2017-01-08T09:58:44.775Z", + "artifact": "zeppelin-bubblechart@0.0.2", + "author": "leemoonsoo", + "description": "Animated bubble chart", + "license": "BSD-2-Clause", + "icon": "" + }, + "0.0.3": { + "type": "VISUALIZATION", + "name": "zeppelin-bubblechart", + "version": "0.0.3", + "published": "2017-01-08T10:41:15.275Z", + "artifact": "zeppelin-bubblechart@0.0.3", + "author": "leemoonsoo", + "description": "Animated bubble chart", + "license": "BSD-2-Clause", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-bubblechart", + "version": "0.0.4", + "published": "2017-01-23T20:42:34.373Z", + "artifact": "zeppelin-bubblechart@0.0.4", + "author": "leemoonsoo", + "description": "Animated bubble chart", + "license": "BSD-2-Clause", + "icon": "" + } + }, +"zeppelin-bigquery": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-bigquery", + "version": "0.8.2", + "published": "2019-09-26T04:21:08+00:00", + "artifact": "zeppelin-bigquery@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-bigquery" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-bigquery", + "version": "0.8.1", + "published": "2019-01-17T03:13:50+00:00", + "artifact": "zeppelin-bigquery@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-bigquery" + } + }, +"zeppelin-cassandra": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-cassandra", + "version": "0.6.0", + "published": "2016-06-27T23:21:51+00:00", + "artifact": "zeppelin-cassandra@0.6.0", + "description": "Zeppelin cassandra support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-cassandra" + } + }, +"zeppelin-aggrid": { + "0.1.0": { + "type": "VISUALIZATION", + "name": "zeppelin-aggrid", + "version": "0.1.0", + "published": "2018-02-19T21:16:27.226Z", + "artifact": "zeppelin-aggrid@0.1.0", + "author": "Eugene Matveyev", + "description": "Data visualization with Ag-Grid for Apache Zeppelin", + "license": "MIT", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-aggrid", + "version": "0.1.1", + "published": "2018-02-19T21:47:14.621Z", + "artifact": "zeppelin-aggrid@0.1.1", + "author": "Eugene Matveyev", + "description": "Data visualization with Ag-Grid for Apache Zeppelin", + "license": "MIT", + "icon": "" + } + }, +"zeppelin-cassandra_2.10": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-cassandra_2.10", + "version": "0.8.2", + "published": "2019-09-26T04:24:04+00:00", + "artifact": "zeppelin-cassandra_2.10@0.8.2", + "description": "Zeppelin cassandra support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-cassandra_2.10" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-cassandra_2.10", + "version": "0.8.1", + "published": "2019-01-17T03:14:04+00:00", + "artifact": "zeppelin-cassandra_2.10@0.8.1", + "description": "Zeppelin cassandra support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-cassandra_2.10" + } + }, +"zeppelin-cassandra_2.11": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-cassandra_2.11", + "version": "0.8.2", + "published": "2019-09-26T04:22:50+00:00", + "artifact": "zeppelin-cassandra_2.11@0.8.2", + "description": "Zeppelin cassandra support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-cassandra_2.11" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-cassandra_2.11", + "version": "0.8.1", + "published": "2019-01-17T03:14:16+00:00", + "artifact": "zeppelin-cassandra_2.11@0.8.1", + "description": "Zeppelin cassandra support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-cassandra_2.11" + } + }, +"zeppelin-csv-spell": { + "latest": { + "type": "SPELL", + "name": "zeppelin-csv-spell", + "version": "0.0.1", + "published": "2017-02-28T04:57:05.463Z", + "artifact": "zeppelin-csv-spell@0.0.1", + "author": "1ambda", + "description": "Parse CSV to table for Apache Zeppelin", + "license": "MIT", + "icon": "", + "spell": {"magic": "%csv", "usage": "%csv "} + } + }, +"zeppelin-d3-spell": { + "latest": { + "type": "SPELL", + "name": "zeppelin-d3-spell", + "version": "0.0.1", + "published": "2017-03-08T10:08:51.022Z", + "artifact": "zeppelin-d3-spell@0.0.1", + "author": "datalayer", + "description": "D3.js Network Visualization for Apache Zeppelin", + "license": "Apache-2.0", + "icon": "" + } + }, +"zeppelin-display_2.11": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-display_2.11", + "version": "0.7.3", + "published": "2017-09-18T16:29:38+00:00", + "artifact": "zeppelin-display_2.11@0.7.3", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-display_2.11" + } + }, +"zeppelin-display_2.10": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-display_2.10", + "version": "0.7.3", + "published": "2017-09-18T16:29:25+00:00", + "artifact": "zeppelin-display_2.10@0.7.3", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-display_2.10" + } + }, +"zeppelin-display": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-display", + "version": "0.8.2", + "published": "2019-09-26T04:23:21+00:00", + "artifact": "zeppelin-display@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-display" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-display", + "version": "0.8.1", + "published": "2019-01-17T03:14:28+00:00", + "artifact": "zeppelin-display@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-display" + } + }, +"zeppelin-echo-spell": { + "1.0.4": { + "type": "SPELL", + "name": "zeppelin-echo-spell", + "version": "1.0.4", + "published": "2017-02-01T02:38:02.497Z", + "artifact": "zeppelin-echo-spell@1.0.4", + "author": "1ambda", + "description": "Zeppelin Echo Spell (example)", + "license": "Apache-2.0", + "icon": "", + "config": { + "repeat": { + "type": "number", + "description": "How many times to repeat", + "defaultValue": 1 + } + }, + "spell": {"magic": "%echo", "usage": "%echo "} + }, + "1.0.5": { + "type": "SPELL", + "name": "zeppelin-echo-spell", + "version": "1.0.5", + "published": "2017-03-05T03:37:00.185Z", + "artifact": "zeppelin-echo-spell@1.0.5", + "author": "1ambda", + "description": "Zeppelin Echo Spell (example)", + "license": "Apache-2.0", + "icon": "", + "config": { + "repeat": { + "type": "number", + "description": "How many times to repeat", + "defaultValue": 1 + } + }, + "spell": {"magic": "%echo", "usage": "%echo "} + }, + "latest": { + "type": "SPELL", + "name": "zeppelin-echo-spell", + "version": "1.0.7", + "published": "2017-03-30T20:33:47.324Z", + "artifact": "zeppelin-echo-spell@1.0.7", + "author": "1ambda", + "description": "Zeppelin Echo Spell (example)", + "license": "Apache-2.0", + "icon": "", + "config": { + "repeat": { + "type": "number", + "description": "How many times to repeat", + "defaultValue": 1 + }, + "delay": { + "type": "number", + "description": "Time to wait", + "defaultValue": 1000 + } + }, + "spell": {"magic": "%echo", "usage": "%echo "} + } + }, +"zeppelin-elasticsearch": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-elasticsearch", + "version": "0.8.2", + "published": "2019-09-26T04:29:02+00:00", + "artifact": "zeppelin-elasticsearch@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-elasticsearch" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-elasticsearch", + "version": "0.8.1", + "published": "2019-01-17T03:14:40+00:00", + "artifact": "zeppelin-elasticsearch@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-elasticsearch" + } + }, +"zeppelin-file": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-file", + "version": "0.8.2", + "published": "2019-09-26T04:24:29+00:00", + "artifact": "zeppelin-file@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-file" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-file", + "version": "0.8.1", + "published": "2019-01-17T03:14:52+00:00", + "artifact": "zeppelin-file@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-file" + } + }, +"zeppelin-flink": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-flink", + "version": "0.6.0", + "published": "2016-06-27T23:22:19+00:00", + "artifact": "zeppelin-flink@0.6.0", + "description": "Zeppelin flink support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-flink" + } + }, +"zeppelin-flink_2.10": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-flink_2.10", + "version": "0.8.2", + "published": "2019-09-26T04:28:44+00:00", + "artifact": "zeppelin-flink_2.10@0.8.2", + "description": "Zeppelin flink support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-flink_2.10" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-flink_2.10", + "version": "0.8.1", + "published": "2019-01-17T03:15:03+00:00", + "artifact": "zeppelin-flink_2.10@0.8.1", + "description": "Zeppelin flink support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-flink_2.10" + } + }, +"zeppelin-flink_2.11": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-flink_2.11", + "version": "0.8.2", + "published": "2019-09-26T04:29:35+00:00", + "artifact": "zeppelin-flink_2.11@0.8.2", + "description": "Zeppelin flink support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-flink_2.11" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-flink_2.11", + "version": "0.8.1", + "published": "2019-01-17T03:15:15+00:00", + "artifact": "zeppelin-flink_2.11@0.8.1", + "description": "Zeppelin flink support", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-flink_2.11" + } + }, +"zeppelin-flowchart-spell": { + "0.0.1": { + "type": "SPELL", + "name": "zeppelin-flowchart-spell", + "version": "0.0.1", + "published": "2017-02-11T02:13:08.734Z", + "artifact": "zeppelin-flowchart-spell@0.0.1", + "author": "1ambda", + "description": "Draw flowchart using http://flowchart.js.org", + "license": "MIT", + "icon": "", + "spell": {"magic": "%flowchart", "usage": "%flowchart "} + }, + "latest": { + "type": "SPELL", + "name": "zeppelin-flowchart-spell", + "version": "0.0.2", + "published": "2017-02-13T11:05:56.651Z", + "artifact": "zeppelin-flowchart-spell@0.0.2", + "author": "1ambda", + "description": "Draw flowchart using http://flowchart.js.org", + "license": "MIT", + "icon": "", + "spell": {"magic": "%flowchart", "usage": "%flowchart "} + } + }, +"zeppelin-groovy": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-groovy", + "version": "0.8.2", + "published": "2019-09-26T04:21:52+00:00", + "artifact": "zeppelin-groovy@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-groovy" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-groovy", + "version": "0.8.1", + "published": "2019-01-17T03:15:26+00:00", + "artifact": "zeppelin-groovy@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-groovy" + } + }, +"zeppelin-hbase": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-hbase", + "version": "0.8.2", + "published": "2019-09-26T04:29:14+00:00", + "artifact": "zeppelin-hbase@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-hbase" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-hbase", + "version": "0.8.1", + "published": "2019-01-17T03:15:38+00:00", + "artifact": "zeppelin-hbase@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-hbase" + } + }, +"zeppelin-highcharts-bubble": { + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-bubble", + "version": "0.0.2", + "published": "2017-02-14T12:30:44.199Z", + "artifact": "zeppelin-highcharts-bubble@0.0.2", + "author": "1ambda", + "description": "Bubble Chart for Apache Zeppelin using highcharts.js", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"zeppelin-highcharts-columnrange": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-columnrange", + "version": "0.0.1", + "published": "2017-02-11T08:09:32.044Z", + "artifact": "zeppelin-highcharts-columnrange@0.0.1", + "author": "1ambda", + "description": "Column Range Chart for Apache Zeppelin using highcharts.js", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-columnrange", + "version": "0.0.4", + "published": "2017-02-11T17:05:07.668Z", + "artifact": "zeppelin-highcharts-columnrange@0.0.4", + "author": "1ambda", + "description": "Column Range Chart for Apache Zeppelin using highcharts.js", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"zeppelin-highcharts-scatterplot": { + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-scatterplot", + "version": "0.0.2", + "published": "2017-02-14T12:17:22.411Z", + "artifact": "zeppelin-highcharts-scatterplot@0.0.2", + "author": "1ambda", + "description": "Scatter plot for Apache Zeppelin using highcharts.js", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"zeppelin-highcharts-spline": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-spline", + "version": "0.0.1", + "published": "2017-01-16T08:53:38.470Z", + "artifact": "zeppelin-highcharts-spline@0.0.1", + "author": "ahyoung", + "description": "Draw spline graph using Highcharts library", + "license": "Apache-2.0", + "icon": "" + }, + "0.0.2": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-spline", + "version": "0.0.2", + "published": "2017-01-16T10:49:56.230Z", + "artifact": "zeppelin-highcharts-spline@0.0.2", + "author": "AhyoungRyu", + "description": "Draw spline graph using Highcharts library", + "license": "Apache-2.0", + "icon": "" + }, + "0.0.3": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-spline", + "version": "0.0.3", + "published": "2017-01-20T05:39:22.177Z", + "artifact": "zeppelin-highcharts-spline@0.0.3", + "author": "AhyoungRyu", + "description": "Draw spline graph using Highcharts library", + "license": "Apache-2.0", + "icon": "" + }, + "0.0.4": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-spline", + "version": "0.0.4", + "published": "2017-01-20T07:13:35.958Z", + "artifact": "zeppelin-highcharts-spline@0.0.4", + "author": "AhyoungRyu", + "description": "Draw spline graph using Highcharts library", + "license": "Apache-2.0", + "icon": "⌇" + }, + "0.0.5": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-spline", + "version": "0.0.5", + "published": "2017-01-20T09:07:45.758Z", + "artifact": "zeppelin-highcharts-spline@0.0.5", + "author": "AhyoungRyu", + "description": "Draw spline graph using Highcharts library", + "license": "Apache-2.0", + "icon": "⌇" + }, + "0.0.6": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-spline", + "version": "0.0.6", + "published": "2017-01-22T13:33:58.177Z", + "artifact": "zeppelin-highcharts-spline@0.0.6", + "author": "AhyoungRyu", + "description": "Draw spline graph using Highcharts library", + "license": "Apache-2.0", + "icon": "⌇" + }, + "0.0.7": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-spline", + "version": "0.0.7", + "published": "2017-01-22T13:38:48.895Z", + "artifact": "zeppelin-highcharts-spline@0.0.7", + "author": "AhyoungRyu", + "description": "Draw spline graph using Highcharts library", + "license": "Apache-2.0", + "icon": "" + }, + "0.0.8": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-spline", + "version": "0.0.8", + "published": "2017-01-23T05:36:57.596Z", + "artifact": "zeppelin-highcharts-spline@0.0.8", + "author": "AhyoungRyu", + "description": "Draw spline graph using Highcharts library", + "license": "Apache-2.0", + "icon": "" + }, + "0.0.9": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-spline", + "version": "0.0.9", + "published": "2017-02-12T05:47:43.594Z", + "artifact": "zeppelin-highcharts-spline@0.0.9", + "author": "AhyoungRyu", + "description": "Draw spline graph using Highcharts library", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "0.1.0": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-spline", + "version": "0.1.0", + "published": "2017-02-12T05:56:54.943Z", + "artifact": "zeppelin-highcharts-spline@0.1.0", + "author": "AhyoungRyu", + "description": "Draw spline graph using Highcharts library", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-spline", + "version": "0.1.1", + "published": "2017-02-12T06:00:23.310Z", + "artifact": "zeppelin-highcharts-spline@0.1.1", + "author": "AhyoungRyu", + "description": "Draw spline graph using Highcharts library", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"zeppelin-ignite": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-ignite", + "version": "0.6.0", + "published": "2016-06-27T23:22:33+00:00", + "artifact": "zeppelin-ignite@0.6.0", + "description": "Zeppelin: Apache Ignite interpreter", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-ignite" + } + }, +"zeppelin-ignite_2.10": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-ignite_2.10", + "version": "0.8.2", + "published": "2019-09-26T04:27:42+00:00", + "artifact": "zeppelin-ignite_2.10@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-ignite_2.10" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-ignite_2.10", + "version": "0.8.1", + "published": "2019-01-17T03:15:50+00:00", + "artifact": "zeppelin-ignite_2.10@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-ignite_2.10" + } + }, +"zeppelin-ignite_2.11": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-ignite_2.11", + "version": "0.8.2", + "published": "2019-09-26T04:28:09+00:00", + "artifact": "zeppelin-ignite_2.11@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-ignite_2.11" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-ignite_2.11", + "version": "0.8.1", + "published": "2019-01-17T03:16:02+00:00", + "artifact": "zeppelin-ignite_2.11@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-ignite_2.11" + } + }, +"zeppelin-jdbc": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-jdbc", + "version": "0.8.2", + "published": "2019-09-26T04:30:34+00:00", + "artifact": "zeppelin-jdbc@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-jdbc" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-jdbc", + "version": "0.8.1", + "published": "2019-01-17T03:16:27+00:00", + "artifact": "zeppelin-jdbc@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-jdbc" + } + }, +"zeppelin-markdown-spell": { + "latest": { + "type": "SPELL", + "name": "zeppelin-markdown-spell", + "version": "0.0.3", + "published": "2017-03-07T19:17:35.975Z", + "artifact": "zeppelin-markdown-spell@0.0.3", + "author": "1ambda", + "description": "Parse markdown using https://github.com/evilstreak/markdown-js", + "license": "MIT", + "icon": "", + "spell": {"magic": "%markdown", "usage": "%markdown "} + } + }, +"zeppelin-livy": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-livy", + "version": "0.8.2", + "published": "2019-09-26T04:29:26+00:00", + "artifact": "zeppelin-livy@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-livy" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-livy", + "version": "0.8.1", + "published": "2019-01-17T03:17:15+00:00", + "artifact": "zeppelin-livy@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-livy" + } + }, +"zeppelin-markdown": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-markdown", + "version": "0.8.2", + "published": "2019-09-26T04:22:08+00:00", + "artifact": "zeppelin-markdown@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-markdown" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-markdown", + "version": "0.8.1", + "published": "2019-01-17T03:17:26+00:00", + "artifact": "zeppelin-markdown@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-markdown" + } + }, +"zeppelin-mathjax-spell": { + "latest": { + "type": "SPELL", + "name": "zeppelin-mathjax-spell", + "version": "0.0.1", + "published": "2017-03-08T10:04:18.787Z", + "artifact": "zeppelin-mathjax-spell@0.0.1", + "author": "1ambda", + "description": "MathJax Spell For Apache Zeppelin", + "license": "Apache-2.0", + "icon": "", + "spell": {"magic": "%mathjax", "usage": "%mathjax "} + } + }, +"zeppelin-number": { + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-number", + "version": "1.0.0", + "published": "2018-07-22T18:55:33.637Z", + "artifact": "zeppelin-number@1.0.0", + "author": "Saravanan Elumalai", + "description": "Zeppelin plugin to visualize number", + "license": "Apache-2.0", + "icon": "" + } + }, +"zeppelin-neo4j": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-neo4j", + "version": "0.8.2", + "published": "2019-09-26T04:25:29+00:00", + "artifact": "zeppelin-neo4j@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-neo4j" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-neo4j", + "version": "0.8.1", + "published": "2019-01-17T03:17:38+00:00", + "artifact": "zeppelin-neo4j@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-neo4j" + } + }, +"zeppelin-highmaps": { + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-highmaps", + "version": "1.0.0", + "published": "2018-01-30T15:46:40.265Z", + "artifact": "zeppelin-highmaps@1.0.0", + "author": "odnoklassniki", + "description": "Zeppelin plugin to visualize data using Highmaps", + "license": "Apache-2.0", + "icon": "" + } + }, +"zeppelin-kylin": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-kylin", + "version": "0.8.2", + "published": "2019-09-26T04:23:02+00:00", + "artifact": "zeppelin-kylin@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-kylin" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-kylin", + "version": "0.8.1", + "published": "2019-01-17T03:16:50+00:00", + "artifact": "zeppelin-kylin@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-kylin" + } + }, +"zeppelin-leaflet": { + "1.0.2": { + "type": "VISUALIZATION", + "name": "zeppelin-leaflet", + "version": "1.0.2", + "published": "2017-10-31T09:31:51.660Z", + "artifact": "zeppelin-leaflet@1.0.2", + "author": "Mitchell Yuwono", + "description": "Geospatial visualization using the Leaflet map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "1.0.3": { + "type": "VISUALIZATION", + "name": "zeppelin-leaflet", + "version": "1.0.3", + "published": "2017-11-01T12:47:14.830Z", + "artifact": "zeppelin-leaflet@1.0.3", + "author": "Mitchell Yuwono", + "description": "Geospatial visualization using the Leaflet map library.", + "license": "BSD-2-Clause", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-leaflet", + "version": "1.0.4", + "published": "2017-11-05T08:24:52.404Z", + "artifact": "zeppelin-leaflet@1.0.4", + "author": "Mitchell Yuwono", + "description": "Geospatial visualization using the Leaflet map library.", + "license": "BSD-2-Clause", + "icon": "" + } + }, +"zeppelin-json-spell": { + "latest": { + "type": "SPELL", + "name": "zeppelin-json-spell", + "version": "0.0.3", + "published": "2017-02-28T04:49:27.897Z", + "artifact": "zeppelin-json-spell@0.0.3", + "author": "1ambda", + "description": "Use JSON editor in paragraphs", + "license": "Apache-2.0", + "icon": "", + "spell": {"magic": "%json", "usage": "%json "} + } + }, +"zeppelin-lens": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-lens", + "version": "0.8.2", + "published": "2019-09-26T04:30:46+00:00", + "artifact": "zeppelin-lens@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-lens" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-lens", + "version": "0.8.1", + "published": "2019-01-17T03:17:03+00:00", + "artifact": "zeppelin-lens@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-lens" + } + }, +"zeppelin-highcharts-heatmap": { + "0.0.4": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-heatmap", + "version": "0.0.4", + "published": "2017-02-11T07:47:56.338Z", + "artifact": "zeppelin-highcharts-heatmap@0.0.4", + "author": "1ambda", + "description": "Heatmap Charts for Apache Zeppelin using highcharts.js", + "license": "SEE LICENSE IN ", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-highcharts-heatmap", + "version": "0.0.5", + "published": "2017-02-14T12:46:02.732Z", + "artifact": "zeppelin-highcharts-heatmap@0.0.5", + "author": "1ambda", + "description": "Heatmap Charts for Apache Zeppelin using highcharts.js", + "license": "SEE LICENSE IN ", + "icon": "" + } + }, +"zeppelin-pig": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-pig", + "version": "0.8.2", + "published": "2019-09-26T04:22:07+00:00", + "artifact": "zeppelin-pig@0.8.2", + "description": "Zeppelin interpreter for Apache Pig", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-pig" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-pig", + "version": "0.8.1", + "published": "2019-01-17T03:17:50+00:00", + "artifact": "zeppelin-pig@0.8.1", + "description": "Zeppelin interpreter for Apache Pig", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-pig" + } + }, +"zeppelin-plotly-bubble": { + "0.0.1": { + "type": "VISUALIZATION", + "name": "zeppelin-plotly-bubble", + "version": "0.0.1", + "published": "2017-09-20T09:18:12.714Z", + "artifact": "zeppelin-plotly-bubble@0.0.1", + "author": "Jun Kim", + "description": "Bubble chart of Plotly.js for Apache Zeppelin", + "license": "MIT", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-plotly-bubble", + "version": "0.0.2", + "published": "2017-11-07T09:38:48.307Z", + "artifact": "zeppelin-plotly-bubble@0.0.2", + "author": "Jun Kim", + "description": "Bubble chart of Plotly.js for Apache Zeppelin", + "license": "MIT", + "icon": "" + } + }, +"zeppelin-shell": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-shell", + "version": "0.8.2", + "published": "2019-09-26T04:23:42+00:00", + "artifact": "zeppelin-shell@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-shell" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-shell", + "version": "0.8.1", + "published": "2019-01-17T03:19:51+00:00", + "artifact": "zeppelin-shell@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-shell" + } + }, +"zeppelin-postgresql": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-postgresql", + "version": "0.7.3", + "published": "2017-09-18T16:33:12+00:00", + "artifact": "zeppelin-postgresql@0.7.3", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-postgresql" + } + }, +"zeppelin-python": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-python", + "version": "0.8.2", + "published": "2019-09-26T04:30:11+00:00", + "artifact": "zeppelin-python@0.8.2", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-python" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-python", + "version": "0.8.1", + "published": "2019-01-17T03:18:09+00:00", + "artifact": "zeppelin-python@0.8.1", + "description": "Zeppelin project", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-python" + } + }, +"zeppelin-season-table": { + "1.0.0": { + "type": "VISUALIZATION", + "name": "zeppelin-season-table", + "version": "1.0.0", + "published": "2017-09-06T18:48:38.146Z", + "artifact": "zeppelin-season-table@1.0.0", + "author": "sherry", + "description": "", + "license": "ISC", + "icon": "" + }, + "1.0.1": { + "type": "VISUALIZATION", + "name": "zeppelin-season-table", + "version": "1.0.1", + "published": "2017-09-07T06:41:58.860Z", + "artifact": "zeppelin-season-table@1.0.1", + "author": "sherry", + "description": "", + "license": "ISC", + "icon": "" + }, + "1.0.2": { + "type": "VISUALIZATION", + "name": "zeppelin-season-table", + "version": "1.0.2", + "published": "2017-09-07T12:13:52.038Z", + "artifact": "zeppelin-season-table@1.0.2", + "author": "sherry", + "description": "", + "license": "ISC", + "icon": "" + }, + "1.0.3": { + "type": "VISUALIZATION", + "name": "zeppelin-season-table", + "version": "1.0.3", + "published": "2017-09-08T02:11:43.043Z", + "artifact": "zeppelin-season-table@1.0.3", + "author": "sherry", + "description": "", + "license": "ISC", + "icon": "" + }, + "1.0.4": { + "type": "VISUALIZATION", + "name": "zeppelin-season-table", + "version": "1.0.4", + "published": "2017-09-08T07:57:27.600Z", + "artifact": "zeppelin-season-table@1.0.4", + "author": "sherry", + "description": "", + "license": "ISC", + "icon": "" + }, + "1.0.5": { + "type": "VISUALIZATION", + "name": "zeppelin-season-table", + "version": "1.0.5", + "published": "2017-09-11T05:50:20.415Z", + "artifact": "zeppelin-season-table@1.0.5", + "author": "sherry", + "description": "", + "license": "ISC", + "icon": "" + }, + "1.0.6": { + "type": "VISUALIZATION", + "name": "zeppelin-season-table", + "version": "1.0.6", + "published": "2017-09-11T06:22:13.210Z", + "artifact": "zeppelin-season-table@1.0.6", + "author": "sherry", + "description": "", + "license": "ISC", + "icon": "" + }, + "latest": { + "type": "VISUALIZATION", + "name": "zeppelin-season-table", + "version": "1.0.7", + "published": "2017-09-11T08:41:21.586Z", + "artifact": "zeppelin-season-table@1.0.7", + "author": "sherry", + "description": "", + "license": "ISC", + "icon": "" + } + }, +"zeppelin-sigma-spell": { + "0.0.1": { + "type": "SPELL", + "name": "zeppelin-sigma-spell", + "version": "0.0.1", + "published": "2017-03-08T10:09:23.660Z", + "artifact": "zeppelin-sigma-spell@0.0.1", + "author": "datalayer", + "description": "Sigma.js Network Visualization", + "license": "Apache-2.0", + "icon": "" + }, + "latest": { + "type": "SPELL", + "name": "zeppelin-sigma-spell", + "version": "0.0.2", + "published": "2017-03-10T09:31:56.802Z", + "artifact": "zeppelin-sigma-spell@0.0.2", + "author": "datalayer", + "description": "Sigma.js Network Visualization", + "license": "Apache-2.0", + "icon": "" + } + }, +"zeppelin-solr": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-solr", + "version": "0.1.5", + "published": "2019-04-10T14:49:04+00:00", + "artifact": "zeppelin-solr@0.1.5", + "description": "Zeppelin Solr support", + "license": "Apache-2.0", + "icon": "", + "groupId": "com.lucidworks.zeppelin", + "artifactId": "zeppelin-solr" + }, + "0.1.4": { + "type": "INTERPRETER", + "name": "zeppelin-solr", + "version": "0.1.4", + "published": "2019-02-28T20:07:17+00:00", + "artifact": "zeppelin-solr@0.1.4", + "description": "Zeppelin Solr support", + "license": "Apache-2.0", + "icon": "", + "groupId": "com.lucidworks.zeppelin", + "artifactId": "zeppelin-solr" + }, + "0.1.3": { + "type": "INTERPRETER", + "name": "zeppelin-solr", + "version": "0.1.3", + "published": "2019-02-08T17:38:24+00:00", + "artifact": "zeppelin-solr@0.1.3", + "description": "Zeppelin Solr support", + "license": "Apache-2.0", + "icon": "", + "groupId": "com.lucidworks.zeppelin", + "artifactId": "zeppelin-solr" + }, + "0.1.2": { + "type": "INTERPRETER", + "name": "zeppelin-solr", + "version": "0.1.2", + "published": "2019-02-08T17:22:52+00:00", + "artifact": "zeppelin-solr@0.1.2", + "description": "Zeppelin Solr support", + "license": "Apache-2.0", + "icon": "", + "groupId": "com.lucidworks.zeppelin", + "artifactId": "zeppelin-solr" + }, + "0.1.1": { + "type": "INTERPRETER", + "name": "zeppelin-solr", + "version": "0.1.1", + "published": "2018-12-21T20:31:26+00:00", + "artifact": "zeppelin-solr@0.1.1", + "description": "Zeppelin Solr support", + "license": "Apache-2.0", + "icon": "", + "groupId": "com.lucidworks.zeppelin", + "artifactId": "zeppelin-solr" + } + }, +"zeppelin-toc-spell": { + "0.0.1": { + "type": "SPELL", + "name": "zeppelin-toc-spell", + "version": "0.0.1", + "published": "2017-10-14T01:09:22.968Z", + "artifact": "zeppelin-toc-spell@0.0.1", + "author": "Ryan Munro", + "description": "Table of Contents for Zeppelin Notebooks", + "license": "ISC", + "icon": "", + "spell": {"magic": "%toc", "usage": "%toc"} + }, + "latest": { + "type": "SPELL", + "name": "zeppelin-toc-spell", + "version": "0.0.2", + "published": "2017-10-14T01:22:54.639Z", + "artifact": "zeppelin-toc-spell@0.0.2", + "author": "Ryan Munro", + "description": "Table of Contents for Zeppelin Notebooks", + "license": "ISC", + "icon": "", + "spell": {"magic": "%toc", "usage": "%toc"} + } + }, +"zeppelin-translator-spell": { + "latest": { + "type": "SPELL", + "name": "zeppelin-translator-spell", + "version": "0.0.1", + "published": "2017-03-05T07:52:58.069Z", + "artifact": "zeppelin-translator-spell@0.0.1", + "author": "1ambda", + "description": "Translate text using Google Translator API", + "license": "Apache-2.0", + "icon": "", + "config": { + "access-token": { + "type": "string", + "description": "access token for Google Translation API", + "defaultValue": "EXAMPLE-TOKEN" + } + }, + "spell": { + "magic": "%translator", + "usage": "%translator source= target= " + } + } + }, +"zeppelin-viyadb": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-viyadb", + "version": "0.7.3", + "published": "2017-09-25T18:44:57+00:00", + "artifact": "zeppelin-viyadb@0.7.3", + "description": "Zeppelin interpreter extension for ViyaDB", + "license": "Apache-2.0", + "icon": "", + "groupId": "com.github.viyadb", + "artifactId": "zeppelin-viyadb" + } + }, +"zeppelin-zengine": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-zengine", + "version": "0.8.2", + "published": "2019-09-26T04:27:36+00:00", + "artifact": "zeppelin-zengine@0.8.2", + "description": "Zeppelin Zengine", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-zengine" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-zengine", + "version": "0.8.1", + "published": "2019-01-17T03:21:30+00:00", + "artifact": "zeppelin-zengine@0.8.1", + "description": "Zeppelin Zengine", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-zengine" + } + }, +"zeppelin-zrinterpreter_2.11": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-zrinterpreter_${scala.binary.version}", + "version": "0.8.0", + "published": "2018-06-24T02:06:29+00:00", + "artifact": "zeppelin-zrinterpreter_${scala.binary.version}@0.8.0", + "description": "R Interpreter for Zeppelin", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-zrinterpreter_${scala.binary.version}" + }, + "0.7.3": { + "type": "INTERPRETER", + "name": "zeppelin-zrinterpreter_2.11", + "version": "0.7.3", + "published": "2017-09-18T16:56:48+00:00", + "artifact": "zeppelin-zrinterpreter_2.11@0.7.3", + "description": "R Interpreter for Zeppelin", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-zrinterpreter_2.11" + } + }, +"zeppelin-zrinterpreter_2.10": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-zrinterpreter_${scala.binary.version}", + "version": "0.8.0", + "published": "2018-06-24T02:06:12+00:00", + "artifact": "zeppelin-zrinterpreter_${scala.binary.version}@0.8.0", + "description": "R Interpreter for Zeppelin", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-zrinterpreter_${scala.binary.version}" + }, + "0.7.3": { + "type": "INTERPRETER", + "name": "zeppelin-zrinterpreter_2.10", + "version": "0.7.3", + "published": "2017-09-18T16:56:08+00:00", + "artifact": "zeppelin-zrinterpreter_2.10@0.7.3", + "description": "R Interpreter for Zeppelin", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-zrinterpreter_2.10" + } + }, +"zeppelin-zrinterpreter_${scala.binary.version}": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-zrinterpreter_${scala.binary.version}", + "version": "0.8.2", + "published": "2019-09-26T04:22:21+00:00", + "artifact": "zeppelin-zrinterpreter_${scala.binary.version}@0.8.2", + "description": "R Interpreter for Zeppelin", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-zrinterpreter_${scala.binary.version}" + }, + "0.8.1": { + "type": "INTERPRETER", + "name": "zeppelin-zrinterpreter_${scala.binary.version}", + "version": "0.8.1", + "published": "2019-01-17T03:21:57+00:00", + "artifact": "zeppelin-zrinterpreter_${scala.binary.version}@0.8.1", + "description": "R Interpreter for Zeppelin", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-zrinterpreter_${scala.binary.version}" + } + }, +"zeppelin-zrinterpreter": { + "latest": { + "type": "INTERPRETER", + "name": "zeppelin-zrinterpreter", + "version": "0.6.0", + "published": "2016-06-27T23:30:31+00:00", + "artifact": "zeppelin-zrinterpreter@0.6.0", + "description": "R Interpreter for Zeppelin", + "license": "Apache-2.0", + "icon": "", + "groupId": "org.apache.zeppelin", + "artifactId": "zeppelin-zrinterpreter" + } + }}] \ No newline at end of file From 53ff1b3b153846d9e0321d651edbf7c524cc390c Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Tue, 19 Nov 2019 18:18:51 +0200 Subject: [PATCH 480/492] DSR-94 Remove interpreters from helium-repo.json --- conf/helium-repo.json | 1114 +---------------------------------------- 1 file changed, 3 insertions(+), 1111 deletions(-) diff --git a/conf/helium-repo.json b/conf/helium-repo.json index ff55f653d37..bcd9ed49086 100644 --- a/conf/helium-repo.json +++ b/conf/helium-repo.json @@ -1,55 +1,4 @@ -[{"sap": { - "latest": { - "type": "INTERPRETER", - "name": "sap", - "version": "0.8.2", - "published": "2019-09-26T04:22:33+00:00", - "artifact": "sap@0.8.2", - "description": "Zeppelin SAP support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "sap" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "sap", - "version": "0.8.1", - "published": "2019-01-17T03:11:29+00:00", - "artifact": "sap@0.8.1", - "description": "Zeppelin SAP support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "sap" - } - }, -"snappydata-zeppelin": { - "latest": { - "type": "INTERPRETER", - "name": "snappydata-zeppelin", - "version": "0.7.3.6", - "published": "2019-09-03T07:49:48+00:00", - "artifact": "snappydata-zeppelin@0.7.3.6", - "description": "SnappyData distributed data store and execution engine", - "license": "Apache-2.0", - "icon": "", - "groupId": "io.snappydata", - "artifactId": "snappydata-zeppelin" - }, - "0.7.3.5": { - "type": "INTERPRETER", - "name": "snappydata-zeppelin", - "version": "0.7.3.5", - "published": "2019-05-24T11:28:01+00:00", - "artifact": "snappydata-zeppelin@0.7.3.5", - "description": "SnappyData distributed data store and execution engine", - "license": "Apache-2.0", - "icon": "", - "groupId": "io.snappydata", - "artifactId": "snappydata-zeppelin" - } - }, +[{ "sogou-map-geo": { "1.0.0": { "type": "VISUALIZATION", @@ -1055,162 +1004,6 @@ "icon": "" } }, -"spark-scala-2.10": { - "latest": { - "type": "INTERPRETER", - "name": "spark-scala-2.10", - "version": "0.8.2", - "published": "2019-09-26T04:25:03+00:00", - "artifact": "spark-scala-2.10@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "spark-scala-2.10" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "spark-scala-2.10", - "version": "0.8.1", - "published": "2019-01-17T03:12:05+00:00", - "artifact": "spark-scala-2.10@0.8.1", - "description": "Zeppelin Spark Support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "spark-scala-2.10" - } - }, -"spark-scala-2.11": { - "latest": { - "type": "INTERPRETER", - "name": "spark-scala-2.11", - "version": "0.8.2", - "published": "2019-09-26T04:26:09+00:00", - "artifact": "spark-scala-2.11@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "spark-scala-2.11" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "spark-scala-2.11", - "version": "0.8.1", - "published": "2019-01-17T03:12:18+00:00", - "artifact": "spark-scala-2.11@0.8.1", - "description": "Zeppelin Spark Support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "spark-scala-2.11" - } - }, -"spark-scala-parent": { - "latest": { - "type": "INTERPRETER", - "name": "spark-scala-parent", - "version": "0.8.2", - "published": "2019-09-26T04:24:19+00:00", - "artifact": "spark-scala-parent@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "spark-scala-parent" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "spark-scala-parent", - "version": "0.8.1", - "published": "2019-01-17T03:12:23+00:00", - "artifact": "spark-scala-parent@0.8.1", - "description": "Zeppelin Spark Support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "spark-scala-parent" - } - }, -"spark-shims": { - "latest": { - "type": "INTERPRETER", - "name": "spark-shims", - "version": "0.8.2", - "published": "2019-09-26T04:30:27+00:00", - "artifact": "spark-shims@0.8.2", - "description": "Zeppelin Spark Support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "spark-shims" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "spark-shims", - "version": "0.8.1", - "published": "2019-01-17T03:12:35+00:00", - "artifact": "spark-shims@0.8.1", - "description": "Zeppelin Spark Support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "spark-shims" - } - }, -"spark2-shims": { - "latest": { - "type": "INTERPRETER", - "name": "spark2-shims", - "version": "0.8.2", - "published": "2019-09-26T04:21:31+00:00", - "artifact": "spark2-shims@0.8.2", - "description": "Zeppelin Spark Support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "spark2-shims" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "spark2-shims", - "version": "0.8.1", - "published": "2019-01-17T03:12:58+00:00", - "artifact": "spark2-shims@0.8.1", - "description": "Zeppelin Spark Support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "spark2-shims" - } - }, -"spark1-shims": { - "latest": { - "type": "INTERPRETER", - "name": "spark1-shims", - "version": "0.8.2", - "published": "2019-09-26T04:24:06+00:00", - "artifact": "spark1-shims@0.8.2", - "description": "Zeppelin Spark Support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "spark1-shims" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "spark1-shims", - "version": "0.8.1", - "published": "2019-01-17T03:12:47+00:00", - "artifact": "spark1-shims@0.8.1", - "description": "Zeppelin Spark Support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "spark1-shims" - } - }, "ultimate-area-chart": { "latest": { "type": "VISUALIZATION", @@ -1473,84 +1266,6 @@ "icon": "" } }, -"zeppelin-beam": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-beam", - "version": "0.8.2", - "published": "2019-09-26T04:28:02+00:00", - "artifact": "zeppelin-beam@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-beam" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-beam", - "version": "0.8.1", - "published": "2019-01-17T03:13:39+00:00", - "artifact": "zeppelin-beam@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-beam" - } - }, -"zeppelin-angular": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-angular", - "version": "0.8.2", - "published": "2019-09-26T04:26:55+00:00", - "artifact": "zeppelin-angular@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-angular" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-angular", - "version": "0.8.1", - "published": "2019-01-17T03:13:27+00:00", - "artifact": "zeppelin-angular@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-angular" - } - }, -"zeppelin-alluxio": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-alluxio", - "version": "0.8.2", - "published": "2019-09-26T04:28:40+00:00", - "artifact": "zeppelin-alluxio@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-alluxio" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-alluxio", - "version": "0.8.1", - "published": "2019-01-17T03:13:15+00:00", - "artifact": "zeppelin-alluxio@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-alluxio" - } - }, "zeppelin-bubblechart": { "0.0.1": { "type": "VISUALIZATION", @@ -1597,46 +1312,6 @@ "icon": "" } }, -"zeppelin-bigquery": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-bigquery", - "version": "0.8.2", - "published": "2019-09-26T04:21:08+00:00", - "artifact": "zeppelin-bigquery@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-bigquery" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-bigquery", - "version": "0.8.1", - "published": "2019-01-17T03:13:50+00:00", - "artifact": "zeppelin-bigquery@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-bigquery" - } - }, -"zeppelin-cassandra": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-cassandra", - "version": "0.6.0", - "published": "2016-06-27T23:21:51+00:00", - "artifact": "zeppelin-cassandra@0.6.0", - "description": "Zeppelin cassandra support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-cassandra" - } - }, "zeppelin-aggrid": { "0.1.0": { "type": "VISUALIZATION", @@ -1661,58 +1336,6 @@ "icon": "" } }, -"zeppelin-cassandra_2.10": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-cassandra_2.10", - "version": "0.8.2", - "published": "2019-09-26T04:24:04+00:00", - "artifact": "zeppelin-cassandra_2.10@0.8.2", - "description": "Zeppelin cassandra support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-cassandra_2.10" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-cassandra_2.10", - "version": "0.8.1", - "published": "2019-01-17T03:14:04+00:00", - "artifact": "zeppelin-cassandra_2.10@0.8.1", - "description": "Zeppelin cassandra support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-cassandra_2.10" - } - }, -"zeppelin-cassandra_2.11": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-cassandra_2.11", - "version": "0.8.2", - "published": "2019-09-26T04:22:50+00:00", - "artifact": "zeppelin-cassandra_2.11@0.8.2", - "description": "Zeppelin cassandra support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-cassandra_2.11" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-cassandra_2.11", - "version": "0.8.1", - "published": "2019-01-17T03:14:16+00:00", - "artifact": "zeppelin-cassandra_2.11@0.8.1", - "description": "Zeppelin cassandra support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-cassandra_2.11" - } - }, "zeppelin-csv-spell": { "latest": { "type": "SPELL", @@ -1740,60 +1363,6 @@ "icon": "" } }, -"zeppelin-display_2.11": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-display_2.11", - "version": "0.7.3", - "published": "2017-09-18T16:29:38+00:00", - "artifact": "zeppelin-display_2.11@0.7.3", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-display_2.11" - } - }, -"zeppelin-display_2.10": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-display_2.10", - "version": "0.7.3", - "published": "2017-09-18T16:29:25+00:00", - "artifact": "zeppelin-display_2.10@0.7.3", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-display_2.10" - } - }, -"zeppelin-display": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-display", - "version": "0.8.2", - "published": "2019-09-26T04:23:21+00:00", - "artifact": "zeppelin-display@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-display" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-display", - "version": "0.8.1", - "published": "2019-01-17T03:14:28+00:00", - "artifact": "zeppelin-display@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-display" - } - }, "zeppelin-echo-spell": { "1.0.4": { "type": "SPELL", @@ -1858,124 +1427,6 @@ "spell": {"magic": "%echo", "usage": "%echo "} } }, -"zeppelin-elasticsearch": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-elasticsearch", - "version": "0.8.2", - "published": "2019-09-26T04:29:02+00:00", - "artifact": "zeppelin-elasticsearch@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-elasticsearch" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-elasticsearch", - "version": "0.8.1", - "published": "2019-01-17T03:14:40+00:00", - "artifact": "zeppelin-elasticsearch@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-elasticsearch" - } - }, -"zeppelin-file": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-file", - "version": "0.8.2", - "published": "2019-09-26T04:24:29+00:00", - "artifact": "zeppelin-file@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-file" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-file", - "version": "0.8.1", - "published": "2019-01-17T03:14:52+00:00", - "artifact": "zeppelin-file@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-file" - } - }, -"zeppelin-flink": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-flink", - "version": "0.6.0", - "published": "2016-06-27T23:22:19+00:00", - "artifact": "zeppelin-flink@0.6.0", - "description": "Zeppelin flink support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-flink" - } - }, -"zeppelin-flink_2.10": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-flink_2.10", - "version": "0.8.2", - "published": "2019-09-26T04:28:44+00:00", - "artifact": "zeppelin-flink_2.10@0.8.2", - "description": "Zeppelin flink support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-flink_2.10" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-flink_2.10", - "version": "0.8.1", - "published": "2019-01-17T03:15:03+00:00", - "artifact": "zeppelin-flink_2.10@0.8.1", - "description": "Zeppelin flink support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-flink_2.10" - } - }, -"zeppelin-flink_2.11": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-flink_2.11", - "version": "0.8.2", - "published": "2019-09-26T04:29:35+00:00", - "artifact": "zeppelin-flink_2.11@0.8.2", - "description": "Zeppelin flink support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-flink_2.11" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-flink_2.11", - "version": "0.8.1", - "published": "2019-01-17T03:15:15+00:00", - "artifact": "zeppelin-flink_2.11@0.8.1", - "description": "Zeppelin flink support", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-flink_2.11" - } - }, "zeppelin-flowchart-spell": { "0.0.1": { "type": "SPELL", @@ -2002,58 +1453,6 @@ "spell": {"magic": "%flowchart", "usage": "%flowchart "} } }, -"zeppelin-groovy": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-groovy", - "version": "0.8.2", - "published": "2019-09-26T04:21:52+00:00", - "artifact": "zeppelin-groovy@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-groovy" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-groovy", - "version": "0.8.1", - "published": "2019-01-17T03:15:26+00:00", - "artifact": "zeppelin-groovy@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-groovy" - } - }, -"zeppelin-hbase": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-hbase", - "version": "0.8.2", - "published": "2019-09-26T04:29:14+00:00", - "artifact": "zeppelin-hbase@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-hbase" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-hbase", - "version": "0.8.1", - "published": "2019-01-17T03:15:38+00:00", - "artifact": "zeppelin-hbase@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-hbase" - } - }, "zeppelin-highcharts-bubble": { "latest": { "type": "VISUALIZATION", @@ -2227,98 +1626,6 @@ "icon": "" } }, -"zeppelin-ignite": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-ignite", - "version": "0.6.0", - "published": "2016-06-27T23:22:33+00:00", - "artifact": "zeppelin-ignite@0.6.0", - "description": "Zeppelin: Apache Ignite interpreter", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-ignite" - } - }, -"zeppelin-ignite_2.10": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-ignite_2.10", - "version": "0.8.2", - "published": "2019-09-26T04:27:42+00:00", - "artifact": "zeppelin-ignite_2.10@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-ignite_2.10" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-ignite_2.10", - "version": "0.8.1", - "published": "2019-01-17T03:15:50+00:00", - "artifact": "zeppelin-ignite_2.10@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-ignite_2.10" - } - }, -"zeppelin-ignite_2.11": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-ignite_2.11", - "version": "0.8.2", - "published": "2019-09-26T04:28:09+00:00", - "artifact": "zeppelin-ignite_2.11@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-ignite_2.11" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-ignite_2.11", - "version": "0.8.1", - "published": "2019-01-17T03:16:02+00:00", - "artifact": "zeppelin-ignite_2.11@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-ignite_2.11" - } - }, -"zeppelin-jdbc": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-jdbc", - "version": "0.8.2", - "published": "2019-09-26T04:30:34+00:00", - "artifact": "zeppelin-jdbc@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-jdbc" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-jdbc", - "version": "0.8.1", - "published": "2019-01-17T03:16:27+00:00", - "artifact": "zeppelin-jdbc@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-jdbc" - } - }, "zeppelin-markdown-spell": { "latest": { "type": "SPELL", @@ -2333,58 +1640,6 @@ "spell": {"magic": "%markdown", "usage": "%markdown "} } }, -"zeppelin-livy": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-livy", - "version": "0.8.2", - "published": "2019-09-26T04:29:26+00:00", - "artifact": "zeppelin-livy@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-livy" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-livy", - "version": "0.8.1", - "published": "2019-01-17T03:17:15+00:00", - "artifact": "zeppelin-livy@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-livy" - } - }, -"zeppelin-markdown": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-markdown", - "version": "0.8.2", - "published": "2019-09-26T04:22:08+00:00", - "artifact": "zeppelin-markdown@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-markdown" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-markdown", - "version": "0.8.1", - "published": "2019-01-17T03:17:26+00:00", - "artifact": "zeppelin-markdown@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-markdown" - } - }, "zeppelin-mathjax-spell": { "latest": { "type": "SPELL", @@ -2412,32 +1667,6 @@ "icon": "" } }, -"zeppelin-neo4j": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-neo4j", - "version": "0.8.2", - "published": "2019-09-26T04:25:29+00:00", - "artifact": "zeppelin-neo4j@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-neo4j" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-neo4j", - "version": "0.8.1", - "published": "2019-01-17T03:17:38+00:00", - "artifact": "zeppelin-neo4j@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-neo4j" - } - }, "zeppelin-highmaps": { "latest": { "type": "VISUALIZATION", @@ -2451,32 +1680,6 @@ "icon": "" } }, -"zeppelin-kylin": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-kylin", - "version": "0.8.2", - "published": "2019-09-26T04:23:02+00:00", - "artifact": "zeppelin-kylin@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-kylin" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-kylin", - "version": "0.8.1", - "published": "2019-01-17T03:16:50+00:00", - "artifact": "zeppelin-kylin@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-kylin" - } - }, "zeppelin-leaflet": { "1.0.2": { "type": "VISUALIZATION", @@ -2526,32 +1729,6 @@ "spell": {"magic": "%json", "usage": "%json "} } }, -"zeppelin-lens": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-lens", - "version": "0.8.2", - "published": "2019-09-26T04:30:46+00:00", - "artifact": "zeppelin-lens@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-lens" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-lens", - "version": "0.8.1", - "published": "2019-01-17T03:17:03+00:00", - "artifact": "zeppelin-lens@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-lens" - } - }, "zeppelin-highcharts-heatmap": { "0.0.4": { "type": "VISUALIZATION", @@ -2576,32 +1753,6 @@ "icon": "" } }, -"zeppelin-pig": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-pig", - "version": "0.8.2", - "published": "2019-09-26T04:22:07+00:00", - "artifact": "zeppelin-pig@0.8.2", - "description": "Zeppelin interpreter for Apache Pig", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-pig" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-pig", - "version": "0.8.1", - "published": "2019-01-17T03:17:50+00:00", - "artifact": "zeppelin-pig@0.8.1", - "description": "Zeppelin interpreter for Apache Pig", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-pig" - } - }, "zeppelin-plotly-bubble": { "0.0.1": { "type": "VISUALIZATION", @@ -2626,72 +1777,6 @@ "icon": "" } }, -"zeppelin-shell": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-shell", - "version": "0.8.2", - "published": "2019-09-26T04:23:42+00:00", - "artifact": "zeppelin-shell@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-shell" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-shell", - "version": "0.8.1", - "published": "2019-01-17T03:19:51+00:00", - "artifact": "zeppelin-shell@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-shell" - } - }, -"zeppelin-postgresql": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-postgresql", - "version": "0.7.3", - "published": "2017-09-18T16:33:12+00:00", - "artifact": "zeppelin-postgresql@0.7.3", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-postgresql" - } - }, -"zeppelin-python": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-python", - "version": "0.8.2", - "published": "2019-09-26T04:30:11+00:00", - "artifact": "zeppelin-python@0.8.2", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-python" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-python", - "version": "0.8.1", - "published": "2019-01-17T03:18:09+00:00", - "artifact": "zeppelin-python@0.8.1", - "description": "Zeppelin project", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-python" - } - }, "zeppelin-season-table": { "1.0.0": { "type": "VISUALIZATION", @@ -2806,68 +1891,6 @@ "icon": "" } }, -"zeppelin-solr": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-solr", - "version": "0.1.5", - "published": "2019-04-10T14:49:04+00:00", - "artifact": "zeppelin-solr@0.1.5", - "description": "Zeppelin Solr support", - "license": "Apache-2.0", - "icon": "", - "groupId": "com.lucidworks.zeppelin", - "artifactId": "zeppelin-solr" - }, - "0.1.4": { - "type": "INTERPRETER", - "name": "zeppelin-solr", - "version": "0.1.4", - "published": "2019-02-28T20:07:17+00:00", - "artifact": "zeppelin-solr@0.1.4", - "description": "Zeppelin Solr support", - "license": "Apache-2.0", - "icon": "", - "groupId": "com.lucidworks.zeppelin", - "artifactId": "zeppelin-solr" - }, - "0.1.3": { - "type": "INTERPRETER", - "name": "zeppelin-solr", - "version": "0.1.3", - "published": "2019-02-08T17:38:24+00:00", - "artifact": "zeppelin-solr@0.1.3", - "description": "Zeppelin Solr support", - "license": "Apache-2.0", - "icon": "", - "groupId": "com.lucidworks.zeppelin", - "artifactId": "zeppelin-solr" - }, - "0.1.2": { - "type": "INTERPRETER", - "name": "zeppelin-solr", - "version": "0.1.2", - "published": "2019-02-08T17:22:52+00:00", - "artifact": "zeppelin-solr@0.1.2", - "description": "Zeppelin Solr support", - "license": "Apache-2.0", - "icon": "", - "groupId": "com.lucidworks.zeppelin", - "artifactId": "zeppelin-solr" - }, - "0.1.1": { - "type": "INTERPRETER", - "name": "zeppelin-solr", - "version": "0.1.1", - "published": "2018-12-21T20:31:26+00:00", - "artifact": "zeppelin-solr@0.1.1", - "description": "Zeppelin Solr support", - "license": "Apache-2.0", - "icon": "", - "groupId": "com.lucidworks.zeppelin", - "artifactId": "zeppelin-solr" - } - }, "zeppelin-toc-spell": { "0.0.1": { "type": "SPELL", @@ -2917,136 +1940,5 @@ "usage": "%translator source= target= " } } - }, -"zeppelin-viyadb": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-viyadb", - "version": "0.7.3", - "published": "2017-09-25T18:44:57+00:00", - "artifact": "zeppelin-viyadb@0.7.3", - "description": "Zeppelin interpreter extension for ViyaDB", - "license": "Apache-2.0", - "icon": "", - "groupId": "com.github.viyadb", - "artifactId": "zeppelin-viyadb" - } - }, -"zeppelin-zengine": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-zengine", - "version": "0.8.2", - "published": "2019-09-26T04:27:36+00:00", - "artifact": "zeppelin-zengine@0.8.2", - "description": "Zeppelin Zengine", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-zengine" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-zengine", - "version": "0.8.1", - "published": "2019-01-17T03:21:30+00:00", - "artifact": "zeppelin-zengine@0.8.1", - "description": "Zeppelin Zengine", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-zengine" - } - }, -"zeppelin-zrinterpreter_2.11": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-zrinterpreter_${scala.binary.version}", - "version": "0.8.0", - "published": "2018-06-24T02:06:29+00:00", - "artifact": "zeppelin-zrinterpreter_${scala.binary.version}@0.8.0", - "description": "R Interpreter for Zeppelin", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-zrinterpreter_${scala.binary.version}" - }, - "0.7.3": { - "type": "INTERPRETER", - "name": "zeppelin-zrinterpreter_2.11", - "version": "0.7.3", - "published": "2017-09-18T16:56:48+00:00", - "artifact": "zeppelin-zrinterpreter_2.11@0.7.3", - "description": "R Interpreter for Zeppelin", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-zrinterpreter_2.11" - } - }, -"zeppelin-zrinterpreter_2.10": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-zrinterpreter_${scala.binary.version}", - "version": "0.8.0", - "published": "2018-06-24T02:06:12+00:00", - "artifact": "zeppelin-zrinterpreter_${scala.binary.version}@0.8.0", - "description": "R Interpreter for Zeppelin", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-zrinterpreter_${scala.binary.version}" - }, - "0.7.3": { - "type": "INTERPRETER", - "name": "zeppelin-zrinterpreter_2.10", - "version": "0.7.3", - "published": "2017-09-18T16:56:08+00:00", - "artifact": "zeppelin-zrinterpreter_2.10@0.7.3", - "description": "R Interpreter for Zeppelin", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-zrinterpreter_2.10" - } - }, -"zeppelin-zrinterpreter_${scala.binary.version}": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-zrinterpreter_${scala.binary.version}", - "version": "0.8.2", - "published": "2019-09-26T04:22:21+00:00", - "artifact": "zeppelin-zrinterpreter_${scala.binary.version}@0.8.2", - "description": "R Interpreter for Zeppelin", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-zrinterpreter_${scala.binary.version}" - }, - "0.8.1": { - "type": "INTERPRETER", - "name": "zeppelin-zrinterpreter_${scala.binary.version}", - "version": "0.8.1", - "published": "2019-01-17T03:21:57+00:00", - "artifact": "zeppelin-zrinterpreter_${scala.binary.version}@0.8.1", - "description": "R Interpreter for Zeppelin", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-zrinterpreter_${scala.binary.version}" - } - }, -"zeppelin-zrinterpreter": { - "latest": { - "type": "INTERPRETER", - "name": "zeppelin-zrinterpreter", - "version": "0.6.0", - "published": "2016-06-27T23:30:31+00:00", - "artifact": "zeppelin-zrinterpreter@0.6.0", - "description": "R Interpreter for Zeppelin", - "license": "Apache-2.0", - "icon": "", - "groupId": "org.apache.zeppelin", - "artifactId": "zeppelin-zrinterpreter" - } - }}] \ No newline at end of file + } +}] From 92f2bc0132a06784a69a242dc05c8e5fb3a43c19 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Tue, 19 Nov 2019 18:20:52 +0200 Subject: [PATCH 481/492] DSR-94 Remove not working Helium plugins --- conf/helium-repo.json | 529 ------------------------------------------ 1 file changed, 529 deletions(-) diff --git a/conf/helium-repo.json b/conf/helium-repo.json index bcd9ed49086..6d6965c63cd 100644 --- a/conf/helium-repo.json +++ b/conf/helium-repo.json @@ -1,358 +1,4 @@ [{ -"sogou-map-geo": { - "1.0.0": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.0", - "published": "2018-07-10T08:15:18.760Z", - "artifact": "sogou-map-geo@1.0.0", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.1": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.1", - "published": "2018-07-10T08:56:45.109Z", - "artifact": "sogou-map-geo@1.0.1", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.2": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.2", - "published": "2018-07-10T10:25:47.391Z", - "artifact": "sogou-map-geo@1.0.2", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.3": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.3", - "published": "2018-07-10T12:08:27.865Z", - "artifact": "sogou-map-geo@1.0.3", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.4": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.4", - "published": "2018-07-10T12:16:29.034Z", - "artifact": "sogou-map-geo@1.0.4", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.5": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.5", - "published": "2018-07-10T13:14:21.726Z", - "artifact": "sogou-map-geo@1.0.5", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.6": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.6", - "published": "2018-07-11T01:51:24.623Z", - "artifact": "sogou-map-geo@1.0.6", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.7": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.7", - "published": "2018-07-11T03:30:42.712Z", - "artifact": "sogou-map-geo@1.0.7", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.8": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.8", - "published": "2018-07-11T05:37:58.867Z", - "artifact": "sogou-map-geo@1.0.8", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.9": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.9", - "published": "2018-07-11T06:25:05.749Z", - "artifact": "sogou-map-geo@1.0.9", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.10": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.10", - "published": "2018-07-11T06:56:08.847Z", - "artifact": "sogou-map-geo@1.0.10", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.11": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.11", - "published": "2018-07-11T07:20:35.348Z", - "artifact": "sogou-map-geo@1.0.11", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.13": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.13", - "published": "2018-07-11T08:16:30.781Z", - "artifact": "sogou-map-geo@1.0.13", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.14": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.14", - "published": "2018-07-11T09:07:34.729Z", - "artifact": "sogou-map-geo@1.0.14", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.15": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.15", - "published": "2018-07-11T09:49:49.293Z", - "artifact": "sogou-map-geo@1.0.15", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.16": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.16", - "published": "2018-07-11T11:15:05.062Z", - "artifact": "sogou-map-geo@1.0.16", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.17": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.17", - "published": "2018-07-11T11:39:10.139Z", - "artifact": "sogou-map-geo@1.0.17", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.18": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.18", - "published": "2018-07-11T12:04:48.987Z", - "artifact": "sogou-map-geo@1.0.18", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.19": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.19", - "published": "2018-07-11T12:45:08.942Z", - "artifact": "sogou-map-geo@1.0.19", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.20": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.20", - "published": "2018-07-11T12:58:55.105Z", - "artifact": "sogou-map-geo@1.0.20", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.21": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.21", - "published": "2018-07-12T02:38:26.001Z", - "artifact": "sogou-map-geo@1.0.21", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.22": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.22", - "published": "2018-07-12T02:51:35.057Z", - "artifact": "sogou-map-geo@1.0.22", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.23": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.23", - "published": "2018-07-12T03:18:07.254Z", - "artifact": "sogou-map-geo@1.0.23", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.24": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.24", - "published": "2018-07-12T03:25:09.263Z", - "artifact": "sogou-map-geo@1.0.24", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.25": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.25", - "published": "2018-07-12T06:34:36.735Z", - "artifact": "sogou-map-geo@1.0.25", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.26": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.26", - "published": "2018-07-12T07:24:08.297Z", - "artifact": "sogou-map-geo@1.0.26", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.27": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.27", - "published": "2018-07-12T08:48:13.695Z", - "artifact": "sogou-map-geo@1.0.27", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.28": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.28", - "published": "2018-07-12T08:59:59.555Z", - "artifact": "sogou-map-geo@1.0.28", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.29": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.29", - "published": "2018-07-12T11:52:31.785Z", - "artifact": "sogou-map-geo@1.0.29", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.30": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.30", - "published": "2018-07-12T12:28:33.936Z", - "artifact": "sogou-map-geo@1.0.30", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "1.0.31": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.31", - "published": "2018-07-12T12:51:02.920Z", - "artifact": "sogou-map-geo@1.0.31", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - }, - "latest": { - "type": "VISUALIZATION", - "name": "sogou-map-geo", - "version": "1.0.32", - "published": "2018-07-12T13:26:44.816Z", - "artifact": "sogou-map-geo@1.0.32", - "author": "Robin Liew", - "description": "Geospatial visualization using the sogou map library.", - "license": "BSD-2-Clause", - "icon": "" - } - }, "sogou-map-vis": { "1.0.0": { "type": "VISUALIZATION", @@ -1350,19 +996,6 @@ "spell": {"magic": "%csv", "usage": "%csv "} } }, -"zeppelin-d3-spell": { - "latest": { - "type": "SPELL", - "name": "zeppelin-d3-spell", - "version": "0.0.1", - "published": "2017-03-08T10:08:51.022Z", - "artifact": "zeppelin-d3-spell@0.0.1", - "author": "datalayer", - "description": "D3.js Network Visualization for Apache Zeppelin", - "license": "Apache-2.0", - "icon": "" - } - }, "zeppelin-echo-spell": { "1.0.4": { "type": "SPELL", @@ -1427,32 +1060,6 @@ "spell": {"magic": "%echo", "usage": "%echo "} } }, -"zeppelin-flowchart-spell": { - "0.0.1": { - "type": "SPELL", - "name": "zeppelin-flowchart-spell", - "version": "0.0.1", - "published": "2017-02-11T02:13:08.734Z", - "artifact": "zeppelin-flowchart-spell@0.0.1", - "author": "1ambda", - "description": "Draw flowchart using http://flowchart.js.org", - "license": "MIT", - "icon": "", - "spell": {"magic": "%flowchart", "usage": "%flowchart "} - }, - "latest": { - "type": "SPELL", - "name": "zeppelin-flowchart-spell", - "version": "0.0.2", - "published": "2017-02-13T11:05:56.651Z", - "artifact": "zeppelin-flowchart-spell@0.0.2", - "author": "1ambda", - "description": "Draw flowchart using http://flowchart.js.org", - "license": "MIT", - "icon": "", - "spell": {"magic": "%flowchart", "usage": "%flowchart "} - } - }, "zeppelin-highcharts-bubble": { "latest": { "type": "VISUALIZATION", @@ -1503,129 +1110,6 @@ "icon": "" } }, -"zeppelin-highcharts-spline": { - "0.0.1": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-spline", - "version": "0.0.1", - "published": "2017-01-16T08:53:38.470Z", - "artifact": "zeppelin-highcharts-spline@0.0.1", - "author": "ahyoung", - "description": "Draw spline graph using Highcharts library", - "license": "Apache-2.0", - "icon": "" - }, - "0.0.2": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-spline", - "version": "0.0.2", - "published": "2017-01-16T10:49:56.230Z", - "artifact": "zeppelin-highcharts-spline@0.0.2", - "author": "AhyoungRyu", - "description": "Draw spline graph using Highcharts library", - "license": "Apache-2.0", - "icon": "" - }, - "0.0.3": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-spline", - "version": "0.0.3", - "published": "2017-01-20T05:39:22.177Z", - "artifact": "zeppelin-highcharts-spline@0.0.3", - "author": "AhyoungRyu", - "description": "Draw spline graph using Highcharts library", - "license": "Apache-2.0", - "icon": "" - }, - "0.0.4": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-spline", - "version": "0.0.4", - "published": "2017-01-20T07:13:35.958Z", - "artifact": "zeppelin-highcharts-spline@0.0.4", - "author": "AhyoungRyu", - "description": "Draw spline graph using Highcharts library", - "license": "Apache-2.0", - "icon": "⌇" - }, - "0.0.5": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-spline", - "version": "0.0.5", - "published": "2017-01-20T09:07:45.758Z", - "artifact": "zeppelin-highcharts-spline@0.0.5", - "author": "AhyoungRyu", - "description": "Draw spline graph using Highcharts library", - "license": "Apache-2.0", - "icon": "⌇" - }, - "0.0.6": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-spline", - "version": "0.0.6", - "published": "2017-01-22T13:33:58.177Z", - "artifact": "zeppelin-highcharts-spline@0.0.6", - "author": "AhyoungRyu", - "description": "Draw spline graph using Highcharts library", - "license": "Apache-2.0", - "icon": "⌇" - }, - "0.0.7": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-spline", - "version": "0.0.7", - "published": "2017-01-22T13:38:48.895Z", - "artifact": "zeppelin-highcharts-spline@0.0.7", - "author": "AhyoungRyu", - "description": "Draw spline graph using Highcharts library", - "license": "Apache-2.0", - "icon": "" - }, - "0.0.8": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-spline", - "version": "0.0.8", - "published": "2017-01-23T05:36:57.596Z", - "artifact": "zeppelin-highcharts-spline@0.0.8", - "author": "AhyoungRyu", - "description": "Draw spline graph using Highcharts library", - "license": "Apache-2.0", - "icon": "" - }, - "0.0.9": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-spline", - "version": "0.0.9", - "published": "2017-02-12T05:47:43.594Z", - "artifact": "zeppelin-highcharts-spline@0.0.9", - "author": "AhyoungRyu", - "description": "Draw spline graph using Highcharts library", - "license": "SEE LICENSE IN ", - "icon": "" - }, - "0.1.0": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-spline", - "version": "0.1.0", - "published": "2017-02-12T05:56:54.943Z", - "artifact": "zeppelin-highcharts-spline@0.1.0", - "author": "AhyoungRyu", - "description": "Draw spline graph using Highcharts library", - "license": "SEE LICENSE IN ", - "icon": "" - }, - "latest": { - "type": "VISUALIZATION", - "name": "zeppelin-highcharts-spline", - "version": "0.1.1", - "published": "2017-02-12T06:00:23.310Z", - "artifact": "zeppelin-highcharts-spline@0.1.1", - "author": "AhyoungRyu", - "description": "Draw spline graph using Highcharts library", - "license": "SEE LICENSE IN ", - "icon": "" - } - }, "zeppelin-markdown-spell": { "latest": { "type": "SPELL", @@ -1667,19 +1151,6 @@ "icon": "" } }, -"zeppelin-highmaps": { - "latest": { - "type": "VISUALIZATION", - "name": "zeppelin-highmaps", - "version": "1.0.0", - "published": "2018-01-30T15:46:40.265Z", - "artifact": "zeppelin-highmaps@1.0.0", - "author": "odnoklassniki", - "description": "Zeppelin plugin to visualize data using Highmaps", - "license": "Apache-2.0", - "icon": "" - } - }, "zeppelin-leaflet": { "1.0.2": { "type": "VISUALIZATION", From 68ecd3ba685795802d8f6450284b94f95ffef5d0 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Thu, 21 Nov 2019 18:37:43 +0200 Subject: [PATCH 482/492] MZEP-182 Install git package in DSR images --- scripts/mapr-dsr/centos7/Dockerfile | 3 ++- scripts/mapr-dsr/kubeflow/Dockerfile | 3 ++- scripts/mapr-dsr/ubuntu16/Dockerfile | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/mapr-dsr/centos7/Dockerfile b/scripts/mapr-dsr/centos7/Dockerfile index 08751dec306..ddb457930d0 100644 --- a/scripts/mapr-dsr/centos7/Dockerfile +++ b/scripts/mapr-dsr/centos7/Dockerfile @@ -43,7 +43,8 @@ RUN /opt/mapr/installer/docker/mapr-setup.sh -r "$MAPR_REPO_ROOT" container clie RUN mkdir -p /opt/mapr/zeppelin && \ echo "$ZEPPELIN_VERSION" > /opt/mapr/zeppelin/zeppelinversion -RUN yum install -y gcc python-devel python-setuptools && \ +RUN yum install -y git less nano patch vim && \ + yum install -y gcc python-devel python-setuptools && \ easy_install pip && \ pip install matplotlib numpy pandas diff --git a/scripts/mapr-dsr/kubeflow/Dockerfile b/scripts/mapr-dsr/kubeflow/Dockerfile index bc3f1301b54..5a5a1f8c08c 100644 --- a/scripts/mapr-dsr/kubeflow/Dockerfile +++ b/scripts/mapr-dsr/kubeflow/Dockerfile @@ -43,7 +43,8 @@ RUN /opt/mapr/installer/docker/mapr-setup.sh -r "$MAPR_REPO_ROOT" container clie RUN mkdir -p /opt/mapr/zeppelin && \ echo "$ZEPPELIN_VERSION" > /opt/mapr/zeppelin/zeppelinversion -RUN yum install -y gcc python-devel python-setuptools && \ +RUN yum install -y git less nano patch vim && \ + yum install -y gcc python-devel python-setuptools && \ easy_install pip && \ pip install matplotlib numpy pandas requests diff --git a/scripts/mapr-dsr/ubuntu16/Dockerfile b/scripts/mapr-dsr/ubuntu16/Dockerfile index edce7c2d6d1..6aab35c614f 100644 --- a/scripts/mapr-dsr/ubuntu16/Dockerfile +++ b/scripts/mapr-dsr/ubuntu16/Dockerfile @@ -49,6 +49,7 @@ RUN mkdir -p /opt/mapr/zeppelin && \ RUN export DEBIAN_FRONTEND=noninteractive && \ apt update && \ + apt install --no-install-recommends -q -y ca-certificates git less nano patch ssh-client vim && \ apt install --no-install-recommends -q -y gcc python-dev python-setuptools && \ easy_install pip && \ pip install matplotlib numpy pandas From 5062d3bdc418278833c5fdbd6e6f9d51b79ba857 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Tue, 26 Nov 2019 16:16:49 +0200 Subject: [PATCH 483/492] Update release version to 1912 and switch to new GCR project --- scripts/mapr-dsr/build.sh | 8 ++++---- scripts/mapr-dsr/centos7/Dockerfile | 2 +- scripts/mapr-dsr/kubeflow/Dockerfile | 2 +- scripts/mapr-dsr/spark/Dockerfile | 4 ++-- scripts/mapr-dsr/ubuntu16/Dockerfile | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/mapr-dsr/build.sh b/scripts/mapr-dsr/build.sh index 06e92620074..912070d48ba 100755 --- a/scripts/mapr-dsr/build.sh +++ b/scripts/mapr-dsr/build.sh @@ -1,6 +1,6 @@ #!/bin/sh -MAPR_VERSION_DSR=${MAPR_VERSION_DSR:-"v1.4.0"} +MAPR_VERSION_DSR=${MAPR_VERSION_DSR:-"v1.4.1"} MAPR_VERSION_CORE=${MAPR_VERSION_CORE:-"6.1.0"} MAPR_VERSION_MEP=${MAPR_VERSION_MEP:-"6.3.0"} @@ -47,7 +47,7 @@ if [ "$RELEASE" = true ]; then DOCKER_REPO=${DOCKER_REPO:-"maprtech/data-science-refinery"} IMAGE_VERSION=${IMAGE_VERSION:-"${MAPR_VERSION_DSR}_${MAPR_VERSION_CORE}_${MAPR_VERSION_MEP}"} ZEPPELIN_GIT_REPO=${ZEPPELIN_GIT_REPO:-"git@github.com:mapr/zeppelin.git"} - ZEPPELIN_GIT_TAG=${ZEPPELIN_GIT_TAG:-"0.8.2-mapr-1911"} + ZEPPELIN_GIT_TAG=${ZEPPELIN_GIT_TAG:-"0.8.2-mapr-1912"} MAPR_REPO_ROOT=${MAPR_REPO_ROOT:-"https://package.mapr.com/releases"} MAPR_MAVEN_REPO=${MAPR_MAVEN_REPO:-"http://repository.mapr.com/maven/"} else @@ -58,9 +58,9 @@ else MAPR_REPO_ROOT=${MAPR_REPO_ROOT:-"http://artifactory.devops.lab/artifactory/prestage/releases-dev"} MAPR_MAVEN_REPO=${MAPR_MAVEN_REPO:-"http://maven.corp.maprtech.com/nexus/content/groups/public/"} fi -KUBEFLOW_REPO=${KUBEFLOW_REPO:-"us.gcr.io/mapreng-1/maprtech/zeppelin"} +KUBEFLOW_REPO=${KUBEFLOW_REPO:-"gcr.io/mapr-252711/zeppelin-0.8.2"} KUBEFLOW_IMAGE_VERSION=${KUBEFLOW_IMAGE_VERSION:-"$IMAGE_VERSION"} -SPARK_REPO="us.gcr.io/mapreng-1/maprtech/spark-zeppelin-2.4.4" +SPARK_REPO="gcr.io/mapr-252711/spark-zeppelin-2.4.4" SPARK_IMAGE_VERSION=${SPARK_IMAGE_VERSION:-"$IMAGE_VERSION"} if [ "$BUILD_ALL" = true ]; then diff --git a/scripts/mapr-dsr/centos7/Dockerfile b/scripts/mapr-dsr/centos7/Dockerfile index ddb457930d0..6c60aa056cb 100644 --- a/scripts/mapr-dsr/centos7/Dockerfile +++ b/scripts/mapr-dsr/centos7/Dockerfile @@ -2,7 +2,7 @@ FROM centos:centos7 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" -ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1911" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ diff --git a/scripts/mapr-dsr/kubeflow/Dockerfile b/scripts/mapr-dsr/kubeflow/Dockerfile index 5a5a1f8c08c..61cdc24a2a1 100644 --- a/scripts/mapr-dsr/kubeflow/Dockerfile +++ b/scripts/mapr-dsr/kubeflow/Dockerfile @@ -2,7 +2,7 @@ FROM centos:centos7 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" -ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1911" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ diff --git a/scripts/mapr-dsr/spark/Dockerfile b/scripts/mapr-dsr/spark/Dockerfile index 1361d320ee6..e51ee5da74a 100644 --- a/scripts/mapr-dsr/spark/Dockerfile +++ b/scripts/mapr-dsr/spark/Dockerfile @@ -2,7 +2,7 @@ FROM centos:centos7 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" -ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1911" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ @@ -21,7 +21,7 @@ RUN --mount=type=cache,id=mvncache,target=/root/.m2 --mount=type=cache,id=npmcac mv "./zeppelin-distribution/target/zeppelin-${ZEPPELIN_MAVEN_VERSION}/zeppelin-${ZEPPELIN_MAVEN_VERSION}" /zeppelin_build -FROM us.gcr.io/mapreng-1/maprtech/spark-2.4.4:latest +FROM gcr.io/mapr-252711/spark-2.4.4:latest ARG ZEPPELIN_VERSION="0.8.2" diff --git a/scripts/mapr-dsr/ubuntu16/Dockerfile b/scripts/mapr-dsr/ubuntu16/Dockerfile index 6aab35c614f..5055c3efae4 100644 --- a/scripts/mapr-dsr/ubuntu16/Dockerfile +++ b/scripts/mapr-dsr/ubuntu16/Dockerfile @@ -2,7 +2,7 @@ FROM ubuntu:16.04 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" -ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1911" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" RUN export DEBIAN_FRONTEND=noninteractive && \ From 6d476510d3b29c9df232f40110f0202abc35bf36 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Tue, 30 Jun 2020 19:07:35 +0300 Subject: [PATCH 484/492] DSR-99 Posix /mapr mount disappears after DSR container restart (#143) --- bin/entrypoint.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/entrypoint.sh b/bin/entrypoint.sh index c24af4e792f..077e3429a09 100755 --- a/bin/entrypoint.sh +++ b/bin/entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/bash -if [ ! -e "/.entrypoint_stage_one" ]; then +if [ "$$" = 1 ]; then # # Setup envinronment for Kubernetes deployment. # @@ -90,7 +90,6 @@ if [ ! -e "/.entrypoint_stage_one" ]; then # # Continue execution of this entrypoint as MAPR_CONTAINER_USER # - touch /.entrypoint_stage_one # Following piece copied from "container_post_client" function of "mapr-setup.sh" exec sudo -E -H -n -u $MAPR_CONTAINER_USER \ -g ${MAPR_CONTAINER_GROUP:-$MAPR_GROUP} "$0" "$@" From 5e1bda645467bcf2df5b2fcac314dabc73c1ea44 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Wed, 1 Jul 2020 16:45:30 +0300 Subject: [PATCH 485/492] Update ZEPPELIN_GIT_TAG --- scripts/mapr-dsr/build.sh | 2 +- scripts/mapr-dsr/centos7/Dockerfile | 2 +- scripts/mapr-dsr/kubeflow/Dockerfile | 2 +- scripts/mapr-dsr/spark/Dockerfile | 2 +- scripts/mapr-dsr/ubuntu16/Dockerfile | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/mapr-dsr/build.sh b/scripts/mapr-dsr/build.sh index 912070d48ba..b3ba5b165ad 100755 --- a/scripts/mapr-dsr/build.sh +++ b/scripts/mapr-dsr/build.sh @@ -47,7 +47,7 @@ if [ "$RELEASE" = true ]; then DOCKER_REPO=${DOCKER_REPO:-"maprtech/data-science-refinery"} IMAGE_VERSION=${IMAGE_VERSION:-"${MAPR_VERSION_DSR}_${MAPR_VERSION_CORE}_${MAPR_VERSION_MEP}"} ZEPPELIN_GIT_REPO=${ZEPPELIN_GIT_REPO:-"git@github.com:mapr/zeppelin.git"} - ZEPPELIN_GIT_TAG=${ZEPPELIN_GIT_TAG:-"0.8.2-mapr-1912"} + ZEPPELIN_GIT_TAG=${ZEPPELIN_GIT_TAG:-"0.8.2-mapr-1912-r1"} MAPR_REPO_ROOT=${MAPR_REPO_ROOT:-"https://package.mapr.com/releases"} MAPR_MAVEN_REPO=${MAPR_MAVEN_REPO:-"http://repository.mapr.com/maven/"} else diff --git a/scripts/mapr-dsr/centos7/Dockerfile b/scripts/mapr-dsr/centos7/Dockerfile index 6c60aa056cb..dd855eb7ce3 100644 --- a/scripts/mapr-dsr/centos7/Dockerfile +++ b/scripts/mapr-dsr/centos7/Dockerfile @@ -2,7 +2,7 @@ FROM centos:centos7 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" -ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r1" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ diff --git a/scripts/mapr-dsr/kubeflow/Dockerfile b/scripts/mapr-dsr/kubeflow/Dockerfile index 61cdc24a2a1..9fb422dd78c 100644 --- a/scripts/mapr-dsr/kubeflow/Dockerfile +++ b/scripts/mapr-dsr/kubeflow/Dockerfile @@ -2,7 +2,7 @@ FROM centos:centos7 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" -ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r1" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ diff --git a/scripts/mapr-dsr/spark/Dockerfile b/scripts/mapr-dsr/spark/Dockerfile index e51ee5da74a..3976e3b6b53 100644 --- a/scripts/mapr-dsr/spark/Dockerfile +++ b/scripts/mapr-dsr/spark/Dockerfile @@ -2,7 +2,7 @@ FROM centos:centos7 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" -ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r1" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ diff --git a/scripts/mapr-dsr/ubuntu16/Dockerfile b/scripts/mapr-dsr/ubuntu16/Dockerfile index 5055c3efae4..c564f694841 100644 --- a/scripts/mapr-dsr/ubuntu16/Dockerfile +++ b/scripts/mapr-dsr/ubuntu16/Dockerfile @@ -2,7 +2,7 @@ FROM ubuntu:16.04 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" -ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r1" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" RUN export DEBIAN_FRONTEND=noninteractive && \ From d3e460e9bad000c18706c1aa50ed7f49772d5796 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Tue, 30 Jun 2020 19:09:25 +0300 Subject: [PATCH 486/492] Use MapR release artifacts --- hbase/pom.xml | 2 +- hive/pom.xml | 2 +- jdbc/pom.xml | 2 +- lens/pom.xml | 2 +- livy/pom.xml | 2 +- pig/pom.xml | 4 ++-- pom.xml | 2 +- spark/interpreter/pom.xml | 2 +- spark/pom.xml | 2 +- spark/spark-shims/pom.xml | 2 +- zeppelin-server/pom.xml | 2 +- zeppelin-zengine/pom.xml | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/hbase/pom.xml b/hbase/pom.xml index 32f70fca938..9fd7bbfb3d2 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -36,7 +36,7 @@ hbase 1.0.0 - 2.7.0-mapr-1710-SNAPSHOT + 2.7.0-mapr-1808 1.6.8 2.5.0 1.1 diff --git a/hive/pom.xml b/hive/pom.xml index f2efe8863a6..9d90ee72a35 100644 --- a/hive/pom.xml +++ b/hive/pom.xml @@ -36,7 +36,7 @@ hive - 2.3.6-mapr-SNAPSHOT + 2.3.6-mapr-1912 diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 1fa1016f92d..3621cc32688 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -232,7 +232,7 @@ jdbc 9.4-1201-jdbc41 - 2.7.0-mapr-1710-SNAPSHOT + 2.7.0-mapr-1808 1.4.190 2.0.1 diff --git a/lens/pom.xml b/lens/pom.xml index f95eff50084..6f9e5c63dbe 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -37,7 +37,7 @@ lens 2.5.0-beta 1.1.0.RELEASE - 2.7.0-mapr-1710-SNAPSHOT + 2.7.0-mapr-1808 1.9.1 1.9.13 1.9.11 diff --git a/livy/pom.xml b/livy/pom.xml index 23d813287a6..67b1f5534a2 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -44,7 +44,7 @@ 0.5.0-incubating 2.1.0 - 2.7.0-mapr-1710-SNAPSHOT + 2.7.0-mapr-1808 2.16 1.8 diff --git a/pig/pom.xml b/pig/pom.xml index 75c083c9b14..2cab5845156 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -38,8 +38,8 @@ pig - 0.16.0-mapr-1901 - 2.7.0-mapr-1710-SNAPSHOT + 0.16.0-mapr-1912 + 2.7.0-mapr-1808 0.7.0 1.6.3 2.10 diff --git a/pom.xml b/pom.xml index e0bd19c3b8f..b7fcc12b274 100644 --- a/pom.xml +++ b/pom.xml @@ -334,7 +334,7 @@ org.apache.zookeeper zookeeper - 3.4.11-mapr-SNAPSHOT + 3.4.11-mapr-1808 provided diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index e9cefe49361..2c5a5258261 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -144,7 +144,7 @@ org.apache.hadoop hadoop-client - 2.7.0-mapr-1710-SNAPSHOT + 2.7.0-mapr-1808 provided diff --git a/spark/pom.xml b/spark/pom.xml index 13e86b0db0d..bfe23e04372 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -196,7 +196,7 @@ true - 2.4.4.0-mapr-SNAPSHOT + 2.4.4.0-mapr-630 2.5.0 0.10.7 diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 747dda71fda..ee00e2bf4d0 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -49,7 +49,7 @@ org.apache.hadoop hadoop-common - 2.7.0-mapr-1710-SNAPSHOT + 2.7.0-mapr-1808 provided diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 84868b140b3..74201d1d0fe 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -378,7 +378,7 @@ org.apache.hadoop hadoop-common - 2.7.0-mapr-1710-SNAPSHOT + 2.7.0-mapr-1808 provided diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index ab3d6d1d258..f375eced655 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -36,7 +36,7 @@ - 2.7.0-mapr-1710-SNAPSHOT + 2.7.0-mapr-1808 3.7 2.0 1.14.0 From 40a8a6c74e48563b5754ae690b170806420e13a0 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Tue, 30 Jun 2020 19:10:36 +0300 Subject: [PATCH 487/492] Zeppelin 1912-r1 release --- alluxio/pom.xml | 4 ++-- angular/pom.xml | 4 ++-- beam/pom.xml | 4 ++-- bigquery/pom.xml | 4 ++-- cassandra/pom.xml | 4 ++-- drill/pom.xml | 4 ++-- elasticsearch/pom.xml | 4 ++-- file/pom.xml | 4 ++-- flink/pom.xml | 4 ++-- geode/pom.xml | 4 ++-- groovy/pom.xml | 4 ++-- hbase/pom.xml | 4 ++-- helium-dev/pom.xml | 4 ++-- hive/pom.xml | 4 ++-- ignite/pom.xml | 4 ++-- interpreter-parent/pom.xml | 4 ++-- jdbc/pom.xml | 4 ++-- kylin/pom.xml | 4 ++-- lens/pom.xml | 4 ++-- livy/pom.xml | 4 ++-- maprdb/pom.xml | 4 ++-- markdown/pom.xml | 4 ++-- neo4j/pom.xml | 4 ++-- pig/pom.xml | 4 ++-- pom.xml | 2 +- python/pom.xml | 4 ++-- r/pom.xml | 4 ++-- sap/pom.xml | 4 ++-- scalding/pom.xml | 4 ++-- scio/pom.xml | 4 ++-- shell/pom.xml | 4 ++-- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 4 ++-- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-dependencies/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- zeppelin-display/pom.xml | 4 ++-- zeppelin-distribution/pom.xml | 2 +- zeppelin-examples/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-clock/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-horizontalbar/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-echo/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-markdown/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-translator/pom.xml | 4 ++-- zeppelin-integration/pom.xml | 4 ++-- zeppelin-interpreter/pom.xml | 4 ++-- zeppelin-jupyter/pom.xml | 4 ++-- zeppelin-server/pom.xml | 4 ++-- zeppelin-web/pom.xml | 4 ++-- zeppelin-zengine/pom.xml | 4 ++-- 55 files changed, 108 insertions(+), 108 deletions(-) diff --git a/alluxio/pom.xml b/alluxio/pom.xml index 8ce6e74a266..236f0f72093 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Alluxio interpreter diff --git a/angular/pom.xml b/angular/pom.xml index 6b21f10006a..0e0ab181556 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Angular interpreter diff --git a/beam/pom.xml b/beam/pom.xml index 1d9c459ec19..d2c029501e8 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Beam interpreter diff --git a/bigquery/pom.xml b/bigquery/pom.xml index 744b8648b42..fedbad6c030 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: BigQuery interpreter diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 713bd485879..7ba7c9040d0 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-cassandra_2.11 jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support diff --git a/drill/pom.xml b/drill/pom.xml index a55c44a14d3..72480ba26e4 100644 --- a/drill/pom.xml +++ b/drill/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-drill jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: JDBC Drill interpreter diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index 0bb19d818fb..fecfa6b8411 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent zeppelin-elasticsearch jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Elasticsearch interpreter diff --git a/file/pom.xml b/file/pom.xml index 87925ca0989..493b5e6bbd1 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: File System Interpreters diff --git a/flink/pom.xml b/flink/pom.xml index 9ee8da9dad4..a617539ce4d 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-flink_2.11 jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Flink Zeppelin flink support diff --git a/geode/pom.xml b/geode/pom.xml index 4e6d0d6ba8e..de559c40bdb 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Apache Geode interpreter diff --git a/groovy/pom.xml b/groovy/pom.xml index 3ef439b9ed3..af02145a02a 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Groovy interpreter diff --git a/hbase/pom.xml b/hbase/pom.xml index 9fd7bbfb3d2..9b41924a366 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: HBase interpreter diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index bd0cf24a8d5..b9d2d2dfa25 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -24,13 +24,13 @@ org.apache.zeppelin interpreter-parent - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Helium development interpreter diff --git a/hive/pom.xml b/hive/pom.xml index 9d90ee72a35..3c50708223e 100644 --- a/hive/pom.xml +++ b/hive/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-hive jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: JDBC Hive interpreter diff --git a/ignite/pom.xml b/ignite/pom.xml index e7b23b9196d..0956937abc2 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent zeppelin-ignite_2.11 jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Apache Ignite interpreter diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index 8244641a980..39be56c72e1 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin interpreter-parent pom - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Interpreter Parent diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 3621cc32688..59c52e72461 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: JDBC interpreter diff --git a/kylin/pom.xml b/kylin/pom.xml index 02102c3cbaf..ac3789792fa 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -23,7 +23,7 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent 4.0.0 @@ -31,7 +31,7 @@ org.apache.zeppelin zeppelin-kylin jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Kylin interpreter diff --git a/lens/pom.xml b/lens/pom.xml index 6f9e5c63dbe..4b923b4044f 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Lens interpreter diff --git a/livy/pom.xml b/livy/pom.xml index 67b1f5534a2..397caf5da67 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-livy jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Livy interpreter diff --git a/maprdb/pom.xml b/maprdb/pom.xml index 38dc35d9727..518ed60cbe1 100644 --- a/maprdb/pom.xml +++ b/maprdb/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-maprdb jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: MapR-DB Shell interpreter diff --git a/markdown/pom.xml b/markdown/pom.xml index facec56056f..ddd0a036d6d 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Markdown interpreter diff --git a/neo4j/pom.xml b/neo4j/pom.xml index 66463479e46..dcac9f386c4 100644 --- a/neo4j/pom.xml +++ b/neo4j/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-neo4j jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Neo4j interpreter diff --git a/pig/pom.xml b/pig/pom.xml index 2cab5845156..4a50bf283d1 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org diff --git a/pom.xml b/pom.xml index b7fcc12b274..0f4fe5dce5d 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin Zeppelin project http://zeppelin.apache.org diff --git a/python/pom.xml b/python/pom.xml index cc4d138dd72..8ea1eb95ce3 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Python interpreter diff --git a/r/pom.xml b/r/pom.xml index 77f8ba5cb4f..6c2637dcce7 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-zrinterpreter_${scala.binary.version} jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: R Interpreter R Interpreter for Zeppelin http://zeppelin.apache.org diff --git a/sap/pom.xml b/sap/pom.xml index 90a0aaf616f..c47b44e7e66 100644 --- a/sap/pom.xml +++ b/sap/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin sap jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Sap Zeppelin SAP support diff --git a/scalding/pom.xml b/scalding/pom.xml index 943d79e1ffb..b57e7cf082f 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-scalding_2.11 jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Scalding interpreter diff --git a/scio/pom.xml b/scio/pom.xml index 6f23c2cbae0..6c42af64cdc 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-scio_2.11 jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Scio Zeppelin Scio support diff --git a/shell/pom.xml b/shell/pom.xml index 2ec3fda16f9..4b9df8c4278 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Shell interpreter diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 2c5a5258261..01907d46786 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index bfe23e04372..063b74fdd10 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../interpreter-parent/pom.xml org.apache.zeppelin spark-parent pom - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Spark Parent Zeppelin Spark Support diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index 62258c9f7c5..e2b267853fa 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 jar Zeppelin: Spark Interpreter Scala_2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index d2ed0d39016..33b16e6270d 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 jar Zeppelin: Spark Interpreter Scala_2.11 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index e82fc409d98..0e17c62d3a3 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-spark-dependencies jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Spark dependencies Zeppelin spark support diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index 07997f7f39b..5239e8092a0 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ org.apache.zeppelin interpreter-parent - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../../interpreter-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 pom Zeppelin: Spark Scala Parent diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index ee00e2bf4d0..0aaa8ce639f 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index e1a6d26a027..8f6341531b3 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index 38a1df7e89a..4db0f9124d7 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 jar Zeppelin: Spark2 Shims diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index 16d2c716dc8..557ab8f7361 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-display jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Display system apis diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index f4e18499440..dc925a07071 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -23,7 +23,7 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 60c441fb61d..537c0a374e5 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-examples pom - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Examples Zeppelin examples diff --git a/zeppelin-examples/zeppelin-example-clock/pom.xml b/zeppelin-examples/zeppelin-example-clock/pom.xml index 34229a75b01..227e3962b86 100644 --- a/zeppelin-examples/zeppelin-example-clock/pom.xml +++ b/zeppelin-examples/zeppelin-example-clock/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index bb27659ff1a..75463fe1dbf 100644 --- a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml +++ b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Example application - Horizontal Bar chart diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml index 4ce562f99d6..6f056347161 100644 --- a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index 3a65e44b3b2..0cb2e2667c1 100644 --- a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index e0d3359fb1f..ec260bf895f 100644 --- a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml index a8b88baaa7b..1ab9b3a5095 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml index f1b04c1b937..077afab609d 100644 --- a/zeppelin-integration/pom.xml +++ b/zeppelin-integration/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-integration jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Integration Test diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index 00325399556..59d2b429b20 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Interpreter Zeppelin Interpreter diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index 846ca9bc67d..3635bb29833 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,13 +24,13 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. zeppelin-jupyter jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 74201d1d0fe..0867afcdb7c 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-server jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Server diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index a701b9737e8..18f9f2bb2b8 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-web war - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: web Application diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index f375eced655..b464e7d6cb4 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,14 +23,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 .. org.apache.zeppelin zeppelin-zengine jar - 0.8.2-mapr-SNAPSHOT + 0.8.2-mapr-1912-r1 Zeppelin: Zengine Zeppelin Zengine From 9be8cfe2bbaeb718a79d0bc4ebf557741ad552b8 Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Wed, 1 Jul 2020 16:41:06 +0300 Subject: [PATCH 488/492] Remove development options in Dockerfiles --- scripts/mapr-dsr/centos7/Dockerfile | 3 +-- scripts/mapr-dsr/kubeflow/Dockerfile | 3 +-- scripts/mapr-dsr/spark/Dockerfile | 3 +-- scripts/mapr-dsr/ubuntu16/Dockerfile | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/scripts/mapr-dsr/centos7/Dockerfile b/scripts/mapr-dsr/centos7/Dockerfile index dd855eb7ce3..74b86291393 100644 --- a/scripts/mapr-dsr/centos7/Dockerfile +++ b/scripts/mapr-dsr/centos7/Dockerfile @@ -1,4 +1,3 @@ -# syntax=docker/dockerfile:1.0.2-experimental FROM centos:centos7 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" @@ -11,7 +10,7 @@ RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ tar xf maven.tar.gz --strip-components=1 -C /opt/maven && \ ln -s /opt/maven/bin/mvn /usr/local/bin/mvn -RUN --mount=type=cache,id=mvncache,target=/root/.m2 --mount=type=cache,id=npmcache,target=/root/.npm --mount=type=ssh \ +RUN \ mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts && \ git clone "$ZEPPELIN_GIT_REPO" zeppelin && \ cd zeppelin && \ diff --git a/scripts/mapr-dsr/kubeflow/Dockerfile b/scripts/mapr-dsr/kubeflow/Dockerfile index 9fb422dd78c..88e58272b67 100644 --- a/scripts/mapr-dsr/kubeflow/Dockerfile +++ b/scripts/mapr-dsr/kubeflow/Dockerfile @@ -1,4 +1,3 @@ -# syntax=docker/dockerfile:1.0.2-experimental FROM centos:centos7 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" @@ -11,7 +10,7 @@ RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ tar xf maven.tar.gz --strip-components=1 -C /opt/maven && \ ln -s /opt/maven/bin/mvn /usr/local/bin/mvn -RUN --mount=type=cache,id=mvncache,target=/root/.m2 --mount=type=cache,id=npmcache,target=/root/.npm --mount=type=ssh \ +RUN \ mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts && \ git clone "$ZEPPELIN_GIT_REPO" zeppelin && \ cd zeppelin && \ diff --git a/scripts/mapr-dsr/spark/Dockerfile b/scripts/mapr-dsr/spark/Dockerfile index 3976e3b6b53..39ef5048abd 100644 --- a/scripts/mapr-dsr/spark/Dockerfile +++ b/scripts/mapr-dsr/spark/Dockerfile @@ -1,4 +1,3 @@ -# syntax=docker/dockerfile:1.0.2-experimental FROM centos:centos7 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" @@ -11,7 +10,7 @@ RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ tar xf maven.tar.gz --strip-components=1 -C /opt/maven && \ ln -s /opt/maven/bin/mvn /usr/local/bin/mvn -RUN --mount=type=cache,id=mvncache,target=/root/.m2 --mount=type=cache,id=npmcache,target=/root/.npm --mount=type=ssh \ +RUN \ mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts && \ git clone "$ZEPPELIN_GIT_REPO" zeppelin && \ cd zeppelin && \ diff --git a/scripts/mapr-dsr/ubuntu16/Dockerfile b/scripts/mapr-dsr/ubuntu16/Dockerfile index c564f694841..aaffd72f8aa 100644 --- a/scripts/mapr-dsr/ubuntu16/Dockerfile +++ b/scripts/mapr-dsr/ubuntu16/Dockerfile @@ -1,4 +1,3 @@ -# syntax=docker/dockerfile:1.0.2-experimental FROM ubuntu:16.04 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" @@ -13,7 +12,7 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ tar xf maven.tar.gz --strip-components=1 -C /opt/maven && \ ln -s /opt/maven/bin/mvn /usr/local/bin/mvn -RUN --mount=type=cache,id=mvncache,target=/root/.m2 --mount=type=cache,id=npmcache,target=/root/.npm --mount=type=ssh \ +RUN \ mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts && \ git clone "$ZEPPELIN_GIT_REPO" zeppelin && \ cd zeppelin && \ From 3fa27e2d65dbebc97b0d0dd52e1c868c53b8d67b Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Wed, 1 Jul 2020 16:51:19 +0300 Subject: [PATCH 489/492] Use HTTPS Zeppelin GitHub repository rather than SSH --- scripts/mapr-dsr/centos7/Dockerfile | 3 +-- scripts/mapr-dsr/ubuntu16/Dockerfile | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/mapr-dsr/centos7/Dockerfile b/scripts/mapr-dsr/centos7/Dockerfile index 74b86291393..8ef7c4069b3 100644 --- a/scripts/mapr-dsr/centos7/Dockerfile +++ b/scripts/mapr-dsr/centos7/Dockerfile @@ -1,6 +1,6 @@ FROM centos:centos7 as zeppelin_builder -ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" +ARG ZEPPELIN_GIT_REPO="https://github.com/mapr/zeppelin.git" ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r1" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" @@ -11,7 +11,6 @@ RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ ln -s /opt/maven/bin/mvn /usr/local/bin/mvn RUN \ - mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts && \ git clone "$ZEPPELIN_GIT_REPO" zeppelin && \ cd zeppelin && \ git checkout "$ZEPPELIN_GIT_TAG" && \ diff --git a/scripts/mapr-dsr/ubuntu16/Dockerfile b/scripts/mapr-dsr/ubuntu16/Dockerfile index aaffd72f8aa..9e602c97520 100644 --- a/scripts/mapr-dsr/ubuntu16/Dockerfile +++ b/scripts/mapr-dsr/ubuntu16/Dockerfile @@ -1,6 +1,6 @@ FROM ubuntu:16.04 as zeppelin_builder -ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" +ARG ZEPPELIN_GIT_REPO="https://github.com/mapr/zeppelin.git" ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r1" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" @@ -13,7 +13,6 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ ln -s /opt/maven/bin/mvn /usr/local/bin/mvn RUN \ - mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts && \ git clone "$ZEPPELIN_GIT_REPO" zeppelin && \ cd zeppelin && \ git checkout "$ZEPPELIN_GIT_TAG" && \ From eee749cd9005a0f5e75449e74e807b894d61d8da Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Thu, 26 Nov 2020 18:55:11 +0200 Subject: [PATCH 490/492] Update release to 0.8.2-mapr-1912-r2 --- alluxio/pom.xml | 4 ++-- angular/pom.xml | 4 ++-- beam/pom.xml | 4 ++-- bigquery/pom.xml | 4 ++-- cassandra/pom.xml | 4 ++-- drill/pom.xml | 4 ++-- elasticsearch/pom.xml | 4 ++-- file/pom.xml | 4 ++-- flink/pom.xml | 4 ++-- geode/pom.xml | 4 ++-- groovy/pom.xml | 4 ++-- hbase/pom.xml | 4 ++-- helium-dev/pom.xml | 4 ++-- hive/pom.xml | 4 ++-- ignite/pom.xml | 4 ++-- interpreter-parent/pom.xml | 4 ++-- jdbc/pom.xml | 4 ++-- kylin/pom.xml | 4 ++-- lens/pom.xml | 4 ++-- livy/pom.xml | 4 ++-- maprdb/pom.xml | 4 ++-- markdown/pom.xml | 4 ++-- neo4j/pom.xml | 4 ++-- pig/pom.xml | 4 ++-- pom.xml | 2 +- python/pom.xml | 4 ++-- r/pom.xml | 4 ++-- sap/pom.xml | 4 ++-- scalding/pom.xml | 4 ++-- scio/pom.xml | 4 ++-- shell/pom.xml | 4 ++-- spark/interpreter/pom.xml | 4 ++-- spark/pom.xml | 4 ++-- spark/scala-2.10/pom.xml | 4 ++-- spark/scala-2.11/pom.xml | 4 ++-- spark/spark-dependencies/pom.xml | 4 ++-- spark/spark-scala-parent/pom.xml | 4 ++-- spark/spark-shims/pom.xml | 4 ++-- spark/spark1-shims/pom.xml | 4 ++-- spark/spark2-shims/pom.xml | 4 ++-- zeppelin-display/pom.xml | 4 ++-- zeppelin-distribution/pom.xml | 2 +- zeppelin-examples/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-clock/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-horizontalbar/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-echo/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-markdown/pom.xml | 4 ++-- zeppelin-examples/zeppelin-example-spell-translator/pom.xml | 4 ++-- zeppelin-integration/pom.xml | 4 ++-- zeppelin-interpreter/pom.xml | 4 ++-- zeppelin-jupyter/pom.xml | 4 ++-- zeppelin-server/pom.xml | 4 ++-- zeppelin-web/pom.xml | 4 ++-- zeppelin-zengine/pom.xml | 4 ++-- 55 files changed, 108 insertions(+), 108 deletions(-) diff --git a/alluxio/pom.xml b/alluxio/pom.xml index 236f0f72093..97b9511fd67 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Alluxio interpreter diff --git a/angular/pom.xml b/angular/pom.xml index 0e0ab181556..474a8e737c2 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Angular interpreter diff --git a/beam/pom.xml b/beam/pom.xml index d2c029501e8..2f3f3fc5d92 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Beam interpreter diff --git a/bigquery/pom.xml b/bigquery/pom.xml index fedbad6c030..ebcd12738fd 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: BigQuery interpreter diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 7ba7c9040d0..00227175585 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-cassandra_2.11 jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support diff --git a/drill/pom.xml b/drill/pom.xml index 72480ba26e4..2d8d6bc00af 100644 --- a/drill/pom.xml +++ b/drill/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-drill jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: JDBC Drill interpreter diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index fecfa6b8411..eadcd688787 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent zeppelin-elasticsearch jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Elasticsearch interpreter diff --git a/file/pom.xml b/file/pom.xml index 493b5e6bbd1..e6b6ffa56b9 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: File System Interpreters diff --git a/flink/pom.xml b/flink/pom.xml index a617539ce4d..977f72e3b86 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-flink_2.11 jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Flink Zeppelin flink support diff --git a/geode/pom.xml b/geode/pom.xml index de559c40bdb..fa7e0cfed7c 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Apache Geode interpreter diff --git a/groovy/pom.xml b/groovy/pom.xml index af02145a02a..83bc0ec7f3a 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Groovy interpreter diff --git a/hbase/pom.xml b/hbase/pom.xml index 9b41924a366..bec2529bb14 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: HBase interpreter diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index b9d2d2dfa25..bee308bb7d5 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -24,13 +24,13 @@ org.apache.zeppelin interpreter-parent - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Helium development interpreter diff --git a/hive/pom.xml b/hive/pom.xml index 3c50708223e..945887157fa 100644 --- a/hive/pom.xml +++ b/hive/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-hive jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: JDBC Hive interpreter diff --git a/ignite/pom.xml b/ignite/pom.xml index 0956937abc2..a64bc9eeb53 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -22,13 +22,13 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent zeppelin-ignite_2.11 jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Apache Ignite interpreter diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml index 39be56c72e1..561a31bd461 100644 --- a/interpreter-parent/pom.xml +++ b/interpreter-parent/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin interpreter-parent pom - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Interpreter Parent diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 59c52e72461..0332e998a11 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: JDBC interpreter diff --git a/kylin/pom.xml b/kylin/pom.xml index ac3789792fa..1fce0871f67 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -23,7 +23,7 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent 4.0.0 @@ -31,7 +31,7 @@ org.apache.zeppelin zeppelin-kylin jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Kylin interpreter diff --git a/lens/pom.xml b/lens/pom.xml index 4b923b4044f..a31bff67be9 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Lens interpreter diff --git a/livy/pom.xml b/livy/pom.xml index 397caf5da67..66f1adc2189 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-livy jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Livy interpreter diff --git a/maprdb/pom.xml b/maprdb/pom.xml index 518ed60cbe1..53d8e3222ae 100644 --- a/maprdb/pom.xml +++ b/maprdb/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-maprdb jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: MapR-DB Shell interpreter diff --git a/markdown/pom.xml b/markdown/pom.xml index ddd0a036d6d..0fea4cff9b5 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Markdown interpreter diff --git a/neo4j/pom.xml b/neo4j/pom.xml index dcac9f386c4..1504d40684f 100644 --- a/neo4j/pom.xml +++ b/neo4j/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-neo4j jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Neo4j interpreter diff --git a/pig/pom.xml b/pig/pom.xml index 4a50bf283d1..8c5728ecce5 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org diff --git a/pom.xml b/pom.xml index 0f4fe5dce5d..2beaa79a4dd 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin Zeppelin project http://zeppelin.apache.org diff --git a/python/pom.xml b/python/pom.xml index 8ea1eb95ce3..a31499015ba 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Python interpreter diff --git a/r/pom.xml b/r/pom.xml index 6c2637dcce7..81bbdce4bc6 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -23,14 +23,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-zrinterpreter_${scala.binary.version} jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: R Interpreter R Interpreter for Zeppelin http://zeppelin.apache.org diff --git a/sap/pom.xml b/sap/pom.xml index c47b44e7e66..0f7a14cd474 100644 --- a/sap/pom.xml +++ b/sap/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin sap jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Sap Zeppelin SAP support diff --git a/scalding/pom.xml b/scalding/pom.xml index b57e7cf082f..228750cb5db 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-scalding_2.11 jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Scalding interpreter diff --git a/scio/pom.xml b/scio/pom.xml index 6c42af64cdc..093ad763155 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-scio_2.11 jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Scio Zeppelin Scio support diff --git a/shell/pom.xml b/shell/pom.xml index 4b9df8c4278..ca1b2ba83eb 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -22,14 +22,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Shell interpreter diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml index 01907d46786..7b41f9a7112 100644 --- a/spark/interpreter/pom.xml +++ b/spark/interpreter/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../pom.xml org.apache.zeppelin spark-interpreter jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Spark Interpreter Zeppelin spark support diff --git a/spark/pom.xml b/spark/pom.xml index 063b74fdd10..ccc85892295 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -24,14 +24,14 @@ interpreter-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../interpreter-parent/pom.xml org.apache.zeppelin spark-parent pom - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Spark Parent Zeppelin Spark Support diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml index e2b267853fa..1bf458c6e8a 100644 --- a/spark/scala-2.10/pom.xml +++ b/spark/scala-2.10/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.10 - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 jar Zeppelin: Spark Interpreter Scala_2.10 diff --git a/spark/scala-2.11/pom.xml b/spark/scala-2.11/pom.xml index 33b16e6270d..f4a4ab630d2 100644 --- a/spark/scala-2.11/pom.xml +++ b/spark/scala-2.11/pom.xml @@ -21,14 +21,14 @@ org.apache.zeppelin spark-scala-parent - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../spark-scala-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-2.11 - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 jar Zeppelin: Spark Interpreter Scala_2.11 diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml index 0e17c62d3a3..0a45079c8fd 100644 --- a/spark/spark-dependencies/pom.xml +++ b/spark/spark-dependencies/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-spark-dependencies jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Spark dependencies Zeppelin spark support diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml index 5239e8092a0..9b7adec3b49 100644 --- a/spark/spark-scala-parent/pom.xml +++ b/spark/spark-scala-parent/pom.xml @@ -23,14 +23,14 @@ org.apache.zeppelin interpreter-parent - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../../interpreter-parent/pom.xml 4.0.0 org.apache.zeppelin spark-scala-parent - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 pom Zeppelin: Spark Scala Parent diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml index 0aaa8ce639f..93f8c5af4fc 100644 --- a/spark/spark-shims/pom.xml +++ b/spark/spark-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../pom.xml 4.0.0 org.apache.zeppelin spark-shims - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 jar Zeppelin: Spark Shims diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml index 8f6341531b3..a645559b097 100644 --- a/spark/spark1-shims/pom.xml +++ b/spark/spark1-shims/pom.xml @@ -23,14 +23,14 @@ spark-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../pom.xml 4.0.0 org.apache.zeppelin spark1-shims - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 jar Zeppelin: Spark1 Shims diff --git a/spark/spark2-shims/pom.xml b/spark/spark2-shims/pom.xml index 4db0f9124d7..d4645b92c2e 100644 --- a/spark/spark2-shims/pom.xml +++ b/spark/spark2-shims/pom.xml @@ -22,14 +22,14 @@ spark-parent org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 ../pom.xml 4.0.0 org.apache.zeppelin spark2-shims - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 jar Zeppelin: Spark2 Shims diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index 557ab8f7361..72e7b4b62c3 100644 --- a/zeppelin-display/pom.xml +++ b/zeppelin-display/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-display jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Display system apis diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index dc925a07071..2137fcef782 100644 --- a/zeppelin-distribution/pom.xml +++ b/zeppelin-distribution/pom.xml @@ -23,7 +23,7 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 537c0a374e5..04a2aa14d17 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-examples pom - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Examples Zeppelin examples diff --git a/zeppelin-examples/zeppelin-example-clock/pom.xml b/zeppelin-examples/zeppelin-example-clock/pom.xml index 227e3962b86..754abd05f21 100644 --- a/zeppelin-examples/zeppelin-example-clock/pom.xml +++ b/zeppelin-examples/zeppelin-example-clock/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index 75463fe1dbf..79d88c39689 100644 --- a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml +++ b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Example application - Horizontal Bar chart diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml index 6f056347161..4729c555c21 100644 --- a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index 0cb2e2667c1..488d971e9ba 100644 --- a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index ec260bf895f..59608cbc430 100644 --- a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml index 1ab9b3a5095..2adf052a1cb 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml index 077afab609d..3e8dbeb3da1 100644 --- a/zeppelin-integration/pom.xml +++ b/zeppelin-integration/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-integration jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Integration Test diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index 59d2b429b20..f684f7759f3 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,14 +24,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Interpreter Zeppelin Interpreter diff --git a/zeppelin-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index 3635bb29833..5df73f3662b 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,13 +24,13 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. zeppelin-jupyter jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 0867afcdb7c..2fd6517c942 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-server jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Server diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 18f9f2bb2b8..cd7fcecdfea 100644 --- a/zeppelin-web/pom.xml +++ b/zeppelin-web/pom.xml @@ -22,14 +22,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-web war - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: web Application diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index b464e7d6cb4..8637f1e3577 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,14 +23,14 @@ zeppelin org.apache.zeppelin - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-zengine jar - 0.8.2-mapr-1912-r1 + 0.8.2-mapr-1912-r2 Zeppelin: Zengine Zeppelin Zengine From e7816e5e2cf4a004d1c18e64b786d251ad6705af Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Thu, 26 Nov 2020 18:56:26 +0200 Subject: [PATCH 491/492] Update ZEPPELIN_GIT_TAG to 0.8.2-mapr-1912-r2 --- scripts/mapr-dsr/build.sh | 2 +- scripts/mapr-dsr/centos7/Dockerfile | 2 +- scripts/mapr-dsr/kubeflow/Dockerfile | 2 +- scripts/mapr-dsr/spark/Dockerfile | 2 +- scripts/mapr-dsr/ubuntu16/Dockerfile | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/mapr-dsr/build.sh b/scripts/mapr-dsr/build.sh index b3ba5b165ad..627b464e9c0 100755 --- a/scripts/mapr-dsr/build.sh +++ b/scripts/mapr-dsr/build.sh @@ -47,7 +47,7 @@ if [ "$RELEASE" = true ]; then DOCKER_REPO=${DOCKER_REPO:-"maprtech/data-science-refinery"} IMAGE_VERSION=${IMAGE_VERSION:-"${MAPR_VERSION_DSR}_${MAPR_VERSION_CORE}_${MAPR_VERSION_MEP}"} ZEPPELIN_GIT_REPO=${ZEPPELIN_GIT_REPO:-"git@github.com:mapr/zeppelin.git"} - ZEPPELIN_GIT_TAG=${ZEPPELIN_GIT_TAG:-"0.8.2-mapr-1912-r1"} + ZEPPELIN_GIT_TAG=${ZEPPELIN_GIT_TAG:-"0.8.2-mapr-1912-r2"} MAPR_REPO_ROOT=${MAPR_REPO_ROOT:-"https://package.mapr.com/releases"} MAPR_MAVEN_REPO=${MAPR_MAVEN_REPO:-"http://repository.mapr.com/maven/"} else diff --git a/scripts/mapr-dsr/centos7/Dockerfile b/scripts/mapr-dsr/centos7/Dockerfile index 8ef7c4069b3..6278b036c06 100644 --- a/scripts/mapr-dsr/centos7/Dockerfile +++ b/scripts/mapr-dsr/centos7/Dockerfile @@ -1,7 +1,7 @@ FROM centos:centos7 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="https://github.com/mapr/zeppelin.git" -ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r1" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r2" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ diff --git a/scripts/mapr-dsr/kubeflow/Dockerfile b/scripts/mapr-dsr/kubeflow/Dockerfile index 88e58272b67..2b06653368f 100644 --- a/scripts/mapr-dsr/kubeflow/Dockerfile +++ b/scripts/mapr-dsr/kubeflow/Dockerfile @@ -1,7 +1,7 @@ FROM centos:centos7 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" -ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r1" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r2" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ diff --git a/scripts/mapr-dsr/spark/Dockerfile b/scripts/mapr-dsr/spark/Dockerfile index 39ef5048abd..8ea6612237e 100644 --- a/scripts/mapr-dsr/spark/Dockerfile +++ b/scripts/mapr-dsr/spark/Dockerfile @@ -1,7 +1,7 @@ FROM centos:centos7 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="git@github.com:mapr/zeppelin.git" -ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r1" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r2" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" RUN yum install -y git wget java-1.8.0-openjdk-devel which bzip2 && \ diff --git a/scripts/mapr-dsr/ubuntu16/Dockerfile b/scripts/mapr-dsr/ubuntu16/Dockerfile index 9e602c97520..b61e00fa0a3 100644 --- a/scripts/mapr-dsr/ubuntu16/Dockerfile +++ b/scripts/mapr-dsr/ubuntu16/Dockerfile @@ -1,7 +1,7 @@ FROM ubuntu:16.04 as zeppelin_builder ARG ZEPPELIN_GIT_REPO="https://github.com/mapr/zeppelin.git" -ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r1" +ARG ZEPPELIN_GIT_TAG="0.8.2-mapr-1912-r2" ARG MAPR_MAVEN_REPO="http://repository.mapr.com/maven/" RUN export DEBIAN_FRONTEND=noninteractive && \ From fee9dcd8421b18fd8f43d870758ee22979e5e6be Mon Sep 17 00:00:00 2001 From: Ivan Dzikovskyi Date: Thu, 26 Nov 2020 18:57:37 +0200 Subject: [PATCH 492/492] Change DSR version to 1.4.1.2 --- scripts/mapr-dsr/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mapr-dsr/build.sh b/scripts/mapr-dsr/build.sh index 627b464e9c0..ffd0837f7c3 100755 --- a/scripts/mapr-dsr/build.sh +++ b/scripts/mapr-dsr/build.sh @@ -1,6 +1,6 @@ #!/bin/sh -MAPR_VERSION_DSR=${MAPR_VERSION_DSR:-"v1.4.1"} +MAPR_VERSION_DSR=${MAPR_VERSION_DSR:-"v1.4.1.2"} MAPR_VERSION_CORE=${MAPR_VERSION_CORE:-"6.1.0"} MAPR_VERSION_MEP=${MAPR_VERSION_MEP:-"6.3.0"}