diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE index 526b4147827..25b263abea7 100644 --- a/.github/PULL_REQUEST_TEMPLATE +++ b/.github/PULL_REQUEST_TEMPLATE @@ -14,7 +14,9 @@ First time? Check out the contributing guide - https://zeppelin.apache.org/contr * Put link here, and add [ZEPPELIN-*Jira number*] in PR title, eg. [ZEPPELIN-533] ### How should this be tested? -Outline the steps to test the PR here. +* 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. ### Screenshots (if appropriate) 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/.travis.yml b/.travis.yml index 099fb385d60..e01a71544c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,9 @@ language: java sudo: false +before_cache: + - sudo chown -R travis:travis $HOME/.m2 + cache: apt: true directories: @@ -30,21 +33,21 @@ cache: addons: apt: sources: - - r-packages-precise + - r-packages-trusty packages: - r-base-dev 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: # Test License compliance using RAT tool - - jdk: "oraclejdk7" - dist: precise - env: SCALA_VER="2.11" SPARK_VER="2.0.2" HADOOP_VER="2.6" PROFILE="-Prat" BUILD_FLAG="clean" TEST_FLAG="org.apache.rat:apache-rat-plugin:check" TEST_PROJECTS="" + - jdk: "openjdk7" + dist: trusty + 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 @@ -53,69 +56,82 @@ 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: CI="true" WEB_E2E="true" PYTHON="2" SCALA_VER="2.11" SPARK_VER="2.1.0" HADOOP_VER="2.6" PROFILE="-Phadoop2 -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: # HeliumApplicationFactoryTest - https://issues.apache.org/jira/browse/ZEPPELIN-2470 # 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" + - sudo: required + jdk: "oraclejdk8" + dist: trusty + env: PYTHON="3" SPARKR="true" 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=**/SparkIntegrationTest.java,**/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" + - jdk: "oraclejdk8" + dist: trusty + addons: + firefox: "31.0" + env: CI="true" 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: "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="" + - jdk: "openjdk7" + dist: trusty + env: PYTHON="3" 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="" - # 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" - - # 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" - - # 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" - - # 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" - - # 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" - - # Test python/pyspark with python 2, livy 0.2 + # Run Spark integration test and unit test separately for each spark version + + # ZeppelinSparkClusterTest24, SparkIntegrationTest24, JdbcIntegrationTest, Unit test of Spark 2.4 + - sudo: required + jdk: "oraclejdk8" + dist: trusty + env: PYTHON="2" SCALA_VER="2.11" PROFILE="-Pspark-2.4 -Pscala-2.11 -Phadoop2 -Pintegration" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat -am" TEST_FLAG="test -DskipRat -am" MODULES="-pl zeppelin-server,zeppelin-web,spark/interpreter,spark/spark-dependencies" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest24,SparkIntegrationTest24,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + + # ZeppelinSparkClusterTest23, SparkIntegrationTest23, Unit test of Spark 2.3 + - sudo: required + jdk: "oraclejdk8" + dist: trusty + env: PYTHON="2" SCALA_VER="2.11" PROFILE="-Pspark-2.3 -Pscala-2.11 -Phadoop2 -Pintegration" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat -am" TEST_FLAG="test -DskipRat -am" MODULES="-pl zeppelin-server,zeppelin-web,spark/interpreter,spark/spark-dependencies" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest23,SparkIntegrationTest23,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + + # ZeppelinSparkClusterTest22, SparkIntegrationTest22, Unit test of Spark 2.2 + - sudo: required + jdk: "oraclejdk8" + dist: trusty + env: PYTHON="3" SCALA_VER="2.10" PROFILE="-Pspark-2.2 -Pscala-2.10 -Phadoop2 -Pintegration" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat -am" TEST_FLAG="test -DskipRat -am" MODULES="-pl zeppelin-server,zeppelin-web,spark/interpreter,spark/spark-dependencies" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest22,SparkIntegrationTest22,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + + # ZeppelinSparkClusterTest21, SparkIntegrationTest21, Unit test of Spark 2.1 - 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" + jdk: "oraclejdk8" + dist: trusty + env: PYTHON="3" SCALA_VER="2.10" PROFILE="-Pspark-2.1 -Phadoop2 -Pscala-2.10 -Pintegration" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat -am" TEST_FLAG="test -DskipRat -am" MODULES="-pl zeppelin-server,zeppelin-web,spark/interpreter,spark/spark-dependencies" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest21,SparkIntegrationTest21,org.apache.zeppelin.spark.* -DfailIfNoTests=false" - # Test python/pyspark with python 3, livy 0.3 + # ZeppelinSparkClusterTest20, SparkIntegrationTest20, Unit test of Spark 2.0 - 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" + jdk: "oraclejdk8" + dist: trusty + env: PYTHON="3" SCALA_VER="2.10" PROFILE="-Pspark-2.0 -Phadoop2 -Pscala-2.10 -Pintegration" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat -am" TEST_FLAG="test -DskipRat -am" MODULES="-pl zeppelin-server,zeppelin-web,spark/interpreter,spark/spark-dependencies" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest20,SparkIntegrationTest20,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + + # ZeppelinSparkClusterTest16, SparkIntegrationTest16, Unit test of Spark 1.6 + - sudo: required + jdk: "oraclejdk8" + dist: trusty + env: PYTHON="3" SCALA_VER="2.10" PROFILE="-Pspark-1.6 -Phadoop2 -Pscala-2.10 -Pintegration" SPARKR="true" BUILD_FLAG="install -DskipTests -DskipRat -am" TEST_FLAG="test -DskipRat -am" MODULES="-pl zeppelin-server,zeppelin-web,spark/interpreter,spark/spark-dependencies" TEST_PROJECTS="-Dtest=ZeppelinSparkClusterTest16,SparkIntegrationTest16,org.apache.zeppelin.spark.* -DfailIfNoTests=false" + + # Test python/pyspark with python 2, livy 0.5 + - sudo: required + dist: trusty + jdk: "openjdk7" + env: PYTHON="2" SPARK_VER="1.6.3" HADOOP_VER="2.6" LIVY_VER="0.5.0-incubating" PROFILE="" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl livy" TEST_PROJECTS="" + + # Test livy 0.5 with spark 2.2.0 under python3 + - sudo: required + dist: trusty + jdk: "openjdk8" + env: PYTHON="3" SPARK_VER="2.2.0" HADOOP_VER="2.6" LIVY_VER="0.5.0-incubating" PROFILE="" BUILD_FLAG="install -am -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" MODULES="-pl livy" TEST_PROJECTS="" before_install: # check files included in commit range, clear bower_components if a bower.json file has changed. @@ -125,13 +141,13 @@ before_install: - hasbowerchanged=$(echo $changedfiles | grep -c "bower.json" || true); - gitlog=$(git log $TRAVIS_COMMIT_RANGE 2>/dev/null) || gitlog="" - clearcache=$(echo $gitlog | grep -c -E "clear bower|bower clear" || true) - - if [ "$hasbowerchanged" -gt 0 ] || [ "$clearcache" -gt 0 ]; then echo "Clearing bower_components cache"; rm -r zeppelin-web/bower_components; npm cache clear; else echo "Using cached bower_components."; fi + - if [ "$hasbowerchanged" -gt 0 ] || [ "$clearcache" -gt 0 ]; then echo "Clearing bower_components cache"; rm -r zeppelin-web/bower_components; npm cache verify; else echo "Using cached bower_components."; fi - echo "MAVEN_OPTS='-Xms1024M -Xmx2048M -XX:MaxPermSize=1024m -XX:-UseGCOverheadLimit -Dorg.slf4j.simpleLogger.defaultLogLevel=warn'" >> ~/.mavenrc - ./testing/install_external_dependencies.sh - 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: @@ -141,10 +157,13 @@ install: before_script: - if [[ -n $SPARK_VER ]]; then travis_retry ./testing/downloadSpark.sh $SPARK_VER $HADOOP_VER; fi - if [[ -n $LIVY_VER ]]; then ./testing/downloadLivy.sh $LIVY_VER; fi - - if [[ -n $LIVY_VER ]]; then export LIVY_HOME=`pwd`/livy-server-$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 - - 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 @@ -162,15 +181,13 @@ 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 - cat spark-*/logs/* - cat livy/target/tmp/*/output.log - - ls -R livy/target/tmp/MiniYarnMain/target/com.cloudera.livy.test.framework.MiniYarnMain/* - - cat livy/target/tmp/MiniYarnMain/target/com.cloudera.livy.test.framework.MiniYarnMain/*/*/*/stdout - - cat livy/target/tmp/MiniYarnMain/target/com.cloudera.livy.test.framework.MiniYarnMain/*/*/*/stderr - cat livy/target/tmp/livy-int-test/*/output.log - - ls -R livy/target/tmp/livy-int-test/MiniYarnMain/target/com.cloudera.livy.test.framework.MiniYarnMain/* - - cat livy/target/tmp/livy-int-test/MiniYarnMain/target/com.cloudera.livy.test.framework.MiniYarnMain/*/*/*/stdout - - cat livy/target/tmp/livy-int-test/MiniYarnMain/target/com.cloudera.livy.test.framework.MiniYarnMain/*/*/*/stderr + - ls -R livy/target/tmp/livy-int-test/MiniYarnMain/target/org.apache.livy.test.framework.MiniYarnMain/* + - cat livy/target/tmp/livy-int-test/MiniYarnMain/target/org.apache.livy.test.framework.MiniYarnMain/*/*/*/stdout + - cat livy/target/tmp/livy-int-test/MiniYarnMain/target/org.apache.livy.test.framework.MiniYarnMain/*/*/*/stderr diff --git a/LICENSE b/LICENSE index a6c02de9caf..3b340531246 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 @@ -256,6 +257,9 @@ 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) + (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 @@ -269,6 +273,10 @@ The following components are provided under the BSD 3-Clause license. See file (BSD 3 Clause) portions of Scala (http://www.scala-lang.org/download) - http://www.scala-lang.org/download/#License r/src/main/scala/scala/Console.scala + (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 ======================================================================== @@ -282,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/README.md b/README.md index e12d2aedb51..126520a3a5d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Apache Zeppelin -**Documentation:** [User Guide](http://zeppelin.apache.org/docs/latest/index.html)
-**Mailing Lists:** [User and Dev mailing list](http://zeppelin.apache.org/community.html)
+**Documentation:** [User Guide](https://zeppelin.apache.org/docs/latest/index.html)
+**Mailing Lists:** [User and Dev mailing list](https://zeppelin.apache.org/community.html)
**Continuous Integration:** [![Build Status](https://travis-ci.org/apache/zeppelin.svg?branch=master)](https://travis-ci.org/apache/zeppelin)
**Contributing:** [Contribution Guide](https://zeppelin.apache.org/contribution/contributions.html)
**Issue Tracker:** [Jira](https://issues.apache.org/jira/browse/ZEPPELIN)
@@ -15,15 +15,15 @@ Core feature: * Built-in Apache Spark support -To know more about Zeppelin, visit our web site [http://zeppelin.apache.org](http://zeppelin.apache.org) +To know more about Zeppelin, visit our web site [http://zeppelin.apache.org](https://zeppelin.apache.org) ## Getting Started ### Install binary package -Please go to [install](http://zeppelin.apache.org/docs/snapshot/install/install.html) to install Apache Zeppelin from binary package. +Please go to [install](https://zeppelin.apache.org/docs/latest/install/install.html) to install Apache Zeppelin from binary package. ### Build from source -Please check [Build from source](http://zeppelin.apache.org/docs/snapshot/install/build.html) to build Zeppelin from source. +Please check [Build from source](https://zeppelin.apache.org/docs/latest/install/build.html) to build Zeppelin from source. diff --git a/alluxio/pom.xml b/alluxio/pom.xml index 38135b81793..97b9511fd67 100644 --- a/alluxio/pom.xml +++ b/alluxio/pom.xml @@ -20,20 +20,21 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-alluxio jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Alluxio interpreter 1.0.0 + alluxio @@ -47,6 +48,7 @@ com.google.guava guava + 15.0 @@ -128,54 +130,12 @@ maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/alluxio - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/alluxio - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin diff --git a/alluxio/src/main/resources/interpreter-setting.json b/alluxio/src/main/resources/interpreter-setting.json index b9ab898e18b..7a8cd62d51b 100644 --- a/alluxio/src/main/resources/interpreter-setting.json +++ b/alluxio/src/main/resources/interpreter-setting.json @@ -20,7 +20,8 @@ } }, "editor": { - "editOnDblClick": false + "editOnDblClick": false, + "completionSupport": true } } ] diff --git a/angular/pom.xml b/angular/pom.xml index be43e496a49..474a8e737c2 100644 --- a/angular/pom.xml +++ b/angular/pom.xml @@ -20,18 +20,22 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-angular jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Angular interpreter + + angular + + ${project.groupId} @@ -61,54 +65,12 @@ maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/angular - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/angular - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin diff --git a/angular/src/main/resources/interpreter-setting.json b/angular/src/main/resources/interpreter-setting.json index 4ff59781b48..723348d25e9 100644 --- a/angular/src/main/resources/interpreter-setting.json +++ b/angular/src/main/resources/interpreter-setting.json @@ -6,7 +6,8 @@ "properties": { }, "editor": { - "editOnDblClick": true + "editOnDblClick": true, + "completionSupport": false } } ] 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..2f3f3fc5d92 100644 --- a/beam/pom.xml +++ b/beam/pom.xml @@ -20,27 +20,28 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-beam jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Beam interpreter 2.3.0 1.6.2 - 0.2.0-incubating + 2.0.0 4.1.1.Final 3.1.0 1.3 + beam @@ -52,7 +53,7 @@ org.apache.spark - spark-core_2.10 + spark-core_2.11 ${beam.spark.version} @@ -64,15 +65,15 @@ io.netty - akka-actor_2.10 + akka-actor_2.11 org.spark-project.akka - akka-remote_2.10 + akka-remote_2.11 org.spark-project.akka - akka-slf4j_2.10 + akka-slf4j_2.11 org.spark-project.akka @@ -80,7 +81,7 @@ org.apache.spark - spark-streaming_2.10 + spark-streaming_2.11 ${beam.spark.version} @@ -211,6 +212,14 @@ ${beam.beam.version} jar + + + org.apache.beam + beam-runners-flink_${scala.binary.version} + ${beam.beam.version} + + + ${project.groupId} @@ -232,69 +241,18 @@ + - - - org.apache.maven.plugins - maven-deploy-plugin - - true - - - maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/beam - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/beam - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - - + + maven-resources-plugin + diff --git a/bigquery/README.md b/bigquery/README.md index fc097631a29..0dff5feb7c8 100644 --- a/bigquery/README.md +++ b/bigquery/README.md @@ -1,10 +1,6 @@ # Overview BigQuery interpreter for Apache Zeppelin -# Pre requisities -You can follow the instructions at [Apache Zeppelin on Dataproc](https://github.com/GoogleCloudPlatform/dataproc-initialization-actions/blob/master/apache-zeppelin/README.MD) to bring up Zeppelin on Google dataproc. -You could also install and bring up Zeppelin on Google compute Engine. - # Unit Tests BigQuery Unit tests are excluded as these tests depend on the BigQuery external service. This is because BigQuery does not have a local mock at this point. @@ -14,34 +10,6 @@ If you like to run these tests manually, please follow the following steps: * Copy the project ID that you created and add it to the property "projectId" in `resources/constants.json` * Run the command mvn -Dbigquery.text.exclude='' test -pl bigquery -am - -# Interpreter Configuration - -Configure the following properties during Interpreter creation. - - - - - - - - - - - - - - - - - - - - - - -
NameDefault ValueDescription
zeppelin.bigquery.project_id Google Project Id
zeppelin.bigquery.wait_time5000Query Timeout in Milliseconds
zeppelin.bigquery.max_no_of_rows100000Max result set size
- # Connection The Interpreter opens a connection with the BigQuery Service using the supplied Google project ID and the compute environment variables. @@ -51,59 +19,6 @@ The Interpreter opens a connection with the BigQuery Service using the supplied We have used the curated veneer version of the Java APIs versus [Idiomatic Java client] (https://github.com/GoogleCloudPlatform/gcloud-java/tree/master/gcloud-java-bigquery) to build the interpreter. This is mainly for usability reasons. -# Enabling the BigQuery Interpreter - -In a notebook, to enable the **BigQuery** interpreter, click the **Gear** icon and select **bigquery**. - -# Using the BigQuery Interpreter - -In a paragraph, use `%bigquery.sql` to select the **BigQuery** interpreter and then input SQL statements against your datasets stored in BigQuery. -You can use [BigQuery SQL Reference](https://cloud.google.com/bigquery/query-reference) to build your own SQL. - -For Example, SQL to query for top 10 departure delays across airports using the flights public dataset - -```bash -%bigquery.sql -SELECT departure_airport,count(case when departure_delay>0 then 1 else 0 end) as no_of_delays -FROM [bigquery-samples:airline_ontime_data.flights] -group by departure_airport -order by 2 desc -limit 10 -``` - -Another Example, SQL to query for most commonly used java packages from the github data hosted in BigQuery - -```bash -%bigquery.sql -SELECT - package, - COUNT(*) count -FROM ( - SELECT - REGEXP_EXTRACT(line, r' ([a-z0-9\._]*)\.') package, - id - FROM ( - SELECT - SPLIT(content, '\n') line, - id - FROM - [bigquery-public-data:github_repos.sample_contents] - WHERE - content CONTAINS 'import' - AND sample_path LIKE '%.java' - HAVING - LEFT(line, 6)='import' ) - GROUP BY - package, - id ) -GROUP BY - 1 -ORDER BY - count DESC -LIMIT - 40 -``` - # Sample Screenshot ![Zeppelin BigQuery](https://cloud.githubusercontent.com/assets/10060731/16938817/b9213ea0-4db6-11e6-8c3b-8149a0bdf874.png) diff --git a/bigquery/pom.xml b/bigquery/pom.xml index f974b988153..ebcd12738fd 100644 --- a/bigquery/pom.xml +++ b/bigquery/pom.xml @@ -21,15 +21,16 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-bigquery jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: BigQuery interpreter @@ -41,6 +42,7 @@ v2-rev265-1.21.0 2.6 + bigquery @@ -99,12 +101,12 @@ maven-enforcer-plugin - - - enforce - none - - + + + maven-dependency-plugin + + + maven-resources-plugin @@ -116,63 +118,22 @@ - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/bqsql - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/bqsql - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + maven-assembly-plugin + + + + + org.apache.zeppelin.bigquery.BigQueryInterpreter + + + + + jar-with-dependencies + + - - maven-assembly-plugin - - - - - org.apache.zeppelin.bigquery.BigQueryInterpreter - - - - - jar-with-dependencies - - - + 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 ca06964129e..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; @@ -109,7 +109,7 @@ public class BigQueryInterpreter extends Interpreter { static final String PROJECT_ID = "zeppelin.bigquery.project_id"; static final String WAIT_TIME = "zeppelin.bigquery.wait_time"; static final String MAX_ROWS = "zeppelin.bigquery.max_no_of_rows"; - static final String LEGACY_SQL = "zeppelin.bigquery.use_legacy_sql"; + static final String SQL_DIALECT = "zeppelin.bigquery.sql_dialect"; private static String jobId = null; private static String projectId = null; @@ -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(); @@ -246,8 +247,19 @@ private InterpreterResult executeSql(String sql) { String projId = getProperty(PROJECT_ID); long wTime = Long.parseLong(getProperty(WAIT_TIME)); long maxRows = Long.parseLong(getProperty(MAX_ROWS)); - String legacySql = getProperty(LEGACY_SQL); - boolean useLegacySql = legacySql == null ? true : Boolean.parseBoolean(legacySql); + String sqlDialect = getProperty(SQL_DIALECT, "").toLowerCase(); + Boolean useLegacySql; + switch (sqlDialect) { + case "standardsql": + useLegacySql = false; + break; + case "legacysql": + useLegacySql = true; + break; + default: + // Enable query prefix like '#standardSQL' if specified + useLegacySql = null; + } Iterator pages; try { pages = run(sql, projId, wTime, maxRows, useLegacySql); @@ -267,7 +279,7 @@ private InterpreterResult executeSql(String sql) { //Function to run the SQL on bigQuery service public static Iterator run(final String queryString, - final String projId, final long wTime, final long maxRows, boolean useLegacySql) + final String projId, final long wTime, final long maxRows, Boolean useLegacySql) throws IOException { try { logger.info("Use legacy sql: {}", useLegacySql); diff --git a/bigquery/src/main/resources/interpreter-setting.json b/bigquery/src/main/resources/interpreter-setting.json index 3e1f27a083d..8023bed1522 100644 --- a/bigquery/src/main/resources/interpreter-setting.json +++ b/bigquery/src/main/resources/interpreter-setting.json @@ -22,19 +22,21 @@ "envName": null, "propertyName": "zeppelin.bigquery.max_no_of_rows", "defaultValue": "100000", - "description": "Maximum number of rows to fetch from BigQuery" + "description": "Maximum number of rows to fetch from BigQuery", + "type": "number" }, - "zeppelin.bigquery.use_legacy_sql": { + "zeppelin.bigquery.sql_dialect": { "envName": null, - "propertyName": "zeppelin.bigquery.use_legacy_sql", - "defaultValue": "true", - "description": "set true to use legacy sql", - "type": "checkbox" + "propertyName": "zeppelin.bigquery.sql_dialect", + "defaultValue": "", + "description": "BigQuery SQL dialect (standardSQL or legacySQL). If empty, query prefix like '#standardSQL' can be used.", + "type": "string" } }, "editor": { "language": "sql", - "editOnDblClick": false + "editOnDblClick": false, + "completionSupport": false } } ] 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 53c4dc30943..64c6e17a4fe 100644 --- a/bigquery/src/test/java/org/apache/zeppelin/bigquery/BigQueryInterpreterTest.java +++ b/bigquery/src/test/java/org/apache/zeppelin/bigquery/BigQueryInterpreterTest.java @@ -90,6 +90,7 @@ public void setUp() throws Exception { p.setProperty("zeppelin.bigquery.project_id", CONSTANTS.getProjectId()); p.setProperty("zeppelin.bigquery.wait_time", "5000"); p.setProperty("zeppelin.bigquery.max_no_of_rows", "100"); + p.setProperty("zeppelin.bigquery.sql_dialect", ""); intpGroup = new InterpreterGroup(); @@ -102,7 +103,6 @@ public void setUp() throws Exception { @Test public void sqlSuccess() { InterpreterResult ret = bqInterpreter.interpret(CONSTANTS.getOne(), context); - assertEquals(InterpreterResult.Code.SUCCESS, ret.code()); assertEquals(ret.message().get(0).getType(), InterpreterResult.Type.TABLE); @@ -111,8 +111,22 @@ public void sqlSuccess() { @Test public void badSqlSyntaxFails() { InterpreterResult ret = bqInterpreter.interpret(CONSTANTS.getWrong(), context); - assertEquals(InterpreterResult.Code.ERROR, ret.code()); } + @Test + public void testWithQueryPrefix() { + InterpreterResult ret = bqInterpreter.interpret( + "#standardSQL\n WITH t AS (select 1) SELECT * FROM t", context); + assertEquals(InterpreterResult.Code.SUCCESS, ret.code()); + } + + @Test + public void testInterpreterOutputData() { + InterpreterResult ret = bqInterpreter.interpret("SELECT 1 AS col1, 2 AS col2", context); + String[] lines = ret.message().get(0).getData().split("\\n"); + assertEquals(2, lines.length); + assertEquals("col1\tcol2", lines[0]); + assertEquals("1\t2", lines[1]); + } } diff --git a/bin/common.cmd b/bin/common.cmd index 13f33e5484c..b9e4ea06294 100644 --- a/bin/common.cmd +++ b/bin/common.cmd @@ -71,6 +71,14 @@ if not defined ZEPPELIN_JAVA_OPTS ( set ZEPPELIN_JAVA_OPTS=%ZEPPELIN_JAVA_OPTS% -Dfile.encoding=%ZEPPELIN_ENCODING% %ZEPPELIN_MEM% ) +if defined ZEPPELIN_JMX_ENABLE ( + if not defined ZEPPELIN_JMX_PORT ( + set ZEPPELIN_JMX_PORT="9996" + ) + set JMX_JAVA_OPTS=" -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=${ZEPPELIN_JMX_PORT} -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false" + set ZEPPELIN_JAVA_OPTS=%JMX_JAVA_OPTS% %ZEPPELIN_JAVA_OPTS +) + if not defined JAVA_OPTS ( set JAVA_OPTS=%ZEPPELIN_JAVA_OPTS% ) else ( diff --git a/bin/common.sh b/bin/common.sh index c7100c7d022..4ab4689042f 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -22,6 +22,8 @@ else FWDIR=$(dirname "${BASH_SOURCE-$0}") fi +export MAPR_HOME=${MAPR_HOME:-"/opt/mapr"} + if [[ -z "${ZEPPELIN_HOME}" ]]; then # Make ZEPPELIN_HOME look cleaner in logs by getting rid of the # extra ../ @@ -121,8 +123,24 @@ JAVA_OPTS+=" ${ZEPPELIN_JAVA_OPTS} -Dfile.encoding=${ZEPPELIN_ENCODING} ${ZEPPEL JAVA_OPTS+=" -Dlog4j.configuration=file://${ZEPPELIN_CONF_DIR}/log4j.properties" export JAVA_OPTS +if [[ x"${ZEPPELIN_JMX_ENABLE}" == x"true" ]]; then + if [[ -z "${ZEPPELIN_JMX_PORT}" ]]; then + ZEPPELIN_JMX_PORT="9996" + fi + JMX_JAVA_OPTS+=" -Dcom.sun.management.jmxremote" + JMX_JAVA_OPTS+=" -Dcom.sun.management.jmxremote.port=${ZEPPELIN_JMX_PORT}" + JMX_JAVA_OPTS+=" -Dcom.sun.management.jmxremote.authenticate=false" + JMX_JAVA_OPTS+=" -Dcom.sun.management.jmxremote.ssl=false" + JAVA_OPTS="${JMX_JAVA_OPTS} ${JAVA_OPTS}" +fi +export JAVA_OPTS + JAVA_INTP_OPTS="${ZEPPELIN_INTP_JAVA_OPTS} -Dfile.encoding=${ZEPPELIN_ENCODING}" -JAVA_INTP_OPTS+=" -Dlog4j.configuration=file://${ZEPPELIN_CONF_DIR}/log4j.properties" +if [[ -z "${ZEPPELIN_SPARK_YARN_CLUSTER}" ]]; then + JAVA_INTP_OPTS+=" -Dlog4j.configuration=file://${ZEPPELIN_CONF_DIR}/log4j.properties" +else + JAVA_INTP_OPTS+=" -Dlog4j.configuration=log4j_yarn_cluster.properties" +fi export JAVA_INTP_OPTS diff --git a/bin/entrypoint.sh b/bin/entrypoint.sh new file mode 100755 index 00000000000..077e3429a09 --- /dev/null +++ b/bin/entrypoint.sh @@ -0,0 +1,534 @@ +#!/bin/bash + +if [ "$$" = 1 ]; 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 + # + # 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/bin/interpreter.cmd b/bin/interpreter.cmd index eb59799952f..78467c55630 100644 --- a/bin/interpreter.cmd +++ b/bin/interpreter.cmd @@ -27,6 +27,7 @@ if /I "%~1"=="-d" ( set INTERPRETER_ID=%~n2 ) if /I "%~1"=="-p" set PORT=%~2 +if /I "%~1"=="-c" set CALLBACK_HOST=%~2 if /I "%~1"=="-l" set LOCAL_INTERPRETER_REPO=%~2 shift goto loop @@ -66,7 +67,7 @@ if not exist "%ZEPPELIN_LOG_DIR%" ( if /I "%INTERPRETER_ID%"=="spark" ( if defined SPARK_HOME ( set SPARK_SUBMIT=%SPARK_HOME%\bin\spark-submit.cmd - for %%d in ("%ZEPPELIN_HOME%\interpreter\spark\zeppelin-spark*.jar") do ( + for %%d in ("%ZEPPELIN_HOME%\interpreter\spark\spark-interpreter*.jar") do ( set SPARK_APP_JAR=%%d ) set ZEPPELIN_CLASSPATH="!SPARK_APP_JAR!" @@ -127,11 +128,11 @@ if not defined ZEPPELIN_CLASSPATH_OVERRIDES ( if defined SPARK_SUBMIT ( set JAVA_INTP_OPTS=%JAVA_INTP_OPTS% -Dzeppelin.log.file='%ZEPPELIN_LOGFILE%' - "%SPARK_SUBMIT%" --class %ZEPPELIN_SERVER% --jars %CLASSPATH% --driver-java-options "!JAVA_INTP_OPTS!" %SPARK_SUBMIT_OPTIONS% "%SPARK_APP_JAR%" %PORT% + "%SPARK_SUBMIT%" --class %ZEPPELIN_SERVER% --jars %CLASSPATH% --driver-java-options "!JAVA_INTP_OPTS!" %SPARK_SUBMIT_OPTIONS% "%SPARK_APP_JAR%" "%CALLBACK_HOST%" %PORT% ) else ( set JAVA_INTP_OPTS=%JAVA_INTP_OPTS% -Dzeppelin.log.file="%ZEPPELIN_LOGFILE%" - "%ZEPPELIN_RUNNER%" !JAVA_INTP_OPTS! %ZEPPELIN_INTP_MEM% -cp %ZEPPELIN_CLASSPATH_OVERRIDES%;%CLASSPATH% %ZEPPELIN_SERVER% %PORT% + "%ZEPPELIN_RUNNER%" !JAVA_INTP_OPTS! %ZEPPELIN_INTP_MEM% -cp %ZEPPELIN_CLASSPATH_OVERRIDES%;%CLASSPATH% %ZEPPELIN_SERVER% "%CALLBACK_HOST%" %PORT% ) exit /b diff --git a/bin/interpreter.sh b/bin/interpreter.sh index 1344e319fc8..88ec1b375ad 100755 --- a/bin/interpreter.sh +++ b/bin/interpreter.sh @@ -16,14 +16,15 @@ # limitations under the License. # + bin=$(dirname "${BASH_SOURCE-$0}") bin=$(cd "${bin}">/dev/null; pwd) function usage() { - echo "usage) $0 -p -d -l -g " + echo "usage) $0 -p -r -d -l -g " } -while getopts "hp:d:l:v:u:g:" o; do +while getopts "hc:p:r:d:l:v:u:g:" o; do case ${o} in h) usage @@ -32,8 +33,14 @@ while getopts "hp:d:l:v:u:g:" o; do d) INTERPRETER_DIR=${OPTARG} ;; + c) + CALLBACK_HOST=${OPTARG} # This will be used callback host + ;; p) - PORT=${OPTARG} + PORT=${OPTARG} # This will be used for callback port + ;; + r) + INTP_PORT=${OPTARG} # This will be used for interpreter process port ;; l) LOCAL_INTERPRETER_REPO=${OPTARG} @@ -44,14 +51,9 @@ while getopts "hp: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_GROUP_NAME=${OPTARG} + INTERPRETER_SETTING_NAME=${OPTARG} ;; esac done @@ -62,6 +64,10 @@ if [ -z "${PORT}" ] || [ -z "${INTERPRETER_DIR}" ]; then exit 1 fi +if [ -n "$CALLBACK_HOST" ] && [ -n "$HOST_IP" ]; then + CALLBACK_HOST="$HOST_IP" +fi + . "${bin}/common.sh" ZEPPELIN_INTP_CLASSPATH="${CLASSPATH}" @@ -88,14 +94,21 @@ ZEPPELIN_SERVER=org.apache.zeppelin.interpreter.remote.RemoteInterpreterServer 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-" -if [[ ! -z "$INTERPRETER_GROUP_NAME" ]]; then - ZEPPELIN_LOGFILE+="${INTERPRETER_GROUP_NAME}-" +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 -ZEPPELIN_LOGFILE+="${INTERPRETER_ID}-${ZEPPELIN_IDENT_STRING}-${HOSTNAME}.log" +ZEPPELIN_LOGFILE+="${ZEPPELIN_IDENT_STRING}-${HOSTNAME}.log" JAVA_INTP_OPTS+=" -Dzeppelin.log.file=${ZEPPELIN_LOGFILE}" if [[ ! -d "${ZEPPELIN_LOG_DIR}" ]]; then @@ -105,9 +118,14 @@ fi # set spark related env variables if [[ "${INTERPRETER_ID}" == "spark" ]]; then + + # run kinit + if [[ -n "${ZEPPELIN_SERVER_KERBEROS_KEYTAB}" ]] && [[ -n "${ZEPPELIN_SERVER_KERBEROS_PRINCIPAL}" ]]; then + kinit -kt ${ZEPPELIN_SERVER_KERBEROS_KEYTAB} ${ZEPPELIN_SERVER_KERBEROS_PRINCIPAL} + 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}" @@ -140,7 +158,13 @@ if [[ "${INTERPRETER_ID}" == "spark" ]]; then export PYTHONPATH="${PYTHONPATH}:${PYSPARKPATH}" fi unset PYSPARKPATH + export SPARK_CLASSPATH+=":${ZEPPELIN_INTP_CLASSPATH}" + fi + if [[ -n "${HADOOP_CONF_DIR}" ]] && [[ -d "${HADOOP_CONF_DIR}" ]]; then + ZEPPELIN_INTP_CLASSPATH+=":${HADOOP_CONF_DIR}" + export HADOOP_CONF_DIR=${HADOOP_CONF_DIR} + else # autodetect HADOOP_CONF_HOME by heuristic if [[ -n "${HADOOP_HOME}" ]] && [[ -z "${HADOOP_CONF_DIR}" ]]; then if [[ -d "${HADOOP_HOME}/etc/hadoop" ]]; then @@ -149,13 +173,20 @@ if [[ "${INTERPRETER_ID}" == "spark" ]]; then export HADOOP_CONF_DIR="/etc/hadoop/conf" fi fi + fi - if [[ -n "${HADOOP_CONF_DIR}" ]] && [[ -d "${HADOOP_CONF_DIR}" ]]; then - ZEPPELIN_INTP_CLASSPATH+=":${HADOOP_CONF_DIR}" - fi + # DSR-21: Prevent from problem with HiveContext cauased by existing metastore_db direcotry in CWD + user_home=$(getent passwd $USER | cut -d: -f6) # Ubuntu Docker container has no properly set HOME vairable + spark_interpreter_cwd="${user_home}/zeppelin/spark_intperpreter_cwd-$(date -u '+%Y%m%d%H%M%S')" + mkdir -p "$spark_interpreter_cwd" + cd "$spark_interpreter_cwd" + +elif [[ "${INTERPRETER_ID}" == "drill" ]]; then + ZEPPELIN_INTP_CLASSPATH+=":${ZEPPELIN_HOME}/interpreter/jdbc/*" + +elif [[ "${INTERPRETER_ID}" == "hive" ]]; then + ZEPPELIN_INTP_CLASSPATH+=":${ZEPPELIN_HOME}/interpreter/jdbc/*:$(mapr classpath 2>/dev/null)" - export SPARK_CLASSPATH+=":${ZEPPELIN_INTP_CLASSPATH}" - fi elif [[ "${INTERPRETER_ID}" == "hbase" ]]; then if [[ -n "${HBASE_CONF_DIR}" ]]; then ZEPPELIN_INTP_CLASSPATH+=":${HBASE_CONF_DIR}" @@ -191,6 +222,7 @@ fi addJarInDirForIntp "${LOCAL_INTERPRETER_REPO}" if [[ ! -z "$ZEPPELIN_IMPERSONATE_USER" ]]; then + if [[ "${INTERPRETER_ID}" != "spark" || "$ZEPPELIN_IMPERSONATE_SPARK_PROXY_USER" == "false" ]]; then suid="$(id -u ${ZEPPELIN_IMPERSONATE_USER})" if [[ -n "${suid}" || -z "${SPARK_SUBMIT}" ]]; then INTERPRETER_RUN_COMMAND=${ZEPPELIN_IMPERSONATE_RUN_CMD}" '" @@ -198,25 +230,28 @@ if [[ ! -z "$ZEPPELIN_IMPERSONATE_USER" ]]; then INTERPRETER_RUN_COMMAND+=" source "${ZEPPELIN_CONF_DIR}'/zeppelin-env.sh;' fi fi + fi +fi + +if [[ "${INTERPRETER_ID}" != "spark" ]]; then + ZEPPELIN_INTP_CLASSPATH+=":$(mapr classpath 2>/dev/null)" 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} ${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} ${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} ${PORT} ` + 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 +echo "Interpreter launch command: $INTERPRETER_RUN_COMMAND" 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 e88c26fc43c..6d1dbc92f91 100755 --- a/bin/zeppelin-daemon.sh +++ b/bin/zeppelin-daemon.sh @@ -46,11 +46,12 @@ BIN=$(cd "${BIN}">/dev/null; pwd) . "${BIN}/common.sh" . "${BIN}/functions.sh" +export ZEPPELIN_PID_DIR="${MAPR_HOME}/pid" HOSTNAME=$(hostname) ZEPPELIN_NAME="Zeppelin" ZEPPELIN_LOGFILE="${ZEPPELIN_LOG_DIR}/zeppelin-${ZEPPELIN_IDENT_STRING}-${HOSTNAME}.log" ZEPPELIN_OUTFILE="${ZEPPELIN_LOG_DIR}/zeppelin-${ZEPPELIN_IDENT_STRING}-${HOSTNAME}.out" -ZEPPELIN_PID="${ZEPPELIN_PID_DIR}/zeppelin-${ZEPPELIN_IDENT_STRING}-${HOSTNAME}.pid" +ZEPPELIN_PID="${ZEPPELIN_PID_DIR}/zeppelin.pid" ZEPPELIN_MAIN=org.apache.zeppelin.server.ZeppelinServer JAVA_OPTS+=" -Dzeppelin.log.file=${ZEPPELIN_LOGFILE}" @@ -67,6 +68,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" @@ -162,9 +167,9 @@ function upstart() { # where the service manager starts and stops the process initialize_default_directories - echo "ZEPPELIN_CLASSPATH: ${ZEPPELIN_CLASSPATH_OVERRIDES}:${CLASSPATH}" >> "${ZEPPELIN_OUTFILE}" + echo "ZEPPELIN_CLASSPATH: ${ZEPPELIN_CLASSPATH_OVERRIDES}:${CLASSPATH}:$(mapr classpath 2>/dev/null)" >> "${ZEPPELIN_OUTFILE}" - $ZEPPELIN_RUNNER $JAVA_OPTS -cp $ZEPPELIN_CLASSPATH_OVERRIDES:$CLASSPATH $ZEPPELIN_MAIN >> "${ZEPPELIN_OUTFILE}" + $ZEPPELIN_RUNNER $JAVA_OPTS -cp $ZEPPELIN_CLASSPATH_OVERRIDES:$CLASSPATH:$(mapr classpath 2>/dev/null) $ZEPPELIN_MAIN >> "${ZEPPELIN_OUTFILE}" } function start() { @@ -182,7 +187,7 @@ function start() { echo "ZEPPELIN_CLASSPATH: ${ZEPPELIN_CLASSPATH_OVERRIDES}:${CLASSPATH}" >> "${ZEPPELIN_OUTFILE}" - nohup nice -n $ZEPPELIN_NICENESS $ZEPPELIN_RUNNER $JAVA_OPTS -cp $ZEPPELIN_CLASSPATH_OVERRIDES:$CLASSPATH $ZEPPELIN_MAIN >> "${ZEPPELIN_OUTFILE}" 2>&1 < /dev/null & + nohup nice -n $ZEPPELIN_NICENESS $ZEPPELIN_RUNNER $JAVA_OPTS -cp $ZEPPELIN_CLASSPATH_OVERRIDES:$CLASSPATH:$(mapr classpath 2>/dev/null) $ZEPPELIN_MAIN >> "${ZEPPELIN_OUTFILE}" 2>&1 < /dev/null & pid=$! if [[ -z "${pid}" ]]; then action_msg "${ZEPPELIN_NAME} start" "${SET_ERROR}" @@ -213,18 +218,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/bin/zeppelin.sh b/bin/zeppelin.sh index 44fc2cfe89a..ca5ff3dca8f 100755 --- a/bin/zeppelin.sh +++ b/bin/zeppelin.sh @@ -39,6 +39,8 @@ bin=$(cd "${bin}">/dev/null; pwd) . "${bin}/common.sh" +export ZEPPELIN_PID_DIR="${MAPR_HOME}/pid" + if [ "$1" == "--version" ] || [ "$1" == "-v" ]; then getZeppelinVersion fi @@ -73,6 +75,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}") @@ -83,4 +89,4 @@ if [[ ! -d "${ZEPPELIN_PID_DIR}" ]]; then $(mkdir -p "${ZEPPELIN_PID_DIR}") fi -exec $ZEPPELIN_RUNNER $JAVA_OPTS -cp $ZEPPELIN_CLASSPATH_OVERRIDES:${ZEPPELIN_CLASSPATH} $ZEPPELIN_SERVER "$@" +exec $ZEPPELIN_RUNNER $JAVA_OPTS -cp $ZEPPELIN_CLASSPATH_OVERRIDES:${ZEPPELIN_CLASSPATH}:$(mapr classpath 2>/dev/null) $ZEPPELIN_SERVER "$@" diff --git a/cassandra/pom.xml b/cassandra/pom.xml index 05108e9fd48..00227175585 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -20,16 +20,16 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin - zeppelin-cassandra_2.10 + zeppelin-cassandra_2.11 jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Apache Cassandra interpreter Zeppelin cassandra support @@ -49,6 +49,7 @@ 2.15.2 1.0 1.7.1 + cassandra @@ -241,55 +242,14 @@ maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/cassandra - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/cassandra - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin + diff --git a/cassandra/src/main/resources/interpreter-setting.json b/cassandra/src/main/resources/interpreter-setting.json index 407de9cdff3..0f0d58c29da 100644 --- a/cassandra/src/main/resources/interpreter-setting.json +++ b/cassandra/src/main/resources/interpreter-setting.json @@ -224,7 +224,7 @@ "cassandra.ssl.enabled": { "envName": null, "propertyName": "cassandra.ssl.enabled", - "defaultValue": "false", + "defaultValue": false, "description": "Cassandra SSL", "type": "checkbox" }, diff --git a/conf/helium-repo.json b/conf/helium-repo.json new file mode 100644 index 00000000000..6d6965c63cd --- /dev/null +++ b/conf/helium-repo.json @@ -0,0 +1,1415 @@ +[{ +"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": "" + } + }, +"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-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-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-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-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-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-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-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-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-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-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-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= " + } + } + } +}] diff --git a/conf/interpreter-list b/conf/interpreter-list index 9506122f89b..cf5bbb0eabd 100644 --- a/conf/interpreter-list +++ b/conf/interpreter-list @@ -17,22 +17,25 @@ # # [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 +maprdb org.apache.zeppelin:zeppelin-maprdb:0.8.0 Zeppelin: MapR-DB Shell interpreter +drill org.apache.zeppelin:zeppelin-drill:0.8.0 Jdbc Drill interpreter +hive org.apache.zeppelin:zeppelin-hive:0.8.0 Jdbc Hive interpreter diff --git a/conf/log4j.properties b/conf/log4j.properties index b132ce1030f..97f38e35456 100644 --- a/conf/log4j.properties +++ b/conf/log4j.properties @@ -22,7 +22,6 @@ log4j.appender.stdout.layout = org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%5p [%d] ({%t} %F[%M]:%L) - %m%n log4j.appender.dailyfile.DatePattern=.yyyy-MM-dd -log4j.appender.dailyfile.Threshold = INFO log4j.appender.dailyfile = org.apache.log4j.DailyRollingFileAppender log4j.appender.dailyfile.File = ${zeppelin.log.file} log4j.appender.dailyfile.layout = org.apache.log4j.PatternLayout diff --git a/conf/log4j_yarn_cluster.properties b/conf/log4j_yarn_cluster.properties new file mode 100644 index 00000000000..532fc5ef5f1 --- /dev/null +++ b/conf/log4j_yarn_cluster.properties @@ -0,0 +1,23 @@ +# +# 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. +# + +log4j.rootLogger = INFO, stdout + +log4j.appender.stdout = org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout = org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%5p [%d] ({%t} %F[%M]:%L) - %m%n + diff --git a/conf/shiro.ini.template b/conf/shiro.ini.template index 06ad9712a5c..eda44bf3d8e 100644 --- a/conf/shiro.ini.template +++ b/conf/shiro.ini.template @@ -20,9 +20,9 @@ # To use a different strategy (LDAP / Database / ...) check the shiro doc at http://shiro.apache.org/configuration.html#Configuration-INISections # To enable admin user, uncomment the following line and set an appropriate password. #admin = password1, admin -user1 = password2, role1, role2 -user2 = password3, role3 -user3 = password4, role2 +#user1 = password2, role1, role2 +#user2 = password3, role3 +#user3 = password4, role2 # Sample LDAP configuration, for user Authentication, currently tested for single Realm [main] @@ -47,8 +47,10 @@ user3 = password4, role2 #ldapRealm.contextFactory.authenticationMechanism = simple ### A sample PAM configuration -#pamRealm=org.apache.zeppelin.realm.PamRealm -#pamRealm.service=sshd +pamRealm=org.apache.zeppelin.realm.PamRealm +pamRealm.service=login + +securityManager.realms = $pamRealm ### A sample for configuring ZeppelinHub Realm #zeppelinHubRealm = org.apache.zeppelin.realm.ZeppelinHubRealm @@ -56,12 +58,34 @@ 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.logoutAPI = true +#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 #cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager #securityManager.cacheManager = $cacheManager +### Enables 'HttpOnly' flag in Zeppelin cookies +cookie = org.apache.shiro.web.servlet.SimpleCookie +cookie.name = JSESSIONID +cookie.httpOnly = true +### Uncomment the below line only when Zeppelin is running over HTTPS +#cookie.secure = true +sessionManager.sessionIdCookie = $cookie + securityManager.sessionManager = $sessionManager # 86,400,000 milliseconds = 24 hour securityManager.sessionManager.globalSessionTimeout = 86400000 @@ -88,8 +112,11 @@ admin = * # uncomment the line second last line (/** = anon) and comment the last line (/** = authc) # /api/version = anon -/api/interpreter/** = authc, roles[admin] -/api/configurations/** = authc, roles[admin] -/api/credential/** = authc, roles[admin] +# 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] #/** = anon /** = authc diff --git a/conf/zeppelin-env.cmd.template b/conf/zeppelin-env.cmd.template index e40429af9e6..e69f23b0718 100644 --- a/conf/zeppelin-env.cmd.template +++ b/conf/zeppelin-env.cmd.template @@ -22,6 +22,8 @@ REM set ZEPPELIN_JAVA_OPTS REM Additional jvm options. for example, set Z REM set ZEPPELIN_MEM REM Zeppelin jvm mem options Default -Xms1024m -Xmx1024m -XX:MaxPermSize=512m REM set ZEPPELIN_INTP_MEM REM zeppelin interpreter process jvm mem options. Default -Xmx1024m -Xms1024m -XX:MaxPermSize=512m REM set ZEPPELIN_INTP_JAVA_OPTS REM zeppelin interpreter process jvm options. +REM set ZEPPELIN_JMX_ENABLE REM Enable JMX feature by defining it like "true" +REM set ZEPPELIN_JMX_PORT REM Port number which JMX uses. Default: "9996" REM set ZEPPELIN_LOG_DIR REM Where log files are stored. PWD by default. REM set ZEPPELIN_PID_DIR REM The pid files are stored. /tmp by default. diff --git a/conf/zeppelin-env.sh.template b/conf/zeppelin-env.sh.template index 7bc38d633e1..a17b5984c8b 100644 --- a/conf/zeppelin-env.sh.template +++ b/conf/zeppelin-env.sh.template @@ -18,11 +18,15 @@ # export JAVA_HOME= # export MASTER= # Spark master url. eg. spark://master_addr:7077. Leave empty if you want to use local mode. +# export ZEPPELIN_ADDR # Bind address (default 127.0.0.1) +# export ZEPPELIN_PORT # port number to listen (default 8080) # export ZEPPELIN_JAVA_OPTS # Additional jvm options. for example, export ZEPPELIN_JAVA_OPTS="-Dspark.executor.memory=8g -Dspark.cores.max=16" # export ZEPPELIN_MEM # Zeppelin jvm mem options Default -Xms1024m -Xmx1024m -XX:MaxPermSize=512m # export ZEPPELIN_INTP_MEM # zeppelin interpreter process jvm mem options. Default -Xms1024m -Xmx1024m -XX:MaxPermSize=512m # export ZEPPELIN_INTP_JAVA_OPTS # zeppelin interpreter process jvm options. # export ZEPPELIN_SSL_PORT # ssl port (used when ssl environment variable is set to true) +# export ZEPPELIN_JMX_ENABLE # Enable JMX feature by defining "true" +# export ZEPPELIN_JMX_PORT # Port number which JMX uses. Default: "9996" # export ZEPPELIN_LOG_DIR # Where log files are stored. PWD by default. # export ZEPPELIN_PID_DIR # The pid files are stored. ${ZEPPELIN_HOME}/run by default. @@ -30,16 +34,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 @@ -99,4 +109,123 @@ #### Zeppelin impersonation configuration # export ZEPPELIN_IMPERSONATE_CMD # Optional, when user want to run interpreter as end web user. eg) 'sudo -H -u ${ZEPPELIN_IMPERSONATE_USER} bash -c ' +export ZEPPELIN_IMPERSONATE_CMD='sudo -H -u ${ZEPPELIN_IMPERSONATE_USER} bash -c ' # export ZEPPELIN_IMPERSONATE_SPARK_PROXY_USER #Optional, by default is true; can be set to false if you don't want to use --proxy-user option with Spark interpreter when impersonation enabled + + +export MAPR_HOME=${MAPR_HOME:-"/opt/mapr"} + +ZEPPELIN_VERSION_FILE="${MAPR_HOME}/zeppelin/zeppelinversion" +if [ -z "$ZEPPELIN_HOME" ] && [ -e "$ZEPPELIN_VERSION_FILE" ]; then + ZEPPELIN_VERSION=$(cat "$ZEPPELIN_VERSION_FILE") + export ZEPPELIN_HOME="${MAPR_HOME}/zeppelin/zeppelin-${ZEPPELIN_VERSION}" +fi + +SPARK_VERSION_FILE="${MAPR_HOME}/spark/sparkversion" +if [ -z "$SPARK_HOME" ] && [ -e "$SPARK_VERSION_FILE" ]; then + SPARK_VERSION=$(cat "$SPARK_VERSION_FILE") + export SPARK_HOME="${MAPR_HOME}/spark/spark-${SPARK_VERSION}" +fi + +HADOOP_VERSION_FILE="${MAPR_HOME}/hadoop/hadoopversion" +if [ -z "$HADOOP_HOME" ] && [ -e "$HADOOP_VERSION_FILE" ]; then + HADOOP_VERSION=$(cat "$HADOOP_VERSION_FILE") + export HADOOP_HOME="${MAPR_HOME}/hadoop/hadoop-${HADOOP_VERSION}" +fi + +if [ -z "$HADOOP_CONF_DIR" ] && [ -n "$HADOOP_HOME" ]; then + export HADOOP_CONF_DIR="${HADOOP_HOME}/etc/hadoop" +fi + + +# HTTPS by default +export ZEPPELIN_SSL=${ZEPPELIN_SSL:-"true"} +export ZEPPELIN_SSL_PORT="${ZEPPELIN_SSL_PORT:-"9995"}" +export ZEPPELIN_SSL_KEYSTORE_PATH="${ZEPPELIN_HOME}/conf/keys/ssl_keystore" +export ZEPPELIN_SSL_KEYSTORE_PASSWORD="mapr123" +export ZEPPELIN_SSL_KEYSTORE_TYPE="JKS" + + +# Explicitly set Java Trust Store, as it can be overwritten on initialization of MapR libs +# to /opt/mapr/conf/ssl_truststore, which is not exists on client setup, and may cause problems +# on fetching something from internet using HTTPS +if [ -e "/etc/debian_version" ]; then + export ZEPPELIN_JAVA_OPTS="$ZEPPELIN_JAVA_OPTS -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts " +elif [ -e "/etc/redhat-release" ]; then + export ZEPPELIN_JAVA_OPTS="$ZEPPELIN_JAVA_OPTS -Djavax.net.ssl.trustStore=/etc/pki/java/cacerts " +fi + + +# Notebook Storage setup +if [ -n "$ZEPPELIN_NOTEBOOK_DIR" ]; then + if echo "$ZEPPELIN_NOTEBOOK_DIR" | grep -q "^maprfs:"; then + export ZEPPELIN_NOTEBOOK_STORAGE="org.apache.zeppelin.notebook.repo.FileSystemNotebookRepo" + else + mkdir -p "$ZEPPELIN_NOTEBOOK_DIR" + fi +fi + +# Config Storage setup +if [ -n "$ZEPPELIN_CONFIG_FS_DIR" ]; then + if echo "$ZEPPELIN_CONFIG_FS_DIR" | grep -q "^maprfs:"; then + export ZEPPELIN_CONFIG_STORAGE_CLASS="org.apache.zeppelin.storage.FileSystemConfigStorage" + else + mkdir -p "$ZEPPELIN_CONFIG_FS_DIR" + fi +fi + +export ZEPPELIN_HELIUM_REGISTRY=${ZEPPELIN_HELIUM_REGISTRY:-"helium,file://${ZEPPELIN_HOME}/conf/helium-repo.json"} + + +# Container specific configuration +if [ -n "${MAPR_CONTAINER_USER}" ]; then + export ZEPPELIN_INTERPRETER_USER="${MAPR_CONTAINER_USER}" +fi + +ensure_port() { + local links="$1" + local default_port="$2" + local res="" + local link host port + local IFS="," + for link in $links; do + host=$(echo "$link" | cut -d ':' -f 1) + port=$(echo "$link" | cut -d ':' -f 2) + if [ "$host" = "$port" ]; then + port="$default_port" + fi + if [ -z "$res" ]; then + res="$host:$port" + else + res="$res,$host:$port" + fi + done + echo "$res" +} + +# Build Hive JDBC URL +ZEPPELIN_JDBC_URL_HIVE="jdbc:hive2://hivenode:10000/default" +if [ -e "${MAPR_TICKETFILE_LOCATION}" ]; then + ZEPPELIN_JDBC_URL_HIVE+=";auth=MAPRSASL;ssl=true" +fi +export ZEPPELIN_JDBC_URL_HIVE + +# Build Drill JDBC URL +if [ -n "$MAPR_DRILLBITS_HOSTS" ]; then + mapr_drillbits_hosts_cleaned=$(ensure_port "$MAPR_DRILLBITS_HOSTS" 31010) + ZEPPELIN_JDBC_URL_DRILL="jdbc:drill:drillbit=${mapr_drillbits_hosts_cleaned}" +elif [ -n "$MAPR_ZK_QUORUM" ]; then + mapr_zk_quorum_cleaned=$(ensure_port "$MAPR_ZK_QUORUM" 5181) + ZEPPELIN_JDBC_URL_DRILL="jdbc:drill:zk=${mapr_zk_quorum_cleaned}/drill/${MAPR_CLUSTER}-drillbits" +else + ZEPPELIN_JDBC_URL_DRILL="jdbc:drill:drillbit=drillbitnode:31010" +fi +if [ -e "${MAPR_TICKETFILE_LOCATION}" ]; then + ZEPPELIN_JDBC_URL_DRILL+=";auth=MAPRSASL" +fi +export ZEPPELIN_JDBC_URL_DRILL + + +if [ -e "${ZEPPELIN_HOME}/conf/zeppelin-env-dsr.sh" ]; then + . "${ZEPPELIN_HOME}/conf/zeppelin-env-dsr.sh" +fi diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template index adf58102cab..c52d5e368ea 100755 --- a/conf/zeppelin-site.xml.template +++ b/conf/zeppelin-site.xml.template @@ -22,18 +22,18 @@ zeppelin.server.addr 0.0.0.0 - Server address + Server binding address zeppelin.server.port - 8080 + 9995 Server port. zeppelin.server.ssl.port - 8443 + 9995 Server ssl port. (used when ssl property is set to true) @@ -67,6 +67,23 @@ hide homescreen notebook from list when this value set to true + + @@ -138,6 +155,16 @@ --> + + + + + + + + + + + + + + + + + + + + zeppelin.notebook.cron.enable + true + Notebook enable cron scheduler feature + + diff --git a/dev/change_zeppelin_version.sh b/dev/change_zeppelin_version.sh index cb9c0179ca3..faccb62c79c 100755 --- a/dev/change_zeppelin_version.sh +++ b/dev/change_zeppelin_version.sh @@ -76,5 +76,5 @@ if is_dev_version "${FROM_VERSION}" || ! is_dev_version "${TO_VERSION}"; then # Change interpreter's maven version in docs and interpreter-list sed -i '' 's/:'"${FROM_VERSION}"'/:'"${TO_VERSION}"'/g' conf/interpreter-list - sed -i '' 's/:'"${FROM_VERSION}"'/:'"${TO_VERSION}"'/g' docs/manual/interpreterinstallation.md + sed -i '' 's/:'"${FROM_VERSION}"'/:'"${TO_VERSION}"'/g' docs/usage/interpreter/installation.md fi diff --git a/dev/common_release.sh b/dev/common_release.sh index 6b7e901b130..bc3a03d59b3 100644 --- a/dev/common_release.sh +++ b/dev/common_release.sh @@ -31,6 +31,7 @@ if [[ -z "${WORKING_DIR}" ]]; then WORKING_DIR="/tmp/zeppelin-release" fi +rm -rf "${WORKING_DIR}" mkdir "${WORKING_DIR}" # If set to 'yes', release script will deploy artifacts to SNAPSHOT repository. @@ -42,7 +43,7 @@ usage() { exit 1 } -function git_clone() { +function git_clone() { echo "Clone the source" # clone source git clone https://git-wip-us.apache.org/repos/asf/zeppelin.git "${WORKING_DIR}/zeppelin" @@ -58,7 +59,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/create_release.sh b/dev/create_release.sh index 9cb61e0351c..f0529d51f95 100755 --- a/dev/create_release.sh +++ b/dev/create_release.sh @@ -55,11 +55,7 @@ function make_source_package() { echo "${GPG_PASSPHRASE}" | gpg --passphrase-fd 0 --armor \ --output "zeppelin-${RELEASE_VERSION}.tgz.asc" \ --detach-sig "${WORKING_DIR}/zeppelin-${RELEASE_VERSION}.tgz" - echo "${GPG_PASSPHRASE}" | gpg --passphrase-fd 0 \ - --print-md MD5 "zeppelin-${RELEASE_VERSION}.tgz" > \ - "${WORKING_DIR}/zeppelin-${RELEASE_VERSION}.tgz.md5" - echo "${GPG_PASSPHRASE}" | gpg --passphrase-fd 0 \ - --print-md SHA512 "zeppelin-${RELEASE_VERSION}.tgz" > \ + ${SHASUM} -a 512 "zeppelin-${RELEASE_VERSION}.tgz" > \ "${WORKING_DIR}/zeppelin-${RELEASE_VERSION}.tgz.sha512" } @@ -89,15 +85,11 @@ function make_binary_release() { echo "${GPG_PASSPHRASE}" | gpg --passphrase-fd 0 --armor \ --output "zeppelin-${RELEASE_VERSION}-bin-${BIN_RELEASE_NAME}.tgz.asc" \ --detach-sig "zeppelin-${RELEASE_VERSION}-bin-${BIN_RELEASE_NAME}.tgz" - echo "${GPG_PASSPHRASE}" | gpg --passphrase-fd 0 --print-md MD5 \ - "zeppelin-${RELEASE_VERSION}-bin-${BIN_RELEASE_NAME}.tgz" > \ - "zeppelin-${RELEASE_VERSION}-bin-${BIN_RELEASE_NAME}.tgz.md5" ${SHASUM} -a 512 "zeppelin-${RELEASE_VERSION}-bin-${BIN_RELEASE_NAME}.tgz" > \ "zeppelin-${RELEASE_VERSION}-bin-${BIN_RELEASE_NAME}.tgz.sha512" mv "zeppelin-${RELEASE_VERSION}-bin-${BIN_RELEASE_NAME}.tgz" "${WORKING_DIR}/" mv "zeppelin-${RELEASE_VERSION}-bin-${BIN_RELEASE_NAME}.tgz.asc" "${WORKING_DIR}/" - mv "zeppelin-${RELEASE_VERSION}-bin-${BIN_RELEASE_NAME}.tgz.md5" "${WORKING_DIR}/" mv "zeppelin-${RELEASE_VERSION}-bin-${BIN_RELEASE_NAME}.tgz.sha512" "${WORKING_DIR}/" # clean up build dir @@ -106,8 +98,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..83db3053f27 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="-Pspark-2.2 -Pr" PROJECT_OPTIONS="-pl !zeppelin-distribution" NEXUS_STAGING="https://repository.apache.org/service/local/staging" NEXUS_PROFILE="153446d1ac37c4" @@ -54,7 +54,6 @@ NEXUS_PROFILE="153446d1ac37c4" function cleanup() { echo "Remove working directory and maven local repository" rm -rf ${WORKING_DIR} - rm -rf ${tmp_repo} } function curl_error() { @@ -126,13 +125,13 @@ function publish_to_maven() { echo "Created Nexus staging repository: ${staged_repo_id}" - tmp_repo="$(mktemp -d /tmp/zeppelin-repo-XXXXX)" + rm -rf $HOME/.m2/repository/org/apache/zeppelin # build with scala-2.10 echo "mvn clean install -DskipTests \ - -Dmaven.repo.local=${tmp_repo} -Pscala-2.10 -Pbeam \ + -Pscala-2.10 -Pbeam \ ${PUBLISH_PROFILES} ${PROJECT_OPTIONS}" - mvn clean install -DskipTests -Dmaven.repo.local="${tmp_repo}" -Pscala-2.10 -Pbeam \ + mvn clean install -DskipTests -Pscala-2.10 -Pbeam \ ${PUBLISH_PROFILES} ${PROJECT_OPTIONS} if [[ $? -ne 0 ]]; then echo "Build with scala 2.10 failed." @@ -143,23 +142,22 @@ function publish_to_maven() { "${BASEDIR}/change_scala_version.sh" 2.11 echo "mvn clean install -DskipTests \ - -Dmaven.repo.local=${tmp_repo} -Pscala-2.11 \ + -Pscala-2.11 \ ${PUBLISH_PROFILES} ${PROJECT_OPTIONS}" - mvn clean install -DskipTests -Dmaven.repo.local="${tmp_repo}" -Pscala-2.11 \ + mvn clean install -DskipTests -Pscala-2.11 \ ${PUBLISH_PROFILES} ${PROJECT_OPTIONS} if [[ $? -ne 0 ]]; then echo "Build with scala 2.11 failed." exit 1 fi - pushd "${tmp_repo}/org/apache/zeppelin" + pushd "${HOME}/.m2/repository/org/apache/zeppelin" find . -type f | grep -v '\.jar$' | grep -v '\.pom$' |grep -v '\.war$' | xargs rm echo "Creating hash and signature files" for file in $(find . -type f); do echo "${GPG_PASSPHRASE}" | gpg --passphrase-fd 0 --output "${file}.asc" \ --detach-sig --armor "${file}" - md5 -q "${file}" > "${file}.md5" ${SHASUM} -a 1 "${file}" | cut -f1 -d' ' > "${file}.sha1" done diff --git a/docs/README.md b/docs/README.md index 4dc810edf18..c1fee0304da 100644 --- a/docs/README.md +++ b/docs/README.md @@ -56,8 +56,10 @@ If you wish to help us and contribute to Zeppelin Documentation, please look at ``` 2. checkout ASF repo + ``` svn co https://svn.apache.org/repos/asf/zeppelin asf-zeppelin ``` + 3. copy `zeppelin/docs/_site` to `asf-zeppelin/site/docs/[VERSION]` - 4. ```svn commit``` + 4. `svn commit` diff --git a/docs/_config.yml b/docs/_config.yml index 69d0d836982..372bc8069e5 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -21,7 +21,7 @@ author : twitter : ASF feedburner : feedname -ZEPPELIN_VERSION : 0.8.0-SNAPSHOT +ZEPPELIN_VERSION : 0.8.0 # The production_url is only used when full-domain names are needed # such as sitemap.txt @@ -59,7 +59,7 @@ JB : # - Only the following values are falsy: ["", null, false] # - When setting BASE_PATH it must be a valid url. # This means always setting the protocol (http|https) or prefixing with "/" - BASE_PATH : /docs/0.8.0-SNAPSHOT + BASE_PATH : /docs/0.8.0 # By default, the asset_path is automatically defined relative to BASE_PATH plus the enabled theme. # ex: [BASE_PATH]/assets/themes/[THEME-NAME] diff --git a/docs/_includes/themes/zeppelin/_navigation.html b/docs/_includes/themes/zeppelin/_navigation.html index ecdccbd7ffd..acedc29e217 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
  • @@ -61,6 +61,9 @@
  • Publishing Paragraphs
  • Personalized Mode
  • Customizing Zeppelin Homepage
  • +
  • Notebook Actions
  • +
  • Cron Scheduler
  • +
  • Zeppelin Context
  • REST API
  • Interpreter API
  • @@ -137,6 +140,7 @@
  • Lens
  • Livy
  • Markdown
  • +
  • Neo4j
  • Pig
  • Postgresql, HAWQ
  • R
  • @@ -155,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
  • @@ -179,3 +183,4 @@ + diff --git a/docs/assets/themes/zeppelin/img/available_interpreters.png b/docs/assets/themes/zeppelin/img/available_interpreters.png index cb1a384c766..3be54af6bc1 100644 Binary files a/docs/assets/themes/zeppelin/img/available_interpreters.png and b/docs/assets/themes/zeppelin/img/available_interpreters.png differ 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 00000000000..e68af7b219e Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/cron_scheduler_dialog_box.png differ diff --git a/docs/assets/themes/zeppelin/img/docs-img/neo4j-config.png b/docs/assets/themes/zeppelin/img/docs-img/neo4j-config.png new file mode 100644 index 00000000000..2de3699e8a5 Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/neo4j-config.png differ diff --git a/docs/assets/themes/zeppelin/img/docs-img/neo4j-dynamic-forms.png b/docs/assets/themes/zeppelin/img/docs-img/neo4j-dynamic-forms.png new file mode 100644 index 00000000000..177e0a5e764 Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/neo4j-dynamic-forms.png differ diff --git a/docs/assets/themes/zeppelin/img/docs-img/neo4j-graph.png b/docs/assets/themes/zeppelin/img/docs-img/neo4j-graph.png new file mode 100644 index 00000000000..396b960db6c Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/neo4j-graph.png differ diff --git a/docs/assets/themes/zeppelin/img/docs-img/neo4j-interpreter-video.gif b/docs/assets/themes/zeppelin/img/docs-img/neo4j-interpreter-video.gif new file mode 100644 index 00000000000..28c191516fd Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/neo4j-interpreter-video.gif differ diff --git a/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-comboboxes.png b/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-comboboxes.png new file mode 100644 index 00000000000..7eae6a59764 Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-comboboxes.png differ diff --git a/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-table.png b/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-table.png new file mode 100644 index 00000000000..6c7b8e6df4e Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-table.png differ diff --git a/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_button.png b/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_button.png new file mode 100644 index 00000000000..168809cc454 Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_button.png differ diff --git a/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_diff.png b/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_diff.png new file mode 100644 index 00000000000..c1092e9c0c7 Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_diff.png differ diff --git a/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_paragraph.png b/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_paragraph.png new file mode 100644 index 00000000000..c559c458ffa Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_paragraph.png differ 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 00000000000..156c3575c9b Binary files /dev/null and b/docs/assets/themes/zeppelin/img/screenshots/conf_interpreter.png differ diff --git a/docs/assets/themes/zeppelin/img/screenshots/credential_entry.png b/docs/assets/themes/zeppelin/img/screenshots/credential_entry.png new file mode 100644 index 00000000000..745e91d1b8b Binary files /dev/null and b/docs/assets/themes/zeppelin/img/screenshots/credential_entry.png differ diff --git a/docs/assets/themes/zeppelin/img/screenshots/credential_injection_setting.PNG b/docs/assets/themes/zeppelin/img/screenshots/credential_injection_setting.PNG new file mode 100644 index 00000000000..ca98ca58074 Binary files /dev/null and b/docs/assets/themes/zeppelin/img/screenshots/credential_injection_setting.PNG differ 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 18ed125c9b0..1668678f31e Binary files a/docs/assets/themes/zeppelin/img/ui-img/about_menu.png and b/docs/assets/themes/zeppelin/img/ui-img/about_menu.png differ 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 f4e81546371..9f19f5c4976 Binary files a/docs/assets/themes/zeppelin/img/ui-img/settings_menu.png and b/docs/assets/themes/zeppelin/img/ui-img/settings_menu.png differ diff --git a/docs/assets/themes/zeppelin/note/FixReaders/note.json b/docs/assets/themes/zeppelin/note/FixReaders/note.json new file mode 100644 index 00000000000..49630b4eb94 --- /dev/null +++ b/docs/assets/themes/zeppelin/note/FixReaders/note.json @@ -0,0 +1,210 @@ +{ + "paragraphs": [ + { + "text": "%md\n### Description\nNew type of permission appeared in version 0.8 - [Runners](http://zeppelin.apache.org/docs/0.8.0/setup/security/notebook_authorization.html#authorization-setting)\nRunners are between Writers and Readers.\nAs Runners list is empty in note so everybody can view note although Readers list is not empty.\n\nThe code below will fill Runners as Writes for each note.\n\n**You must be owner of all notes or have role `zeppelin.notebook.default.owner.username` defined in zeppelin-site.xml**", + "user": "admin", + "dateUpdated": "2018-08-06 19:19:29.675", + "config": { + "colWidth": 12.0, + "fontSize": 9.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "markdown", + "editOnDblClick": true, + "completionKey": "TAB", + "completionSupport": false + }, + "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\u003ch3\u003eDescription\u003c/h3\u003e\n\u003cp\u003eNew type of permission appeared in version 0.8 - \u003ca href\u003d\"http://zeppelin.apache.org/docs/0.8.0/setup/security/notebook_authorization.html#authorization-setting\"\u003eRunners\u003c/a\u003e\u003cbr/\u003eRunners are between Writers and Readers.\u003cbr/\u003eAs Runners list is empty in note so everybody can view note although Readers list is not empty.\u003c/p\u003e\n\u003cp\u003eThe code below will fill Runners as Writes for each note.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eYou must be owner of all notes or have role \u003ccode\u003ezeppelin.notebook.default.owner.username\u003c/code\u003e defined in zeppelin-site.xml\u003c/strong\u003e\u003c/p\u003e\n\u003c/div\u003e" + } + ] + }, + "apps": [], + "jobName": "paragraph_1533132991280_-82433086", + "id": "20180801-171631_359485954", + "dateCreated": "2018-08-01 17:16:31.280", + "dateStarted": "2018-08-06 19:19:29.678", + "dateFinished": "2018-08-06 19:19:32.163", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Zeppelin Config", + "text": "%spark.pyspark\n\nzeppelin_host \u003d z.input(\"Zeppelin host\", \"localhost\")\nzeppelin_port \u003d z.input(\"Zeppelin port\", \"8080\")\nzeppelin_user \u003d z.input(\"User\", \"admin\")\nzeppelin_password \u003d z.input(\"Password\", \"\")", + "user": "admin", + "dateUpdated": "2018-08-01 19:31:26.954", + "config": { + "colWidth": 12.0, + "fontSize": 9.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "python", + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": true + }, + "editorMode": "ace/mode/python", + "editorHide": true, + "tableHide": false, + "runOnSelectionChange": true, + "title": true + }, + "settings": { + "params": { + "Zeppelin host": "localhost", + "Zeppelin port": "8080", + "User": "admin", + "Password": "password1", + "Zeppelin host\n": "localhost", + "Zeppelin host\n\n": "localhost", + "Zeppelin port\n": "8080", + "\nZeppelin host\n\n": "localhost", + "\nZeppelin port\n": "8080", + "\tZeppelin host\n\n": "localhost", + "formName": "option1" + }, + "forms": { + "Zeppelin host": { + "type": "TextBox", + "name": "Zeppelin host", + "displayName": "Zeppelin host", + "defaultValue": "localhost", + "hidden": false + }, + "Zeppelin port": { + "type": "TextBox", + "name": "Zeppelin port", + "displayName": "Zeppelin port", + "defaultValue": "8080", + "hidden": false + }, + "User": { + "type": "TextBox", + "name": "User", + "displayName": "User", + "defaultValue": "admin", + "hidden": false + }, + "Password": { + "type": "TextBox", + "name": "Password", + "displayName": "Password", + "defaultValue": "", + "hidden": false + } + } + }, + "results": { + "code": "SUCCESS", + "msg": [] + }, + "apps": [], + "jobName": "paragraph_1533118971566_949439379", + "id": "20180801-132251_306203866", + "dateCreated": "2018-08-01 13:22:51.566", + "dateStarted": "2018-08-01 19:31:27.023", + "dateFinished": "2018-08-01 19:32:11.266", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "title": "Update permissions", + "text": "%spark.pyspark\n\nimport requests\nimport json\n\n\nclass Izpepelin:\n def __init__(self):\n self.session \u003d requests.Session()\n self.session.auth \u003d (zeppelin_user, zeppelin_password)\n self.base_url \u003d \u0027http://\u0027 + zeppelin_host + \u0027:\u0027 + str(zeppelin_port)\n\n def get_notes(self):\n url \u003d self.base_url + \u0027/api/notebook\u0027\n response \u003d self.session.get(url).json()\n if \u0027exception\u0027 in response:\n raise requests.exceptions.RequestException(url)\n return response\n\n def get_note_permission(self, note_id):\n url \u003d self.base_url + \u0027/api/notebook/{}/permissions\u0027.format(note_id)\n response \u003d self.session.get(url).json()\n if \u0027exception\u0027 in response:\n raise requests.exceptions.RequestException(url)\n return response\n\n def set_note_permission(self, note_id, data):\n json_data \u003d json.dumps(data)\n url \u003d self.base_url + \u0027/api/notebook/{}/permissions\u0027.format(note_id)\n response \u003d self.session.put(url, data\u003djson_data).json()\n if \u0027exception\u0027 in response:\n raise requests.exceptions.RequestException(url)\n return response\n\ntry:\n zepp \u003d Izpepelin()\n response \u003d zepp.get_notes()[\u0027body\u0027]\n for note in response:\n auth_info \u003d zepp.get_note_permission(note[\u0027id\u0027])[\u0027body\u0027]\n if auth_info[\u0027writers\u0027] and not auth_info[\u0027runners\u0027]:\n print(\"Processing note: name - {}, id - {}\"\n .format(note[\u0027id\u0027], note[\u0027name\u0027]))\n print(\"Before:\")\n print(auth_info)\n auth_info[\u0027runners\u0027] \u003d auth_info[\u0027writers\u0027]\n _ \u003d zepp.set_note_permission(note[\u0027id\u0027], auth_info)\n new_auth_info \u003d zepp.get_note_permission(note[\u0027id\u0027])[\u0027body\u0027]\n assert auth_info \u003d\u003d new_auth_info\n print(\"After:\")\n print(new_auth_info)\n print(\"------------\")\nexcept requests.exceptions.RequestException as e:\n print(\" \".join([\n \"%html\", \"\u003ccenter\u003e\u003ch4\u003e\u003cp\u003e\u003cb\u003eRequest Failed\u003c/b\u003e\u003c/p\u003e\",\n \"\u003cp\u003e\u003ccode\u003e{}\u003c/code\u003e\u003c/p\u003e\u003c/h4\u003e\u003c/center\u003e\".format(str(e))\n ]))\n", + "user": "admin", + "dateUpdated": "2018-08-01 19:32:11.281", + "config": { + "colWidth": 12.0, + "fontSize": 9.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "python", + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": true + }, + "editorMode": "ace/mode/python", + "title": true, + "lineNumbers": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "results": { + "code": "SUCCESS", + "msg": [ + { + "type": "TEXT", + "data": "Processing note: name - 2DKAF1WNV, id - 2DKAF1WNV\nBefore:\n{\u0027readers\u0027: [\u0027user1\u0027], \u0027owners\u0027: [\u0027user1\u0027], \u0027writers\u0027: [\u0027user1\u0027], \u0027runners\u0027: []}\nAfter:\n{\u0027readers\u0027: [\u0027user1\u0027], \u0027owners\u0027: [\u0027user1\u0027], \u0027writers\u0027: [\u0027user1\u0027], \u0027runners\u0027: [\u0027user1\u0027]}\n------------\nProcessing note: name - 2DKUA35W5, id - 2DKUA35W5\nBefore:\n{\u0027readers\u0027: [], \u0027owners\u0027: [\u0027user2\u0027], \u0027writers\u0027: [\u0027user2\u0027], \u0027runners\u0027: []}\nAfter:\n{\u0027readers\u0027: [], \u0027owners\u0027: [\u0027user2\u0027], \u0027writers\u0027: [\u0027user2\u0027], \u0027runners\u0027: [\u0027user2\u0027]}\n------------\nProcessing note: name - 2DP6AMY7K, id - Angular\nBefore:\n{\u0027readers\u0027: [], \u0027owners\u0027: [\u0027user3\u0027], \u0027writers\u0027: [\u0027user3\u0027], \u0027runners\u0027: []}\nAfter:\n{\u0027readers\u0027: [], \u0027owners\u0027: [\u0027user3\u0027], \u0027writers\u0027: [\u0027user3\u0027], \u0027runners\u0027: [\u0027user3\u0027]}\n------------\n" + } + ] + }, + "apps": [], + "jobName": "paragraph_1533118760432_702537472", + "id": "20180801-131920_952360200", + "dateCreated": "2018-08-01 13:19:20.432", + "dateStarted": "2018-08-01 19:32:11.316", + "dateFinished": "2018-08-01 19:32:11.875", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%spark.pyspark\n", + "user": "admin", + "dateUpdated": "2018-08-01 19:32:11.916", + "config": { + "colWidth": 12.0, + "fontSize": 9.0, + "enabled": true, + "results": {}, + "editorSetting": { + "language": "python", + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": true + }, + "editorMode": "ace/mode/python" + }, + "settings": { + "params": {}, + "forms": {} + }, + "apps": [], + "jobName": "paragraph_1533132839581_1307691002", + "id": "20180801-171359_89710812", + "dateCreated": "2018-08-01 17:13:59.581", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + } + ], + "name": "System/Migrate from 0.7", + "id": "FixReaders", + "noteParams": {}, + "noteForms": {}, + "angularObjects": { + "md:shared_process": [], + "python:shared_process": [], + "spark:shared_process": [] + }, + "config": { + "isZeppelinNotebookCronEnable": true, + "looknfeel": "default", + "personalizedMode": "false" + }, + "info": {} +} diff --git a/docs/development/contribution/how_to_contribute_code.md b/docs/development/contribution/how_to_contribute_code.md index b172aa193be..290c8d1a5b7 100644 --- a/docs/development/contribution/how_to_contribute_code.md +++ b/docs/development/contribution/how_to_contribute_code.md @@ -51,13 +51,13 @@ First of all, you need Zeppelin source code. The official location of Zeppelin i Get the source code on your development machine using git. -``` +```bash git clone git://git.apache.org/zeppelin.git zeppelin ``` You may also want to develop against a specific branch. For example, for branch-0.5.6 -``` +```bash git clone -b branch-0.5.6 git://git.apache.org/zeppelin.git zeppelin ``` @@ -69,19 +69,19 @@ Before making a pull request, please take a look [Contribution Guidelines](http: ### Build -``` +```bash mvn install ``` To skip test -``` +```bash mvn install -DskipTests ``` To build with specific spark / hadoop version -``` +```bash mvn install -Dspark.version=x.x.x -Dhadoop.version=x.x.x ``` @@ -89,29 +89,48 @@ 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 + +```bash 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="" +HADOOP_HOME=YOUR_HADOOP_HOME JAVA_HOME=YOUR_JAVA_HOME \ +mvn exec:java -Dexec.mainClass="org.apache.zeppelin.server.ZeppelinServer" -Dexec.args="" ``` -> **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. +#### Option 2 - Daemon Script -or use daemon script +> **Note:** Make sure you first run +```bash +mvn clean install -DskipTests ``` + +in your zeppelin root directory, otherwise your server build will fail to find the required dependencies in the local repro. + +or use daemon script + +```bash 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. To regenerate the code, install **thrift-0.9.2** and then run the following command to generate thrift code. - -``` +```bash cd /zeppelin-interpreter/src/main/thrift ./genthrift.sh ``` @@ -120,14 +139,16 @@ cd /zeppelin-interpreter/src/main/thrift Zeppelin has [set of integration tests](https://github.com/apache/zeppelin/tree/master/zeppelin-server/src/test/java/org/apache/zeppelin/integration) using Selenium. To run these test, first build and run Zeppelin and make sure Zeppelin is running on port 8080. Then you can run test using following command -``` -TEST_SELENIUM=true mvn test -Dtest=[TEST_NAME] -DfailIfNoTests=false -pl 'zeppelin-interpreter,zeppelin-zengine,zeppelin-server' +```bash +TEST_SELENIUM=true mvn test -Dtest=[TEST_NAME] -DfailIfNoTests=false \ +-pl 'zeppelin-interpreter,zeppelin-zengine,zeppelin-server' ``` For example, to run [ParagraphActionIT](https://github.com/apache/zeppelin/blob/master/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java), -``` -TEST_SELENIUM=true mvn test -Dtest=ParagraphActionsIT -DfailIfNoTests=false -pl 'zeppelin-interpreter,zeppelin-zengine,zeppelin-server' +```bash +TEST_SELENIUM=true mvn test -Dtest=ParagraphActionsIT -DfailIfNoTests=false \ +-pl 'zeppelin-interpreter,zeppelin-zengine,zeppelin-server' ``` You'll need Firefox web browser installed in your development environment. While CI server uses [Firefox 31.0](https://ftp.mozilla.org/pub/firefox/releases/31.0/) to run selenium test, it is good idea to install the same version (disable auto update to keep the version). diff --git a/docs/development/contribution/how_to_contribute_website.md b/docs/development/contribution/how_to_contribute_website.md index d5d3b5a28c5..1b7c2d93678 100644 --- a/docs/development/contribution/how_to_contribute_website.md +++ b/docs/development/contribution/how_to_contribute_website.md @@ -39,7 +39,7 @@ Documentation website is hosted in 'master' branch under `/docs/` dir. First of all, you need the website source code. The official location of mirror for Zeppelin is [http://git.apache.org/zeppelin.git](http://git.apache.org/zeppelin.git). Get the source code on your development machine using git. -``` +```bash git clone git://git.apache.org/zeppelin.git cd docs ``` diff --git a/docs/development/contribution/useful_developer_tools.md b/docs/development/contribution/useful_developer_tools.md index 326986afd46..17ca40307f5 100644 --- a/docs/development/contribution/useful_developer_tools.md +++ b/docs/development/contribution/useful_developer_tools.md @@ -37,7 +37,7 @@ Check [zeppelin-web: Local Development](https://github.com/apache/zeppelin/tree/ this script would be helpful when changing JDK version frequently. -``` +```bash function setjdk() { if [ $# -ne 0 ]; then # written based on OSX. @@ -59,7 +59,7 @@ you can use this function like `setjdk 1.8` / `setjdk 1.7` ### Building Submodules Selectively -``` +```bash # build `zeppelin-web` only mvn clean -pl 'zeppelin-web' package -DskipTests; @@ -71,7 +71,8 @@ mvn clean package -pl 'spark,spark-dependencies,zeppelin-server' --am -DskipTest # build spark related modules with profiles: scala 2.11, spark 2.1 hadoop 2.7 ./dev/change_scala_version.sh 2.11 -mvn clean package -Pspark-2.1 -Phadoop-2.7 -Pscala-2.11 -pl 'spark,spark-dependencies,zeppelin-server' --am -DskipTests +mvn clean package -Pspark-2.1 -Phadoop-2.7 -Pscala-2.11 \ +-pl 'spark,spark-dependencies,zeppelin-server' --am -DskipTests # build `zeppelin-server` and `markdown` with dependencies mvn clean package -pl 'markdown,zeppelin-server' --am -DskipTests @@ -79,7 +80,7 @@ mvn clean package -pl 'markdown,zeppelin-server' --am -DskipTests ### Running Individual Tests -``` +```bash # run the `HeliumBundleFactoryTest` test class mvn test -pl 'zeppelin-server' --am -DfailIfNoTests=false -Dtest=HeliumBundleFactoryTest ``` @@ -88,13 +89,15 @@ mvn test -pl 'zeppelin-server' --am -DfailIfNoTests=false -Dtest=HeliumBundleFac Make sure that Zeppelin instance is started to execute integration tests (= selenium tests). -``` +```bash # run the `SparkParagraphIT` test class -TEST_SELENIUM="true" mvn test -pl 'zeppelin-server' --am -DfailIfNoTests=false -Dtest=SparkParagraphIT +TEST_SELENIUM="true" mvn test -pl 'zeppelin-server' --am \ +-DfailIfNoTests=false -Dtest=SparkParagraphIT # run the `testSqlSpark` test function only in the `SparkParagraphIT` class # but note that, some test might be dependent on the previous tests -TEST_SELENIUM="true" mvn test -pl 'zeppelin-server' --am -DfailIfNoTests=false -Dtest=SparkParagraphIT#testSqlSpark +TEST_SELENIUM="true" mvn test -pl 'zeppelin-server' --am \ +-DfailIfNoTests=false -Dtest=SparkParagraphIT#testSqlSpark ``` diff --git a/docs/development/helium/writing_application.md b/docs/development/helium/writing_application.md index 366d3e74aea..d128671e23b 100644 --- a/docs/development/helium/writing_application.md +++ b/docs/development/helium/writing_application.md @@ -147,7 +147,7 @@ Resouce name is a string which will be compared with the name of objects in the Application may require two or more resources. Required resources can be listed inside of the json array. For example, if the application requires object "name1", "name2" and "className1" type of object to run, resources field can be -``` +```json resources: [ [ "name1", "name2", ":className1", ...] ] @@ -155,7 +155,7 @@ resources: [ If Application can handle alternative combination of required resources, alternative set can be listed as below. -``` +```json resources: [ [ "name", ":className"], [ "altName", ":altClassName1"], @@ -165,7 +165,7 @@ resources: [ Easier way to understand this scheme is -``` +```json resources: [ [ 'resource' AND 'resource' AND ... ] OR [ 'resource' AND 'resource' AND ... ] OR diff --git a/docs/development/helium/writing_visualization_basic.md b/docs/development/helium/writing_visualization_basic.md index 3f7c1dcbac0..207e8b54b78 100644 --- a/docs/development/helium/writing_visualization_basic.md +++ b/docs/development/helium/writing_visualization_basic.md @@ -190,7 +190,7 @@ e.g. #### 4. Run in dev mode -Place your __Helium package file__ in local registry (ZEPPELIN_HOME/helium). +Place your __Helium package file__ in local registry (`ZEPPELIN_HOME/helium`). Run Zeppelin. And then run zeppelin-web in visualization dev mode. ```bash @@ -198,7 +198,7 @@ cd zeppelin-web yarn run dev:helium ``` -You can browse localhost:9000. Everytime refresh your browser, Zeppelin will rebuild your visualization and reload changes. +You can browse `localhost:9000`. Everytime refresh your browser, Zeppelin will rebuild your visualization and reload changes. #### 5. Publish your visualization diff --git a/docs/development/writing_zeppelin_interpreter.md b/docs/development/writing_zeppelin_interpreter.md index 6ba24bc44a7..c4737ef6bf5 100644 --- a/docs/development/writing_zeppelin_interpreter.md +++ b/docs/development/writing_zeppelin_interpreter.md @@ -40,7 +40,49 @@ In 'Separate Interpreter(scoped / isolated) for each note' mode which you can se ## Make your own Interpreter Creating a new interpreter is quite simple. Just extend [org.apache.zeppelin.interpreter](https://github.com/apache/zeppelin/blob/master/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java) abstract class and implement some methods. -You can include `org.apache.zeppelin:zeppelin-interpreter:[VERSION]` artifact in your build system. And you should put your jars under your interpreter directory with a specific directory name. Zeppelin server reads interpreter directories recursively and initializes interpreters including your own interpreter. +For your interpreter project, you need to make `interpreter-parent` as your parent project and use plugin `maven-enforcer-plugin`, `maven-dependency-plugin` and `maven-resources-plugin`. Here's one sample pom.xml + +```xml + + 4.0.0 + + + interpreter-parent + org.apache.zeppelin + 0.8.0-SNAPSHOT + ../interpreter-parent + + + ... + + + + org.apache.zeppelin + zeppelin-interpreter + ${project.version} + provided + + + + + + + maven-enforcer-plugin + + + maven-dependency-plugin + + + maven-resources-plugin + + + + + +``` + +You should include `org.apache.zeppelin:zeppelin-interpreter:[VERSION]` as your interpreter's dependency in `pom.xml`. Bes +And you should put your jars under your interpreter directory with a specific directory name. Zeppelin server reads interpreter directories recursively and initializes interpreters including your own interpreter. There are three locations where you can store your interpreter group, name and other information. Zeppelin server tries to find the location below. Next, Zeppelin tries to find `interpreter-setting.json` in your interpreter jar. @@ -74,7 +116,8 @@ Here is an example of `interpreter-setting.json` on your own interpreter. }, "editor": { "language": "your-syntax-highlight-language", - "editOnDblClick": false + "editOnDblClick": false, + "completionKey": "TAB" } }, { @@ -85,7 +128,7 @@ Here is an example of `interpreter-setting.json` on your own interpreter. Finally, Zeppelin uses static initialization with the following: -``` +```java static { Interpreter.register("MyInterpreterName", MyClassName.class.getName()); } @@ -114,7 +157,7 @@ If you want to add a new set of syntax highlighting, 1. Add the `mode-*.js` file to [zeppelin-web/bower.json](https://github.com/apache/zeppelin/blob/master/zeppelin-web/bower.json) (when built, [zeppelin-web/src/index.html](https://github.com/apache/zeppelin/blob/master/zeppelin-web/src/index.html) will be changed automatically). 2. Add `language` field to `editor` object. Note that if you don't specify language field, your interpreter will use plain text mode for syntax highlighting. Let's say you want to set your language to `java`, then add: - ``` + ```json "editor": { "language": "java" } @@ -123,11 +166,24 @@ If you want to add a new set of syntax highlighting, ### Edit on double click If your interpreter uses mark-up language such as markdown or HTML, set `editOnDblClick` to `true` so that text editor opens on pargraph double click and closes on paragraph run. Otherwise set it to `false`. -``` +```json "editor": { "editOnDblClick": false } ``` + +### Completion key (Optional) +By default, `Ctrl+dot(.)` brings autocompletion list in the editor. +Through `completionKey`, each interpreter can configure autocompletion key. +Currently `TAB` is only available option. + +```json +"editor": { + "completionKey": "TAB" +} +``` + + ## Install your interpreter binary Once you have built your interpreter, you can place it under the interpreter directory with all its dependencies. @@ -145,7 +201,7 @@ To configure your interpreter you need to follow these steps: Property value is comma separated [INTERPRETER\_CLASS\_NAME]. For example, - ``` + ```xml zeppelin.interpreters org.apache.zeppelin.spark.SparkInterpreter,org.apache.zeppelin.spark.PySparkInterpreter,org.apache.zeppelin.spark.SparkSqlInterpreter,org.apache.zeppelin.spark.DepInterpreter,org.apache.zeppelin.markdown.Markdown,org.apache.zeppelin.shell.ShellInterpreter,org.apache.zeppelin.hive.HiveInterpreter,com.me.MyNewInterpreter @@ -169,7 +225,7 @@ Note that the first interpreter configuration in zeppelin.interpreters will be t For example, -``` +```scala %myintp val a = "My interpreter" @@ -179,11 +235,11 @@ println(a) ### 0.6.0 and later Inside of a note, `%[INTERPRETER_GROUP].[INTERPRETER_NAME]` directive will call your interpreter. -You can omit either [INTERPRETER\_GROUP] or [INTERPRETER\_NAME]. If you omit [INTERPRETER\_NAME], then first available interpreter will be selected in the [INTERPRETER\_GROUP]. -Likewise, if you skip [INTERPRETER\_GROUP], then [INTERPRETER\_NAME] will be chosen from default interpreter group. +You can omit either `[INTERPRETER\_GROUP]` or `[INTERPRETER\_NAME]`. If you omit `[INTERPRETER\_NAME]`, then first available interpreter will be selected in the `[INTERPRETER\_GROUP]`. +Likewise, if you skip `[INTERPRETER\_GROUP]`, then `[INTERPRETER\_NAME]` will be chosen from default interpreter group. -For example, if you have two interpreter myintp1 and myintp2 in group mygrp, you can call myintp1 like +For example, if you have two interpreter `myintp1` and `myintp2` in group `mygrp`, you can call myintp1 like ``` %mygrp.myintp1 @@ -191,7 +247,7 @@ For example, if you have two interpreter myintp1 and myintp2 in group mygrp, you codes for myintp1 ``` -and you can call myintp2 like +and you can call `myintp2` like ``` %mygrp.myintp2 @@ -199,7 +255,7 @@ and you can call myintp2 like codes for myintp2 ``` -If you omit your interpreter name, it'll select first available interpreter in the group ( myintp1 ). +If you omit your interpreter name, it'll select first available interpreter in the group ( `myintp1` ). ``` %mygrp diff --git a/docs/index.md b/docs/index.md index 102af4cdbea..6fe044a6255 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 @@ -72,6 +73,8 @@ limitations under the License. * [Publishing Paragraphs](./usage/other_features/publishing_paragraphs.html) results into your external website * [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) @@ -102,6 +105,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 @@ -123,11 +127,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) @@ -135,7 +134,7 @@ limitations under the License. * [BigQuery](./interpreter/bigquery.html) * [Cassandra](./interpreter/cassandra.html) * [Elasticsearch](./interpreter/elasticsearch.html) - * [flink](./interpreter/flink.html) + * [Flink](./interpreter/flink.html) * [Geode](./interpreter/geode.html) * [Groovy](./interpreter/groovy.html) * [HBase](./interpreter/hbase.html) @@ -146,13 +145,21 @@ limitations under the License. * [Kylin](./interpreter/kylin.html) * [Lens](./interpreter/lens.html) * [Livy](./interpreter/livy.html) - * [markdown](./interpreter/markdown.html) + * [Mahout](./interpreter/mahout.html) + * [Markdown](./interpreter/markdown.html) + * [Neo4j](./interpreter/neo4j.html) * [Pig](./interpreter/pig.html) * [Postgresql, HAWQ](./interpreter/postgresql.html) * [Python](./interpreter/python.html) * [R](./interpreter/r.html) + * [SAP](./interpreter/sap.html) * [Scalding](./interpreter/scalding.html) * [Scio](./interpreter/scio.html) - * [Shell](./interpreter/Shell.html) + * [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) + 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()); diff --git a/docs/interpreter/bigquery.md b/docs/interpreter/bigquery.md index 7ebe2e2fda8..cdac762f6db 100644 --- a/docs/interpreter/bigquery.md +++ b/docs/interpreter/bigquery.md @@ -48,6 +48,11 @@ limitations under the License. 100000 Max result set size + + zeppelin.bigquery.sql_dialect + + BigQuery SQL dialect (standardSQL or legacySQL). If empty, [query prefix](https://cloud.google.com/bigquery/docs/reference/standard-sql/enabling-standard-sql#sql-prefix) like '#standardSQL' can be used. + @@ -58,20 +63,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 +### Provide Application Default 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): +Within Google Cloud Platform (e.g. Google App Engine, Google Compute Engine), +built-in credentials are used by default. - - 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. +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/interpreter/cassandra.md b/docs/interpreter/cassandra.md index e91d995093b..5a20d82b632 100644 --- a/docs/interpreter/cassandra.md +++ b/docs/interpreter/cassandra.md @@ -69,27 +69,27 @@ The **Cassandra** interpreter accepts the following commands Help command - HELP + `HELP` Display the interactive help menu Schema commands - DESCRIBE KEYSPACE, DESCRIBE CLUSTER, DESCRIBE TABLES ... + `DESCRIBE KEYSPACE`, `DESCRIBE CLUSTER`, `DESCRIBE TABLES` ... Custom commands to describe the Cassandra schema Option commands - @consistency, @retryPolicy, @fetchSize ... + `@consistency`, `@retryPolicy`, `@fetchSize` ... Inject runtime options to all statements in the paragraph Prepared statement commands - @prepare, @bind, @remove_prepared + `@prepare`, `@bind`, `@remove_prepared` Let you register a prepared command and re-use it later by injecting bound values Native CQL statements - All CQL-compatible statements (SELECT, INSERT, CREATE ...) + All CQL-compatible statements (`SELECT`, `INSERT`, `CREATE`, ...) All CQL statements are executed directly against the Cassandra server @@ -107,15 +107,15 @@ SELECT * FROM users WHERE login='jdoe'; Each statement should be separated by a semi-colon ( **;** ) except the special commands below: -1. @prepare -2. @bind -3. @remove_prepare -4. @consistency -5. @serialConsistency -6. @timestamp -7. @retryPolicy -8. @fetchSize -9. @requestTimeOut +1. `@prepare` +2. `@bind` +3. `@remove_prepare` +4. `@consistency` +5. `@serialConsistency` +6. `@timestamp` +7. `@retryPolicy` +8. `@fetchSize` +9. `@requestTimeOut` Multi-line statements as well as multiple statements on the same line are also supported as long as they are separated by a semi-colon. Ex: @@ -130,7 +130,7 @@ FROM artists WHERE login='jlennon'; ``` -Batch statements are supported and can span multiple lines, as well as DDL(CREATE/ALTER/DROP) statements: +Batch statements are supported and can span multiple lines, as well as DDL (`CREATE`/`ALTER`/`DROP`) statements: ```sql @@ -429,7 +429,7 @@ Some remarks about query parameters: > 1. **many** query parameters can be set in the same paragraph > 2. if the **same** query parameter is set many time with different values, the interpreter only take into account the first value -> 3. each query parameter applies to **all CQL statements** in the same paragraph, unless you override the option using plain CQL text (like forcing timestamp with the USING clause) +> 3. each query parameter applies to **all CQL statements** in the same paragraph, unless you override the option using plain CQL text (like forcing timestamp with the `USING` clause) > 4. the order of each query parameter with regard to CQL statement does not matter ## Support for Prepared Statements @@ -463,7 +463,7 @@ saves the generated prepared statement in an **internal hash map**, using the pr > Please note that this internal prepared statement map is shared with **all notebooks** and **all paragraphs** because there is only one instance of the interpreter for Cassandra -> If the interpreter encounters **many** @prepare for the **same _statement-name_ (key)**, only the **first** statement will be taken into account. +> If the interpreter encounters **many** `@prepare` for the **same _statement-name_ (key)**, only the **first** statement will be taken into account. Example: @@ -474,7 +474,7 @@ Example: ``` For the above example, the prepared statement is `SELECT * FROM spark_demo.albums LIMIT ?`. -`SELECT * FROM spark_demo.artists LIMIT ? is ignored because an entry already exists in the prepared statements map with the key select. +`SELECT * FROM spark_demo.artists LIMIT ?` is ignored because an entry already exists in the prepared statements map with the key _select_. In the context of **Zeppelin**, a notebook can be scheduled to be executed at regular interval, thus it is necessary to **avoid re-preparing many time the same statement (considered an anti-pattern)**. @@ -488,18 +488,18 @@ Once the statement is prepared (possibly in a separated notebook/paragraph). You Bound values are not mandatory for the **@bind** statement. However if you provide bound values, they need to comply to some syntax: -* String values should be enclosed between simple quotes ( ‘ ) -* Date values should be enclosed between simple quotes ( ‘ ) and respect the formats: +* String values should be enclosed between simple quotes (**'**) +* Date values should be enclosed between simple quotes (**'**) and respect the formats (full list is in the [documentation](https://docs.datastax.com/en/cql/3.3/cql/cql_reference/timestamp_type_r.html)): 1. yyyy-MM-dd HH:MM:ss 2. yyyy-MM-dd HH:MM:ss.SSS * **null** is parsed as-is -* **boolean** (true|false) are parsed as-is +* **boolean** (`true`|`false`) are parsed as-is * collection values must follow the **[standard CQL syntax]**: - * list: [‘list_item1’, ’list_item2’, ...] - * set: {‘set_item1’, ‘set_item2’, …} - * map: {‘key1’: ‘val1’, ‘key2’: ‘val2’, …} -* **tuple** values should be enclosed between parenthesis (see **[Tuple CQL syntax]**): (‘text’, 123, true) -* **udt** values should be enclosed between brackets (see **[UDT CQL syntax]**): {stree_name: ‘Beverly Hills’, number: 104, zip_code: 90020, state: ‘California’, …} + * list: ['list_item1', 'list_item2', ...] + * set: {'set_item1', 'set_item2', …} + * map: {'key1': 'val1', 'key2': 'val2', …} +* **tuple** values should be enclosed between parenthesis (see **[Tuple CQL syntax]**): ('text', 123, true) +* **udt** values should be enclosed between brackets (see **[UDT CQL syntax]**): {stree_name: 'Beverly Hills', number: 104, zip_code: 90020, state: 'California', …} > It is possible to use the @bind statement inside a batch: > @@ -540,8 +540,7 @@ Example: AND styles CONTAINS '${style=Rock}'; {% endraw %} - -In the above example, the first CQL query will be executed for _performer='Sheryl Crow' AND style='Rock'_. +In the above example, the first CQL query will be executed for `performer='Sheryl Crow' AND style='Rock'`. For subsequent queries, you can change the value directly using the form. > Please note that we enclosed the **$\{ \}** block between simple quotes ( **'** ) because Cassandra expects a String here. @@ -550,14 +549,12 @@ For subsequent queries, you can change the value directly using the form. It is also possible to use dynamic forms for **prepared statements**: {% raw %} - @bind[select]=='${performer=Sheryl Crow|Doof|Fanfarlo|Los Paranoia}', '${style=Rock}' - {% endraw %} ## Shared states -It is possible to execute many paragraphs in parallel. However, at the back-end side, we’re still using synchronous queries. +It is possible to execute many paragraphs in parallel. However, at the back-end side, we're still using synchronous queries. _Asynchronous execution_ is only possible when it is possible to return a `Future` value in the `InterpreterResult`. It may be an interesting proposal for the **Zeppelin** project. @@ -570,7 +567,7 @@ Long story short, you have 3 available bindings: - **isolated**: _different JVM_ running a _single Interpreter instance_, one JVM for each note Using the **shared** binding, the same `com.datastax.driver.core.Session` object is used for **all** notes and paragraphs. -Consequently, if you use the **USE _keyspace name_;** statement to log into a keyspace, it will change the keyspace for +Consequently, if you use the `USE keyspace_name;` statement to log into a keyspace, it will change the keyspace for **all current users** of the **Cassandra** interpreter because we only create 1 `com.datastax.driver.core.Session` object per instance of **Cassandra** interpreter. @@ -588,7 +585,7 @@ To configure the **Cassandra** interpreter, go to the **Interpreter** menu and s The **Cassandra** interpreter is using the official **[Cassandra Java Driver]** and most of the parameters are used to configure the Java driver -Below are the configuration parameters and their default value. +Below are the configuration parameters and their default values. @@ -597,41 +594,41 @@ Below are the configuration parameters and their default value. - + - - - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -722,74 +719,74 @@ Below are the configuration parameters and their default value. - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/docs/interpreter/groovy.md b/docs/interpreter/groovy.md index f64cbded242..679b5bcce6d 100644 --- a/docs/interpreter/groovy.md +++ b/docs/interpreter/groovy.md @@ -91,6 +91,7 @@ g.table( * `String g.getProperty('PROPERTY_NAME')` + ```groovy g.PROPERTY_NAME g.'PROPERTY_NAME' diff --git a/docs/interpreter/hbase.md b/docs/interpreter/hbase.md index 12e05174359..fd6334acebc 100644 --- a/docs/interpreter/hbase.md +++ b/docs/interpreter/hbase.md @@ -70,9 +70,9 @@ mvn clean package -DskipTests -Phadoop-2.6 -Dhadoop.version=2.6.0 -P build-distr If you want to connect to HBase running on a cluster, you'll need to follow the next step. ### Export HBASE_HOME -In **conf/zeppelin-env.sh**, export `HBASE_HOME` environment variable with your HBase installation path. This ensures `hbase-site.xml` can be loaded. +In `conf/zeppelin-env.sh`, export `HBASE_HOME` environment variable with your HBase installation path. This ensures `hbase-site.xml` can be loaded. -for example +For example ```bash export HBASE_HOME=/usr/lib/hbase diff --git a/docs/interpreter/ignite.md b/docs/interpreter/ignite.md index 0b4e27b2720..49e432f3622 100644 --- a/docs/interpreter/ignite.md +++ b/docs/interpreter/ignite.md @@ -42,8 +42,8 @@ In order to use Ignite interpreters, you may install Apache Ignite in some simpl > **Tip. If you want to run Ignite examples on the cli not IDE, you can export executable Jar file from IDE. Then run it by using below command.** -``` -$ nohup java -jar +```bash +nohup java -jar ``` ## Configuring Ignite Interpreter @@ -96,7 +96,7 @@ In order to execute SQL query, use ` %ignite.ignitesql ` prefix.
    Supposing you are running `org.apache.ignite.examples.streaming.wordcount.StreamWords`, then you can use "words" cache( Of course you have to specify this cache name to the Ignite interpreter setting section `ignite.jdbc.url` of Zeppelin ). For example, you can select top 10 words in the words cache using the following query -``` +```sql %ignite.ignitesql select _val, count(_val) as cnt from String group by _val order by cnt desc limit 10 ``` @@ -105,7 +105,7 @@ select _val, count(_val) as cnt from String group by _val order by cnt desc limi As long as your Ignite version and Zeppelin Ignite version is same, you can also use scala code. Please check the Zeppelin Ignite version before you download your own Ignite. -``` +```scala %ignite import org.apache.ignite._ import org.apache.ignite.cache.affinity._ diff --git a/docs/interpreter/jdbc.md b/docs/interpreter/jdbc.md index 44f8aab9002..b0f5d6ba0db 100644 --- a/docs/interpreter/jdbc.md +++ b/docs/interpreter/jdbc.md @@ -124,6 +124,11 @@ The JDBC interpreter properties are defined by default like below. + + + + + @@ -192,6 +197,14 @@ There are more JDBC interpreter properties you can specify like below. + + + + + + + +
    Default Value
    cassandra.cluster`cassandra.cluster` Name of the Cassandra cluster to connect to Test Cluster
    cassandra.compression.protocolOn wire compression. Possible values are: NONE, SNAPPY, LZ4NONE`cassandra.compression.protocol`On wire compression. Possible values are: `NONE`, `SNAPPY`, `LZ4``NONE`
    cassandra.credentials.username`cassandra.credentials.username` If security is enable, provide the login none
    cassandra.credentials.password`cassandra.credentials.password` If security is enable, provide the password none
    cassandra.hosts`cassandra.hosts` Comma separated Cassandra hosts (DNS name or IP address).
    - Ex: '192.168.0.12,node2,node3' + Ex: `192.168.0.12,node2,node3`
    localhost`localhost`
    cassandra.interpreter.parallelism`cassandra.interpreter.parallelism` Number of concurrent paragraphs(queries block) that can be executed 10
    cassandra.keyspace`cassandra.keyspace` Default keyspace to connect to. @@ -640,80 +637,80 @@ Below are the configuration parameters and their default value. in all of your queries system`system`
    cassandra.load.balancing.policy`cassandra.load.balancing.policy` - Load balancing policy. Default = new TokenAwarePolicy(new DCAwareRoundRobinPolicy()) - To Specify your own policy, provide the fully qualify class name (FQCN) of your policy. + Load balancing policy. Default = `new TokenAwarePolicy(new DCAwareRoundRobinPolicy())` + To Specify your own policy, provide the fully qualify class name (FQCN) of your policy. At runtime the interpreter will instantiate the policy using Class.forName(FQCN) DEFAULT
    cassandra.max.schema.agreement.wait.second`cassandra.max.schema.agreement.wait.second` Cassandra max schema agreement wait in second 10
    cassandra.pooling.core.connection.per.host.local`cassandra.pooling.core.connection.per.host.local` Protocol V2 and below default = 2. Protocol V3 and above default = 1 2
    cassandra.pooling.core.connection.per.host.remote`cassandra.pooling.core.connection.per.host.remote` Protocol V2 and below default = 1. Protocol V3 and above default = 1 1
    cassandra.pooling.heartbeat.interval.seconds`cassandra.pooling.heartbeat.interval.seconds` Cassandra pool heartbeat interval in secs 30
    cassandra.pooling.idle.timeout.seconds`cassandra.pooling.idle.timeout.seconds` Cassandra idle time out in seconds 120
    cassandra.pooling.max.connection.per.host.local`cassandra.pooling.max.connection.per.host.local` Protocol V2 and below default = 8. Protocol V3 and above default = 1 8
    cassandra.pooling.max.connection.per.host.remote`cassandra.pooling.max.connection.per.host.remote` Protocol V2 and below default = 2. Protocol V3 and above default = 1 2
    cassandra.pooling.max.request.per.connection.local`cassandra.pooling.max.request.per.connection.local` Protocol V2 and below default = 128. Protocol V3 and above default = 1024 128
    cassandra.pooling.max.request.per.connection.remote`cassandra.pooling.max.request.per.connection.remote` Protocol V2 and below default = 128. Protocol V3 and above default = 256 128
    cassandra.pooling.new.connection.threshold.local`cassandra.pooling.new.connection.threshold.local` Protocol V2 and below default = 100. Protocol V3 and above default = 800 100
    cassandra.pooling.new.connection.threshold.remote`cassandra.pooling.new.connection.threshold.remote` Protocol V2 and below default = 100. Protocol V3 and above default = 200 100
    cassandra.pooling.pool.timeout.millisecs`cassandra.pooling.pool.timeout.millisecs` Cassandra pool time out in millisecs 5000
    cassandra.protocol.version`cassandra.protocol.version` Cassandra binary protocol version 4
    Cassandra query default consistency level
    - Available values: ONE, TWO, THREE, QUORUM, LOCAL_ONE, LOCAL_QUORUM, EACH_QUORUM, ALL + Available values: `ONE`, `TWO`, `THREE`, `QUORUM`, `LOCAL_ONE`, `LOCAL_QUORUM`, `EACH_QUORUM`, `ALL`
    ONE`ONE`
    cassandra.query.default.fetchSize`cassandra.query.default.fetchSize` Cassandra query default fetch size 5000
    cassandra.query.default.serial.consistency`cassandra.query.default.serial.consistency` Cassandra query default serial consistency level
    - Available values: SERIAL, LOCAL_SERIAL + Available values: `SERIAL`, `LOCAL_SERIAL`
    SERIAL`SERIAL`
    cassandra.reconnection.policy`cassandra.reconnection.policy` Cassandra Reconnection Policy. - Default = new ExponentialReconnectionPolicy(1000, 10 * 60 * 1000) - To Specify your own policy, provide the fully qualify class name (FQCN) of your policy. + Default = `new ExponentialReconnectionPolicy(1000, 10 * 60 * 1000)` + To Specify your own policy, provide the fully qualify class name (FQCN) of your policy. At runtime the interpreter will instantiate the policy using Class.forName(FQCN) DEFAULT
    cassandra.retry.policy`cassandra.retry.policy` Cassandra Retry Policy. - Default = DefaultRetryPolicy.INSTANCE - To Specify your own policy, provide the fully qualify class name (FQCN) of your policy. + Default = `DefaultRetryPolicy.INSTANCE` + To Specify your own policy, provide the fully qualify class name (FQCN) of your policy. At runtime the interpreter will instantiate the policy using Class.forName(FQCN) DEFAULT
    cassandra.socket.connection.timeout.millisecs`cassandra.socket.connection.timeout.millisecs` Cassandra socket default connection timeout in millisecs 500
    cassandra.socket.read.timeout.millisecs`cassandra.socket.read.timeout.millisecs` Cassandra socket read timeout in millisecs 12000
    cassandra.socket.tcp.no_delay`cassandra.socket.tcp.no_delay` Cassandra socket TCP no delay true
    cassandra.speculative.execution.policy`cassandra.speculative.execution.policy` Cassandra Speculative Execution Policy. - Default = NoSpeculativeExecutionPolicy.INSTANCE - To Specify your own policy, provide the fully qualify class name (FQCN) of your policy. + Default = `NoSpeculativeExecutionPolicy.INSTANCE` + To Specify your own policy, provide the fully qualify class name (FQCN) of your policy. At runtime the interpreter will instantiate the policy using Class.forName(FQCN) DEFAULT
    cassandra.ssl.enabled`cassandra.ssl.enabled` Enable support for connecting to the Cassandra configured with SSL. To connect to Cassandra configured with SSL use true @@ -798,14 +795,14 @@ Below are the configuration parameters and their default value. false
    cassandra.ssl.truststore.path`cassandra.ssl.truststore.path` Filepath for the truststore file to use for connection to Cassandra with SSL.
    cassandra.ssl.truststore.password`cassandra.ssl.truststore.password` Password for the truststore file to use for connection to Cassandra with SSL. Some SQL which executes every time after initialization of the interpreter (see Binding mode)
    default.statementPrecodeSQL code which executed before the SQL from paragraph, in the same database session (database connection)
    default.completer.schemaFilters default.jceks.credentialKey jceks credential key
    zeppelin.jdbc.interpolationEnables ZeppelinContext variable interpolation into paragraph text. Default value is false.
    zeppelin.jdbc.maxConnLifetimeMaximum of connection lifetime in milliseconds. A value of zero or less means the connection has an infinite lifetime.
    You can also add more properties by using this [method](http://docs.oracle.com/javase/7/docs/api/java/sql/DriverManager.html#getConnection%28java.lang.String,%20java.util.Properties%29). @@ -240,7 +253,7 @@ You can leverage [Zeppelin Dynamic Form](../usage/dynamic_form/intro.html) insid %jdbc_interpreter_name SELECT name, country, performer FROM demo.performers -WHERE name='{{"{{performer=Sheryl Crow|Doof|Fanfarlo|Los Paranoia"}}}}' +WHERE name='${performer=Sheryl Crow|Doof|Fanfarlo|Los Paranoia}' ``` ### Usage *precode* You can set *precode* for each data source. Code runs once while opening the connection. @@ -306,7 +319,7 @@ Returns value of `search_path` which is set in the *default.precode*. ```sql -%jdbc(mysql) +%mysql select @v ``` Returns value of `v` which is set in the *mysql.precode*. @@ -724,5 +737,29 @@ Before Adding one of the below dependencies, check the Phoenix version first. [Maven Repository: org.apache.tajo:tajo-jdbc](https://mvnrepository.com/artifact/org.apache.tajo/tajo-jdbc) +## Object Interpolation +The JDBC interpreter also supports interpolation of `ZeppelinContext` objects into the paragraph text. +The following example shows one use of this facility: + +####In Scala cell: + +```scala +z.put("country_code", "KR") + // ... +``` + +####In later JDBC cell: + +```sql +%jdbc_interpreter_name +select * from patents_list where +priority_country = '{country_code}' and filing_date like '2015-%' +``` + +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 +[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. diff --git a/docs/interpreter/kylin.md b/docs/interpreter/kylin.md index e1d27d9907b..1f2b0f3ab44 100644 --- a/docs/interpreter/kylin.md +++ b/docs/interpreter/kylin.md @@ -75,7 +75,7 @@ To get start with Apache Kylin, please see [Apache Kylin Quickstart](https://kyl ## Using the Apache Kylin Interpreter In a paragraph, use `%kylin(project_name)` to select the **kylin** interpreter, **project name** and then input **sql**. If no project name defined, will use the default project name from the above configuration. -``` +```sql %kylin(learn_project) select count(*) from kylin_sales group by part_dt ``` diff --git a/docs/interpreter/lens.md b/docs/interpreter/lens.md index 4f07c71d9f7..cd00d1ca769 100644 --- a/docs/interpreter/lens.md +++ b/docs/interpreter/lens.md @@ -35,8 +35,8 @@ In order to use Lens interpreters, you may install Apache Lens in some simple st 2. Before running Lens, you have to set HIVE_HOME and HADOOP_HOME. If you want to get more information about this, please refer to [here](http://lens.apache.org/lenshome/install-and-run.html#Installation). Lens also provides Pseudo Distributed mode. [Lens pseudo-distributed setup](http://lens.apache.org/lenshome/pseudo-distributed-setup.html) is done by using [docker](https://www.docker.com/). Hive server and hadoop daemons are run as separate processes in lens pseudo-distributed setup. 3. Now, you can start lens server (or stop). -``` -./bin/lens-ctl start (or stop) +```bash +./bin/lens-ctl start # (or stop) ``` ## Configuring Lens Interpreter @@ -102,11 +102,11 @@ For more interpreter binding information see [here](../usage/interpreter/overvie ### How to use You can analyze your data by using [OLAP Cube](http://lens.apache.org/user/olap-cube.html) [QL](http://lens.apache.org/user/cli.html) which is a high level SQL like language to query and describe data sets organized in data cubes. You may experience OLAP Cube like this [Video tutorial](https://cwiki.apache.org/confluence/display/LENS/2015/07/13/20+Minute+video+demo+of+Apache+Lens+through+examples). -As you can see in this video, they are using Lens Client Shell(./bin/lens-cli.sh). All of these functions also can be used on Zeppelin by using Lens interpreter. +As you can see in this video, they are using Lens Client Shell(`./bin/lens-cli.sh`). All of these functions also can be used on Zeppelin by using Lens interpreter. -
  • Create and Use(Switch) Databases. +
  • Create and Use (Switch) Databases. -``` +```sql create database newDb ``` @@ -161,17 +161,21 @@ create fact your/path/to/lens/client/examples/resources/sales-raw-fact.xml
  • Add partitions to Dimtable and Fact. ``` -dimtable add single-partition --dimtable_name customer_table --storage_name local --path your/path/to/lens/client/examples/resources/customer-local-part.xml +dimtable add single-partition --dimtable_name customer_table --storage_name local +--path your/path/to/lens/client/examples/resources/customer-local-part.xml ``` ``` -fact add partitions --fact_name sales_raw_fact --storage_name local --path your/path/to/lens/client/examples/resources/sales-raw-local-parts.xml +fact add partitions --fact_name sales_raw_fact --storage_name local +--path your/path/to/lens/client/examples/resources/sales-raw-local-parts.xml ```
  • Now, you can run queries on cubes. ``` -query execute cube select customer_city_name, product_details.description, product_details.category, product_details.color, store_sales from sales where time_range_in(delivery_time, '2015-04-11-00', '2015-04-13-00') +query execute cube select customer_city_name, product_details.description, +product_details.category, product_details.color, store_sales from sales +where time_range_in(delivery_time, '2015-04-11-00', '2015-04-13-00') ``` ![Lens Query Result]({{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/lens-result.png) diff --git a/docs/interpreter/livy.md b/docs/interpreter/livy.md index 1741a80c8b7..954eb8cfe02 100644 --- a/docs/interpreter/livy.md +++ b/docs/interpreter/livy.md @@ -144,7 +144,12 @@ Example: `spark.driver.memory` to `livy.spark.driver.memory` zeppelin.livy.ssl.trustStorePassword password for trustStore file. Used when livy ssl is enabled - + + + zeppelin.livy.http.headers + key_1: value_1; key_2: value_2 + custom http headers when calling livy rest api. Each http header is separated by `;`, and each header is one key value pair where key value is separated by `:` + **We remove livy.spark.master in zeppelin-0.7. Because we sugguest user to use livy 0.3 in zeppelin-0.7. And livy 0.3 don't allow to specify livy.spark.master, it enfornce yarn-cluster mode.** @@ -172,7 +177,7 @@ Basically, you can use **spark** -``` +```scala %livy.spark sc.version ``` @@ -180,14 +185,14 @@ sc.version **pyspark** -``` +```python %livy.pyspark print "1" ``` **sparkR** -``` +```r %livy.sparkr hello <- function( name ) { sprintf( "Hello, %s", name ); @@ -203,12 +208,18 @@ i.e. sends extra parameter for creating and running a session ("proxyUser": "${l This is particularly useful when multi users are sharing a Notebook server. ## Apply Zeppelin Dynamic Forms -You can leverage [Zeppelin Dynamic Form](../usage/dynamic_form/intro.html). You can use both the `text input` and `select form` parameterization features. +You can leverage [Zeppelin Dynamic Form](../usage/dynamic_form/intro.html). Form templates is only avalible for livy sql interpreter. +```sql +%livy.sql +select * from products where ${product_id=1} ``` -%livy.pyspark -print "${group_by=product_id,product_id|product_name|customer_id|store_id}" -``` + +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 diff --git a/docs/interpreter/mahout.md b/docs/interpreter/mahout.md index c3b4146f62a..0b1d5292f8b 100644 --- a/docs/interpreter/mahout.md +++ b/docs/interpreter/mahout.md @@ -29,6 +29,7 @@ Apache Mahout is a collection of packages that enable machine learning and matri ### Easy Installation To quickly and easily get up and running using Apache Mahout, run the following command from the top-level directory of the Zeppelin install: + ```bash python scripts/mahout/add_mahout.py ``` @@ -39,34 +40,34 @@ This will create the `%sparkMahout` and `%flinkMahout` interpreters, and restart The `add_mahout.py` script contains several command line arguments for advanced users. - +
    - + - + - + - + - - + + - + - + @@ -165,6 +166,7 @@ Resource Pools are a powerful Zeppelin feature that lets us share information be ### Setting up a Resource Pool in Flink In Spark based interpreters resource pools are accessed via the ZeppelinContext API. To put and get things from the resource pool one can be done simple + ```scala val myVal = 1 z.put("foo", myVal) diff --git a/docs/interpreter/markdown.md b/docs/interpreter/markdown.md index d5581d9b10d..609f20495ee 100644 --- a/docs/interpreter/markdown.md +++ b/docs/interpreter/markdown.md @@ -71,7 +71,6 @@ For more information, please see [Mathematical Expression](../usage/display_syst ### Markdown4j Parser -Since pegdown parser is more accurate and provides much more markdown syntax -`markdown4j` option might be removed later. But keep this parser for the backward compatibility. +Since `pegdown` parser is more accurate and provides much more markdown syntax `markdown4j` option might be removed later. But keep this parser for the backward compatibility. diff --git a/docs/interpreter/neo4j.md b/docs/interpreter/neo4j.md new file mode 100644 index 00000000000..1b14127d523 --- /dev/null +++ b/docs/interpreter/neo4j.md @@ -0,0 +1,117 @@ +--- +layout: page +title: "Neo4j Interpreter for Apache Zeppelin" +description: "Neo4j is a native graph database, designed to store and process graphs from bottom to top." +group: interpreter +--- + +{% include JB/setup %} + +# Neo4j Interpreter for Apache Zeppelin + +
    + +## Overview +[Neo4j](https://neo4j.com/product/) is a native graph database, designed to store and process graphs from bottom to top. + + +![Neo4j - Interpreter - Video]({{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/neo4j-interpreter-video.gif) + +## Configuration +
    Argument Description Example
    --zeppelin_home`--zeppelin_home` This is the path to the Zeppelin installation. This flag is not needed if the script is run from the top-level installation directory or from the `zeppelin/scripts/mahout` directory./path/to/zeppelin`/path/to/zeppelin`
    --mahout_home`--mahout_home` If the user has already installed Mahout, this flag can set the path to `MAHOUT_HOME`. If this is set, downloading Mahout will be skipped./path/to/mahout_home`/path/to/mahout_home`
    --restart_laterRestarting is necessary for updates to take effect. By default the script will restart Zeppelin for you- restart will be skipped if this flag is set.`--restart_later`Restarting is necessary for updates to take effect. By default the script will restart Zeppelin for you. Restart will be skipped if this flag is set. NA
    --force_download`--force_download` This flag will force the script to re-download the binary even if it already exists. This is useful for previously failed downloads. NA
    --overwrite_existing`--overwrite_existing` This flag will force the script to overwrite existing `%sparkMahout` and `%flinkMahout` interpreters. Useful when you want to just start over. NA
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PropertyDefaultDescription
    neo4j.urlbolt://localhost:7687The Neo4j's BOLT url.
    neo4j.auth.typeBASICThe Neo4j's authentication type (NONE, BASIC).
    neo4j.auth.userneo4jThe Neo4j user name.
    neo4j.auth.passwordneo4jThe Neo4j user password.
    neo4j.max.concurrency50Max concurrency call from Zeppelin to Neo4j server.
    + +
    + ![Interpreter configuration]({{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/neo4j-config.png) +
    + + +## Enabling the Neo4j Interpreter +In a notebook, to enable the **Neo4j** interpreter, click the **Gear** icon and select **Neo4j**. + +## Using the Neo4j Interpreter +In a paragraph, use `%neo4j` to select the Neo4j interpreter and then input the Cypher commands. +For list of Cypher commands please refer to the official [Cyper Refcard](http://neo4j.com/docs/cypher-refcard/current/) + +``` +%neo4j +//Sample the TrumpWorld dataset +WITH +'https://docs.google.com/spreadsheets/u/1/d/1Z5Vo5pbvxKJ5XpfALZXvCzW26Cl4we3OaN73K9Ae5Ss/export?format=csv&gid=1996904412' AS url +LOAD CSV WITH HEADERS FROM url AS row +RETURN row.`Entity A`, row.`Entity A Type`, row.`Entity B`, row.`Entity B Type`, row.Connection, row.`Source(s)` +LIMIT 10 +``` + +The Neo4j interpreter leverages the [Network display system](../usage/display_system/basic.html#network) allowing to visualize the them directly from the paragraph. + + +### Write your Cypher queries and navigate your graph + +This query: + +``` +%neo4j +MATCH (vp:Person {name:"VLADIMIR PUTIN"}), (dt:Person {name:"DONALD J. TRUMP"}) +MATCH path = allShortestPaths( (vp)-[*]-(dt) ) +RETURN path +``` +produces the following result_ +![Neo4j - Graph - Result]({{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/neo4j-graph.png) + +### Apply Zeppelin Dynamic Forms +You can leverage [Zeppelin Dynamic Form](../usage/dynamic_form/intro.html) inside your queries. This query: + +``` +%neo4j +MATCH (o:Organization)-[r]-() +RETURN o.name, count(*), collect(distinct type(r)) AS types +ORDER BY count(*) DESC +LIMIT ${Show top=10} +``` + +produces the following result: +![Neo4j - Zeppelin - Dynamic Forms]({{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/neo4j-dynamic-forms.png) + diff --git a/docs/interpreter/python.md b/docs/interpreter/python.md index b4b5ca86514..82280ac2681 100644 --- a/docs/interpreter/python.md +++ b/docs/interpreter/python.md @@ -70,34 +70,51 @@ The interpreter can use all modules already installed (with pip, easy_install... - get the Conda Infomation: - ```%python.conda info``` + ``` + %python.conda info + ``` - list the Conda environments: - ```%python.conda env list``` + ``` + %python.conda env list + ``` - create a conda enviornment: - ```%python.conda create --name [ENV NAME]``` + + ``` + %python.conda create --name [ENV NAME] + ``` - activate an environment (python interpreter will be restarted): - ```%python.conda activate [ENV NAME]``` + ``` + %python.conda activate [ENV NAME] + ``` - deactivate - ```%python.conda deactivate``` + ``` + %python.conda deactivate + ``` - get installed package list inside the current environment - ```%python.conda list``` + ``` + %python.conda list + ``` - install package - ```%python.conda install [PACKAGE NAME]``` + ``` + %python.conda install [PACKAGE NAME] + ``` - uninstall package - ```%python.conda uninstall [PACKAGE NAME]``` + ``` + %python.conda uninstall [PACKAGE NAME] + ``` ### Docker @@ -171,7 +188,8 @@ If Zeppelin cannot find the matplotlib backend files (which should usually be fo then the backend will automatically be set to agg, and the (otherwise deprecated) instructions below can be used for more limited inline plotting. If you are unable to load the inline backend, use `z.show(plt)`: - ```python + +```python %python import matplotlib.pyplot as plt plt.figure() @@ -232,6 +250,71 @@ 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` + - protobuf `pip install protobuf` + +If you already install anaconda, then you just need to install `grpcio` as Jupyter is already included in anaconda. For grpcio version >= 1.12.0 you'll also need to install protobuf separately. + +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/r.md b/docs/interpreter/r.md index 3c6a9a92702..966dc1e9383 100644 --- a/docs/interpreter/r.md +++ b/docs/interpreter/r.md @@ -40,12 +40,30 @@ R -e "print(1+1)" To enjoy plots, install additional libraries with: -``` -+ devtools with `R -e "install.packages('devtools', repos = 'http://cran.us.r-project.org')"` -+ knitr with `R -e "install.packages('knitr', repos = 'http://cran.us.r-project.org')"` -+ ggplot2 with `R -e "install.packages('ggplot2', repos = 'http://cran.us.r-project.org')"` -+ Other vizualisation librairies: `R -e "install.packages(c('devtools','mplot', 'googleVis'), repos = 'http://cran.us.r-project.org'); require(devtools); install_github('ramnathv/rCharts')"` -``` ++ devtools with + + ```bash + R -e "install.packages('devtools', repos = 'http://cran.us.r-project.org')" + ``` + ++ knitr with + + ```bash + R -e "install.packages('knitr', repos = 'http://cran.us.r-project.org')" + ``` + ++ ggplot2 with + + ```bash + R -e "install.packages('ggplot2', repos = 'http://cran.us.r-project.org')" + ``` + ++ Other visualization libraries: + + ```bash + R -e "install.packages(c('devtools','mplot', 'googleVis'), repos = 'http://cran.us.r-project.org'); + require(devtools); install_github('ramnathv/rCharts')" + ``` We recommend you to also install the following optional R libraries for happy data analytics: diff --git a/docs/interpreter/sap.md b/docs/interpreter/sap.md new file mode 100644 index 00000000000..fa492deff93 --- /dev/null +++ b/docs/interpreter/sap.md @@ -0,0 +1,172 @@ +--- + +layout: page + +title: "SAP BusinessObjects Interpreter for Apache Zeppelin" + +description: "SAP BusinessObjects BI platform can simplify the lives of business users and IT staff. SAP BusinessObjects is based on universes. The universe contains dual-semantic layer model. The users make queries upon universes. This interpreter is new interface for universes." + +group: interpreter + +--- + + + +{% include JB/setup %} + +# SAP BusinessObjects (Universe) Interpreter for Apache Zeppelin + +
    + +## Overview + +[SAP BusinessObjects BI platform (universes)](https://help.sap.com/viewer/p/SAP_BUSINESSOBJECTS_BUSINESS_INTELLIGENCE_PLATFORM) can simplify the lives of business users and IT staff. SAP BusinessObjects is based on universes. The universe contains dual-semantic layer model. The users make queries upon universes. This interpreter is new interface for universes. + +*Disclaimer* SAP interpreter is not official interpreter for SAP BusinessObjects BI platform. It uses [BI Semantic Layer REST API](https://help.sap.com/viewer/5431204882b44fc98d56bd752e69f132/4.2.5/en-US/ec54808e6fdb101497906a7cb0e91070.html) + +This interpreter is not directly supported by SAP AG. + +Tested with versions 4.2SP3 (14.2.3.2220) and 4.2SP5. There is no support for filters in UNX-universes converted from old UNV format. + +The universe name must be unique. + +## Configuring SAP Universe Interpreter + +At the "Interpreters" menu, you can edit SAP interpreter or create new one. Zeppelin provides these properties for SAP. + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Property NameValueDescription
    universe.api.urlhttp://localhost:6405/biprwsThe base url for the SAP BusinessObjects BI platform. You have to edit "localhost" that you may use (ex. http://0.0.0.0:6405/biprws)
    universe.authTypesecEnterpriseThe type of authentication for API of Universe. Available values: secEnterprise, secLDAP, secWinAD, secSAPR3
    universe.passwordThe BI platform user password
    universe.userAdministratorThe BI platform user login
    + +![SAP Interpreter Setting]({{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/sap-interpreter-setting.png) + +### How to use + +
  • Choose the universe +
  • Choose dimensions and measures in `select` statement +
  • Define conditions in `where` statement +You can compare two dimensions/measures or use Filter (without value). +Dimesions/Measures can be compared with static values, may be `is null` or `is not null`, contains or not in list. +Available the nested conditions (using braces "()"). "and" operator have more priority than "or". + + +If generated query contains promtps, then promtps will appear as dynamic form after paragraph submitting. + +Example query + +```sql +%sap + +universe [Universe Name]; + +select + + [Folder1].[Dimension2], + + [Folder2].[Dimension3], + + [Measure1] + +where + + [Filter1] + + and [Date] > '2018-01-01 00:00:00' + + and [Folder1].[Dimension4] is not null + + and [Folder1].[Dimension5] in ('Value1', 'Value2'); +``` + +<<<<<<< HEAD +### `distinct` keyword +You can write keyword `distinct` after keyword `select` to return only distinct (different) values. + +Example query +```sql +%sap +universe [Universe Name]; + +select distinct + [Folder1].[Dimension2], [Measure1] +where + [Filter1]; +``` + +### `limit` keyword +You can write keyword `limit` and limit value in the end of query to limit the number of records returned based on a limit value. + +Example query +```sql +%sap +universe [Universe Name]; + +select + [Folder1].[Dimension2], [Measure1] +where + [Filter1] +limit 100; +``` + +## Object Interpolation +The SAP interpreter also supports interpolation of `ZeppelinContext` objects into the paragraph text. +To enable this feature set `universe.interpolation` to `true`. The following example shows one use of this facility: + +####In Scala cell: + +```scala +z.put("curr_date", "2018-01-01 00:00:00") +``` + +####In later SAP cell: + +```sql +where + [Filter1] + and [Date] > '{curr_date}' +``` + diff --git a/docs/interpreter/scalding.md b/docs/interpreter/scalding.md index f2e3461d88c..02c5fb8b31f 100644 --- a/docs/interpreter/scalding.md +++ b/docs/interpreter/scalding.md @@ -28,7 +28,7 @@ limitations under the License. ## Building the Scalding Interpreter You have to first build the Scalding interpreter by enable the **scalding** profile as follows: -``` +```bash mvn clean package -Pscalding -DskipTests ``` @@ -66,20 +66,19 @@ and directories with custom jar files you need for your scalding commands. **Set arguments to the scalding repl** -The default arguments are: "--local --repl" +The default arguments are: `--local --repl` -For hdfs mode you need to add: "--hdfs --repl" +For hdfs mode you need to add: `--hdfs --repl` -If you want to add custom jars, you need to add: -"-libjars directory/*:directory/*" +If you want to add custom jars, you need to add: `-libjars directory/*:directory/*` For reducer estimation, you need to add something like: -"-Dscalding.reducer.estimator.classes=com.twitter.scalding.reducer_estimation.InputSizeReducerEstimator" +`-Dscalding.reducer.estimator.classes=com.twitter.scalding.reducer_estimation.InputSizeReducerEstimator` **Set max.open.instances** If you want to control the maximum number of open interpreters, you have to select "scoped" interpreter for note -option and set max.open.instances argument. +option and set `max.open.instances` argument. ## Testing the Interpreter @@ -88,7 +87,7 @@ option and set max.open.instances argument. In example, by using the [Alice in Wonderland](https://gist.github.com/johnynek/a47699caa62f4f38a3e2) tutorial, we will count words (of course!), and plot a graph of the top 10 words in the book. -``` +```scala %scalding import scala.io.Source @@ -144,7 +143,7 @@ res4: com.twitter.scalding.Mode = Hdfs(true,Configuration: core-default.xml, cor **Test HDFS read** -``` +```scala val testfile = TypedPipe.from(TextLine("/user/x/testfile")) testfile.dump ``` @@ -153,7 +152,7 @@ This command should print the contents of the hdfs file /user/x/testfile. **Test map-reduce job** -``` +```scala val testfile = TypedPipe.from(TextLine("/user/x/testfile")) val a = testfile.groupAll.size.values a.toList diff --git a/docs/interpreter/shell.md b/docs/interpreter/shell.md index 9d4bfe77879..d44a42559c8 100644 --- a/docs/interpreter/shell.md +++ b/docs/interpreter/shell.md @@ -35,7 +35,7 @@ At the "Interpreters" menu in Zeppelin dropdown menu, you can set the property v - + @@ -43,6 +43,11 @@ At the "Interpreters" menu in Zeppelin dropdown menu, you can set the property v + + + + + @@ -58,6 +63,11 @@ At the "Interpreters" menu in Zeppelin dropdown menu, you can set the property v + + + + +
    NameValueDefault Description
    60000 Shell command time out in millisecs
    shell.working.directory.user.homefalseIf this set to true, the shell's working directory will be set to user home
    zeppelin.shell.auth.type The path to the keytab file
    zeppelin.shell.interpolationfalseEnable ZeppelinContext variable interpolation into paragraph text
    ## Example @@ -77,3 +87,28 @@ export LAUNCH_KERBEROS_REFRESH_INTERVAL=4h # Change kinit number retries (default value is 5), which means if the kinit command fails for 5 retries consecutively it will close the interpreter. export KINIT_FAIL_THRESHOLD=10 ``` + +## Object Interpolation +The shell interpreter also supports interpolation of `ZeppelinContext` objects into the paragraph text. +The following example shows one use of this facility: + +####In Scala cell: + +```scala +z.put("dataFileName", "members-list-003.parquet") + // ... +val members = spark.read.parquet(z.get("dataFileName")) + // ... +``` + +####In later Shell cell: + +```bash +%sh +rm -rf {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 [Zeppelin-Context](../usage/other_features/zeppelin_context.html) + diff --git a/docs/interpreter/spark.md b/docs/interpreter/spark.md index 122c8db3b84..b0ee3517612 100644 --- a/docs/interpreter/spark.md +++ b/docs/interpreter/spark.md @@ -104,13 +104,13 @@ You can also set other Spark properties which are not listed in the table. For a Local repository for dependency loader - PYSPARK_PYTHON + PYSPARK_PYTHON python Python binary executable to use for PySpark in both driver and workers (default is python). Property spark.pyspark.python take precedence if it is set - PYSPARK_DRIVER_PYTHON + PYSPARK_DRIVER_PYTHON python Python binary executable to use for PySpark in driver only (default is PYSPARK_PYTHON). Property spark.pyspark.driver.python take precedence if it is set @@ -145,6 +145,16 @@ You can also set other Spark properties which are not listed in the table. For a true Do not change - developer only setting, not for production use + + zeppelin.spark.sql.interpolation + false + Enable ZeppelinContext variable interpolation into paragraph text + + + zeppelin.spark.uiWebUrl + + Overrides Spark UI default URL. Value should be a full URL (ex: http://{hostName}/{uniquePath} + Without any configuration, Spark interpreter works out of box in local mode. But if you want to connect to your Spark cluster, you'll need to follow below two simple steps. @@ -181,13 +191,22 @@ For example, * **local[*]** in local mode * **spark://master:7077** in standalone cluster * **yarn-client** in Yarn client mode + * **yarn-cluster** in Yarn cluster mode * **mesos://host:5050** in Mesos cluster -That's it. Zeppelin will work with any version of Spark and any deployment type without rebuilding Zeppelin in this way. +That's it. Zeppelin will work with any version of Spark and any deployment type without rebuilding Zeppelin in this way. For the further information about Spark & Zeppelin version compatibility, please refer to "Available Interpreters" section in [Zeppelin download page](https://zeppelin.apache.org/download.html). > Note that without exporting `SPARK_HOME`, it's running in local mode with included version of Spark. The included version may vary depending on the build profile. +### 3. Yarn mode +Zeppelin support both yarn client and yarn cluster mode (yarn cluster mode is supported from 0.8.0). For yarn mode, you must specify `SPARK_HOME` & `HADOOP_CONF_DIR`. +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 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. Staring from 0.6.1 SparkSession is available as variable `spark` when you are using Spark 2.x. @@ -196,6 +215,13 @@ Staring from 0.6.1 SparkSession is available as variable `spark` when you are us +### How to pass property to SparkConf + +There're 2 kinds of properties that would be passed to SparkConf + + * Standard spark property (prefix with `spark.`). e.g. `spark.executor.memory` will be passed to `SparkConf` + * Non-standard spark property (prefix with `zeppelin.spark.`). e.g. `zeppelin.spark.property_1`, `property_1` will be passed to `SparkConf` + ## Dependency Management There are two ways to load external libraries in Spark interpreter. First is using interpreter setting menu and second is loading Spark properties. @@ -203,7 +229,7 @@ There are two ways to load external libraries in Spark interpreter. First is usi Please see [Dependency Management](../usage/interpreter/dependency_management.html) for the details. ### 2. Loading Spark Properties -Once `SPARK_HOME` is set in `conf/zeppelin-env.sh`, Zeppelin uses `spark-submit` as spark interpreter runner. `spark-submit` supports two ways to load configurations. +Once `SPARK_HOME` is set in `conf/zeppelin-env.sh`, Zeppelin uses `spark-submit` as spark interpreter runner. `spark-submit` supports two ways to load configurations. The first is command line options such as --master and Zeppelin can pass these options to `spark-submit` by exporting `SPARK_SUBMIT_OPTIONS` in `conf/zeppelin-env.sh`. Second is reading configuration options from `SPARK_HOME/conf/spark-defaults.conf`. Spark properties that user can set to distribute libraries are: @@ -236,7 +262,7 @@ Here are few examples: ```bash export SPARK_SUBMIT_OPTIONS="--packages com.databricks:spark-csv_2.10:1.2.0 --jars /path/mylib1.jar,/path/mylib2.jar --files /path/mylib1.py,/path/mylib2.zip,/path/mylib3.egg" ``` - + * `SPARK_HOME/conf/spark-defaults.conf` ``` @@ -296,124 +322,28 @@ 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 %} - -
    -
    - -### 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`, -a popular plotting library for python. More details can be found in the [python interpreter documentation](../interpreter/python.html), -since matplotlib support is identical. More advanced interactive plotting can be done with pyspark through +Both the `python` and `pyspark` interpreters have built-in support for inline visualization using `matplotlib`, +a popular plotting library for python. More details can be found in the [python interpreter documentation](../interpreter/python.html), +since matplotlib support is identical. More advanced interactive plotting can be done with pyspark through utilizing Zeppelin's built-in [Angular Display System](../usage/display_system/angular_backend.html), as shown below: ## Interpreter setting option -You can choose one of `shared`, `scoped` and `isolated` options wheh you configure Spark interpreter. -Spark interpreter creates separated Scala compiler per each notebook but share a single SparkContext in `scoped` mode (experimental). +You can choose one of `shared`, `scoped` and `isolated` options wheh you configure Spark interpreter. +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.pyspark.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: @@ -430,9 +360,12 @@ This is to make the server communicate with KDC. 3. Add the two properties below to Spark configuration (`[SPARK_HOME]/conf/spark-defaults.conf`): - spark.yarn.principal - spark.yarn.keytab + ``` + spark.yarn.principal + spark.yarn.keytab + ``` > **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/setup/basics/how_to_build.md b/docs/setup/basics/how_to_build.md index f5eb96945d1..0d81152295e 100644 --- a/docs/setup/basics/how_to_build.md +++ b/docs/setup/basics/how_to_build.md @@ -51,7 +51,7 @@ If you haven't installed Git and Maven yet, check the [Build requirements](#buil #### 1. Clone the Apache Zeppelin repository -``` +```bash git clone https://github.com/apache/zeppelin.git ``` @@ -60,7 +60,7 @@ git clone https://github.com/apache/zeppelin.git You can build Zeppelin with following maven command: -``` +```bash mvn clean package -DskipTests [Options] ``` @@ -167,7 +167,7 @@ Available profiles are #### -Pexamples (optional) -Bulid examples under zeppelin-examples directory +Build examples under zeppelin-examples directory ### Build command examples @@ -248,18 +248,21 @@ plugin.frontend.yarnDownloadRoot # default https://github.com/yarnpkg/yarn/relea If you don't have requirements prepared, install it. (The installation method may vary according to your environment, example is for Ubuntu.) -``` +```bash sudo apt-get update 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 ``` ### Install maven -``` + +```bash wget http://www.eu.apache.org/dist/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.tar.gz sudo tar -zxf apache-maven-3.3.9-bin.tar.gz -C /usr/local/ sudo ln -s /usr/local/apache-maven-3.3.9/bin/mvn /usr/local/bin/mvn @@ -278,7 +281,7 @@ If you're behind the proxy, you'll need to configure maven and npm to pass throu First of all, configure maven in your `~/.m2/settings.xml`. -``` +```xml @@ -307,7 +310,7 @@ First of all, configure maven in your `~/.m2/settings.xml`. Then, next commands will configure npm. -``` +```bash npm config set proxy http://localhost:3128 npm config set https-proxy http://localhost:3128 npm config set registry "http://registry.npmjs.org/" @@ -316,7 +319,7 @@ npm config set strict-ssl false Configure git as well -``` +```bash git config --global http.proxy http://localhost:3128 git config --global https.proxy http://localhost:3128 git config --global url."http://".insteadOf git:// diff --git a/docs/setup/basics/multi_user_support.md b/docs/setup/basics/multi_user_support.md index 15d911c71ee..e61b723ee00 100644 --- a/docs/setup/basics/multi_user_support.md +++ b/docs/setup/basics/multi_user_support.md @@ -25,8 +25,8 @@ limitations under the License. This page describes about multi-user support. -- multiple users login / logout using [Shiro Authentication](../setup/security/shiro_authentication.html) -- managing [Notebook Permission](../setup/security/notebook_authorization.html) +- multiple users login / logout using [Shiro Authentication](../security/shiro_authentication.html) +- managing [Notebook Permission](../security/notebook_authorization.html) - how to setup [impersonation for interpreters](../../usage/interpreter/user_impersonation.html) - different contexts per user / note using [Interpreter Binding Mode](../../usage/interpreter/interpreter_binding_mode.html) - a paragraph in a notebook can be [Personalized](../../usage/other_features/personalized_mode.html) diff --git a/docs/setup/deployment/cdh.md b/docs/setup/deployment/cdh.md index 9fb508fddb2..d35292e2a92 100644 --- a/docs/setup/deployment/cdh.md +++ b/docs/setup/deployment/cdh.md @@ -29,14 +29,14 @@ limitations under the License. You can import the Docker image by pulling it from Cloudera Docker Hub. -``` +```bash docker pull cloudera/quickstart:latest ``` ### 2. Run docker -``` +```bash docker run -it \ -p 80:80 \ -p 4040:4040 \ @@ -75,7 +75,7 @@ To verify the application is running well, check the web UI for HDFS on `http:// ### 4. Configure Spark interpreter in Zeppelin Set following configurations to `conf/zeppelin-env.sh`. -``` +```bash export MASTER=yarn-client export HADOOP_CONF_DIR=[your_hadoop_conf_path] export SPARK_HOME=[your_spark_home_path] diff --git a/docs/setup/deployment/docker.md b/docs/setup/deployment/docker.md index c0cdb6966d7..746986d6080 100644 --- a/docs/setup/deployment/docker.md +++ b/docs/setup/deployment/docker.md @@ -33,7 +33,7 @@ You need to [install docker](https://docs.docker.com/engine/installation/) on yo ### Running docker image -``` +```bash docker run -p 8080:8080 --rm --name zeppelin apache/zeppelin: ``` @@ -41,7 +41,7 @@ docker run -p 8080:8080 --rm --name zeppelin apache/zeppelin: If you want to specify `logs` and `notebook` dir, -``` +```bash docker run -p 8080:8080 --rm \ -v $PWD/logs:/logs \ -v $PWD/notebook:/notebook \ @@ -52,7 +52,7 @@ docker run -p 8080:8080 --rm \ ### Building dockerfile locally -``` +```bash cd $ZEPPELIN_HOME cd scripts/docker/zeppelin/bin diff --git a/docs/setup/deployment/flink_and_spark_cluster.md b/docs/setup/deployment/flink_and_spark_cluster.md index 11188a494f1..50948409715 100644 --- a/docs/setup/deployment/flink_and_spark_cluster.md +++ b/docs/setup/deployment/flink_and_spark_cluster.md @@ -20,7 +20,7 @@ limitations under the License. {% include JB/setup %} -# Install with flink and spark cluster +# Install with Flink and Spark cluster
    @@ -48,24 +48,24 @@ For git, openssh-server, and OpenJDK 7 we will be using the apt package manager. ##### git From the command prompt: -``` +```bash sudo apt-get install git ``` ##### openssh-server -``` +```bash sudo apt-get install openssh-server ``` ##### OpenJDK 7 -``` +```bash sudo apt-get install openjdk-7-jdk openjdk-7-jre-lib ``` *A note for those using Ubuntu 16.04*: To install `openjdk-7` on Ubuntu 16.04, one must add a repository. [Source](http://askubuntu.com/questions/761127/ubuntu-16-04-and-openjdk-7) -``` bash +```bash sudo add-apt-repository ppa:openjdk-r/ppa sudo apt-get update sudo apt-get install openjdk-7-jdk openjdk-7-jre-lib @@ -76,26 +76,26 @@ Zeppelin requires maven version 3.x. The version available in the repositories Purge any existing versions of maven. -``` +```bash sudo apt-get purge maven maven2 ``` Download the maven 3.3.9 binary. -``` +```bash wget "http://www.us.apache.org/dist/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.tar.gz" ``` Unarchive the binary and move to the `/usr/local` directory. -``` +```bash tar -zxvf apache-maven-3.3.9-bin.tar.gz sudo mv ./apache-maven-3.3.9 /usr/local ``` Create symbolic links in `/usr/bin`. -``` +```bash sudo ln -s /usr/local/apache-maven-3.3.9/bin/mvn /usr/bin/mvn ``` @@ -105,19 +105,19 @@ This provides a quick overview of Zeppelin installation from source, however the From the command prompt: Clone Zeppelin. -``` +```bash git clone https://github.com/apache/zeppelin.git ``` Enter the Zeppelin root directory. -``` +```bash cd zeppelin ``` Package Zeppelin. -``` +```bash mvn clean package -DskipTests -Pspark-1.6 -Dflink.version=1.1.3 -Pscala-2.10 ``` @@ -145,7 +145,7 @@ As long as you didn't edit any code, it is unlikely the build is failing because Start the Zeppelin daemon. -``` +```bash bin/zeppelin-daemon.sh start ``` @@ -158,9 +158,7 @@ See the [Zeppelin tutorial](../../quickstart/tutorial.html) for basic Zeppelin u ##### Flink Test Create a new notebook named "Flink Test" and copy and paste the following code. - ```scala - %flink // let Zeppelin know what interpreter to use. val text = benv.fromElements("In the time of chimpanzees, I was a monkey", // some lines of text to analyze @@ -238,7 +236,7 @@ Run the code to make sure the built-in Zeppelin Flink interpreter is working pro Finally, stop the Zeppelin daemon. From the command prompt run: -``` +```bash bin/zeppelin-daemon.sh stop ``` @@ -273,7 +271,7 @@ See the [Flink Installation guide](https://github.com/apache/flink/blob/master/R Return to the directory where you have been downloading, this tutorial assumes that is `$HOME`. Clone Flink, check out release-1.1.3-rc2, and build. -``` +```bash cd $HOME git clone https://github.com/apache/flink.git cd flink @@ -283,7 +281,7 @@ mvn clean install -DskipTests Start the Flink Cluster in stand-alone mode -``` +```bash build-target/bin/start-cluster.sh ``` @@ -297,14 +295,16 @@ In a browser, navigate to http://`yourip`:8082 to see the Flink Web-UI. Click o If no task managers are present, restart the Flink cluster with the following commands: (if binaries) -``` + +```bash flink-1.1.3/bin/stop-cluster.sh flink-1.1.3/bin/start-cluster.sh ``` (if built from source) -``` + +```bash build-target/bin/stop-cluster.sh build-target/bin/start-cluster.sh ``` @@ -339,13 +339,13 @@ Return to the directory where you have been downloading, this tutorial assumes t the time of writing. You are free to check out other version, just make sure you build Zeppelin against the correct version of Spark. However if you use Spark 2.0, the word count example will need to be changed as Spark 2.0 is not compatible with the following examples. -``` +```bash cd $HOME ``` Clone, check out, and build Spark version 1.6.x. -``` +```bash git clone https://github.com/apache/spark.git cd spark git checkout branch-1.6 @@ -362,7 +362,7 @@ cd $HOME Start the Spark cluster in stand alone mode, specifying the webui-port as some port other than 8080 (the webui-port of Zeppelin). -``` +```bash spark/sbin/start-master.sh --webui-port 8082 ``` **Note:** Why `--webui-port 8082`? There is a digression toward the end of this document that explains this. @@ -375,13 +375,13 @@ Toward the top of the page there will be a *URL*: spark://`yourhost`:7077. Note Start the slave using the URI from the Spark master WebUI: -``` +```bash spark/sbin/start-slave.sh spark://yourhostname:7077 ``` Return to the root directory and start the Zeppelin daemon. -``` +```bash cd $HOME zeppelin/bin/zeppelin-daemon.sh start diff --git a/docs/setup/deployment/spark_cluster_mode.md b/docs/setup/deployment/spark_cluster_mode.md index 7abaecdd1da..94102bf0abe 100644 --- a/docs/setup/deployment/spark_cluster_mode.md +++ b/docs/setup/deployment/spark_cluster_mode.md @@ -38,14 +38,14 @@ You can simply set up Spark standalone environment with below steps. ### 1. Build Docker file You can find docker script files under `scripts/docker/spark-cluster-managers`. -``` +```bash cd $ZEPPELIN_HOME/scripts/docker/spark-cluster-managers/spark_standalone docker build -t "spark_standalone" . ``` ### 2. Run docker -``` +```bash docker run -it \ -p 8080:8080 \ -p 7077:7077 \ @@ -70,7 +70,7 @@ After running single paragraph with Spark interpreter in Zeppelin, browse `https You can also simply verify that Spark is running well in Docker with below command. -``` +```bash ps -ef | grep spark ``` @@ -83,14 +83,14 @@ You can simply set up [Spark on YARN](http://spark.apache.org/docs/latest/runnin ### 1. Build Docker file You can find docker script files under `scripts/docker/spark-cluster-managers`. -``` +```bash cd $ZEPPELIN_HOME/scripts/docker/spark-cluster-managers/spark_yarn_cluster docker build -t "spark_yarn" . ``` ### 2. Run docker -``` +```bash docker run -it \ -p 5000:5000 \ -p 9000:9000 \ @@ -120,7 +120,7 @@ Note that `sparkmaster` hostname used here to run docker container should be def You can simply verify the processes of Spark and YARN are running well in Docker with below command. -``` +```bash ps -ef ``` @@ -129,7 +129,7 @@ You can also check each application web UI for HDFS on `http://:50070/ ### 4. Configure Spark interpreter in Zeppelin Set following configurations to `conf/zeppelin-env.sh`. -``` +```bash export MASTER=yarn-client export HADOOP_CONF_DIR=[your_hadoop_conf_path] export SPARK_HOME=[your_spark_home_path] @@ -154,7 +154,7 @@ You can simply set up [Spark on Mesos](http://spark.apache.org/docs/latest/runni ### 1. Build Docker file -``` +```bash cd $ZEPPELIN_HOME/scripts/docker/spark-cluster-managers/spark_mesos docker build -t "spark_mesos" . ``` @@ -162,7 +162,7 @@ docker build -t "spark_mesos" . ### 2. Run docker -``` +```bash docker run --net=host -it \ -p 8080:8080 \ -p 7077:7077 \ @@ -183,7 +183,7 @@ Note that `sparkmaster` hostname used here to run docker container should be def You can simply verify the processes of Spark and Mesos are running well in Docker with below command. -``` +```bash ps -ef ``` @@ -192,7 +192,7 @@ You can also check each application web UI for Mesos on `http://:5050/ ### 4. Configure Spark interpreter in Zeppelin -``` +```bash export MASTER=mesos://127.0.1.1:5050 export MESOS_NATIVE_JAVA_LIBRARY=[PATH OF libmesos.so] export SPARK_HOME=[PATH OF SPARK HOME] @@ -234,4 +234,4 @@ W0103 20:17:24.040252 339 sched.cpp:736] Ignoring framework registered message W0103 20:17:26.150250 339 sched.cpp:736] Ignoring framework registered message because it was sentfrom 'master@127.0.0.1:5050' instead of the leading master 'master@127.0.1.1:5050' W0103 20:17:26.737604 339 sched.cpp:736] Ignoring framework registered message because it was sentfrom 'master@127.0.0.1:5050' instead of the leading master 'master@127.0.1.1:5050' W0103 20:17:35.241714 336 sched.cpp:736] Ignoring framework registered message because it was sentfrom 'master@127.0.0.1:5050' instead of the leading master 'master@127.0.1.1:5050' -``` \ No newline at end of file +``` diff --git a/docs/setup/deployment/virtual_machine.md b/docs/setup/deployment/virtual_machine.md index 21beba6420e..a50d1a2ba52 100644 --- a/docs/setup/deployment/virtual_machine.md +++ b/docs/setup/deployment/virtual_machine.md @@ -25,9 +25,7 @@ limitations under the License. ## Overview -Apache Zeppelin distribution includes a script directory - - `scripts/vagrant/zeppelin-dev` +Apache Zeppelin distribution includes a script directory `scripts/vagrant/zeppelin-dev` This script creates a virtual machine that launches a repeatable, known set of core dependencies required for developing Zeppelin. It can also be used to run an existing Zeppelin build if you don't plan to build from source. For PySpark users, this script includes several helpful [Python Libraries](#python-extras). @@ -44,7 +42,7 @@ If you are running Windows and don't yet have python installed, [install Python 1. Download and Install Vagrant: [Vagrant Downloads](http://www.vagrantup.com/downloads.html) 2. Install Ansible: [Ansible Python pip install](http://docs.ansible.com/ansible/intro_installation.html#latest-releases-via-pip) - ``` + ```bash sudo easy_install pip sudo pip install ansible ansible --version @@ -58,7 +56,7 @@ Thats it ! You can now run `vagrant ssh` and this will place you into the guest If you don't wish to build Zeppelin from scratch, run the z-manager installer script while running in the guest VM: -``` +```bash curl -fsSL https://raw.githubusercontent.com/NFLabs/z-manager/master/zeppelin-installer.sh | bash ``` @@ -67,7 +65,7 @@ curl -fsSL https://raw.githubusercontent.com/NFLabs/z-manager/master/zeppelin-in You can now -``` +```bash git clone git://git.apache.org/zeppelin.git ``` @@ -87,8 +85,8 @@ By default, Vagrant will share your project directory (the directory with the Va Running the following commands in the guest machine should display these expected versions: -`node --version` should report *v0.12.7* -`mvn --version` should report *Apache Maven 3.3.9* and *Java version: 1.7.0_85* +* `node --version` should report *v0.12.7* +* `mvn --version` should report *Apache Maven 3.3.9* and *Java version: 1.7.0_85* The virtual machine consists of: @@ -108,7 +106,7 @@ The virtual machine consists of: This assumes you've already cloned the project either on the host machine in the zeppelin-dev directory (to be shared with the guest machine) or cloned directly into a directory while running inside the guest machine. The following build steps will also include Python and R support via PySpark and SparkR: -``` +```bash cd /zeppelin mvn clean package -Pspark-1.6 -Phadoop-2.4 -DskipTests ./bin/zeppelin-daemon.sh start @@ -189,4 +187,4 @@ show(plt) ### R Extras With zeppelin running, an R Tutorial notebook will be available. The R packages required to run the examples and graphs in this tutorial notebook were installed by this virtual machine. -The installed R Packages include: Knitr, devtools, repr, rCharts, ggplot2, googleVis, mplot, htmltools, base64enc, data.table +The installed R Packages include: `knitr`, `devtools`, `repr`, `rCharts`, `ggplot2`, `googleVis`, `mplot`, `htmltools`, `base64enc`, `data.table`. diff --git a/docs/setup/deployment/yarn_install.md b/docs/setup/deployment/yarn_install.md index fc46bc2cb3d..b5967992a4a 100644 --- a/docs/setup/deployment/yarn_install.md +++ b/docs/setup/deployment/yarn_install.md @@ -105,7 +105,7 @@ hdp-select status hadoop-client | sed 's/hadoop-client - \(.*\)/\1/' ## Start/Stop ### Start Zeppelin -``` +```bash cd /home/zeppelin/zeppelin bin/zeppelin-daemon.sh start ``` @@ -113,7 +113,7 @@ After successful start, visit http://[zeppelin-server-host-name]:8080 with your ### Stop Zeppelin -``` +```bash bin/zeppelin-daemon.sh stop ``` diff --git a/docs/setup/operation/configuration.md b/docs/setup/operation/configuration.md index e91a7df2ba5..61756c82258 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. @@ -39,6 +39,12 @@ If both are defined, then the **environment variables** will take priority.
    + + + + + + @@ -53,6 +59,18 @@ If both are defined, then the **environment variables** will take priority. + + + + + + + + + + + + @@ -77,7 +95,19 @@ If both are defined, then the **environment variables** will take priority. - + + + + + + + + + + + + + @@ -203,6 +233,12 @@ If both are defined, then the **environment variables** will take priority. + + + + + + @@ -311,6 +347,30 @@ If both are defined, then the **environment variables** will take priority. + + + + + + + + + + + + + + + + + + + + + + + +
    Default value Description
    ZEPPELIN_ADDR
    zeppelin.server.addr
    127.0.0.1Zeppelin server binding address
    ZEPPELIN_PORT
    zeppelin.server.port
    8443 Zeppelin Server ssl port (used when ssl environment/property is set to true)
    ZEPPELIN_JMX_ENABLE
    N/A
    Enable JMX by defining "true"
    ZEPPELIN_JMX_PORT
    N/A
    9996Port number which JMX uses
    ZEPPELIN_MEM
    N/A* Enables a way to specify a ',' separated list of allowed origins for REST and websockets.
    e.g. http://localhost:8080
    ZEPPELIN_CREDENTIALS_PERSIST
    zeppelin.credentials.persist
    truePersist credentials on a JSON file (credentials.json)
    ZEPPELIN_CREDENTIALS_ENCRYPT_KEY
    zeppelin.credentials.encryptKey
    If provided, encrypt passwords on the credentials.json file (passwords will be stored as plain-text otherwise
    N/A
    zeppelin.anonymous.allowed
    truefalse Save notebooks to S3 with server-side encryption enabled
    ZEPPELIN_NOTEBOOK_S3_SIGNEROVERRIDE
    zeppelin.notebook.s3.signerOverride
    Optional override to control which signature algorithm should be used to sign AWS requests
    ZEPPELIN_NOTEBOOK_AZURE_CONNECTION_STRING
    zeppelin.notebook.azure.connectionString
    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
    tokenGitHub username. By default it is `token` to use GitHub's API
    ZEPPELIN_NOTEBOOK_GIT_REMOTE_ACCESS_TOKEN
    zeppelin.notebook.git.remote.access-token
    tokenGitHub 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
    tokenGitHub remote name. Default is `origin`
    @@ -326,8 +386,9 @@ A condensed example can be found in the top answer to this [StackOverflow post]( The keystore holds the private key and certificate on the server end. The trustore holds the trusted client certificates. Be sure that the path and password for these two stores are correctly configured in the password fields below. They can be obfuscated using the Jetty password tool. After Maven pulls in all the dependency to build Zeppelin, one of the Jetty jars contain the Password tool. Invoke this command from the Zeppelin home build directory with the appropriate version, user, and password. -``` -java -cp ./zeppelin-server/target/lib/jetty-all-server-.jar org.eclipse.jetty.util.security.Password +```bash +java -cp ./zeppelin-server/target/lib/jetty-all-server-.jar \ +org.eclipse.jetty.util.security.Password ``` If you are using a self-signed, a certificate signed by an untrusted CA, or if client authentication is enabled, then the client must have a browser create exceptions for both the normal HTTPS port and WebSocket port. This can by done by trying to establish an HTTPS connection to both ports in a browser (e.g. if the ports are 443 and 8443, then visit https://127.0.0.1:443 and https://127.0.0.1:8443). This step can be skipped if the server certificate is signed by a trusted CA and client auth is disabled. @@ -336,7 +397,7 @@ If you are using a self-signed, a certificate signed by an untrusted CA, or if c The following properties needs to be updated in the `zeppelin-site.xml` in order to enable server side SSL. -``` +```xml zeppelin.server.ssl.port 8443 @@ -379,7 +440,7 @@ The following properties needs to be updated in the `zeppelin-site.xml` in order The following properties needs to be updated in the `zeppelin-site.xml` in order to enable client side certificate authentication. -``` +```xml zeppelin.server.ssl.port 8443 @@ -411,6 +472,20 @@ 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. + +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. + +You can generate an appropriate encryption key any way you'd like - for instance, by using the openssl tool: + +```bash +openssl enc -aes-128-cbc -k secret -P -md sha1 +``` + +*Important*: storing your encryption key in a configuration file is _not advised_. Depending on your environment security needs, you may want to consider utilizing a credentials server, storing the ZEPPELIN_CREDENTIALS_ENCRYPT_KEY as an OS env variable, or any other approach that would not colocate the encryption key and the encrypted content (the credentials.json file). + ### Obfuscating Passwords using the Jetty Password Tool @@ -420,7 +495,7 @@ The Password tool documentation can be found [here](http://www.eclipse.org/jetty After using the tool: -``` +```bash java -cp $ZEPPELIN_HOME/zeppelin-server/target/lib/jetty-util-9.2.15.v20160210.jar \ org.eclipse.jetty.util.security.Password \ password @@ -433,7 +508,7 @@ MD5:5f4dcc3b5aa765d61d8327deb882cf99 update your configuration with the obfuscated password : -``` +```xml zeppelin.ssl.keystore.password OBF:1v2j1uum1xtv1zej1zer1xtn1uvk1v1v @@ -441,5 +516,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/operation/trouble_shooting.md b/docs/setup/operation/trouble_shooting.md index f16dc8f04a0..5857bd8f100 100644 --- a/docs/setup/operation/trouble_shooting.md +++ b/docs/setup/operation/trouble_shooting.md @@ -19,7 +19,7 @@ limitations under the License. --> {% include JB/setup %} -# Trouble Shooting +# Troubleshooting
    diff --git a/docs/setup/operation/upgrading.md b/docs/setup/operation/upgrading.md index 20be7ac6931..75c2327f8e2 100644 --- a/docs/setup/operation/upgrading.md +++ b/docs/setup/operation/upgrading.md @@ -35,11 +35,24 @@ So, copying `notebook` and `conf` directory should be enough. ## Migration Guide +### Breaking changes in 0.8.x + +From 0.8, Zeppelin has a new type of permission - [Runners](http://zeppelin.apache.org/docs/0.8.0/setup/security/notebook_authorization.html#authorization-setting) + +As Runners list is empty in note so everybody can view note although Readers list is not empty. +To set all your "writers" to "runners": +1. Copy `notebook` and `conf` directories to 0.8.0, +2. Move directory **docs/assets/themes/zeppelin/note/FixReaders** to new `notebook` directory, +3. Start the new Zeppelin and run note **System/Migrate from 0.7**. + +### Upgrading from Zeppelin 0.8.1 (and before) to 0.8.2 (and later) + - From 0.8.2, Zeppelin server bind `127.0.0.1` by default instead of `0.0.0.0`. Configure `zeppelin.server.addr` property or `ZEPPELIN_ADDR` env variable to change. + ### Upgrading from Zeppelin 0.7 to 0.8 - From 0.8, we recommend to use `PYSPARK_PYTHON` and `PYSPARK_DRIVER_PYTHON` instead of `zeppelin.pyspark.python` as `zeppelin.pyspark.python` only effects driver. You can use `PYSPARK_PYTHON` and `PYSPARK_DRIVER_PYTHON` as using them in spark. - From 0.8, depending on your device, the keyboard shortcut `Ctrl-L` or `Command-L` which goes to the line somewhere user wants is not supported. - + ### Upgrading from Zeppelin 0.6 to 0.7 - From 0.7, we don't use `ZEPPELIN_JAVA_OPTS` as default value of `ZEPPELIN_INTP_JAVA_OPTS` and also the same for `ZEPPELIN_MEM`/`ZEPPELIN_INTP_MEM`. If user want to configure the jvm opts of interpreter process, please set `ZEPPELIN_INTP_JAVA_OPTS` and `ZEPPELIN_INTP_MEM` explicitly. If you don't set `ZEPPELIN_INTP_MEM`, Zeppelin will set it to `-Xms1024m -Xmx1024m -XX:MaxPermSize=512m` by default. diff --git a/docs/setup/security/authentication_nginx.md b/docs/setup/security/authentication_nginx.md index be4875a43d1..705a21d251f 100644 --- a/docs/setup/security/authentication_nginx.md +++ b/docs/setup/security/authentication_nginx.md @@ -38,7 +38,7 @@ This instruction based on Ubuntu 14.04 LTS but may work with other OS with few c You can install NGINX server with same box where zeppelin installed or separate box where it is dedicated to serve as proxy server. - ``` + ```bash $ apt-get install nginx ``` > **NOTE :** On pre 1.3.13 version of NGINX, Proxy for Websocket may not fully works. Please use latest version of NGINX. See: [NGINX documentation](https://www.nginx.com/blog/websocket-nginx/). @@ -47,7 +47,7 @@ This instruction based on Ubuntu 14.04 LTS but may work with other OS with few c In most cases, NGINX configuration located under `/etc/nginx/sites-available`. Create your own configuration or add your existing configuration at `/etc/nginx/sites-available`. - ``` + ```bash $ cd /etc/nginx/sites-available $ touch my-zeppelin-auth-setting ``` @@ -95,7 +95,7 @@ This instruction based on Ubuntu 14.04 LTS but may work with other OS with few c Then make a symbolic link to this file from `/etc/nginx/sites-enabled/` to enable configuration above when NGINX reloads. - ``` + ```bash $ ln -s /etc/nginx/sites-enabled/my-zeppelin-auth-setting /etc/nginx/sites-available/my-zeppelin-auth-setting ``` @@ -103,17 +103,17 @@ This instruction based on Ubuntu 14.04 LTS but may work with other OS with few c Now you need to setup `.htpasswd` file to serve list of authenticated user credentials for NGINX server. - ``` + ```bash $ cd /etc/nginx $ htpasswd -c htpasswd [YOUR-ID] - $ NEW passwd: [YOUR-PASSWORD] - $ RE-type new passwd: [YOUR-PASSWORD-AGAIN] + NEW passwd: [YOUR-PASSWORD] + RE-type new passwd: [YOUR-PASSWORD-AGAIN] ``` Or you can use your own apache `.htpasswd` files in other location for setting up property: `auth_basic_user_file` Restart NGINX server. - ``` + ```bash $ service nginx restart ``` Then check HTTP Basic Authentication works in browser. If you can see regular basic auth popup and then able to login with credential you entered into `.htpasswd` you are good to go. diff --git a/docs/setup/security/http_security_headers.md b/docs/setup/security/http_security_headers.md index 1c55d18e184..ad4aeef2336 100644 --- a/docs/setup/security/http_security_headers.md +++ b/docs/setup/security/http_security_headers.md @@ -32,7 +32,7 @@ It also prevents MITM attack by not allowing User to override the invalid certif The following property needs to be updated in the zeppelin-site.xml in order to enable HSTS. You can choose appropriate value for "max-age". -``` +```xml zeppelin.server.strict.transport max-age=631138519 @@ -55,7 +55,7 @@ The HTTP X-XSS-Protection response header is a feature of Internet Explorer, Chr The following property needs to be updated in the zeppelin-site.xml in order to set X-XSS-PROTECTION header. -``` +```xml zeppelin.server.xxss.protection 1; mode=block @@ -78,7 +78,7 @@ The X-Frame-Options HTTP response header can indicate browser to avoid clickjack The following property needs to be updated in the zeppelin-site.xml in order to set X-Frame-Options header. -``` +```xml zeppelin.server.xframe.options SAMEORIGIN @@ -89,9 +89,9 @@ The following property needs to be updated in the zeppelin-site.xml in order to You can choose appropriate value from below. -* DENY -* SAMEORIGIN -* ALLOW-FROM _uri_ +* `DENY` +* `SAMEORIGIN` +* `ALLOW-FROM uri` ## Setting up Server Header @@ -99,7 +99,7 @@ Security conscious organisations does not want to reveal the Application Server The following property needs to be updated in the zeppelin-site.xml in order to set Server header. -``` +```xml zeppelin.server.jetty.name Jetty(7.6.0.v20120127) diff --git a/docs/setup/security/notebook_authorization.md b/docs/setup/security/notebook_authorization.md index 0d5510410c4..6410fe97a34 100644 --- a/docs/setup/security/notebook_authorization.md +++ b/docs/setup/security/notebook_authorization.md @@ -36,26 +36,30 @@ 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
    ## Separate notebook workspaces (public vs. private) By default, the authorization rights allow other users to see the newly created note, meaning the workspace is `public`. This behavior is controllable and can be set through either `ZEPPELIN_NOTEBOOK_PUBLIC` variable in `conf/zeppelin-env.sh`, or through `zeppelin.notebook.public` property in `conf/zeppelin-site.xml`. Thus, in order to make newly created note appear only in your `private` workspace by default, you can set either `ZEPPELIN_NOTEBOOK_PUBLIC` to `false` in your `conf/zeppelin-env.sh` as follows: -``` +```bash export ZEPPELIN_NOTEBOOK_PUBLIC="false" ``` or set `zeppelin.notebook.public` property to `false` in `conf/zeppelin-site.xml` as follows: -``` +```xml zeppelin.notebook.public false @@ -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/setup/security/shiro_authentication.md b/docs/setup/security/shiro_authentication.md index 0dcb722e316..c56b77aea5a 100644 --- a/docs/setup/security/shiro_authentication.md +++ b/docs/setup/security/shiro_authentication.md @@ -46,8 +46,8 @@ Set to property **zeppelin.anonymous.allowed** to **false** in `conf/zeppelin-si ### 3. Start Zeppelin -``` -bin/zeppelin-daemon.sh start (or restart) +```bash +bin/zeppelin-daemon.sh start #(or restart) ``` Then you can browse Zeppelin at [http://localhost:8080](http://localhost:8080). @@ -80,7 +80,7 @@ activeDirectoryRealm.groupRolesMap = "CN=aGroupName,OU=groups,DC=SOME_GROUP,DC=C activeDirectoryRealm.authorizationCachingEnabled = false activeDirectoryRealm.principalSuffix = @corp.company.net -ldapRealm = org.apache.zeppelin.server.LdapGroupRealm +ldapRealm = org.apache.zeppelin.realm.LdapGroupRealm # search base for ldap groups (only relevant for LdapGroupRealm): ldapRealm.contextFactory.environment[ldap.searchBase] = dc=COMPANY,dc=COM ldapRealm.contextFactory.url = ldap://ldap.test.com:389 @@ -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 ``` @@ -182,6 +185,17 @@ securityManager.sessionManager = $sessionManager securityManager.realms = $ldapRealm ``` +Also instead of specifying systemPassword in clear text in `shiro.ini` administrator can choose to specify the same in "hadoop credential". +Create a keystore file using the hadoop credential command line: +``` +hadoop credential create ldapRealm.systemPassword -provider jceks://file/user/zeppelin/conf/zeppelin.jceks +``` + +Add the following line in the `shiro.ini` file: +``` +ldapRealm.hadoopSecurityCredentialPath = jceks://file/user/zeppelin/conf/zeppelin.jceks +``` + ### PAM [PAM](https://en.wikipedia.org/wiki/Pluggable_authentication_module) authentication support allows the reuse of existing authentication moduls on the host where Zeppelin is running. On a typical system modules are configured per service for example sshd, passwd, etc. under `/etc/pam.d/`. You can @@ -210,6 +224,45 @@ 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. + +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. @@ -228,20 +281,23 @@ 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] /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/docs/setup/storage/storage.md b/docs/setup/storage/storage.md index 269bc4624e1..4d45b657671 100644 --- a/docs/setup/storage/storage.md +++ b/docs/setup/storage/storage.md @@ -30,9 +30,12 @@ There are few notebook storage systems available for a use out of the box: * (default) use local file system and version it using local Git repository - `GitNotebookRepo` * all notes are saved in the notebook folder in your local File System - `VFSNotebookRepo` + * 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` 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. @@ -43,7 +46,7 @@ By default, only first two of them will be automatically kept in sync by Zeppeli To enable versioning for all your local notebooks though a standard Git repository - uncomment the next property in `zeppelin-site.xml` in order to use GitNotebookRepo class: -``` +```xml zeppelin.notebook.storage org.apache.zeppelin.notebook.repo.GitNotebookRepo @@ -51,6 +54,22 @@ To enable versioning for all your local notebooks though a standard Git reposito ``` +
    + +## Notebook Storage in hadoop compatible file system repository + +Notes may be stored in hadoop compatible file system such as hdfs, so that multiple Zeppelin instances can share the same notes. It supports all the versions of hadoop 2.x. If you use `FileSystemNotebookRepo`, then `zeppelin.notebook.dir` is the path on the hadoop compatible file system. And you need to specify `HADOOP_CONF_DIR` in `zeppelin-env.sh` so that zeppelin can find the right hadoop configuration files. +If your hadoop cluster is kerberized, then you need to specify `zeppelin.server.kerberos.keytab` and `zeppelin.server.kerberos.principal` + +```xml + + zeppelin.notebook.storage + org.apache.zeppelin.notebook.repo.FileSystemNotebookRepo + hadoop compatible file system notebook persistence layer implementation + +``` + +
    ## Notebook Storage in S3 @@ -71,14 +90,14 @@ s3://bucket_name/username/notebook-id/ Configure by setting environment variables in the file **zeppelin-env.sh**: -``` -export ZEPPELIN_NOTEBOOK_S3_BUCKET = bucket_name -export ZEPPELIN_NOTEBOOK_S3_USER = username +```bash +export ZEPPELIN_NOTEBOOK_S3_BUCKET=bucket_name +export ZEPPELIN_NOTEBOOK_S3_USER=username ``` Or using the file **zeppelin-site.xml** uncomment and complete the S3 settings: -``` +```xml zeppelin.notebook.s3.bucket bucket_name @@ -93,7 +112,7 @@ Or using the file **zeppelin-site.xml** uncomment and complete the S3 settings: Uncomment the next property for use S3NotebookRepo class: -``` +```xml zeppelin.notebook.storage org.apache.zeppelin.notebook.repo.S3NotebookRepo @@ -103,7 +122,7 @@ Uncomment the next property for use S3NotebookRepo class: Comment out the next property to disable local git notebook storage (the default): -``` +```xml zeppelin.notebook.storage org.apache.zeppelin.notebook.repo.GitNotebookRepo @@ -117,13 +136,13 @@ Comment out the next property to disable local git notebook storage (the default To use an [AWS KMS](https://aws.amazon.com/kms/) encryption key to encrypt notebooks, set the following environment variable in the file **zeppelin-env.sh**: -``` -export ZEPPELIN_NOTEBOOK_S3_KMS_KEY_ID = kms-key-id +```bash +export ZEPPELIN_NOTEBOOK_S3_KMS_KEY_ID=kms-key-id ``` Or using the following setting in **zeppelin-site.xml**: -``` +```xml zeppelin.notebook.s3.kmsKeyID AWS-KMS-Key-UUID @@ -133,13 +152,13 @@ Or using the following setting in **zeppelin-site.xml**: In order to set custom KMS key region, set the following environment variable in the file **zeppelin-env.sh**: -``` -export ZEPPELIN_NOTEBOOK_S3_KMS_KEY_REGION = kms-key-region +```bash +export ZEPPELIN_NOTEBOOK_S3_KMS_KEY_REGION=kms-key-region ``` Or using the following setting in **zeppelin-site.xml**: -``` +```xml zeppelin.notebook.s3.kmsKeyRegion target-region @@ -153,13 +172,13 @@ Format of `target-region` is described in more details [here](http://docs.aws.am You may use a custom [``EncryptionMaterialsProvider``](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/model/EncryptionMaterialsProvider.html) class as long as it is available in the classpath and able to initialize itself from system properties or another mechanism. To use this, set the following environment variable in the file **zeppelin-env.sh**: -``` -export ZEPPELIN_NOTEBOOK_S3_EMP = class-name +```bash +export ZEPPELIN_NOTEBOOK_S3_EMP=class-name ``` Or using the following setting in **zeppelin-site.xml**: -``` +```xml zeppelin.notebook.s3.encryptionMaterialsProvider provider implementation class name @@ -170,13 +189,13 @@ Or using the following setting in **zeppelin-site.xml**: To request server-side encryption of notebooks, set the following environment variable in the file **zeppelin-env.sh**: -``` -export ZEPPELIN_NOTEBOOK_S3_SSE = true +```bash +export ZEPPELIN_NOTEBOOK_S3_SSE=true ``` Or using the following setting in **zeppelin-site.xml**: -``` +```xml zeppelin.notebook.s3.sse true @@ -191,7 +210,7 @@ Using `AzureNotebookRepo` you can connect your Zeppelin with your Azure account First of all, input your `AccountName`, `AccountKey`, and `Share Name` in the file **zeppelin-site.xml** by commenting out and completing the next properties: -``` +```xml zeppelin.notebook.azure.connectionString DefaultEndpointsProtocol=https;AccountName=;AccountKey= @@ -207,7 +226,7 @@ First of all, input your `AccountName`, `AccountKey`, and `Share Name` in the fi Secondly, you can initialize `AzureNotebookRepo` class in the file **zeppelin-site.xml** by commenting the next property: -``` +```xml zeppelin.notebook.storage org.apache.zeppelin.notebook.repo.GitNotebookRepo @@ -217,7 +236,7 @@ Secondly, you can initialize `AzureNotebookRepo` class in the file **zeppelin-si and commenting out: -``` +```xml zeppelin.notebook.storage org.apache.zeppelin.notebook.repo.AzureNotebookRepo @@ -227,7 +246,7 @@ and commenting out: In case you want to use simultaneously your local git storage with Azure storage use the following property instead: - ``` + ```xml zeppelin.notebook.storage org.apache.zeppelin.notebook.repo.GitNotebookRepo, apache.zeppelin.notebook.repo.AzureNotebookRepo @@ -237,7 +256,7 @@ In case you want to use simultaneously your local git storage with Azure storage Optionally, you can specify Azure folder structure name in the file **zeppelin-site.xml** by commenting out the next property: - ``` + ```xml zeppelin.notebook.azure.user user @@ -245,12 +264,103 @@ 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. + +```xml + + 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: + +```xml + + zeppelin.notebook.storage + org.apache.zeppelin.notebook.repo.GitNotebookRepo + versioned notebook persistence layer implementation + +``` + +and commenting out: + +```xml + + 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: + +```xml + + 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 ZeppelinHub storage layer allows out of the box connection of Zeppelin instance with your ZeppelinHub account. First of all, you need to either comment out the following property in **zeppelin-site.xml**: -``` +```xml +{% 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 (It is removed from 0.8 where it enforces the cron execution user to be the note owner for security purpose) + +You can set the cron executing user by filling in this form and press the enter key. + +### 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. + +> **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/docs/usage/other_features/customizing_homepage.md b/docs/usage/other_features/customizing_homepage.md index 35eb67b8b4a..52b9d4bbf99 100644 --- a/docs/usage/other_features/customizing_homepage.md +++ b/docs/usage/other_features/customizing_homepage.md @@ -58,7 +58,7 @@ or ```zeppelin.notebook.homescreen.hide``` property to hide the new note from th ### Restart Zeppelin Restart your Zeppelin server -``` +```bash ./bin/zeppelin-daemon stop ./bin/zeppelin-daemon start ``` diff --git a/docs/usage/other_features/notebook_actions.md b/docs/usage/other_features/notebook_actions.md new file mode 100644 index 00000000000..36cbe9b2343 --- /dev/null +++ b/docs/usage/other_features/notebook_actions.md @@ -0,0 +1,59 @@ +--- +layout: page +title: "Notebook Actions" +description: "Description of some actions for notebooks" +group: usage/other_features +--- + +{% include JB/setup %} + +# Revisions comparator + +
    + +Apache Zeppelin allows you to compare revisions of notebook. +To see which paragraphs have been changed, removed or added. +This action becomes available if your notebook has more than one revision. + +
    + +## How to compare two revisions + +For compare two revisions need open dialog of comparator (by click button) and click on any revision in the table. + +
    + +Or choose two revisions into comboboxes. + +
    + +After click on any revision in the table or selecting the second revision will see the result of the comparison. + +
    + +## How to read the result of the comparison + +Result it is list of paragraphs which was in both revisions. If paragraph was added in second revision ("Head") +then so it will be marked as added, if was deleted then it will be marked as +deleted. If paragraph exists in both revisions then it marked as there are differences. +To view the comparison click on the section. + +
    + +Сhanges in the text of the paragraph are highlighted in green and red. Red it is line (block of lines) which was deleted, green it is line (block of lines) which was added). + + + + diff --git a/docs/usage/other_features/zeppelin_context.md b/docs/usage/other_features/zeppelin_context.md new file mode 100644 index 00000000000..ced400a955b --- /dev/null +++ b/docs/usage/other_features/zeppelin_context.md @@ -0,0 +1,235 @@ +--- +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: + +```scala +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: + +```scala +z.put("minAge", 35) +``` + +####In later SQL cell: + +```sql +%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 +%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. + + + + diff --git a/docs/usage/rest_api/interpreter.md b/docs/usage/rest_api/interpreter.md index 23a7c66b3a4..c7502893015 100644 --- a/docs/usage/rest_api/interpreter.md +++ b/docs/usage/rest_api/interpreter.md @@ -315,7 +315,7 @@ The role of registered interpreters, settings and interpreters group are describ
     {
    -  "status": "CREATED",
    +  "status": "OK",
       "message": "",
       "body": {
         "id": "2AYW25ANY",
    diff --git a/docs/usage/rest_api/notebook.md b/docs/usage/rest_api/notebook.md
    index dfb491ae8bb..74d98db3a37 100644
    --- a/docs/usage/rest_api/notebook.md
    +++ b/docs/usage/rest_api/notebook.md
    @@ -132,7 +132,7 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete,
            sample JSON response 
           
     {
    -  "status": "CREATED",
    +  "status": "OK",
       "message": "",
       "body": "2AZPHY918"
     }
    @@ -344,7 +344,7 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, sample JSON response
     {
    -  "status": "CREATED",
    +  "status": "OK",
       "message": "",
       "body": "2AZPHY918"
     }
    @@ -455,7 +455,7 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, sample JSON response
     {
    -  "status": "CREATED",
    +  "status": "OK",
       "message": "",
       "body": "2AZPHY918"
     }
    @@ -636,7 +636,7 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, sample JSON response
     {
    -  "status": "CREATED",
    +  "status": "OK",
       "message": "",
       "body": "20151218-100330\_1754029574"
     }
    @@ -752,6 +752,59 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, +
    + +### Update paragraph + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    DescriptionThis ```PUT``` method update paragraph contents using given id, e.g. {"text": "hello"} +
    URL```http://[zeppelin-server]:[zeppelin-port]/api/notebook/[noteId]/paragraph/[paragraphId]```
    Success code200
    Bad Request code400
    Forbidden code403
    Not Found code404
    Fail code500
    sample JSON input
    +{
    +  "title": "Hello world",
    +  "text": "println(\"hello world\")"
    +}
    sample JSON response
    +{
    +  "status": "OK",
    +  "message": ""
    +  }
    +}
    +
    ### Update paragraph configuration @@ -1091,7 +1144,8 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, - @@ -1108,7 +1162,7 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, - + @@ -1152,7 +1206,7 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, @@ -1169,7 +1223,14 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, - +
    DescriptionThis ```POST``` method adds cron job by the given note id. + This ```POST``` method adds cron job by the given note id. + Default value of ```releaseResource``` is ```false```.
    sample JSON input
    {"cron": "cron expression of note"}
    {"cron": "cron expression of note", "releaseResource": "false"}
    sample JSON response
    Description This ```GET``` method gets cron job expression of given note id. - The body field of the returned JSON contains the cron expression. + The body field of the returned JSON contains the cron expression and ```releaseResource``` flag.
    sample JSON response
    {"status": "OK", "body": "* * * * * ?"}
    +{
    +   "status": "OK", 
    +   "body": {
    +      "cron": "0 0/1 * * * ?", 
    +      "releaseResource": true
    +   }
    +}
    @@ -1215,6 +1276,9 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, "owners":[ "user1" ], + "runners":[ + "user2" + ], "writers":[ "user2" ] @@ -1259,6 +1323,9 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, "owners": [ "user2" ], + "runners":[ + "user2" + ], "writers": [ "user1" ] diff --git a/drill/pom.xml b/drill/pom.xml new file mode 100644 index 00000000000..2d8d6bc00af --- /dev/null +++ b/drill/pom.xml @@ -0,0 +1,91 @@ + + + + + 4.0.0 + + + interpreter-parent + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + ../interpreter-parent + + + org.apache.zeppelin + zeppelin-drill + jar + 0.8.2-mapr-1912-r2 + Zeppelin: JDBC Drill interpreter + + + + drill + 1.6.0.1001 + + + + + org.apache.zeppelin + zeppelin-interpreter + ${project.version} + provided + + + + org.apache.zeppelin + zeppelin-jdbc + ${project.version} + provided + + + + com.mapr.drill + DrillJDBC41 + ${simbadrill.version} + + + com.mapr.hadoop + maprfs + + + org.apache.hadoop + hadoop-auth + + + org.apache.hadoop + hadoop-common + + + + + + + + + maven-enforcer-plugin + + + maven-dependency-plugin + + + maven-resources-plugin + + + + diff --git a/drill/src/main/java/org/apache/zeppelin/drill/DrillInterpreter.java b/drill/src/main/java/org/apache/zeppelin/drill/DrillInterpreter.java new file mode 100644 index 00000000000..dddcd9539e8 --- /dev/null +++ b/drill/src/main/java/org/apache/zeppelin/drill/DrillInterpreter.java @@ -0,0 +1,33 @@ +package org.apache.zeppelin.drill; + +import java.util.Properties; + +import org.apache.zeppelin.jdbc.JDBCInterpreter; + +/** + * JDBC interpreter for Zeppelin. This interpreter can also be used for accessing HAWQ, + * GreenplumDB, MariaDB, MySQL, Postgres and Redshift. + * + *
      + *
    • {@code default.url} - JDBC URL to connect to.
    • + *
    • {@code default.user} - JDBC user name..
    • + *
    • {@code default.password} - JDBC password..
    • + *
    • {@code default.driver.name} - JDBC driver name.
    • + *
    • {@code common.max.result} - Max number of SQL result to display.
    • + *
    + * + *

    + * How to use:
    + * {@code %jdbc.sql}
    + * {@code + * SELECT store_id, count(*) + * FROM retail_demo.order_lineitems_pxf + * GROUP BY store_id; + * } + *

    + */ +public class DrillInterpreter extends JDBCInterpreter { + public DrillInterpreter(Properties property) { + super(property); + } +} diff --git a/drill/src/main/resources/interpreter-setting.json b/drill/src/main/resources/interpreter-setting.json new file mode 100644 index 00000000000..4a48df61af8 --- /dev/null +++ b/drill/src/main/resources/interpreter-setting.json @@ -0,0 +1,125 @@ +[ + { + "group": "drill", + "name": "sql", + "className": "org.apache.zeppelin.drill.DrillInterpreter", + "properties": { + "default.url": { + "envName": "ZEPPELIN_JDBC_URL_DRILL", + "propertyName": "default.url", + "defaultValue": "jdbc:drill:drillbit=localhost:31010", + "description": "The URL for Drill JDBC.", + "type": "string" + }, + "default.user": { + "envName": "ZEPPELIN_INTERPRETER_USER", + "propertyName": "default.user", + "defaultValue": "", + "description": "The JDBC user name", + "type": "string" + }, + "default.password": { + "envName": null, + "propertyName": "default.password", + "defaultValue": "", + "description": "The JDBC user password", + "type": "password" + }, + "default.completer.ttlInSeconds": { + "envName": null, + "propertyName": "default.completer.ttlInSeconds", + "defaultValue": "120", + "description": "Time to live sql completer in seconds (-1 to update everytime, 0 to disable update)", + "type": "number" + }, + "default.driver": { + "envName": null, + "propertyName": "default.driver", + "defaultValue": "com.mapr.drill.jdbc41.Driver", + "description": "Drill JDBC Driver Name", + "type": "string" + }, + "default.completer.schemaFilters": { + "envName": null, + "propertyName": "default.completer.schemaFilters", + "defaultValue": "", + "description": "Сomma separated schema (schema = catalog = database) filters to get metadata for completions. Supports '%' symbol is equivalent to any set of characters. (ex. prod_v_%,public%,info)", + "type": "textarea" + }, + "default.precode": { + "envName": null, + "propertyName": "default.precode", + "defaultValue": "", + "description": "SQL which executes while opening connection", + "type": "textarea" + }, + "default.statementPrecode": { + "envName": null, + "propertyName": "default.statementPrecode", + "defaultValue": "", + "description": "Runs before each run of the paragraph, in the same connection" + }, + "default.splitQueries": { + "envName": null, + "propertyName": "default.splitQueries", + "defaultValue": true, + "description": "Each query is executed apart and returns the result", + "type": "checkbox" + }, + "common.max_count": { + "envName": null, + "propertyName": "common.max_count", + "defaultValue": "1000", + "description": "Max number of SQL result to display.", + "type": "number" + }, + "zeppelin.jdbc.auth.type": { + "envName": null, + "propertyName": "zeppelin.jdbc.auth.type", + "defaultValue": "", + "description": "If auth type is needed, Example: KERBEROS", + "type": "string" + }, + "zeppelin.jdbc.concurrent.use": { + "envName": null, + "propertyName": "zeppelin.jdbc.concurrent.use", + "defaultValue": true, + "description": "Use parallel scheduler", + "type": "checkbox" + }, + "zeppelin.jdbc.concurrent.max_connection": { + "envName": null, + "propertyName": "zeppelin.jdbc.concurrent.max_connection", + "defaultValue": "10", + "description": "Number of concurrent execution", + "type": "number" + }, + "zeppelin.jdbc.keytab.location": { + "envName": null, + "propertyName": "zeppelin.jdbc.keytab.location", + "defaultValue": "", + "description": "Kerberos keytab location", + "type": "string" + }, + "zeppelin.jdbc.principal": { + "envName": null, + "propertyName": "zeppelin.jdbc.principal", + "defaultValue": "", + "description": "Kerberos principal", + "type": "string" + }, + "zeppelin.jdbc.interpolation": { + "envName": null, + "propertyName": "zeppelin.jdbc.interpolation", + "defaultValue": false, + "description": "Enable ZeppelinContext variable interpolation into paragraph text", + "type": "checkbox" + } + }, + "editor": { + "language": "sql", + "editOnDblClick": false, + "completionSupport": true + } + } +] diff --git a/elasticsearch/pom.xml b/elasticsearch/pom.xml index 6042a14eaee..eadcd688787 100644 --- a/elasticsearch/pom.xml +++ b/elasticsearch/pom.xml @@ -20,18 +20,19 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent zeppelin-elasticsearch jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Elasticsearch interpreter + elasticsearch 2.4.3 4.0.2 18.0 @@ -93,54 +94,12 @@ maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/elasticsearch - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/elasticsearch - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin diff --git a/elasticsearch/src/main/java/org/apache/zeppelin/elasticsearch/ElasticsearchInterpreter.java b/elasticsearch/src/main/java/org/apache/zeppelin/elasticsearch/ElasticsearchInterpreter.java index 33448df3db4..6251b92512c 100644 --- a/elasticsearch/src/main/java/org/apache/zeppelin/elasticsearch/ElasticsearchInterpreter.java +++ b/elasticsearch/src/main/java/org/apache/zeppelin/elasticsearch/ElasticsearchInterpreter.java @@ -112,7 +112,7 @@ public ElasticsearchInterpreter(Properties property) { @Override public void open() { - logger.info("Properties: {}", getProperty()); + logger.info("Properties: {}", getProperties()); String clientType = getProperty(ELASTICSEARCH_CLIENT_TYPE); clientType = clientType == null ? null : clientType.toLowerCase(); @@ -123,15 +123,15 @@ public void open() { catch (final NumberFormatException e) { this.resultSize = 10; logger.error("Unable to parse " + ELASTICSEARCH_RESULT_SIZE + " : " + - property.get(ELASTICSEARCH_RESULT_SIZE), e); + getProperty(ELASTICSEARCH_RESULT_SIZE), e); } try { if (StringUtils.isEmpty(clientType) || "transport".equals(clientType)) { - elsClient = new TransportBasedClient(getProperty()); + elsClient = new TransportBasedClient(getProperties()); } else if ("http".equals(clientType)) { - elsClient = new HttpBasedClient(getProperty()); + elsClient = new HttpBasedClient(getProperties()); } else { logger.error("Unknown type of Elasticsearch client: " + clientType); diff --git a/elasticsearch/src/main/resources/interpreter-setting.json b/elasticsearch/src/main/resources/interpreter-setting.json index 6fac719f3e5..3e132f28027 100644 --- a/elasticsearch/src/main/resources/interpreter-setting.json +++ b/elasticsearch/src/main/resources/interpreter-setting.json @@ -55,7 +55,8 @@ } }, "editor": { - "editOnDblClick": false + "editOnDblClick": false, + "completionSupport": true } } ] diff --git a/elasticsearch/src/test/java/org/apache/zeppelin/elasticsearch/ElasticsearchInterpreterTest.java b/elasticsearch/src/test/java/org/apache/zeppelin/elasticsearch/ElasticsearchInterpreterTest.java index 4679f297e05..64562b1cb65 100644 --- a/elasticsearch/src/test/java/org/apache/zeppelin/elasticsearch/ElasticsearchInterpreterTest.java +++ b/elasticsearch/src/test/java/org/apache/zeppelin/elasticsearch/ElasticsearchInterpreterTest.java @@ -164,8 +164,8 @@ public static void clean() { private InterpreterContext buildContext(String noteAndParagraphId) { final AngularObjectRegistry angularObjReg = new AngularObjectRegistry("elasticsearch", null); - return new InterpreterContext(noteAndParagraphId, noteAndParagraphId, null, null, null, null, null, - null, angularObjReg , null, null, null); + return new InterpreterContext(noteAndParagraphId, noteAndParagraphId, null, null, null, null, + null, null, null, angularObjReg , null, null, null); } @Theory diff --git a/file/pom.xml b/file/pom.xml index 2493c1fae4c..e6b6ffa56b9 100644 --- a/file/pom.xml +++ b/file/pom.xml @@ -20,24 +20,27 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-file jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: File System Interpreters + file 2.0 2.22.2 2.18.1 + file @@ -79,62 +82,14 @@ - - org.apache.maven.plugins - maven-surefire-plugin - ${plugin.surefire.version} - - maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/file - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/file - false - false - true - - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin diff --git a/file/src/main/java/org/apache/zeppelin/file/FileInterpreter.java b/file/src/main/java/org/apache/zeppelin/file/FileInterpreter.java index d7aad192ee2..cf836727345 100644 --- a/file/src/main/java/org/apache/zeppelin/file/FileInterpreter.java +++ b/file/src/main/java/org/apache/zeppelin/file/FileInterpreter.java @@ -20,6 +20,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.InterpreterResult.Code; import org.apache.zeppelin.interpreter.InterpreterResult.Type; @@ -86,7 +87,7 @@ public void parseArgs() { // Functions that each file system implementation must override - public abstract String listAll(String path); + public abstract String listAll(String path) throws InterpreterException; public abstract boolean isDirectory(String path); diff --git a/file/src/main/java/org/apache/zeppelin/file/HDFSFileInterpreter.java b/file/src/main/java/org/apache/zeppelin/file/HDFSFileInterpreter.java index 244101c9bda..d715ed93a8a 100644 --- a/file/src/main/java/org/apache/zeppelin/file/HDFSFileInterpreter.java +++ b/file/src/main/java/org/apache/zeppelin/file/HDFSFileInterpreter.java @@ -202,7 +202,7 @@ public String listFile(String filePath) { return "No such File or directory"; } - public String listAll(String path) { + public String listAll(String path) throws InterpreterException { String all = ""; if (exceptionOnConnect != null) return "Error connecting to provided endpoint."; diff --git a/file/src/main/resources/interpreter-setting.json b/file/src/main/resources/interpreter-setting.json index ebe5cf6ee3d..83fcb3defa8 100644 --- a/file/src/main/resources/interpreter-setting.json +++ b/file/src/main/resources/interpreter-setting.json @@ -27,7 +27,8 @@ } }, "editor": { - "editOnDblClick": false + "editOnDblClick": false, + "completionSupport": true } } ] diff --git a/flink/pom.xml b/flink/pom.xml index 19e7c5e48b1..977f72e3b86 100644 --- a/flink/pom.xml +++ b/flink/pom.xml @@ -20,21 +20,22 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin - zeppelin-flink_2.10 + zeppelin-flink_2.11 jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Flink Zeppelin flink support + flink 1.1.3 2.3.7 2.0.1 @@ -280,68 +281,16 @@ - - maven-enforcer-plugin - - - enforce - none - - - + + maven-enforcer-plugin + + + maven-dependency-plugin + + + maven-resources-plugin + - - org.apache.maven.plugins - maven-surefire-plugin - - 1 - false - -Xmx1024m -XX:MaxPermSize=256m - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/flink - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/flink - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - - diff --git a/flink/src/main/java/org/apache/zeppelin/flink/FlinkInterpreter.java b/flink/src/main/java/org/apache/zeppelin/flink/FlinkInterpreter.java index 710eace6665..19c77de914a 100644 --- a/flink/src/main/java/org/apache/zeppelin/flink/FlinkInterpreter.java +++ b/flink/src/main/java/org/apache/zeppelin/flink/FlinkInterpreter.java @@ -17,7 +17,6 @@ */ package org.apache.zeppelin.flink; -import java.lang.reflect.InvocationTargetException; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; @@ -34,10 +33,8 @@ import org.apache.flink.runtime.instance.ActorGateway; import org.apache.flink.runtime.messages.JobManagerMessages; import org.apache.flink.runtime.minicluster.LocalFlinkMiniCluster; -import org.apache.flink.runtime.util.EnvironmentInformation; 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.InterpreterUtils; @@ -46,11 +43,8 @@ import org.slf4j.LoggerFactory; import scala.Console; -import scala.None; -import scala.Option; import scala.Some; import scala.collection.JavaConversions; -import scala.collection.immutable.Nil; import scala.concurrent.duration.FiniteDuration; import scala.runtime.AbstractFunction0; import scala.tools.nsc.Settings; @@ -80,7 +74,7 @@ public FlinkInterpreter(Properties property) { public void open() { out = new ByteArrayOutputStream(); flinkConf = new org.apache.flink.configuration.Configuration(); - Properties intpProperty = getProperty(); + Properties intpProperty = getProperties(); for (Object k : intpProperty.keySet()) { String key = (String) k; String val = toString(intpProperty.get(key)); diff --git a/flink/src/test/java/org/apache/zeppelin/flink/FlinkInterpreterTest.java b/flink/src/test/java/org/apache/zeppelin/flink/FlinkInterpreterTest.java index d443508a8ff..c9cb1f63508 100644 --- a/flink/src/test/java/org/apache/zeppelin/flink/FlinkInterpreterTest.java +++ b/flink/src/test/java/org/apache/zeppelin/flink/FlinkInterpreterTest.java @@ -40,7 +40,7 @@ public static void setUp() { Properties p = new Properties(); flink = new FlinkInterpreter(p); flink.open(); - context = new InterpreterContext(null, null, null, null, null, null, null, null, null, null, null, null); + context = new InterpreterContext(null, null, null, null, null, null, null, null, null, null, null, null, null); } @AfterClass diff --git a/geode/pom.xml b/geode/pom.xml index e8eb9fc8462..fa7e0cfed7c 100644 --- a/geode/pom.xml +++ b/geode/pom.xml @@ -21,19 +21,21 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-geode jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Apache Geode interpreter + geode 1.1.0 1.3 @@ -86,54 +88,12 @@ maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/geode - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/geode - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin diff --git a/groovy/pom.xml b/groovy/pom.xml index bee50bd82ec..83bc0ec7f3a 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -20,19 +20,23 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-groovy jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Groovy interpreter + + groovy + + ${project.groupId} @@ -67,81 +71,14 @@ - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - true - - - -Xlint:unchecked - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - true - - - - maven-enforcer-plugin - 1.3.1 - - - enforce - none - - - maven-dependency-plugin - 2.8 - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/groovy - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/groovy - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin diff --git a/groovy/src/main/java/org/apache/zeppelin/groovy/GObject.java b/groovy/src/main/java/org/apache/zeppelin/groovy/GObject.java index 7f6809a8997..babda8f7603 100644 --- a/groovy/src/main/java/org/apache/zeppelin/groovy/GObject.java +++ b/groovy/src/main/java/org/apache/zeppelin/groovy/GObject.java @@ -172,7 +172,7 @@ public Object put(String varName, Object newValue) { /** * starts or continues rendering html/angular and returns MarkupBuilder to build html. *
     g.html().with{
    -   * 	h1("hello")
    +   *  h1("hello")
        *  h2("world")
        * }
    */ @@ -316,12 +316,12 @@ public void run(String paragraphId) { @ZeppelinApi public void run(String noteId, String paragraphId, InterpreterContext context) { if (paragraphId.equals(context.getParagraphId())) { - throw new InterpreterException("Can not run current Paragraph"); + throw new RuntimeException("Can not run current Paragraph"); } List runners = getInterpreterContextRunner(noteId, paragraphId, context); if (runners.size() <= 0) { - throw new InterpreterException("Paragraph " + paragraphId + " not found " + runners.size()); + throw new RuntimeException("Paragraph " + paragraphId + " not found " + runners.size()); } for (InterpreterContextRunner r : runners) { r.run(); @@ -338,7 +338,7 @@ public void runNote(String noteId, InterpreterContext context) { List runners = getInterpreterContextRunner(noteId, context); if (runners.size() <= 0) { - throw new InterpreterException("Note " + noteId + " not found " + runners.size()); + throw new RuntimeException("Note " + noteId + " not found " + runners.size()); } for (InterpreterContextRunner r : runners) { diff --git a/groovy/src/main/java/org/apache/zeppelin/groovy/GroovyInterpreter.java b/groovy/src/main/java/org/apache/zeppelin/groovy/GroovyInterpreter.java index e10828368f5..01e97e6bdd0 100644 --- a/groovy/src/main/java/org/apache/zeppelin/groovy/GroovyInterpreter.java +++ b/groovy/src/main/java/org/apache/zeppelin/groovy/GroovyInterpreter.java @@ -17,8 +17,6 @@ package org.apache.zeppelin.groovy; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.io.StringWriter; import java.io.PrintWriter; import java.io.File; @@ -26,10 +24,8 @@ import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; -import org.apache.zeppelin.interpreter.InterpreterPropertyBuilder; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResult.Code; -import org.apache.zeppelin.interpreter.InterpreterResult.Type; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.scheduler.Scheduler; @@ -40,7 +36,6 @@ import groovy.lang.GroovyShell; import groovy.lang.Script; import org.codehaus.groovy.control.CompilerConfiguration; -import org.codehaus.groovy.runtime.ResourceGroovyMethods; import org.codehaus.groovy.runtime.StackTraceUtils; import java.util.concurrent.ConcurrentHashMap; @@ -167,7 +162,7 @@ public InterpreterResult interpret(String cmd, InterpreterContext contextInterpr //put shared bindings evaluated in this interpreter bindings.putAll(sharedBindings); //put predefined bindings - bindings.put("g", new GObject(log, out, this.getProperty(), contextInterpreter, bindings)); + bindings.put("g", new GObject(log, out, this.getProperties(), contextInterpreter, bindings)); bindings.put("out", new PrintWriter(out, true)); script.run(); @@ -204,7 +199,7 @@ public void cancel(InterpreterContext context) { Thread t = (Thread) object; t.dumpStack(); t.interrupt(); - //t.stop(); //TODO: need some way to terminate maybe through GObject.. + //t.stop(); //TODO(dlukyanov): need some way to terminate maybe through GObject.. } catch (Throwable t) { log.error("Failed to cancel script: " + t, t); } diff --git a/groovy/src/main/resources/interpreter-setting.json b/groovy/src/main/resources/interpreter-setting.json index 45aab84a171..4afec2a8ed7 100644 --- a/groovy/src/main/resources/interpreter-setting.json +++ b/groovy/src/main/resources/interpreter-setting.json @@ -11,6 +11,10 @@ "description": "The path for custom groovy classes location. If empty `./interpreter/groovy/classes`", "type": "textarea" } + }, + "editor": { + "editOnDblClick": false, + "completionSupport": false } } ] \ No newline at end of file diff --git a/hbase/pom.xml b/hbase/pom.xml index 08b0cd70712..bec2529bb14 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -20,21 +20,23 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-hbase jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: HBase interpreter + hbase 1.0.0 - 2.6.0 + 2.7.0-mapr-1808 1.6.8 2.5.0 1.1 @@ -78,11 +80,13 @@ org.apache.hadoop hadoop-yarn-common ${hbase.hadoop.version} + provided
    org.apache.hadoop hadoop-yarn-api ${hbase.hadoop.version} + provided org.apache.hbase @@ -115,55 +119,14 @@ maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/hbase - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/hbase - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin + diff --git a/hbase/src/main/java/org/apache/zeppelin/hbase/HbaseInterpreter.java b/hbase/src/main/java/org/apache/zeppelin/hbase/HbaseInterpreter.java index 74d3ed1aecc..63c19283349 100644 --- a/hbase/src/main/java/org/apache/zeppelin/hbase/HbaseInterpreter.java +++ b/hbase/src/main/java/org/apache/zeppelin/hbase/HbaseInterpreter.java @@ -68,7 +68,7 @@ public HbaseInterpreter(Properties property) { } @Override - public void open() { + public void open() throws InterpreterException { this.scriptingContainer = new ScriptingContainer(LocalContextScope.SINGLETON); this.writer = new StringWriter(); scriptingContainer.setOutput(this.writer); @@ -88,7 +88,7 @@ public void open() { } logger.info("Absolute Ruby Source:" + abs_ruby_src.toString()); - // hirb.rb:41 requires the following system property to be set. + // hirb.rb:41 requires the following system properties to be set. Properties sysProps = System.getProperties(); sysProps.setProperty(HBASE_RUBY_SRC, abs_ruby_src.toString()); diff --git a/hbase/src/main/resources/interpreter-setting.json b/hbase/src/main/resources/interpreter-setting.json index 28dedcc530c..c5d89f083df 100644 --- a/hbase/src/main/resources/interpreter-setting.json +++ b/hbase/src/main/resources/interpreter-setting.json @@ -25,7 +25,8 @@ } }, "editor": { - "editOnDblClick": false + "editOnDblClick": false, + "completionSupport": false } } ] diff --git a/hbase/src/test/java/org/apache/zeppelin/hbase/HbaseInterpreterTest.java b/hbase/src/test/java/org/apache/zeppelin/hbase/HbaseInterpreterTest.java index 38a8b4d1e03..53040f91a53 100644 --- a/hbase/src/test/java/org/apache/zeppelin/hbase/HbaseInterpreterTest.java +++ b/hbase/src/test/java/org/apache/zeppelin/hbase/HbaseInterpreterTest.java @@ -15,6 +15,7 @@ package org.apache.zeppelin.hbase; import org.apache.log4j.BasicConfigurator; +import org.apache.zeppelin.interpreter.InterpreterException; import org.apache.zeppelin.interpreter.InterpreterResult; import org.junit.BeforeClass; import org.junit.Test; @@ -35,7 +36,7 @@ public class HbaseInterpreterTest { private static HbaseInterpreter hbaseInterpreter; @BeforeClass - public static void setUp() throws NullPointerException { + public static void setUp() throws NullPointerException, InterpreterException { BasicConfigurator.configure(); Properties properties = new Properties(); properties.put("hbase.home", ""); diff --git a/helium-dev/pom.xml b/helium-dev/pom.xml index 77c4dee8e02..bee308bb7d5 100644 --- a/helium-dev/pom.xml +++ b/helium-dev/pom.xml @@ -23,15 +23,20 @@ org.apache.zeppelin - zeppelin - 0.8.0-SNAPSHOT + interpreter-parent + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin helium-dev - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Helium development interpreter + + dev + + org.apache.zeppelin @@ -43,46 +48,14 @@ + + maven-enforcer-plugin + maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/helium-dev - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/helium-dev - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin diff --git a/helium-dev/src/main/java/org/apache/zeppelin/helium/DevInterpreter.java b/helium-dev/src/main/java/org/apache/zeppelin/helium/DevInterpreter.java index 7d1c361de78..ba1a564dd72 100644 --- a/helium-dev/src/main/java/org/apache/zeppelin/helium/DevInterpreter.java +++ b/helium-dev/src/main/java/org/apache/zeppelin/helium/DevInterpreter.java @@ -74,7 +74,8 @@ public void rerun() { } @Override - public InterpreterResult interpret(String st, InterpreterContext context) { + public InterpreterResult interpret(String st, InterpreterContext context) + throws InterpreterException { this.context = context; try { return interpreterEvent.interpret(st, context); 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..607839e433a 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 @@ -38,18 +38,18 @@ public class ZeppelinDevServer extends private DevInterpreter interpreter = null; private InterpreterOutput out; - public ZeppelinDevServer(int port) throws TException { - super(port); + public ZeppelinDevServer(int port) throws TException, IOException { + super(null, port, ":"); } @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/hive/pom.xml b/hive/pom.xml new file mode 100644 index 00000000000..945887157fa --- /dev/null +++ b/hive/pom.xml @@ -0,0 +1,100 @@ + + + + + 4.0.0 + + + interpreter-parent + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + ../interpreter-parent + + + org.apache.zeppelin + zeppelin-hive + jar + 0.8.2-mapr-1912-r2 + Zeppelin: JDBC Hive interpreter + + + + hive + 2.3.6-mapr-1912 + + + + + org.apache.zeppelin + zeppelin-interpreter + ${project.version} + provided + + + + org.apache.zeppelin + zeppelin-jdbc + ${project.version} + provided + + + + org.apache.hive + hive-jdbc + ${hive2.version} + + + org.apache.hadoop + * + + + com.mapr.hadoop + maprfs + + + com.mapr.security + mapr-security-web + + + org.apache.logging.log4j + * + + + + com.google.guava + * + + + + + + + + + maven-enforcer-plugin + + + maven-dependency-plugin + + + maven-resources-plugin + + + + diff --git a/hive/src/main/java/org/apache/zeppelin/hive/HiveInterpreter.java b/hive/src/main/java/org/apache/zeppelin/hive/HiveInterpreter.java new file mode 100644 index 00000000000..bb9a4b4be0e --- /dev/null +++ b/hive/src/main/java/org/apache/zeppelin/hive/HiveInterpreter.java @@ -0,0 +1,33 @@ +package org.apache.zeppelin.hive; + +import java.util.Properties; + +import org.apache.zeppelin.jdbc.JDBCInterpreter; + +/** + * JDBC interpreter for Zeppelin. This interpreter can also be used for accessing HAWQ, + * GreenplumDB, MariaDB, MySQL, Postgres and Redshift. + * + *
      + *
    • {@code default.url} - JDBC URL to connect to.
    • + *
    • {@code default.user} - JDBC user name..
    • + *
    • {@code default.password} - JDBC password..
    • + *
    • {@code default.driver.name} - JDBC driver name.
    • + *
    • {@code common.max.result} - Max number of SQL result to display.
    • + *
    + * + *

    + * How to use:
    + * {@code %jdbc.sql}
    + * {@code + * SELECT store_id, count(*) + * FROM retail_demo.order_lineitems_pxf + * GROUP BY store_id; + * } + *

    + */ +public class HiveInterpreter extends JDBCInterpreter { + public HiveInterpreter(Properties property) { + super(property); + } +} diff --git a/hive/src/main/resources/interpreter-setting.json b/hive/src/main/resources/interpreter-setting.json new file mode 100644 index 00000000000..1b558fd6952 --- /dev/null +++ b/hive/src/main/resources/interpreter-setting.json @@ -0,0 +1,125 @@ +[ + { + "group": "hive", + "name": "sql", + "className": "org.apache.zeppelin.hive.HiveInterpreter", + "properties": { + "default.url": { + "envName": "ZEPPELIN_JDBC_URL_HIVE", + "propertyName": "default.url", + "defaultValue": "jdbc:hive2://localhost:10000/default", + "description": "The URL for Hive JDBC.", + "type": "string" + }, + "default.user": { + "envName": "ZEPPELIN_INTERPRETER_USER", + "propertyName": "default.user", + "defaultValue": "", + "description": "The JDBC user name", + "type": "string" + }, + "default.password": { + "envName": null, + "propertyName": "default.password", + "defaultValue": "", + "description": "The JDBC user password", + "type": "password" + }, + "default.completer.ttlInSeconds": { + "envName": null, + "propertyName": "default.completer.ttlInSeconds", + "defaultValue": "120", + "description": "Time to live sql completer in seconds (-1 to update everytime, 0 to disable update)", + "type": "number" + }, + "default.driver": { + "envName": null, + "propertyName": "default.driver", + "defaultValue": "org.apache.hive.jdbc.HiveDriver", + "description": "Hive JDBC Driver Name", + "type": "string" + }, + "default.completer.schemaFilters": { + "envName": null, + "propertyName": "default.completer.schemaFilters", + "defaultValue": "", + "description": "Сomma separated schema (schema = catalog = database) filters to get metadata for completions. Supports '%' symbol is equivalent to any set of characters. (ex. prod_v_%,public%,info)", + "type": "textarea" + }, + "default.precode": { + "envName": null, + "propertyName": "default.precode", + "defaultValue": "", + "description": "SQL which executes while opening connection", + "type": "textarea" + }, + "default.statementPrecode": { + "envName": null, + "propertyName": "default.statementPrecode", + "defaultValue": "", + "description": "Runs before each run of the paragraph, in the same connection" + }, + "default.splitQueries": { + "envName": null, + "propertyName": "default.splitQueries", + "defaultValue": true, + "description": "Each query is executed apart and returns the result", + "type": "checkbox" + }, + "common.max_count": { + "envName": null, + "propertyName": "common.max_count", + "defaultValue": "1000", + "description": "Max number of SQL result to display.", + "type": "number" + }, + "zeppelin.jdbc.auth.type": { + "envName": null, + "propertyName": "zeppelin.jdbc.auth.type", + "defaultValue": "", + "description": "If auth type is needed, Example: KERBEROS", + "type": "string" + }, + "zeppelin.jdbc.concurrent.use": { + "envName": null, + "propertyName": "zeppelin.jdbc.concurrent.use", + "defaultValue": true, + "description": "Use parallel scheduler", + "type": "checkbox" + }, + "zeppelin.jdbc.concurrent.max_connection": { + "envName": null, + "propertyName": "zeppelin.jdbc.concurrent.max_connection", + "defaultValue": "10", + "description": "Number of concurrent execution", + "type": "number" + }, + "zeppelin.jdbc.keytab.location": { + "envName": null, + "propertyName": "zeppelin.jdbc.keytab.location", + "defaultValue": "", + "description": "Kerberos keytab location", + "type": "string" + }, + "zeppelin.jdbc.principal": { + "envName": null, + "propertyName": "zeppelin.jdbc.principal", + "defaultValue": "", + "description": "Kerberos principal", + "type": "string" + }, + "zeppelin.jdbc.interpolation": { + "envName": null, + "propertyName": "zeppelin.jdbc.interpolation", + "defaultValue": false, + "description": "Enable ZeppelinContext variable interpolation into paragraph text", + "type": "checkbox" + } + }, + "editor": { + "language": "sql", + "editOnDblClick": false, + "completionSupport": true + } + } +] diff --git a/ignite/pom.xml b/ignite/pom.xml index aadb9dd329c..a64bc9eeb53 100644 --- a/ignite/pom.xml +++ b/ignite/pom.xml @@ -20,19 +20,20 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent - zeppelin-ignite_2.10 + zeppelin-ignite_2.11 jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Apache Ignite interpreter - 1.9.0 + ignite + 2.3.0 @@ -106,55 +107,12 @@ maven-enforcer-plugin - - - enforce - none - - - - org.apache.maven.plugins maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/ignite - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/ignite - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin diff --git a/ignite/src/main/java/org/apache/zeppelin/ignite/IgniteSqlInterpreter.java b/ignite/src/main/java/org/apache/zeppelin/ignite/IgniteSqlInterpreter.java index 41803bb31be..6af8eb549e5 100644 --- a/ignite/src/main/java/org/apache/zeppelin/ignite/IgniteSqlInterpreter.java +++ b/ignite/src/main/java/org/apache/zeppelin/ignite/IgniteSqlInterpreter.java @@ -93,7 +93,7 @@ public void open() { } @Override - public void close() { + public void close() throws InterpreterException { try { if (conn != null) { conn.close(); diff --git a/ignite/src/main/resources/interpreter-setting.json b/ignite/src/main/resources/interpreter-setting.json index 2342a903756..3601e155ab0 100644 --- a/ignite/src/main/resources/interpreter-setting.json +++ b/ignite/src/main/resources/interpreter-setting.json @@ -32,6 +32,10 @@ "description": "Peer class loading enabled. True or false", "type": "checkbox" } + }, + "editor": { + "editOnDblClick": false, + "completionSupport": false } }, { @@ -46,6 +50,10 @@ "description": "Ignite JDBC connection URL.", "type": "string" } - } + }, + "editor": { + "editOnDblClick": false, + "completionSupport": false + } } ] diff --git a/ignite/src/test/java/org/apache/zeppelin/ignite/IgniteInterpreterTest.java b/ignite/src/test/java/org/apache/zeppelin/ignite/IgniteInterpreterTest.java index 9cb5eafe345..e8f226588a3 100644 --- a/ignite/src/test/java/org/apache/zeppelin/ignite/IgniteInterpreterTest.java +++ b/ignite/src/test/java/org/apache/zeppelin/ignite/IgniteInterpreterTest.java @@ -40,7 +40,7 @@ public class IgniteInterpreterTest { private static final String HOST = "127.0.0.1:47500..47509"; private static final InterpreterContext INTP_CONTEXT = - new InterpreterContext(null, null, null, null, null, null, null, null, null, null, null, null); + new InterpreterContext(null, null, null, null, null, null, null, null, null, null, null, null, null); private IgniteInterpreter intp; private Ignite ignite; 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..4cb2cbb0e2d 100644 --- a/ignite/src/test/java/org/apache/zeppelin/ignite/IgniteSqlInterpreterTest.java +++ b/ignite/src/test/java/org/apache/zeppelin/ignite/IgniteSqlInterpreterTest.java @@ -24,10 +24,10 @@ 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; +import org.apache.zeppelin.interpreter.InterpreterException; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterResult.Code; import org.apache.zeppelin.interpreter.InterpreterResult.Type; @@ -44,7 +44,7 @@ public class IgniteSqlInterpreterTest { private static final String HOST = "127.0.0.1:47500..47509"; private static final InterpreterContext INTP_CONTEXT = - new InterpreterContext(null, null, null, null, null, null, null, null, null, null, null, null); + new InterpreterContext(null, null, null, null, null, null, null, null, null, null, null, null, null); private Ignite ignite; private IgniteSqlInterpreter intp; @@ -83,7 +83,7 @@ public void setUp() { } @After - public void tearDown() { + public void tearDown() throws InterpreterException { intp.close(); ignite.close(); } diff --git a/interpreter-parent/pom.xml b/interpreter-parent/pom.xml new file mode 100644 index 00000000000..561a31bd461 --- /dev/null +++ b/interpreter-parent/pom.xml @@ -0,0 +1,137 @@ + + + + + 4.0.0 + + + zeppelin + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + .. + + + org.apache.zeppelin + interpreter-parent + pom + 0.8.2-mapr-1912-r2 + Zeppelin: Interpreter Parent + + + + + ${project.groupId} + zeppelin-interpreter + ${project.version} + + + + junit + junit + ${junit.version} + test + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + + + + + + + + + maven-enforcer-plugin + + + enforce + none + + + + + + maven-dependency-plugin + + + copy-interpreter-dependencies + package + + copy-dependencies + + + ${project.build.directory}/../../interpreter/${interpreter.name} + false + false + true + runtime + + + + copy-artifact + package + + copy + + + ${project.build.directory}/../../interpreter/${interpreter.name} + 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/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'] diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 71d33109e68..0332e998a11 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -21,21 +21,218 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-jdbc jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: JDBC interpreter + + + + jdbc-hive + + 1.2.1 + 2.1.0 + + + + + org.apache.hive + hive-jdbc + ${hive.version} + + + org.apache.hadoop + hadoop-common + + + org.apache.hadoop + hadoop-auth + + + org.apache.httpcomponents + httpcore + + + org.apache.httpcomponents + httpclient + + + + + + org.apache.httpcomponents + httpcore + 4.4.1 + + + org.apache.httpcomponents + httpclient + 4.4.1 + + + + org.apache.hive.shims + hive-shims-0.23 + ${hive2.version} + + + + + + jdbc-phoenix + + 4.7.0 + + + + org.apache.phoenix + phoenix-core + ${phoenix.version} + + + + xerces + xercesImpl + 2.11.0.SP5 + + + + + + jdbc-hadoop2 + + 2.7.3 + + + + 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 + + + + + + + + jdbc-hadoop3 + + 3.0.0 + + + + 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 + + + + + + + jdbc 9.4-1201-jdbc41 - 2.7.2 + 2.7.0-mapr-1808 1.4.190 2.0.1 @@ -158,54 +355,12 @@ maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/jdbc - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/jdbc - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin diff --git a/jdbc/src/main/java/org/apache/zeppelin/jdbc/JDBCInterpreter.java b/jdbc/src/main/java/org/apache/zeppelin/jdbc/JDBCInterpreter.java index f3f432654a4..ee973ba846b 100644 --- a/jdbc/src/main/java/org/apache/zeppelin/jdbc/JDBCInterpreter.java +++ b/jdbc/src/main/java/org/apache/zeppelin/jdbc/JDBCInterpreter.java @@ -38,14 +38,14 @@ import org.apache.commons.dbcp2.PoolableConnectionFactory; import org.apache.commons.dbcp2.PoolingDriver; import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.exception.ExceptionUtils; +import org.apache.commons.lang.mutable.MutableBoolean; import org.apache.commons.pool2.ObjectPool; import org.apache.commons.pool2.impl.GenericObjectPool; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.alias.CredentialProvider; import org.apache.hadoop.security.alias.CredentialProviderFactory; -import org.apache.thrift.transport.TTransportException; -import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterException; import org.apache.zeppelin.interpreter.InterpreterResult; @@ -61,8 +61,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.base.Throwables; - import static org.apache.commons.lang.StringUtils.containsIgnoreCase; import static org.apache.commons.lang.StringUtils.isEmpty; import static org.apache.commons.lang.StringUtils.isNotEmpty; @@ -105,6 +103,7 @@ public class JDBCInterpreter extends KerberosInterpreter { static final String USER_KEY = "user"; static final String PASSWORD_KEY = "password"; static final String PRECODE_KEY = "precode"; + static final String STATEMENT_PRECODE_KEY = "statementPrecode"; static final String COMPLETER_SCHEMA_FILTERS_KEY = "completer.schemaFilters"; static final String COMPLETER_TTL_KEY = "completer.ttlInSeconds"; static final String DEFAULT_COMPLETER_TTL = "120"; @@ -112,6 +111,7 @@ public class JDBCInterpreter extends KerberosInterpreter { static final String JDBC_JCEKS_FILE = "jceks.file"; static final String JDBC_JCEKS_CREDENTIAL_KEY = "jceks.credentialKey"; static final String PRECODE_KEY_TEMPLATE = "%s.precode"; + static final String STATEMENT_PRECODE_KEY_TEMPLATE = "%s.statementPrecode"; static final String DOT = "."; private static final char WHITESPACE = ' '; @@ -127,18 +127,21 @@ public class JDBCInterpreter extends KerberosInterpreter { static final String DEFAULT_USER = DEFAULT_KEY + DOT + USER_KEY; static final String DEFAULT_PASSWORD = DEFAULT_KEY + DOT + PASSWORD_KEY; static final String DEFAULT_PRECODE = DEFAULT_KEY + DOT + PRECODE_KEY; + static final String DEFAULT_STATEMENT_PRECODE = DEFAULT_KEY + DOT + STATEMENT_PRECODE_KEY; static final String EMPTY_COLUMN_VALUE = ""; private final String CONCURRENT_EXECUTION_KEY = "zeppelin.jdbc.concurrent.use"; private final String CONCURRENT_EXECUTION_COUNT = "zeppelin.jdbc.concurrent.max_connection"; private final String DBCP_STRING = "jdbc:apache:commons:dbcp:"; + private static final String MAX_ROWS_KEY = "zeppelin.jdbc.maxRows"; private final HashMap basePropretiesMap; private final HashMap jdbcUserConfigurationsMap; private final HashMap sqlCompletersMap; private int maxLineResults; + private int maxRows; public JDBCInterpreter(Properties property) { super(property); @@ -171,7 +174,7 @@ public HashMap getPropertiesMap() { @Override public void open() { super.open(); - for (String propertyKey : property.stringPropertyNames()) { + for (String propertyKey : properties.stringPropertyNames()) { logger.debug("propertyKey: {}", propertyKey); String[] keyValue = propertyKey.split("\\.", 2); if (2 == keyValue.length) { @@ -184,7 +187,7 @@ public void open() { prefixProperties = new Properties(); basePropretiesMap.put(keyValue[0].trim(), prefixProperties); } - prefixProperties.put(keyValue[1].trim(), property.getProperty(propertyKey)); + prefixProperties.put(keyValue[1].trim(), getProperty(propertyKey)); } } @@ -206,12 +209,13 @@ public void open() { logger.debug("JDBC PropretiesMap: {}", basePropretiesMap); setMaxLineResults(); + setMaxRows(); } protected boolean isKerboseEnabled() { - if (!isEmpty(property.getProperty("zeppelin.jdbc.auth.type"))) { - UserGroupInformation.AuthenticationMethod authType = JDBCSecurityImpl.getAuthtype(property); + if (!isEmpty(getProperty("zeppelin.jdbc.auth.type"))) { + UserGroupInformation.AuthenticationMethod authType = JDBCSecurityImpl.getAuthtype(properties); if (authType.equals(KERBEROS)) { return true; } @@ -227,6 +231,14 @@ private void setMaxLineResults() { } } + /** + * Fetch MAX_ROWS_KEYS value from property file and set it to + * "maxRows" value. + */ + private void setMaxRows() { + maxRows = Integer.valueOf(getProperty(MAX_ROWS_KEY, "1000")); + } + private SqlCompleter createOrUpdateSqlCompleter(SqlCompleter sqlCompleter, final Connection connection, String propertyKey, final String buf, final int cursor) { String schemaFiltersKey = String.format("%s.%s", propertyKey, COMPLETER_SCHEMA_FILTERS_KEY); @@ -278,11 +290,17 @@ private void initStatementMap() { } private void initConnectionPoolMap() { - for (JDBCUserConfigurations configurations : jdbcUserConfigurationsMap.values()) { + for (String key : jdbcUserConfigurationsMap.keySet()) { try { + closeDBPool(key, DEFAULT_KEY); + } catch (SQLException e) { + logger.error("Error while closing database pool.", e); + } + try { + JDBCUserConfigurations configurations = jdbcUserConfigurationsMap.get(key); configurations.initConnectionPoolMap(); - } catch (Exception e) { - logger.error("Error while closing initConnectionPoolMap...", e); + } catch (SQLException e) { + logger.error("Error while closing initConnectionPoolMap.", e); } } } @@ -349,7 +367,7 @@ private void closeDBPool(String user, String propertyKey) throws SQLException { } private void setUserProperty(String propertyKey, InterpreterContext interpreterContext) - throws SQLException, IOException { + throws SQLException, IOException, InterpreterException { String user = interpreterContext.getAuthenticationInfo().getUser(); @@ -383,7 +401,11 @@ private void createConnectionPool(String url, String user, String propertyKey, new DriverManagerConnectionFactory(url, properties); PoolableConnectionFactory poolableConnectionFactory = new PoolableConnectionFactory( - connectionFactory, null); + connectionFactory, null); + final String maxConnectionLifetime = + StringUtils.defaultIfEmpty(getProperty("zeppelin.jdbc.maxConnLifetime"), "-1"); + poolableConnectionFactory.setMaxConnLifetimeMillis(Long.parseLong(maxConnectionLifetime)); + poolableConnectionFactory.setValidationQuery("show databases"); ObjectPool connectionPool = new GenericObjectPool(poolableConnectionFactory); poolableConnectionFactory.setPool(connectionPool); @@ -417,18 +439,19 @@ public Connection getConnection(String propertyKey, InterpreterContext interpret final Properties properties = jdbcUserConfigurations.getPropertyMap(propertyKey); final String url = properties.getProperty(URL_KEY); - if (isEmpty(property.getProperty("zeppelin.jdbc.auth.type"))) { + if (isEmpty(getProperty("zeppelin.jdbc.auth.type"))) { connection = getConnectionFromPool(url, user, propertyKey, properties); } else { - UserGroupInformation.AuthenticationMethod authType = JDBCSecurityImpl.getAuthtype(property); + UserGroupInformation.AuthenticationMethod authType = + JDBCSecurityImpl.getAuthtype(getProperties()); final String connectionUrl = appendProxyUserToURL(url, user, propertyKey); - JDBCSecurityImpl.createSecureConfiguration(property, authType); + JDBCSecurityImpl.createSecureConfiguration(getProperties(), authType); switch (authType) { case KERBEROS: if (user == null || "false".equalsIgnoreCase( - property.getProperty("zeppelin.jdbc.auth.kerberos.proxy.enable"))) { + getProperty("zeppelin.jdbc.auth.kerberos.proxy.enable"))) { connection = getConnectionFromPool(connectionUrl, user, propertyKey, properties); } else { if (basePropretiesMap.get(propertyKey).containsKey("proxy.user.property")) { @@ -490,7 +513,7 @@ private String appendProxyUserToURL(String url, String user, String propertyKey) return connectionUrl.toString(); } - private String getPassword(Properties properties) throws IOException { + private String getPassword(Properties properties) throws IOException, InterpreterException { if (isNotEmpty(properties.getProperty(PASSWORD_KEY))) { return properties.getProperty(PASSWORD_KEY); } else if (isNotEmpty(properties.getProperty(JDBC_JCEKS_FILE)) @@ -518,7 +541,7 @@ && isNotEmpty(properties.getProperty(JDBC_JCEKS_CREDENTIAL_KEY))) { return null; } - private String getResults(ResultSet resultSet, boolean isTableType) + private String getResults(ResultSet resultSet, boolean isTableType, MutableBoolean isComplete) throws SQLException { ResultSetMetaData md = resultSet.getMetaData(); StringBuilder msg; @@ -532,12 +555,20 @@ private String getResults(ResultSet resultSet, boolean isTableType) if (i > 1) { msg.append(TAB); } - msg.append(replaceReservedChars(md.getColumnName(i))); + if (StringUtils.isNotEmpty(md.getColumnLabel(i))) { + msg.append(replaceReservedChars(md.getColumnLabel(i))); + } else { + msg.append(replaceReservedChars(md.getColumnName(i))); + } } msg.append(NEWLINE); int displayRowCount = 0; - while (displayRowCount < getMaxResult() && resultSet.next()) { + while (resultSet.next()) { + if (displayRowCount >= getMaxResult()) { + isComplete.setValue(false); + break; + } for (int i = 1; i < md.getColumnCount() + 1; i++) { Object resultObject; String resultValue; @@ -579,18 +610,12 @@ protected ArrayList splitSqlQueries(String sql) { for (int item = 0; item < sql.length(); item++) { character = sql.charAt(item); - if ((singleLineComment && (character == '\n' || item == sql.length() - 1)) - || (multiLineComment && character == '/' && sql.charAt(item - 1) == '*')) { + if (singleLineComment && (character == '\n' || item == sql.length() - 1)) { singleLineComment = false; - multiLineComment = false; - if (item == sql.length() - 1 && query.length() > 0) { - queries.add(StringUtils.trim(query.toString())); - } - continue; } - if (singleLineComment || multiLineComment) { - continue; + if (multiLineComment && character == '/' && sql.charAt(item - 1) == '*') { + multiLineComment = false; } if (character == '\'') { @@ -613,16 +638,13 @@ protected ArrayList splitSqlQueries(String sql) { && sql.length() > item + 1) { if (character == '-' && sql.charAt(item + 1) == '-') { singleLineComment = true; - continue; - } - - if (character == '/' && sql.charAt(item + 1) == '*') { + } else if (character == '/' && sql.charAt(item + 1) == '*') { multiLineComment = true; - continue; } } - if (character == ';' && !quoteString && !doubleQuoteString) { + if (character == ';' && !quoteString && !doubleQuoteString && !multiLineComment + && !singleLineComment) { queries.add(StringUtils.trim(query.toString())); query = new StringBuilder(); } else if (item == sql.length() - 1) { @@ -653,7 +675,7 @@ public InterpreterResult executePrecode(InterpreterContext interpreterContext) { private InterpreterResult executeSql(String propertyKey, String sql, InterpreterContext interpreterContext) { - Connection connection; + Connection connection = null; Statement statement; ResultSet resultSet = null; String paragraphId = interpreterContext.getParagraphId(); @@ -668,11 +690,21 @@ private InterpreterResult executeSql(String propertyKey, String sql, InterpreterResult interpreterResult = new InterpreterResult(InterpreterResult.Code.SUCCESS); try { connection = getConnection(propertyKey, interpreterContext); - if (connection == null) { - return new InterpreterResult(Code.ERROR, "Prefix not found."); + } catch (Exception e) { + String errorMsg = ExceptionUtils.getStackTrace(e); + try { + closeDBPool(user, propertyKey); + } catch (SQLException e1) { + logger.error("Cannot close DBPool for user, propertyKey: " + user + propertyKey, e1); } + interpreterResult.add(errorMsg); + return new InterpreterResult(Code.ERROR, interpreterResult.message()); + } + if (connection == null) { + return new InterpreterResult(Code.ERROR, "Prefix not found."); + } - + try { List sqlArray; if (splitQuery) { sqlArray = splitSqlQueries(sql); @@ -686,7 +718,7 @@ private InterpreterResult executeSql(String propertyKey, String sql, // fetch n+1 rows in order to indicate there's more rows available (for large selects) statement.setFetchSize(getMaxResult()); - statement.setMaxRows(getMaxResult() + 1); + statement.setMaxRows(maxRows); if (statement == null) { return new InterpreterResult(Code.ERROR, "Prefix not found."); @@ -695,6 +727,13 @@ private InterpreterResult executeSql(String propertyKey, String sql, try { getJDBCConfiguration(user).saveStatement(paragraphId, statement); + String statementPrecode = + getProperty(String.format(STATEMENT_PRECODE_KEY_TEMPLATE, propertyKey)); + + if (StringUtils.isNotBlank(statementPrecode)) { + statement.execute(statementPrecode); + } + boolean isResultSetAvailable = statement.execute(sqlToExecute); getJDBCConfiguration(user).setConnectionInDBDriverPoolSuccessful(propertyKey); if (isResultSetAvailable) { @@ -706,10 +745,11 @@ private InterpreterResult executeSql(String propertyKey, String sql, interpreterResult.add(InterpreterResult.Type.TEXT, "Query executed successfully."); } else { + MutableBoolean isComplete = new MutableBoolean(true); String results = getResults(resultSet, - !containsIgnoreCase(sqlToExecute, EXPLAIN_PREDICATE)); + !containsIgnoreCase(sqlToExecute, EXPLAIN_PREDICATE), isComplete); interpreterResult.add(results); - if (resultSet.next()) { + if (!isComplete.booleanValue()) { interpreterResult.add(ResultMessages.getExceedsLimitRowsMessage(getMaxResult(), String.format("%s.%s", COMMON_KEY, MAX_LINE_KEY))); } @@ -734,6 +774,12 @@ private InterpreterResult executeSql(String propertyKey, String sql, } } } + } catch (Throwable e) { + logger.error("Cannot run " + sql, e); + String errorMsg = ExceptionUtils.getStackTrace(e); + interpreterResult.add(errorMsg); + return new InterpreterResult(Code.ERROR, interpreterResult.message()); + } finally { //In case user ran an insert/update/upsert statement if (connection != null) { try { @@ -744,16 +790,6 @@ private InterpreterResult executeSql(String propertyKey, String sql, } catch (SQLException e) { /*ignored*/ } } getJDBCConfiguration(user).removeStatement(paragraphId); - } catch (Throwable e) { - logger.error("Cannot run " + sql, e); - String errorMsg = Throwables.getStackTraceAsString(e); - try { - closeDBPool(user, propertyKey); - } catch (SQLException e1) { - logger.error("Cannot close DBPool for user, propertyKey: " + user + propertyKey, e1); - } - interpreterResult.add(errorMsg); - return new InterpreterResult(Code.ERROR, interpreterResult.message()); } return interpreterResult; } @@ -769,7 +805,9 @@ private String replaceReservedChars(String str) { } @Override - public InterpreterResult interpret(String cmd, InterpreterContext contextInterpreter) { + public InterpreterResult interpret(String originalCmd, InterpreterContext contextInterpreter) { + String cmd = Boolean.parseBoolean(getProperty("zeppelin.jdbc.interpolation")) ? + interpolate(originalCmd, contextInterpreter.getResourcePool()) : originalCmd; logger.debug("Run SQL command '{}'", cmd); String propertyKey = getPropertyKey(cmd); @@ -832,7 +870,7 @@ public Scheduler getScheduler() { @Override public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { + InterpreterContext interpreterContext) throws InterpreterException { List candidates = new ArrayList<>(); String propertyKey = getPropertyKey(buf); String sqlCompleterKey = diff --git a/jdbc/src/main/java/org/apache/zeppelin/jdbc/JDBCUserConfigurations.java b/jdbc/src/main/java/org/apache/zeppelin/jdbc/JDBCUserConfigurations.java index d00e1e9b6f5..057938035bb 100644 --- a/jdbc/src/main/java/org/apache/zeppelin/jdbc/JDBCUserConfigurations.java +++ b/jdbc/src/main/java/org/apache/zeppelin/jdbc/JDBCUserConfigurations.java @@ -48,12 +48,6 @@ public void initStatementMap() throws SQLException { } public void initConnectionPoolMap() throws SQLException { - Iterator it = poolingDriverMap.keySet().iterator(); - while (it.hasNext()) { - String driverName = it.next(); - poolingDriverMap.get(driverName).closePool(driverName); - it.remove(); - } poolingDriverMap.clear(); isSuccessful.clear(); } 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/jdbc/src/main/java/org/apache/zeppelin/jdbc/security/JDBCSecurityImpl.java b/jdbc/src/main/java/org/apache/zeppelin/jdbc/security/JDBCSecurityImpl.java index 6e9470f4edc..91946cf599d 100644 --- a/jdbc/src/main/java/org/apache/zeppelin/jdbc/security/JDBCSecurityImpl.java +++ b/jdbc/src/main/java/org/apache/zeppelin/jdbc/security/JDBCSecurityImpl.java @@ -47,10 +47,19 @@ public static void createSecureConfiguration(Properties properties, conf.set("hadoop.security.authentication", KERBEROS.toString()); UserGroupInformation.setConfiguration(conf); try { - UserGroupInformation.loginUserFromKeytab( - properties.getProperty("zeppelin.jdbc.principal"), - properties.getProperty("zeppelin.jdbc.keytab.location") - ); + // Check TGT before calling login + // Ref: https://github.com/apache/hadoop/blob/release-3.0.1-RC1/hadoop-common-project/ + // hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java#L1232 + if (!UserGroupInformation.isSecurityEnabled() + || UserGroupInformation.getCurrentUser().getAuthenticationMethod() != KERBEROS + || !UserGroupInformation.isLoginKeytabBased()) { + UserGroupInformation.loginUserFromKeytab( + properties.getProperty("zeppelin.jdbc.principal"), + properties.getProperty("zeppelin.jdbc.keytab.location")); + } else { + LOGGER.info("The user has already logged in using Keytab and principal, " + + "no action required"); + } } catch (IOException e) { LOGGER.error("Failed to get either keytab location or principal name in the " + "interpreter", e); diff --git a/jdbc/src/main/resources/interpreter-setting.json b/jdbc/src/main/resources/interpreter-setting.json index abbc1cfd9c9..34992d97413 100644 --- a/jdbc/src/main/resources/interpreter-setting.json +++ b/jdbc/src/main/resources/interpreter-setting.json @@ -48,11 +48,17 @@ }, "default.precode": { "envName": null, - "propertyName": "zeppelin.jdbc.precode", + "propertyName": "default.precode", "defaultValue": "", "description": "SQL which executes while opening connection", "type": "textarea" }, + "default.statementPrecode": { + "envName": null, + "propertyName": "default.statementPrecode", + "defaultValue": "", + "description": "Runs before each run of the paragraph, in the same connection" + }, "default.splitQueries": { "envName": null, "propertyName": "default.splitQueries", @@ -101,11 +107,33 @@ "defaultValue": "", "description": "Kerberos principal", "type": "string" + }, + "zeppelin.jdbc.interpolation": { + "envName": null, + "propertyName": "zeppelin.jdbc.interpolation", + "defaultValue": false, + "description": "Enable ZeppelinContext variable interpolation into paragraph text", + "type": "checkbox" + }, + "zeppelin.jdbc.maxConnLifetime": { + "envName": null, + "propertyName": "zeppelin.jdbc.maxConnLifetime", + "defaultValue": "-1", + "description": "Maximum of connection lifetime in milliseconds. A value of zero or less means the connection has an infinite lifetime.", + "type": "number" + }, + "zeppelin.jdbc.maxRows": { + "envName": null, + "propertyName": "zeppelin.jdbc.maxRows", + "defaultValue": "1000", + "description": "Maximum number of rows fetched from the query.", + "type": "number" } }, "editor": { "language": "sql", - "editOnDblClick": false + "editOnDblClick": false, + "completionSupport": true } } ] diff --git a/jdbc/src/test/java/org/apache/zeppelin/jdbc/JDBCInterpreterInterpolationTest.java b/jdbc/src/test/java/org/apache/zeppelin/jdbc/JDBCInterpreterInterpolationTest.java new file mode 100644 index 00000000000..fe7bc80a179 --- /dev/null +++ b/jdbc/src/test/java/org/apache/zeppelin/jdbc/JDBCInterpreterInterpolationTest.java @@ -0,0 +1,181 @@ +/** + * 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.jdbc; + +import com.mockrunner.jdbc.BasicJDBCTestCaseAdapter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.resource.LocalResourcePool; +import org.apache.zeppelin.resource.ResourcePool; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; +import java.util.Properties; + +import static java.lang.String.format; +import static org.junit.Assert.assertEquals; + +/** + * JDBC interpreter Z-variable interpolation unit tests. + */ +public class JDBCInterpreterInterpolationTest extends BasicJDBCTestCaseAdapter { + + private static String jdbcConnection; + private InterpreterContext interpreterContext; + private ResourcePool resourcePool; + + private String getJdbcConnection() throws IOException { + if (null == jdbcConnection) { + Path tmpDir = Files.createTempDirectory("h2-test-"); + tmpDir.toFile().deleteOnExit(); + jdbcConnection = format("jdbc:h2:%s", tmpDir); + } + return jdbcConnection; + } + + @Before + public void setUp() throws Exception { + Class.forName("org.h2.Driver"); + Connection connection = DriverManager.getConnection(getJdbcConnection()); + Statement statement = connection.createStatement(); + statement.execute( + "DROP TABLE IF EXISTS test_table; " + + "CREATE TABLE test_table(id varchar(255), name varchar(255));"); + + Statement insertStatement = connection.createStatement(); + insertStatement.execute("insert into test_table(id, name) values " + + "('pro', 'processor')," + + "('mem', 'memory')," + + "('key', 'keyboard')," + + "('mou', 'mouse');"); + resourcePool = new LocalResourcePool("JdbcInterpolationTest"); + + interpreterContext = new InterpreterContext("", "1", null, "", "", + new AuthenticationInfo("testUser"), null, null, null, null, resourcePool, null, null); + } + + @Test + public void testEnableDisableProperty() throws IOException { + Properties properties = new Properties(); + properties.setProperty("common.max_count", "1000"); + properties.setProperty("common.max_retry", "3"); + properties.setProperty("default.driver", "org.h2.Driver"); + properties.setProperty("default.url", getJdbcConnection()); + properties.setProperty("default.user", ""); + properties.setProperty("default.password", ""); + + resourcePool.put("zid", "mem"); + String sqlQuery = "select * from test_table where id = '{zid}'"; + + // + // Empty result expected because "zeppelin.jdbc.interpolation" is false by default ... + // + JDBCInterpreter t = new JDBCInterpreter(properties); + t.open(); + InterpreterResult interpreterResult = t.interpret(sqlQuery, interpreterContext); + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code()); + assertEquals(InterpreterResult.Type.TABLE, interpreterResult.message().get(0).getType()); + assertEquals(1, interpreterResult.message().size()); + assertEquals("ID\tNAME\n", interpreterResult.message().get(0).getData()); + + // + // 1 result expected because "zeppelin.jdbc.interpolation" set to "true" ... + // + properties.setProperty("zeppelin.jdbc.interpolation", "true"); + t = new JDBCInterpreter(properties); + t.open(); + interpreterResult = t.interpret(sqlQuery, interpreterContext); + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code()); + assertEquals(InterpreterResult.Type.TABLE, interpreterResult.message().get(0).getType()); + assertEquals(1, interpreterResult.message().size()); + assertEquals("ID\tNAME\nmem\tmemory\n", + interpreterResult.message().get(0).getData()); + } + + @Test + public void testNormalQueryInterpolation() throws IOException { + Properties properties = new Properties(); + properties.setProperty("common.max_count", "1000"); + properties.setProperty("common.max_retry", "3"); + properties.setProperty("default.driver", "org.h2.Driver"); + properties.setProperty("default.url", getJdbcConnection()); + properties.setProperty("default.user", ""); + properties.setProperty("default.password", ""); + + properties.setProperty("zeppelin.jdbc.interpolation", "true"); + + JDBCInterpreter t = new JDBCInterpreter(properties); + t.open(); + + // + // Empty result expected because "kbd" is not defined ... + // + String sqlQuery = "select * from test_table where id = '{kbd}'"; + InterpreterResult interpreterResult = t.interpret(sqlQuery, interpreterContext); + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code()); + assertEquals(InterpreterResult.Type.TABLE, interpreterResult.message().get(0).getType()); + assertEquals(1, interpreterResult.message().size()); + assertEquals("ID\tNAME\n", interpreterResult.message().get(0).getData()); + + resourcePool.put("itemId", "key"); + + // + // 1 result expected because z-variable 'item' is 'key' ... + // + sqlQuery = "select * from test_table where id = '{itemId}'"; + interpreterResult = t.interpret(sqlQuery, interpreterContext); + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code()); + assertEquals(InterpreterResult.Type.TABLE, interpreterResult.message().get(0).getType()); + assertEquals(1, interpreterResult.message().size()); + assertEquals("ID\tNAME\nkey\tkeyboard\n", + interpreterResult.message().get(0).getData()); + } + + @Test + public void testEscapedInterpolationPattern() throws IOException { + Properties properties = new Properties(); + properties.setProperty("common.max_count", "1000"); + properties.setProperty("common.max_retry", "3"); + properties.setProperty("default.driver", "org.h2.Driver"); + properties.setProperty("default.url", getJdbcConnection()); + properties.setProperty("default.user", ""); + properties.setProperty("default.password", ""); + + properties.setProperty("zeppelin.jdbc.interpolation", "true"); + + JDBCInterpreter t = new JDBCInterpreter(properties); + t.open(); + + // + // 2 rows (keyboard and mouse) expected when searching names with 2 consecutive vowels ... + // The 'regexp' keyword is specific to H2 database + // + String sqlQuery = "select * from test_table where name regexp '[aeiou]{{2}}'"; + InterpreterResult interpreterResult = t.interpret(sqlQuery, interpreterContext); + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code()); + assertEquals(InterpreterResult.Type.TABLE, interpreterResult.message().get(0).getType()); + assertEquals(1, interpreterResult.message().size()); + assertEquals("ID\tNAME\nkey\tkeyboard\nmou\tmouse\n", + interpreterResult.message().get(0).getData()); + } + +} diff --git a/jdbc/src/test/java/org/apache/zeppelin/jdbc/JDBCInterpreterTest.java b/jdbc/src/test/java/org/apache/zeppelin/jdbc/JDBCInterpreterTest.java index e6f9598f8b7..f6b794d0a7e 100644 --- a/jdbc/src/test/java/org/apache/zeppelin/jdbc/JDBCInterpreterTest.java +++ b/jdbc/src/test/java/org/apache/zeppelin/jdbc/JDBCInterpreterTest.java @@ -17,6 +17,7 @@ import static java.lang.String.format; import static org.apache.zeppelin.jdbc.JDBCInterpreter.DEFAULT_DRIVER; import static org.apache.zeppelin.jdbc.JDBCInterpreter.DEFAULT_PASSWORD; +import static org.apache.zeppelin.jdbc.JDBCInterpreter.DEFAULT_STATEMENT_PRECODE; import static org.apache.zeppelin.jdbc.JDBCInterpreter.DEFAULT_USER; import static org.apache.zeppelin.jdbc.JDBCInterpreter.DEFAULT_URL; import static org.apache.zeppelin.jdbc.JDBCInterpreter.DEFAULT_PRECODE; @@ -24,6 +25,11 @@ import static org.apache.zeppelin.jdbc.JDBCInterpreter.COMMON_MAX_LINE; import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; +import static org.apache.zeppelin.jdbc.JDBCInterpreter.STATEMENT_PRECODE_KEY_TEMPLATE; + + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -37,6 +43,7 @@ import org.apache.zeppelin.completer.CompletionType; 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.FIFOScheduler; @@ -90,8 +97,8 @@ public void setUp() throws Exception { PreparedStatement insertStatement = connection.prepareStatement("insert into test_table(id, name) values ('a', 'a_name'),('b', 'b_name'),('c', ?);"); insertStatement.setString(1, null); insertStatement.execute(); - interpreterContext = new InterpreterContext("", "1", null, "", "", new AuthenticationInfo("testUser"), null, null, null, null, - null, null); + interpreterContext = new InterpreterContext("", "1", null, "", "", + new AuthenticationInfo("testUser"), null, null, null,null, null, null, null); } @@ -173,6 +180,27 @@ public void testSelectQuery() throws SQLException, IOException { assertEquals("ID\tNAME\na\ta_name\nb\tb_name\n", interpreterResult.message().get(0).getData()); } + @Test + public void testColumnAliasQuery() throws IOException { + Properties properties = new Properties(); + properties.setProperty("common.max_count", "1000"); + properties.setProperty("common.max_retry", "3"); + properties.setProperty("default.driver", "org.h2.Driver"); + properties.setProperty("default.url", getJdbcConnection()); + properties.setProperty("default.user", ""); + properties.setProperty("default.password", ""); + JDBCInterpreter t = new JDBCInterpreter(properties); + t.open(); + + String sqlQuery = "select NAME as SOME_OTHER_NAME from test_table limit 1"; + + InterpreterResult interpreterResult = t.interpret(sqlQuery, interpreterContext); + + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code()); + assertEquals(InterpreterResult.Type.TABLE, interpreterResult.message().get(0).getType()); + assertEquals("SOME_OTHER_NAME\na_name\n", interpreterResult.message().get(0).getData()); + } + @Test public void testSplitSqlQuery() throws SQLException, IOException { String sqlQuery = "insert into test_table(id, name) values ('a', ';\"');" + @@ -182,13 +210,16 @@ public void testSplitSqlQuery() throws SQLException, IOException { "select '\n', ';';" + "select replace('A\\;B', '\\', 'text');" + "select '\\', ';';" + - "select '''', ';'"; + "select '''', ';';" + + "select /*+ scan */ * from test_table;" + + "--singleLineComment\nselect * from test_table"; + Properties properties = new Properties(); JDBCInterpreter t = new JDBCInterpreter(properties); t.open(); List multipleSqlArray = t.splitSqlQueries(sqlQuery); - assertEquals(8, multipleSqlArray.size()); + assertEquals(10, multipleSqlArray.size()); assertEquals("insert into test_table(id, name) values ('a', ';\"')", multipleSqlArray.get(0)); assertEquals("select * from test_table", multipleSqlArray.get(1)); assertEquals("select * from test_table WHERE ID = \";'\"", multipleSqlArray.get(2)); @@ -197,6 +228,8 @@ public void testSplitSqlQuery() throws SQLException, IOException { assertEquals("select replace('A\\;B', '\\', 'text')", multipleSqlArray.get(5)); assertEquals("select '\\', ';'", multipleSqlArray.get(6)); assertEquals("select '''', ';'", multipleSqlArray.get(7)); + assertEquals("select /*+ scan */ * from test_table", multipleSqlArray.get(8)); + assertEquals("--singleLineComment\nselect * from test_table", multipleSqlArray.get(9)); } @Test @@ -349,7 +382,7 @@ public void concurrentSettingTest() { } @Test - public void testAutoCompletion() throws SQLException, IOException { + public void testAutoCompletion() throws SQLException, IOException, InterpreterException { Properties properties = new Properties(); properties.setProperty("common.max_count", "1000"); properties.setProperty("common.max_retry", "3"); @@ -417,7 +450,7 @@ public void testMultiTenant() throws SQLException, IOException { // user1 runs jdbc1 jdbc1.open(); InterpreterContext ctx1 = new InterpreterContext("", "1", "jdbc1", "", "", user1Credential, - null, null, null, null, null, null); + null, null, null, null, null, null, null); jdbc1.interpret("", ctx1); JDBCUserConfigurations user1JDBC1Conf = jdbc1.getJDBCConfiguration("user1"); @@ -428,7 +461,7 @@ public void testMultiTenant() throws SQLException, IOException { // user1 runs jdbc2 jdbc2.open(); InterpreterContext ctx2 = new InterpreterContext("", "1", "jdbc2", "", "", user1Credential, - null, null, null, null, null, null); + null, null, null, null, null, null, null); jdbc2.interpret("", ctx2); JDBCUserConfigurations user1JDBC2Conf = jdbc2.getJDBCConfiguration("user1"); @@ -439,7 +472,7 @@ public void testMultiTenant() throws SQLException, IOException { // user2 runs jdbc1 jdbc1.open(); InterpreterContext ctx3 = new InterpreterContext("", "1", "jdbc1", "", "", user2Credential, - null, null, null, null, null, null); + null, null, null, null, null, null, null); jdbc1.interpret("", ctx3); JDBCUserConfigurations user2JDBC1Conf = jdbc1.getJDBCConfiguration("user2"); @@ -450,7 +483,7 @@ public void testMultiTenant() throws SQLException, IOException { // user2 runs jdbc2 jdbc2.open(); InterpreterContext ctx4 = new InterpreterContext("", "1", "jdbc2", "", "", user2Credential, - null, null, null, null, null, null); + null, null, null, null, null, null, null); jdbc2.interpret("", ctx4); JDBCUserConfigurations user2JDBC2Conf = jdbc2.getJDBCConfiguration("user2"); @@ -523,7 +556,67 @@ public void testPrecodeWithAnotherPrefix() throws SQLException, IOException { } @Test - public void testExcludingComments() throws SQLException, IOException { + public void testStatementPrecode() throws SQLException, IOException { + Properties properties = new Properties(); + properties.setProperty("default.driver", "org.h2.Driver"); + properties.setProperty("default.url", getJdbcConnection()); + properties.setProperty("default.user", ""); + properties.setProperty("default.password", ""); + properties.setProperty(DEFAULT_STATEMENT_PRECODE, "set @v='statement'"); + JDBCInterpreter jdbcInterpreter = new JDBCInterpreter(properties); + jdbcInterpreter.open(); + + String sqlQuery = "select @v"; + + InterpreterResult interpreterResult = jdbcInterpreter.interpret(sqlQuery, interpreterContext); + + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code()); + assertEquals(InterpreterResult.Type.TABLE, interpreterResult.message().get(0).getType()); + assertEquals("@V\nstatement\n", interpreterResult.message().get(0).getData()); + } + + @Test + public void testIncorrectStatementPrecode() throws SQLException, IOException { + Properties properties = new Properties(); + properties.setProperty("default.driver", "org.h2.Driver"); + properties.setProperty("default.url", getJdbcConnection()); + properties.setProperty("default.user", ""); + properties.setProperty("default.password", ""); + properties.setProperty(DEFAULT_STATEMENT_PRECODE, "set incorrect"); + JDBCInterpreter jdbcInterpreter = new JDBCInterpreter(properties); + jdbcInterpreter.open(); + + String sqlQuery = "select 1"; + + InterpreterResult interpreterResult = jdbcInterpreter.interpret(sqlQuery, interpreterContext); + + assertEquals(InterpreterResult.Code.ERROR, interpreterResult.code()); + assertEquals(InterpreterResult.Type.TEXT, interpreterResult.message().get(0).getType()); + } + + @Test + public void testStatementPrecodeWithAnotherPrefix() throws SQLException, IOException { + Properties properties = new Properties(); + properties.setProperty("anotherPrefix.driver", "org.h2.Driver"); + properties.setProperty("anotherPrefix.url", getJdbcConnection()); + properties.setProperty("anotherPrefix.user", ""); + properties.setProperty("anotherPrefix.password", ""); + properties.setProperty(String.format(STATEMENT_PRECODE_KEY_TEMPLATE, "anotherPrefix"), + "set @v='statementAnotherPrefix'"); + JDBCInterpreter jdbcInterpreter = new JDBCInterpreter(properties); + jdbcInterpreter.open(); + + String sqlQuery = "(anotherPrefix) select @v"; + + InterpreterResult interpreterResult = jdbcInterpreter.interpret(sqlQuery, interpreterContext); + + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code()); + assertEquals(InterpreterResult.Type.TABLE, interpreterResult.message().get(0).getType()); + assertEquals("@V\nstatementAnotherPrefix\n", interpreterResult.message().get(0).getData()); + } + + @Test + public void testSplitSqlQueryWithComments() throws SQLException, IOException { Properties properties = new Properties(); properties.setProperty("common.max_count", "1000"); properties.setProperty("common.max_retry", "3"); diff --git a/kylin/pom.xml b/kylin/pom.xml index c3559cd04b8..1fce0871f67 100644 --- a/kylin/pom.xml +++ b/kylin/pom.xml @@ -21,18 +21,23 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 + ../interpreter-parent 4.0.0 org.apache.zeppelin zeppelin-kylin jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Kylin interpreter + + kylin + + @@ -61,57 +66,14 @@ maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/kylin - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/kylin - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin - 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/main/resources/interpreter-setting.json b/kylin/src/main/resources/interpreter-setting.json index f5f79f92769..88cf3696929 100644 --- a/kylin/src/main/resources/interpreter-setting.json +++ b/kylin/src/main/resources/interpreter-setting.json @@ -56,7 +56,8 @@ }, "editor": { "language": "sql", - "editOnDblClick": false + "editOnDblClick": false, + "completionSupport": true } } ] 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"); diff --git a/lens/pom.xml b/lens/pom.xml index 0328355793d..a31bff67be9 100644 --- a/lens/pom.xml +++ b/lens/pom.xml @@ -20,23 +20,24 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-lens jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Lens interpreter + lens 2.5.0-beta 1.1.0.RELEASE - 2.4.0 + 2.7.0-mapr-1808 1.9.1 1.9.13 1.9.11 @@ -135,6 +136,7 @@ org.apache.hadoop hadoop-common ${hadoop-common.version} + provided com.sun.jersey @@ -163,69 +165,13 @@ maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/lens - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/lens - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - - - - org.apache.maven.plugins - maven-clean-plugin - - - - ${basedir}/../interpreter/lens - false - - - - - + + maven-resources-plugin + diff --git a/lens/src/main/resources/interpreter-setting.json b/lens/src/main/resources/interpreter-setting.json index 5d345e6ad5d..03fda56e931 100644 --- a/lens/src/main/resources/interpreter-setting.json +++ b/lens/src/main/resources/interpreter-setting.json @@ -53,6 +53,10 @@ "description": "Hadoop cluster username", "type": "string" } + }, + "editor": { + "editOnDblClick": false, + "completionSupport": false } } ] diff --git a/licenses/LICENSE-jsdiff-3.3.0 b/licenses/LICENSE-jsdiff-3.3.0 new file mode 100644 index 00000000000..4e7146ed78a --- /dev/null +++ b/licenses/LICENSE-jsdiff-3.3.0 @@ -0,0 +1,31 @@ +Software License Agreement (BSD License) + +Copyright (c) 2009-2015, Kevin Decker + +All rights reserved. + +Redistribution and use of this software in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of Kevin Decker nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file 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/livy/pom.xml b/livy/pom.xml index 5f9dec72268..66f1adc2189 100644 --- a/livy/pom.xml +++ b/livy/pom.xml @@ -19,544 +19,394 @@ - 4.0.0 + 4.0.0 - - zeppelin - org.apache.zeppelin - 0.8.0-SNAPSHOT - .. - - - org.apache.zeppelin - zeppelin-livy - jar - 0.8.0-SNAPSHOT - Zeppelin: Livy interpreter - - - - 1.3 - 4.3.0.RELEASE - 1.0.1.RELEASE - - - 0.3.0 - 2.1.0 - 2.6.0 - - 2.16 - 1.8 - - - - - ${project.groupId} - zeppelin-interpreter - ${project.version} - provided - - - - org.apache.commons - commons-exec - ${commons.exec.version} - - - - org.slf4j - slf4j-api - - - - org.slf4j - slf4j-log4j12 - - - - org.apache.httpcomponents - httpclient - - - - com.google.code.gson - gson - - - - org.springframework.security.kerberos - spring-security-kerberos-client - ${spring.security.kerberosclient} - - - - org.springframework - spring-web - ${spring.web.version} - - - - junit - junit - test - - - - com.cloudera.livy - livy-integration-test - ${livy.version} - test - - - org.xerial.snappy - snappy-java - - - org.apache.spark - spark-core_${scala.binary.version} - - - org.apache.spark - spark-sql_${scala.binary.version} - - - org.apache.spark - spark-streaming_${scala.binary.version} - - - org.apache.spark - spark-hive_${scala.binary.version} - - - org.apache.spark - spark-repl_${scala.binary.version} - - - 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 - - - - - com.cloudera.livy - livy-test-lib - ${livy.version} - test - - - org.xerial.snappy - snappy-java - - - org.apache.spark - spark-core_${scala.binary.version} - - - org.apache.spark - spark-sql_${scala.binary.version} - - - org.apache.spark - spark-streaming_${scala.binary.version} - - - org.apache.spark - spark-hive_${scala.binary.version} - - - org.apache.spark - spark-repl_${scala.binary.version} - - - org.apache.spark - spark-yarn_${scala.binary.version} - - - + + interpreter-parent + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + ../interpreter-parent + - - org.apache.spark - spark-sql_${scala.binary.version} - ${spark.version} - test - - - com.esotericsoftware - kryo-shaded - - - - - - org.apache.spark - spark-streaming_${scala.binary.version} - ${spark.version} - test - + org.apache.zeppelin + zeppelin-livy + jar + 0.8.2-mapr-1912-r2 + Zeppelin: Livy interpreter + + + + livy + 1.3 + 4.3.0.RELEASE + 1.0.1.RELEASE + + + 0.5.0-incubating + 2.1.0 + 2.7.0-mapr-1808 + + 2.16 + 1.8 + - - org.apache.spark - spark-hive_${scala.binary.version} - ${spark.version} - test - + + + ${project.groupId} + zeppelin-interpreter + ${project.version} + provided + + + org.scala-lang + scala-compiler + + + org.scala-lang + scala-library + + + org.scala-lang + scala-reflect + + + - - org.apache.spark - spark-repl_${scala.binary.version} - ${spark.version} - test - + + org.apache.livy + livy-integration-test + ${livy.version} + test + + + org.xerial.snappy + snappy-java + + + org.apache.spark + spark-core_${scala.binary.version} + + + org.apache.spark + spark-sql_${scala.binary.version} + + + org.apache.spark + spark-streaming_${scala.binary.version} + + + org.apache.spark + spark-hive_${scala.binary.version} + + + org.apache.spark + spark-repl_${scala.binary.version} + + + 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 + + + + + org.apache.livy + livy-test-lib + ${livy.version} + test + + + org.xerial.snappy + snappy-java + + + org.apache.spark + spark-core_${scala.binary.version} + + + org.apache.spark + spark-sql_${scala.binary.version} + + + org.apache.spark + spark-streaming_${scala.binary.version} + + + org.apache.spark + spark-hive_${scala.binary.version} + + + org.apache.spark + spark-repl_${scala.binary.version} + + + org.apache.spark + spark-yarn_${scala.binary.version} + + + - - org.apache.spark - spark-yarn_${scala.binary.version} - ${spark.version} - test - - - org.apache.hadoop - hadoop-yarn-common - - - org.apache.hadoop - hadoop-yarn-server-web-proxy - - - + + org.apache.commons + commons-exec + ${commons.exec.version} + - - org.apache.hadoop - hadoop-auth - ${hadoop.version} - test - + + org.slf4j + slf4j-api + - - org.apache.hadoop - hadoop-common - ${hadoop.version} - test - + + org.slf4j + slf4j-log4j12 + - - org.apache.hadoop - hadoop-common - tests - ${hadoop.version} - test - + + org.apache.httpcomponents + httpclient + - - org.apache.hadoop - hadoop-hdfs - ${hadoop.version} - test - - - io.netty - netty - - - + + com.google.code.gson + gson + - - org.apache.hadoop - hadoop-hdfs - tests - ${hadoop.version} - test - - - io.netty - netty - - - + + org.springframework.security.kerberos + spring-security-kerberos-client + ${spring.security.kerberosclient} + - - org.apache.hadoop - hadoop-client - ${hadoop.version} - test - + + org.springframework + spring-web + ${spring.web.version} + - - org.apache.hadoop - hadoop-yarn-client - ${hadoop.version} - test - + + junit + junit + test + - - org.apache.hadoop - hadoop-yarn-api - ${hadoop.version} - test - + + org.apache.hadoop + hadoop-auth + ${hadoop.version} + test + - - org.apache.hadoop - hadoop-yarn-server-tests - tests - ${hadoop.version} - test - - + + org.apache.hadoop + hadoop-common + ${hadoop.version} + test + + + com.google.guava + guava + + + - - - ossrh - ossrh repository - https://oss.sonatype.org/content/repositories/releases/ - - true - - - false - - - + + org.apache.hadoop + hadoop-common + tests + ${hadoop.version} + test + + + com.google.guava + guava + + + - - - - maven-enforcer-plugin - - - enforce - none - - - + + org.apache.hadoop + hadoop-hdfs + ${hadoop.version} + test + + + io.netty + netty + + + com.google.guava + guava + + + - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/livy - - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/livy - - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - - + + org.apache.hadoop + hadoop-hdfs + tests + ${hadoop.version} + test + + + io.netty + netty + + + com.google.guava + guava + + + - - maven-failsafe-plugin - ${plugin.failsafe.version} - - - - integration-test - verify - - - - - - ${project.build.directory}/tmp - - - ${scala.binary.version} - ${project.build.directory}/tmp - - -Xmx2048m - - + + org.apache.hadoop + hadoop-client + ${hadoop.version} + test + + + com.google.guava + guava + + + - - org.apache.maven.plugins - maven-antrun-plugin - ${plugin.antrun.version} - - - - pre-test-clean - generate-test-resources - - run - - - - - - - - - - - - create-tmp-dir - generate-test-resources - - run - - - - - - - - - - - + + org.apache.hadoop + hadoop-yarn-client + ${hadoop.version} + test + + + com.google.guava + guava + + + - - - livy-0.3 - - true - - - 0.3.0 - 2.1.0 - 2.6.0 - - - com.cloudera.livy - livy-core_${scala.binary.version} - 0.3.0 - test - - - org.xerial.snappy - snappy-java - - - org.apache.spark - spark-core_${scala.binary.version} - - - org.apache.spark - spark-sql_${scala.binary.version} - - - org.apache.spark - spark-streaming_${scala.binary.version} - - - org.apache.spark - spark-hive_${scala.binary.version} - - - org.apache.spark - spark-repl_${scala.binary.version} - - - org.apache.spark - spark-yarn_${scala.binary.version} - - + org.apache.hadoop + hadoop-yarn-api + ${hadoop.version} + test + + + com.google.guava + guava + + - - - - livy-0.2 - - 0.2.0 - 1.6.2 - 2.6.0 - 2.10 - - - com.cloudera.livy - livy-core - 0.2.0 - test - - - org.xerial.snappy - snappy-java - - - org.apache.spark - spark-core_${scala.binary.version} - - - org.apache.spark - spark-sql_${scala.binary.version} - - - org.apache.spark - spark-streaming_${scala.binary.version} - - - org.apache.spark - spark-hive_${scala.binary.version} - - - org.apache.spark - spark-repl_${scala.binary.version} - - - org.apache.spark - spark-yarn_${scala.binary.version} - - + org.apache.hadoop + hadoop-yarn-server-tests + tests + ${hadoop.version} + test + + + com.google.guava + guava + + - - - + + + + + + maven-enforcer-plugin + + + maven-dependency-plugin + + + maven-resources-plugin + + + + maven-failsafe-plugin + ${plugin.failsafe.version} + + + + integration-test + verify + + + + + + ${project.build.directory}/tmp + + + ${scala.binary.version} + ${project.build.directory}/tmp + + -Xmx2048m + + + + + org.apache.maven.plugins + maven-antrun-plugin + ${plugin.antrun.version} + + + + pre-test-clean + generate-test-resources + + run + + + + + + + + + + + + create-tmp-dir + generate-test-resources + + run + + + + + + + + + + + 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 a5c87f8265d..11d88b1790e 100644 --- a/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java +++ b/livy/src/main/java/org/apache/zeppelin/livy/BaseLivyInterpreter.java @@ -17,10 +17,26 @@ package org.apache.zeppelin.livy; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.annotations.SerializedName; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.security.KeyStore; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.net.ssl.SSLContext; + import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.http.auth.AuthSchemeProvider; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; @@ -36,31 +52,34 @@ import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; -import org.apache.commons.lang.exception.ExceptionUtils; -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.InterpreterException; +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; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.security.kerberos.client.KerberosRestTemplate; import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; -import javax.net.ssl.SSLContext; -import java.io.FileInputStream; -import java.io.IOException; -import java.security.KeyStore; -import java.security.Principal; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.SerializedName; /** @@ -70,15 +89,21 @@ public abstract class BaseLivyInterpreter extends Interpreter { protected static final Logger LOGGER = LoggerFactory.getLogger(BaseLivyInterpreter.class); private static Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); - private static String SESSION_NOT_FOUND_PATTERN = "\"Session '\\d+' not found.\""; + private static final String SESSION_NOT_FOUND_PATTERN = "(.*)\"Session '\\d+' not found.\"(.*)"; protected volatile SessionInfo sessionInfo; private String livyURL; private int sessionCreationTimeout; private int pullStatusInterval; + private int maxLogLines; protected boolean displayAppInfo; + private boolean restartDeadSession; protected LivyVersion livyVersion; private RestTemplate restTemplate; + private Map customHeaders = new HashMap<>(); + + // delegate to sharedInterpreter when it is available + protected LivySharedInterpreter sharedInterpreter; Set paragraphsToCancel = Collections.newSetFromMap( new ConcurrentHashMap()); @@ -90,28 +115,89 @@ 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( 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(";"); + for (String header : headers) { + String[] splits = header.split(":", -1); + if (splits.length != 2) { + throw new RuntimeException("Invalid format of http headers: " + header + + ", valid http header format is HEADER_NAME:HEADER_VALUE"); + } + customHeaders.put(splits[0].trim(), envSubstitute(splits[1].trim())); + } + } + } + + private String envSubstitute(String value) { + String newValue = new String(value); + Pattern pattern = Pattern.compile("\\$\\{(.*)\\}"); + Matcher matcher = pattern.matcher(value); + while (matcher.find()) { + String env = matcher.group(1); + newValue = newValue.replace("${" + env + "}", System.getenv(env)); + } + return newValue; + } + + // only for testing + Map getCustomHeaders() { + return customHeaders; } public abstract String getSessionKind(); @Override - public void open() { + 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"; - throw new RuntimeException(msg, e); + throw new InterpreterException(msg, e); } } + 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. @@ -139,14 +225,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; @@ -154,17 +232,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, true); } catch (LivyException e) { LOGGER.error("Fail to interpret:" + st, e); return new InterpreterResult(InterpreterResult.Code.ERROR, @@ -172,19 +263,56 @@ public InterpreterResult interpret(String st, InterpreterContext context) { } } + @Override + public List completion(String buf, int cursor, + InterpreterContext interpreterContext) { + List candidates = Collections.emptyList(); + try { + candidates = callCompletion(new CompletionRequest(buf, getSessionKind(), cursor)); + } catch (SessionNotFoundException e) { + LOGGER.warn("Livy session {} is expired. Will return empty list of candidates.", + getSessionInfo().id); + } catch (LivyException le) { + logger.error("Failed to call code completions. Will return empty list of candidates", le); + } + return candidates; + } + + private List callCompletion(CompletionRequest req) throws LivyException { + List candidates = new ArrayList<>(); + try { + CompletionResponse resp = CompletionResponse.fromJson( + callRestAPI("/sessions/" + getSessionInfo().id + "/completion", "POST", req.toJson())); + for (String candidate : resp.candidates) { + candidates.add(new InterpreterCompletion(candidate, candidate, StringUtils.EMPTY)); + } + } catch (APINotFoundException e) { + logger.debug("completion api seems not to be available. (available from livy 0.5)", e); + } + return candidates; + } + @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."); } @Override public FormType getFormType() { - return FormType.SIMPLE; + return FormType.NATIVE; } @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); @@ -197,7 +325,7 @@ private SessionInfo createSession(String user, String kind) throws LivyException { try { Map conf = new HashMap<>(); - for (Map.Entry entry : property.entrySet()) { + for (Map.Entry entry : getProperties().entrySet()) { if (entry.getKey().toString().startsWith("livy.spark.") && !entry.getValue().toString().isEmpty()) conf.put(entry.getKey().toString().substring(5), entry.getValue().toString()); @@ -213,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); @@ -222,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); } } @@ -237,15 +365,32 @@ 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, + boolean appendSessionExpired, + boolean appendSessionDead) throws LivyException { + return interpret(code, sharedInterpreter.isSupported() ? getSessionKind() : null, + 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)); + stmtInfo = executeStatement(new ExecuteRequest(code, codeType)); } catch (SessionNotFoundException e) { LOGGER.warn("Livy session {} is expired, new session will be created.", sessionInfo.id); sessionExpired = true; @@ -257,8 +402,24 @@ public InterpreterResult interpret(String code, initLivySession(); } } - stmtInfo = executeStatement(new ExecuteRequest(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 while (!stmtInfo.isAvailable()) { if (paragraphId != null && paragraphsToCancel.contains(paragraphId)) { @@ -276,9 +437,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); } @@ -322,21 +483,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; + "Paragraphs that depend on this paragraph need to be re-executed!"); + + } + 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) { @@ -427,15 +594,15 @@ private void cancelStatement(int statementId) throws LivyException { private RestTemplate createRestTemplate() { - String keytabLocation = property.getProperty("zeppelin.livy.keytab"); - String principal = property.getProperty("zeppelin.livy.principal"); + String keytabLocation = getProperty("zeppelin.livy.keytab"); + String principal = getProperty("zeppelin.livy.principal"); boolean isSpnegoEnabled = StringUtils.isNotEmpty(keytabLocation) && StringUtils.isNotEmpty(principal); HttpClient httpClient = null; if (livyURL.startsWith("https:")) { - String keystoreFile = property.getProperty("zeppelin.livy.ssl.trustStore"); - String password = property.getProperty("zeppelin.livy.ssl.trustStorePassword"); + String keystoreFile = getProperty("zeppelin.livy.ssl.trustStore"); + String password = getProperty("zeppelin.livy.ssl.trustStorePassword"); if (StringUtils.isBlank(keystoreFile)) { throw new RuntimeException("No zeppelin.livy.ssl.trustStore specified for livy ssl"); } @@ -496,19 +663,23 @@ public Principal getUserPrincipal() { } } - + RestTemplate restTemplate = null; if (isSpnegoEnabled) { if (httpClient == null) { - return new KerberosRestTemplate(keytabLocation, principal); + restTemplate = new KerberosRestTemplate(keytabLocation, principal); } else { - return new KerberosRestTemplate(keytabLocation, principal, httpClient); + restTemplate = new KerberosRestTemplate(keytabLocation, principal, httpClient); } - } - if (httpClient == null) { - return new RestTemplate(); } else { - return new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); + if (httpClient == null) { + restTemplate = new RestTemplate(); + } else { + restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); + } } + restTemplate.getMessageConverters().add(0, + new StringHttpMessageConverter(Charset.forName("UTF-8"))); + return restTemplate; } private String callRestAPI(String targetURL, String method) throws LivyException { @@ -520,8 +691,11 @@ private String callRestAPI(String targetURL, String method, String jsonData) targetURL = livyURL + targetURL; LOGGER.debug("Call rest api in {}, method: {}, jsonData: {}", targetURL, method, jsonData); HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Type", "application/json"); + headers.add("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE); headers.add("X-Requested-By", "zeppelin"); + for (Map.Entry entry : customHeaders.entrySet()) { + headers.add(entry.getKey(), entry.getValue()); + } ResponseEntity response = null; try { if (method.equals("POST")) { @@ -548,6 +722,14 @@ private String callRestAPI(String targetURL, String method, String jsonData) throw new LivyException(cause.getResponseBodyAsString() + "\n" + ExceptionUtils.getFullStackTrace(ExceptionUtils.getRootCause(e))); } + if (e instanceof HttpServerErrorException) { + HttpServerErrorException errorException = (HttpServerErrorException) e; + String errorResponse = errorException.getResponseBodyAsString(); + if (errorResponse.contains("Session is in state dead")) { + throw new SessionDeadException(); + } + throw new LivyException(errorResponse, e); + } throw new LivyException(e); } if (response == null) { @@ -647,11 +829,26 @@ public static SessionInfo fromJson(String json) { } } - private static class ExecuteRequest { - public final String code; + private static class SessionLog { + public int id; + public int from; + public int size; + public List log; + + SessionLog() { + } - public ExecuteRequest(String code) { + public static SessionLog fromJson(String json) { + return gson.fromJson(json, SessionLog.class); + } + } + + static class ExecuteRequest { + public final String code; + public final String kind; + public ExecuteRequest(String code, String kind) { this.code = code; + this.kind = kind; } public String toJson() { @@ -729,6 +926,34 @@ private static class TableMagic { } } + static class CompletionRequest { + public final String code; + public final String kind; + public final int cursor; + + public CompletionRequest(String code, String kind, int cursor) { + this.code = code; + this.kind = kind; + this.cursor = cursor; + } + + public String toJson() { + return gson.toJson(this); + } + } + + static class CompletionResponse { + public final String[] candidates; + + public CompletionResponse(String[] candidates) { + this.candidates = candidates; + } + + public static CompletionResponse fromJson(String json) { + return gson.fromJson(json, CompletionResponse.class); + } + } + private static class LivyVersionResponse { public String url; public String branch; 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 new file mode 100644 index 00000000000..cef08582a40 --- /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, 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..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()); } @@ -44,10 +44,11 @@ protected String extractAppId() throws LivyException { protected String extractWebUIAddress() throws LivyException { interpret( "val webui=sc.getClass.getMethod(\"ui\").invoke(sc).asInstanceOf[Some[_]].get", - null, false, false); + null, + 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/LivySparkSQLInterpreter.java b/livy/src/main/java/org/apache/zeppelin/livy/LivySparkSQLInterpreter.java index 20d044811bc..2faa350bfb1 100644 --- a/livy/src/main/java/org/apache/zeppelin/livy/LivySparkSQLInterpreter.java +++ b/livy/src/main/java/org/apache/zeppelin/livy/LivySparkSQLInterpreter.java @@ -18,15 +18,18 @@ package org.apache.zeppelin.livy; 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; - /** * Livy SparkSQL Interpreter for Zeppelin. */ @@ -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; @@ -59,12 +63,26 @@ public String getSessionKind() { } @Override - public void open() { + public void open() throws InterpreterException { this.sparkInterpreter = getSparkInterpreter(); // 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() { 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() { 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()); @@ -93,7 +111,7 @@ public void open() { } } - private LivySparkInterpreter getSparkInterpreter() { + private LivySparkInterpreter getSparkInterpreter() throws InterpreterException { LazyOpenInterpreter lazy = null; LivySparkInterpreter spark = null; Interpreter p = getInterpreterInTheSameSessionByClassName(LivySparkInterpreter.class.getName()); @@ -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()) { @@ -159,17 +175,25 @@ public InterpreterResult interpret(String line, InterpreterContext context) { } } + @Override + public FormType getFormType() { + return FormType.SIMPLE; + } + protected List parseSQLOutput(String output) { List rows = new ArrayList<>(); - String[] lines = output.split("\n"); + // Get first line by breaking on \n. We can guarantee + // that \n marks the end of the first line, but not for + // subsequent lines (as it could be in the cells) + String firstLine = output.split("\n", 2)[0]; // at least 4 lines, even for empty sql output // +---+---+ // | a| b| // +---+---+ // +---+---+ - // use the first line to determinte the position of feach cell - String[] tokens = StringUtils.split(lines[0], "\\+"); + // use the first line to determine the position of each cell + String[] tokens = StringUtils.split(firstLine, "\\+"); // pairs keeps the start/end position of each cell. We parse it from the first row // which use '+' as separator List pairs = new ArrayList<>(); @@ -181,17 +205,26 @@ protected List parseSQLOutput(String output) { pairs.add(new Pair(start, end)); } - for (String line : lines) { + // Use the header line to determine the position + // of subsequent lines + int lineStart = 0; + int lineEnd = firstLine.length(); + while (lineEnd < output.length()) { // Only match format "|....|" // skip line like "+---+---+" and "only showing top 1 row" - if (line.matches("^\\|.*\\|$")) { + String line = output.substring(lineStart, lineEnd); + // Use the DOTALL regex mode to match newlines + if (line.matches("(?s)^\\|.*\\|$")) { List cells = new ArrayList<>(); for (Pair pair : pairs) { - // strip the blank space around the cell - cells.add(line.substring(pair.start, pair.end).trim()); + // strip the blank space around the cell and escape the string + cells.add(escapeJavaScript(line.substring(pair.start, pair.end)).trim()); } rows.add(StringUtils.join(cells, "\t")); } + // Determine position of next line skipping newline + lineStart += firstLine.length() + 1; + lineEnd = lineStart + firstLine.length(); } return rows; } @@ -231,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/java/org/apache/zeppelin/livy/SessionDeadException.java b/livy/src/main/java/org/apache/zeppelin/livy/SessionDeadException.java new file mode 100644 index 00000000000..bea805d8815 --- /dev/null +++ b/livy/src/main/java/org/apache/zeppelin/livy/SessionDeadException.java @@ -0,0 +1,25 @@ +/* + * 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; + +/** + * Exception when livy session is dead + */ +public class SessionDeadException extends LivyException { + +} diff --git a/livy/src/main/resources/interpreter-setting.json b/livy/src/main/resources/interpreter-setting.json index ac213ba27bd..2a1720d5ba8 100644 --- a/livy/src/main/resources/interpreter-setting.json +++ b/livy/src/main/resources/interpreter-setting.json @@ -15,10 +15,24 @@ "zeppelin.livy.session.create_timeout": { "envName": "ZEPPELIN_LIVY_SESSION_CREATE_TIMEOUT", "propertyName": "zeppelin.livy.session.create_timeout", - "defaultValue": "120", + "defaultValue": "180", "description": "Livy Server create session timeout (seconds).", "type": "number" }, + "livy.spark.yarn.dist.archives": { + "envName": "ZEPPELIN_SPARK_YARN_DIST_ARCHIVES", + "propertyName": "livy.spark.yarn.dist.archives", + "defaultValue": "", + "description": "Sets additional archives for spark", + "type": "string" + }, + "livy.spark.yarn.appMasterEnv.PYSPARK_PYTHON": { + "envName": "PYSPARK_PYTHON", + "propertyName": "livy.spark.yarn.appMasterEnv.PYSPARK_PYTHON", + "defaultValue": "python", + "description": "Sets default python interpreter for PySpark", + "type": "string" + }, "livy.spark.driver.cores": { "propertyName": "livy.spark.driver.cores", "defaultValue": "", @@ -97,6 +111,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": "", @@ -105,9 +125,15 @@ }, "zeppelin.livy.displayAppInfo": { "propertyName": "zeppelin.livy.displayAppInfo", - "defaultValue": "true", + "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": { @@ -121,7 +147,9 @@ }, "editor": { "language": "scala", - "editOnDblClick": false + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": true } }, { @@ -160,7 +188,9 @@ }, "editor": { "language": "sql", - "editOnDblClick": false + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": true } }, { @@ -180,13 +210,15 @@ }, "editor": { "language": "python", - "editOnDblClick": false + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": true } }, { "group": "livy", - "name": "pyspark3", - "className": "org.apache.zeppelin.livy.LivyPySpark3Interpreter", + "name": "sparkr", + "className": "org.apache.zeppelin.livy.LivySparkRInterpreter", "properties": { }, "option": { @@ -199,14 +231,16 @@ "users": [] }, "editor": { - "language": "python", - "editOnDblClick": false + "language": "r", + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": true } }, { "group": "livy", - "name": "sparkr", - "className": "org.apache.zeppelin.livy.LivySparkRInterpreter", + "name": "shared", + "className": "org.apache.zeppelin.livy.LivySharedInterpreter", "properties": { }, "option": { @@ -217,10 +251,6 @@ "isExistingProcess": false, "setPermission": false, "users": [] - }, - "editor": { - "language": "r", - "editOnDblClick": false } } ] 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 28efe687ceb..0c70ef72307 100644 --- a/livy/src/test/java/org/apache/zeppelin/livy/LivyInterpreterIT.java +++ b/livy/src/test/java/org/apache/zeppelin/livy/LivyInterpreterIT.java @@ -18,21 +18,35 @@ package org.apache.zeppelin.livy; -import com.cloudera.livy.test.framework.Cluster; -import com.cloudera.livy.test.framework.Cluster$; import org.apache.commons.io.IOUtils; -import org.apache.zeppelin.interpreter.*; +import org.apache.livy.test.framework.Cluster; +import org.apache.livy.test.framework.Cluster$; +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.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.LazyOpenInterpreter; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.user.AuthenticationInfo; -import org.junit.*; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; +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; public class LivyInterpreterIT { @@ -74,128 +88,9 @@ public static boolean checkPreCondition() { return true; } - @Test - public void testSparkInterpreterRDD() { - if (!checkPreCondition()) { - return; - } - InterpreterGroup interpreterGroup = new InterpreterGroup("group_1"); - interpreterGroup.put("session_1", new ArrayList()); - final LivySparkInterpreter sparkInterpreter = new LivySparkInterpreter(properties); - sparkInterpreter.setInterpreterGroup(interpreterGroup); - interpreterGroup.get("session_1").add(sparkInterpreter); - AuthenticationInfo authInfo = new AuthenticationInfo("user1"); - MyInterpreterOutputListener outputListener = new MyInterpreterOutputListener(); - InterpreterOutput output = new InterpreterOutput(outputListener); - final InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "livy.spark", - "title", "text", authInfo, null, null, null, null, null, output); - sparkInterpreter.open(); - - try { - // detect spark version - InterpreterResult result = sparkInterpreter.interpret("sc.version", context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(1, result.message().size()); - - boolean isSpark2 = isSpark2(sparkInterpreter, context); - - // test RDD api - result = sparkInterpreter.interpret("sc.parallelize(1 to 10).sum()", context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(1, result.message().size()); - assertTrue(result.message().get(0).getData().contains("Double = 55.0")); - - // single line comment - String singleLineComment = "println(1)// my comment"; - result = sparkInterpreter.interpret(singleLineComment, context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(1, result.message().size()); - - // multiple line comment - String multipleLineComment = "println(1)/* multiple \n" + "line \n" + "comment */"; - result = sparkInterpreter.interpret(multipleLineComment, context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(1, result.message().size()); - - // multi-line string - String multiLineString = "val str = \"\"\"multiple\n" + - "line\"\"\"\n" + - "println(str)"; - result = sparkInterpreter.interpret(multiLineString, context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(1, result.message().size()); - assertTrue(result.message().get(0).getData().contains("multiple\nline")); - - // case class - String caseClassCode = "case class Person(id:Int, \n" + - "name:String)\n" + - "val p=Person(1, \"name_a\")"; - result = sparkInterpreter.interpret(caseClassCode, context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(1, result.message().size()); - assertTrue(result.message().get(0).getData().contains("p: Person = Person(1,name_a)")); - - // object class - String objectClassCode = "object Person {}"; - result = sparkInterpreter.interpret(objectClassCode, context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(1, result.message().size()); - if (!isSpark2) { - assertTrue(result.message().get(0).getData().contains("defined module Person")); - } else { - assertTrue(result.message().get(0).getData().contains("defined object Person")); - } - - // html output - String htmlCode = "println(\"%html

    hello

    \")"; - result = sparkInterpreter.interpret(htmlCode, context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(1, result.message().size()); - assertEquals(InterpreterResult.Type.HTML, result.message().get(0).getType()); - - // error - result = sparkInterpreter.interpret("println(a)", context); - assertEquals(InterpreterResult.Code.ERROR, result.code()); - assertEquals(InterpreterResult.Type.TEXT, result.message().get(0).getType()); - assertTrue(result.message().get(0).getData().contains("error: not found: value a")); - - // incomplete code - result = sparkInterpreter.interpret("if(true){", context); - assertEquals(InterpreterResult.Code.ERROR, result.code()); - assertEquals(InterpreterResult.Type.TEXT, result.message().get(0).getType()); - assertTrue(result.message().get(0).getData().contains("incomplete statement")); - - // cancel - if (sparkInterpreter.livyVersion.newerThanEquals(LivyVersion.LIVY_0_3_0)) { - Thread cancelThread = new Thread() { - @Override - public void run() { - // invoke cancel after 1 millisecond to wait job starting - try { - Thread.sleep(1); - } catch (InterruptedException e) { - e.printStackTrace(); - } - sparkInterpreter.cancel(context); - } - }; - cancelThread.start(); - result = sparkInterpreter - .interpret("sc.parallelize(1 to 10).foreach(e=>Thread.sleep(10*1000))", context); - assertEquals(InterpreterResult.Code.ERROR, result.code()); - String message = result.message().get(0).getData(); - // 2 possibilities, sometimes livy doesn't return the real cancel exception - assertTrue(message.contains("cancelled part of cancelled job group") || - message.contains("Job is cancelled")); - } - - } finally { - sparkInterpreter.close(); - } - } @Test - public void testSparkInterpreterDataFrame() { + public void testSparkInterpreter() throws InterpreterException { if (!checkPreCondition()) { return; } @@ -208,7 +103,7 @@ public void testSparkInterpreterDataFrame() { MyInterpreterOutputListener outputListener = new MyInterpreterOutputListener(); InterpreterOutput output = new InterpreterOutput(outputListener); InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "livy.spark", - "title", "text", authInfo, null, null, null, null, null, output); + "title", "text", authInfo, null, null, null, null, null, null, output); sparkInterpreter.open(); LivySparkSQLInterpreter sqlInterpreter = new LivySparkSQLInterpreter(properties); @@ -223,318 +118,240 @@ public void testSparkInterpreterDataFrame() { assertEquals(1, result.message().size()); boolean isSpark2 = isSpark2(sparkInterpreter, context); + testRDD(sparkInterpreter, isSpark2); + testDataFrame(sparkInterpreter, sqlInterpreter, isSpark2); - // test DataFrame api - 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])")); - } 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); - // test LivySparkSQLInterpreter which share the same SparkContext with LivySparkInterpreter - result = sqlInterpreter.interpret("select * from df where col_1='hello'", context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(InterpreterResult.Type.TABLE, result.message().get(0).getType()); - assertEquals("col_1\tcol_2\nhello\t20", result.message().get(0).getData()); - // double quotes - result = sqlInterpreter.interpret("select * from df where col_1=\"hello\"", context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(InterpreterResult.Type.TABLE, result.message().get(0).getType()); - assertEquals("col_1\tcol_2\nhello\t20", result.message().get(0).getData()); - - // only enable this test in spark2 as spark1 doesn't work for this case - if (isSpark2) { - result = sqlInterpreter.interpret("select * from df where col_1=\"he\\\"llo\" ", context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(InterpreterResult.Type.TABLE, result.message().get(0).getType()); - } - - // single quotes inside attribute value - result = sqlInterpreter.interpret("select * from df where col_1=\"he'llo\"", context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(InterpreterResult.Type.TABLE, result.message().get(0).getType()); - - // test sql with syntax error - result = sqlInterpreter.interpret("select * from df2", context); - assertEquals(InterpreterResult.Code.ERROR, result.code()); - assertEquals(InterpreterResult.Type.TEXT, result.message().get(0).getType()); - - if (!isSpark2) { - assertTrue(result.message().get(0).getData().contains("Table not found")); - } else { - assertTrue(result.message().get(0).getData().contains("Table or view not found")); - } } finally { sparkInterpreter.close(); sqlInterpreter.close(); } } - @Test - public void testSparkSQLInterpreter() { - 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); - sqlInterpreter.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, output); - InterpreterResult result = sqlInterpreter.interpret("show tables", context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(InterpreterResult.Type.TABLE, result.message().get(0).getType()); - assertTrue(result.message().get(0).getData().contains("tableName")); - int r = sqlInterpreter.getProgress(context); - assertTrue(r == 0); - } finally { - sqlInterpreter.close(); - } - } - - - @Test - public void testSparkSQLCancellation() { - if (!checkPreCondition()) { - return; - } - InterpreterGroup interpreterGroup = new InterpreterGroup("group_1"); - interpreterGroup.put("session_1", new ArrayList()); - LivySparkInterpreter sparkInterpreter = new LivySparkInterpreter(properties); - sparkInterpreter.setInterpreterGroup(interpreterGroup); - interpreterGroup.get("session_1").add(sparkInterpreter); + private void testRDD(final LivySparkInterpreter sparkInterpreter, boolean isSpark2) { AuthenticationInfo authInfo = new AuthenticationInfo("user1"); MyInterpreterOutputListener outputListener = new MyInterpreterOutputListener(); InterpreterOutput output = new InterpreterOutput(outputListener); final InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "livy.spark", - "title", "text", authInfo, null, null, null, null, null, output); - sparkInterpreter.open(); - - final LivySparkSQLInterpreter sqlInterpreter = new LivySparkSQLInterpreter(properties); - interpreterGroup.get("session_1").add(sqlInterpreter); - sqlInterpreter.setInterpreterGroup(interpreterGroup); - sqlInterpreter.open(); - - try { - // detect spark version - InterpreterResult result = sparkInterpreter.interpret("sc.version", context); - assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(1, result.message().size()); - - boolean isSpark2 = isSpark2(sparkInterpreter, context); - - // test DataFrame api - 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])")); - } 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); + "title", "text", authInfo, null, null, null, null, null, null, output); + + InterpreterResult result = sparkInterpreter.interpret("sc.parallelize(1 to 10).sum()", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + assertTrue(result.message().get(0).getData().contains("Double = 55.0")); + + // single line comment + String singleLineComment = "println(1)// my comment"; + result = sparkInterpreter.interpret(singleLineComment, context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + + // multiple line comment + String multipleLineComment = "println(1)/* multiple \n" + "line \n" + "comment */"; + result = sparkInterpreter.interpret(multipleLineComment, context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + + // multi-line string + String multiLineString = "val str = \"\"\"multiple\n" + + "line\"\"\"\n" + + "println(str)"; + result = sparkInterpreter.interpret(multiLineString, context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + assertTrue(result.message().get(0).getData().contains("multiple\nline")); + + // case class + String caseClassCode = "case class Person(id:Int, \n" + + "name:String)\n" + + "val p=Person(1, \"name_a\")"; + result = sparkInterpreter.interpret(caseClassCode, context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + assertTrue(result.message().get(0).getData().contains("p: Person = Person(1,name_a)")); + + // object class + String objectClassCode = "object Person {}"; + result = sparkInterpreter.interpret(objectClassCode, context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + if (!isSpark2) { + assertTrue(result.message().get(0).getData().contains("defined module Person")); + } else { + assertTrue(result.message().get(0).getData().contains("defined object Person")); + } - // cancel - if (sqlInterpreter.getLivyVersion().newerThanEquals(LivyVersion.LIVY_0_3_0)) { - Thread cancelThread = new Thread() { - @Override - public void run() { - sqlInterpreter.cancel(context); + // html output + String htmlCode = "println(\"%html

    hello

    \")"; + result = sparkInterpreter.interpret(htmlCode, context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + assertEquals(InterpreterResult.Type.HTML, result.message().get(0).getType()); + + // error + result = sparkInterpreter.interpret("println(a)", context); + assertEquals(InterpreterResult.Code.ERROR, result.code()); + assertEquals(InterpreterResult.Type.TEXT, result.message().get(0).getType()); + assertTrue(result.message().get(0).getData().contains("error: not found: value a")); + + // incomplete code + result = sparkInterpreter.interpret("if(true){", context); + assertEquals(InterpreterResult.Code.ERROR, result.code()); + assertEquals(InterpreterResult.Type.TEXT, result.message().get(0).getType()); + assertTrue(result.message().get(0).getData().contains("incomplete statement")); + + // cancel + if (sparkInterpreter.livyVersion.newerThanEquals(LivyVersion.LIVY_0_3_0)) { + Thread cancelThread = new Thread() { + @Override + public void run() { + // invoke cancel after 1 millisecond to wait job starting + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); } - }; - cancelThread.start(); - //sleep so that cancelThread performs a cancel. - try { - Thread.sleep(1); - } catch (InterruptedException e) { - e.printStackTrace(); + sparkInterpreter.cancel(context); } - result = sqlInterpreter - .interpret("select count(1) from df", context); - if (result.code().equals(InterpreterResult.Code.ERROR)) { - String message = result.message().get(0).getData(); - // 2 possibilities, sometimes livy doesn't return the real cancel exception - assertTrue(message.contains("cancelled part of cancelled job group") || - message.contains("Job is cancelled")); - } - } - } catch (LivyException e) { - } finally { - sparkInterpreter.close(); - sqlInterpreter.close(); + }; + cancelThread.start(); + result = sparkInterpreter + .interpret("sc.parallelize(1 to 10).foreach(e=>Thread.sleep(10*1000))", context); + assertEquals(InterpreterResult.Code.ERROR, result.code()); + String message = result.message().get(0).getData(); + // 2 possibilities, sometimes livy doesn't return the real cancel exception + assertTrue(message.contains("cancelled part of cancelled job group") || + message.contains("Job is cancelled")); } } - @Test - public void testStringWithTruncation() { - if (!checkPreCondition()) { - return; - } - InterpreterGroup interpreterGroup = new InterpreterGroup("group_1"); - interpreterGroup.put("session_1", new ArrayList()); - LivySparkInterpreter sparkInterpreter = new LivySparkInterpreter(properties); - sparkInterpreter.setInterpreterGroup(interpreterGroup); - interpreterGroup.get("session_1").add(sparkInterpreter); + private void testDataFrame(LivySparkInterpreter sparkInterpreter, + final LivySparkSQLInterpreter sqlInterpreter, + boolean isSpark2) throws LivyException { AuthenticationInfo authInfo = new AuthenticationInfo("user1"); MyInterpreterOutputListener outputListener = new MyInterpreterOutputListener(); InterpreterOutput output = new InterpreterOutput(outputListener); - InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "livy.spark", - "title", "text", authInfo, null, null, null, null, null, output); - sparkInterpreter.open(); - - LivySparkSQLInterpreter sqlInterpreter = new LivySparkSQLInterpreter(properties); - interpreterGroup.get("session_1").add(sqlInterpreter); - sqlInterpreter.setInterpreterGroup(interpreterGroup); - sqlInterpreter.open(); + final InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "livy.spark", + "title", "text", authInfo, null, null, null, null, null, null, output); - try { - // detect spark version - InterpreterResult result = sparkInterpreter.interpret("sc.version", context); + InterpreterResult result = null; + // test DataFrame api + 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()); - - boolean isSpark2 = isSpark2(sparkInterpreter, context); - - // test DataFrame api - if (!isSpark2) { - result = sparkInterpreter.interpret( - "val df=sqlContext.createDataFrame(Seq((\"12characters12characters\",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([12characters12characters,20])")); - } else { - result = sparkInterpreter.interpret( - "val df=spark.createDataFrame(Seq((\"12characters12characters\",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([12characters12characters,20])")); - } - sparkInterpreter.interpret("df.registerTempTable(\"df\")", context); - // test LivySparkSQLInterpreter which share the same SparkContext with LivySparkInterpreter - result = sqlInterpreter.interpret("select * from df where col_1='12characters12characters'", context); + assertTrue(result.message().get(0).getData() + .contains("Array[org.apache.spark.sql.Row] = Array([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); + // test LivySparkSQLInterpreter which share the same SparkContext with LivySparkInterpreter + result = sqlInterpreter.interpret("select * from df where col_1='hello'", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(InterpreterResult.Type.TABLE, result.message().get(0).getType()); + assertEquals("col_1\tcol_2\nhello\t20", result.message().get(0).getData()); + // double quotes + result = sqlInterpreter.interpret("select * from df where col_1=\"hello\"", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(InterpreterResult.Type.TABLE, result.message().get(0).getType()); + assertEquals("col_1\tcol_2\nhello\t20", result.message().get(0).getData()); + + // only enable this test in spark2 as spark1 doesn't work for this case + if (isSpark2) { + result = sqlInterpreter.interpret("select * from df where col_1=\"he\\\"llo\" ", context); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); assertEquals(InterpreterResult.Type.TABLE, result.message().get(0).getType()); - assertEquals("col_1\tcol_2\n12characters12cha...\t20", result.message().get(0).getData()); - } finally { - sparkInterpreter.close(); - sqlInterpreter.close(); } - } - @Test - public void testStringWithoutTruncation() { - if (!checkPreCondition()) { - return; - } - InterpreterGroup interpreterGroup = new InterpreterGroup("group_1"); - interpreterGroup.put("session_1", new ArrayList()); - Properties newProps = new Properties(); - for (Object name: properties.keySet()) { - newProps.put(name, properties.get(name)); + // single quotes inside attribute value + result = sqlInterpreter.interpret("select * from df where col_1=\"he'llo\"", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(InterpreterResult.Type.TABLE, result.message().get(0).getType()); + + // test sql with syntax error + result = sqlInterpreter.interpret("select * from df2", context); + assertEquals(InterpreterResult.Code.ERROR, result.code()); + assertEquals(InterpreterResult.Type.TEXT, result.message().get(0).getType()); + + if (!isSpark2) { + assertTrue(result.message().get(0).getData().contains("Table not found")); + } else { + assertTrue(result.message().get(0).getData().contains("Table or view not found")); } - newProps.put(LivySparkSQLInterpreter.ZEPPELIN_LIVY_SPARK_SQL_FIELD_TRUNCATE, "false"); - LivySparkInterpreter sparkInterpreter = new LivySparkInterpreter(newProps); - sparkInterpreter.setInterpreterGroup(interpreterGroup); - interpreterGroup.get("session_1").add(sparkInterpreter); - AuthenticationInfo authInfo = new AuthenticationInfo("user1"); - MyInterpreterOutputListener outputListener = new MyInterpreterOutputListener(); - InterpreterOutput output = new InterpreterOutput(outputListener); - InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "livy.spark", - "title", "text", authInfo, null, null, null, null, null, output); - sparkInterpreter.open(); - LivySparkSQLInterpreter sqlInterpreter = new LivySparkSQLInterpreter(newProps); - interpreterGroup.get("session_1").add(sqlInterpreter); - sqlInterpreter.setInterpreterGroup(interpreterGroup); - sqlInterpreter.open(); + // test sql cancel + if (sqlInterpreter.getLivyVersion().newerThanEquals(LivyVersion.LIVY_0_3_0)) { + Thread cancelThread = new Thread() { + @Override + public void run() { + sqlInterpreter.cancel(context); + } + }; + cancelThread.start(); + //sleep so that cancelThread performs a cancel. + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + result = sqlInterpreter + .interpret("select count(1) from df", context); + if (result.code().equals(InterpreterResult.Code.ERROR)) { + String message = result.message().get(0).getData(); + // 2 possibilities, sometimes livy doesn't return the real cancel exception + assertTrue(message.contains("cancelled part of cancelled job group") || + message.contains("Job is cancelled")); + } + } - try { - // detect spark version - InterpreterResult result = sparkInterpreter.interpret("sc.version", context); + // test result string truncate + if (!isSpark2) { + result = sparkInterpreter.interpret( + "val df=sqlContext.createDataFrame(Seq((\"12characters12characters\",20))).toDF(\"col_1\", \"col_2\")\n" + + "df.collect()", context); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); assertEquals(1, result.message().size()); - - boolean isSpark2 = isSpark2(sparkInterpreter, context); - - // test DataFrame api - if (!isSpark2) { - result = sparkInterpreter.interpret( - "val df=sqlContext.createDataFrame(Seq((\"12characters12characters\",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([12characters12characters,20])")); - } else { - result = sparkInterpreter.interpret( - "val df=spark.createDataFrame(Seq((\"12characters12characters\",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([12characters12characters,20])")); - } - sparkInterpreter.interpret("df.registerTempTable(\"df\")", context); - // test LivySparkSQLInterpreter which share the same SparkContext with LivySparkInterpreter - result = sqlInterpreter.interpret("select * from df where col_1='12characters12characters'", context); + assertTrue(result.message().get(0).getData() + .contains("Array[org.apache.spark.sql.Row] = Array([12characters12characters,20])")); + } else { + result = sparkInterpreter.interpret( + "val df=spark.createDataFrame(Seq((\"12characters12characters\",20))).toDF(\"col_1\", \"col_2\")\n" + + "df.collect()", context); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); - assertEquals(InterpreterResult.Type.TABLE, result.message().get(0).getType()); - assertEquals("col_1\tcol_2\n12characters12characters\t20", result.message().get(0).getData()); - } finally { - sparkInterpreter.close(); - sqlInterpreter.close(); + assertEquals(1, result.message().size()); + assertTrue(result.message().get(0).getData() + .contains("Array[org.apache.spark.sql.Row] = Array([12characters12characters,20])")); } + sparkInterpreter.interpret("df.registerTempTable(\"df\")", context); + // test LivySparkSQLInterpreter which share the same SparkContext with LivySparkInterpreter + result = sqlInterpreter.interpret("select * from df where col_1='12characters12characters'", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(InterpreterResult.Type.TABLE, result.message().get(0).getType()); + assertEquals("col_1\tcol_2\n12characters12cha...\t20", result.message().get(0).getData()); + } @Test - public void testPySparkInterpreter() throws LivyException { + public void testPySparkInterpreter() throws InterpreterException { if (!checkPreCondition()) { return; } 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); final InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "livy.pyspark", - "title", "text", authInfo, null, null, null, null, null, output); + "title", "text", authInfo, null, null, null, null, null, null, output); pysparkInterpreter.open(); // test traceback msg @@ -543,7 +360,7 @@ public void testPySparkInterpreter() throws LivyException { // for livy version >=0.3 , input some erroneous spark code, check the shown result is more than one line InterpreterResult result = pysparkInterpreter.interpret("sc.parallelize(wrongSyntax(1, 2)).count()", context); assertEquals(InterpreterResult.Code.ERROR, result.code()); - assertTrue(result.message().get(0).getData().split("\n").length>1); + assertTrue(result.message().get(0).getData().split("\n").length > 1); assertTrue(result.message().get(0).getData().contains("Traceback")); } catch (APINotFoundException e) { // only livy 0.2 can throw this exception since it doesn't have /version endpoint @@ -551,10 +368,22 @@ public void testPySparkInterpreter() throws LivyException { // traceback InterpreterResult result = pysparkInterpreter.interpret("print(a)", context); assertEquals(InterpreterResult.Code.ERROR, result.code()); - assertTrue(result.message().get(0).getData().split("\n").length>1); + assertTrue(result.message().get(0).getData().split("\n").length > 1); assertTrue(result.message().get(0).getData().contains("Traceback")); } + // test utf-8 Encoding + String utf8Str = "你你你你你你好"; + InterpreterResult reslt = pysparkInterpreter.interpret("print(\"" + utf8Str + "\")", context); + assertEquals(InterpreterResult.Code.SUCCESS, reslt.code()); + assertTrue(reslt.message().get(0).getData().contains(utf8Str)); + + //test special characters + String charStr = "açñiñíûÑoç"; + InterpreterResult res = pysparkInterpreter.interpret("print(\"" + charStr + "\")", context); + assertEquals(InterpreterResult.Code.SUCCESS, res.code()); + assertTrue(res.message().get(0).getData().contains(charStr)); + try { InterpreterResult result = pysparkInterpreter.interpret("sc.version", context); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); @@ -634,7 +463,7 @@ public void run() { } @Test - public void testSparkInterpreterWithDisplayAppInfo() { + public void testSparkInterpreterWithDisplayAppInfo_StringWithoutTruncation() throws InterpreterException { if (!checkPreCondition()) { return; } @@ -644,6 +473,7 @@ public void testSparkInterpreterWithDisplayAppInfo() { properties2.put("zeppelin.livy.displayAppInfo", "true"); // enable spark ui because it is disabled by livy integration test properties2.put("livy.spark.ui.enabled", "true"); + properties2.put(LivySparkSQLInterpreter.ZEPPELIN_LIVY_SPARK_SQL_FIELD_TRUNCATE, "false"); LivySparkInterpreter sparkInterpreter = new LivySparkInterpreter(properties2); sparkInterpreter.setInterpreterGroup(interpreterGroup); interpreterGroup.get("session_1").add(sparkInterpreter); @@ -651,14 +481,20 @@ public void testSparkInterpreterWithDisplayAppInfo() { MyInterpreterOutputListener outputListener = new MyInterpreterOutputListener(); InterpreterOutput output = new InterpreterOutput(outputListener); InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "livy.spark", - "title", "text", authInfo, null, null, null, null, null, output); + "title", "text", authInfo, null, null, null, null, null, null, output); sparkInterpreter.open(); + LivySparkSQLInterpreter sqlInterpreter = new LivySparkSQLInterpreter(properties2); + interpreterGroup.get("session_1").add(sqlInterpreter); + sqlInterpreter.setInterpreterGroup(interpreterGroup); + sqlInterpreter.open(); + try { 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

    \")"; @@ -667,18 +503,51 @@ public void testSparkInterpreterWithDisplayAppInfo() { assertEquals(2, result.message().size()); assertEquals(InterpreterResult.Type.HTML, result.message().get(0).getType()); + // detect spark version + result = sparkInterpreter.interpret("sc.version", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(2, result.message().size()); + + boolean isSpark2 = isSpark2(sparkInterpreter, context); + + if (!isSpark2) { + result = sparkInterpreter.interpret( + "val df=sqlContext.createDataFrame(Seq((\"12characters12characters\",20))).toDF(\"col_1\", \"col_2\")\n" + + "df.collect()", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(2, result.message().size()); + assertTrue(result.message().get(0).getData() + .contains("Array[org.apache.spark.sql.Row] = Array([12characters12characters,20])")); + } else { + result = sparkInterpreter.interpret( + "val df=spark.createDataFrame(Seq((\"12characters12characters\",20))).toDF(\"col_1\", \"col_2\")\n" + + "df.collect()", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(2, result.message().size()); + assertTrue(result.message().get(0).getData() + .contains("Array[org.apache.spark.sql.Row] = Array([12characters12characters,20])")); + } + sparkInterpreter.interpret("df.registerTempTable(\"df\")", context); + // test LivySparkSQLInterpreter which share the same SparkContext with LivySparkInterpreter + result = sqlInterpreter.interpret("select * from df where col_1='12characters12characters'", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(InterpreterResult.Type.TABLE, result.message().get(0).getType()); + assertEquals("col_1\tcol_2\n12characters12characters\t20", result.message().get(0).getData()); } finally { sparkInterpreter.close(); + sqlInterpreter.close(); } } @Test - public void testSparkRInterpreter() throws LivyException { + public void testSparkRInterpreter() throws InterpreterException { if (!checkPreCondition()) { return; } final LivySparkRInterpreter sparkRInterpreter = new LivySparkRInterpreter(properties); + sparkRInterpreter.setInterpreterGroup(mock(InterpreterGroup.class)); + try { sparkRInterpreter.getLivyVersion(); } catch (APINotFoundException e) { @@ -689,7 +558,7 @@ public void testSparkRInterpreter() throws LivyException { MyInterpreterOutputListener outputListener = new MyInterpreterOutputListener(); InterpreterOutput output = new InterpreterOutput(outputListener); final InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "livy.sparkr", - "title", "text", authInfo, null, null, null, null, null, output); + "title", "text", authInfo, null, null, null, null, null, null, output); sparkRInterpreter.open(); try { @@ -735,17 +604,17 @@ 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")); + assertTrue("Actual Result: " + result.message().get(0).getData(), + result.message().get(0).getData().contains("object 'a' not found")); } finally { sparkRInterpreter.close(); } } @Test - public void testLivyTutorialNote() throws IOException { + public void testLivyTutorialNote() throws IOException, InterpreterException { if (!checkPreCondition()) { return; } @@ -766,7 +635,7 @@ public void testLivyTutorialNote() throws IOException { 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, output); + "title", "text", authInfo, null, null, null, null, null, null, output); String p1 = IOUtils.toString(getClass().getResourceAsStream("/livy_tutorial_1.scala")); InterpreterResult result = sparkInterpreter.interpret(p1, context); @@ -782,6 +651,140 @@ public void testLivyTutorialNote() throws IOException { } } + @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")); + } + + // test plotting of python + result = pysparkInterpreter.interpret( + "import matplotlib.pyplot as plt\n" + + "plt.switch_backend('agg')\n" + + "data=[1,2,3,4]\n" + + "plt.figure()\n" + + "plt.plot(data)\n" + + "%matplot plt", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + assertEquals(InterpreterResult.Type.IMG, result.message().get(0).getType()); + + // test plotting of R + result = sparkRInterpreter.interpret( + "hist(mtcars$mpg)", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + assertEquals(InterpreterResult.Type.IMG, result.message().get(0).getType()); + + // test code completion + List completionResult = sparkInterpreter + .completion("df.sho", 6, context); + assertEquals(1, completionResult.size()); + assertEquals("show", completionResult.get(0).name); + + } finally { + sparkInterpreter.close(); + sqlInterpreter.close(); + } + } private boolean isSpark2(BaseLivyInterpreter interpreter, InterpreterContext context) { InterpreterResult result = null; diff --git a/livy/src/test/java/org/apache/zeppelin/livy/LivySQLInterpreterTest.java b/livy/src/test/java/org/apache/zeppelin/livy/LivySQLInterpreterTest.java index fdef9b13d1a..0541b876755 100644 --- a/livy/src/test/java/org/apache/zeppelin/livy/LivySQLInterpreterTest.java +++ b/livy/src/test/java/org/apache/zeppelin/livy/LivySQLInterpreterTest.java @@ -39,9 +39,17 @@ public void setUp() { properties.setProperty("zeppelin.livy.url", "http://localhost:8998"); properties.setProperty("zeppelin.livy.session.create_timeout", "120"); properties.setProperty("zeppelin.livy.spark.sql.maxResult", "3"); + properties.setProperty("zeppelin.livy.http.headers", "HEADER_1: VALUE_1_${HOME}"); sqlInterpreter = new LivySparkSQLInterpreter(properties); } + @Test + public void testHttpHeaders() { + assertEquals(1, sqlInterpreter.getCustomHeaders().size()); + assertTrue(sqlInterpreter.getCustomHeaders().get("HEADER_1").startsWith("VALUE_1_")); + assertNotEquals("VALUE_1_${HOME}", sqlInterpreter.getCustomHeaders().get("HEADER_1")); + } + @Test public void testParseSQLOutput() { // Empty sql output @@ -116,5 +124,49 @@ public void testParseSQLOutput() { assertEquals(2, rows.size()); assertEquals("a", rows.get(0)); assertEquals("1", rows.get(1)); + + + // sql output with 3 rows, 3 columns, showing "only showing top 3 rows" with a line break in the data + // +---+---+---+ + // | a| b| c| + // +---+---+---+ + // | 1a| 1b| 1c| + // | 2a| 2 + // b| 2c| + // | 3a| 3b| 3c| + // +---+---+---+ + // only showing top 3 rows + rows = sqlInterpreter.parseSQLOutput("+---+----+---+\n" + + "| a| b| c|\n" + + "+---+----+---+\n" + + "| 1a| 1b| 1c|\n" + + "| 2a| 2\nb| 2c|\n" + + "| 3a| 3b| 3c|\n" + + "+---+---+---+\n" + + "only showing top 3 rows"); + assertEquals(4, rows.size()); + assertEquals("a\tb\tc", rows.get(0)); + assertEquals("1a\t1b\t1c", rows.get(1)); + assertEquals("2a\t2\\nb\t2c", rows.get(2)); + assertEquals("3a\t3b\t3c", rows.get(3)); + + + // sql output with 2 rows and one containing a tab + // +---+---+ + // | a| b| + // +---+---+ + // | 1| \ta| + // | 2| 2b| + // +---+---+ + rows = sqlInterpreter.parseSQLOutput("+---+---+\n" + + "| a| b|\n" + + "+---+---+\n" + + "| 1| \ta|\n" + + "| 2| 2b|\n" + + "+---+---+"); + assertEquals(3, rows.size()); + assertEquals("a\tb", rows.get(0)); + assertEquals("1\t\\ta", rows.get(1)); + assertEquals("2\t2b", rows.get(2)); } } diff --git a/maprdb/pom.xml b/maprdb/pom.xml new file mode 100644 index 00000000000..53d8e3222ae --- /dev/null +++ b/maprdb/pom.xml @@ -0,0 +1,93 @@ + + + + + 4.0.0 + + + interpreter-parent + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + ../interpreter-parent + + + org.apache.zeppelin + zeppelin-maprdb + jar + 0.8.2-mapr-1912-r2 + Zeppelin: MapR-DB Shell interpreter + + + maprdb + + + 3.4 + 1.3 + + + + + ${project.groupId} + zeppelin-interpreter + ${project.version} + + + + org.slf4j + slf4j-api + + + + org.slf4j + slf4j-log4j12 + + + + org.apache.commons + commons-exec + ${commons.exec.version} + + + + org.apache.commons + commons-lang3 + ${commons.lang3.version} + + + + junit + junit + test + + + + + + + maven-enforcer-plugin + + + maven-dependency-plugin + + + maven-resources-plugin + + + + + diff --git a/maprdb/src/main/java/org/apache/zeppelin/maprdb/MapRDBShellInterpreter.java b/maprdb/src/main/java/org/apache/zeppelin/maprdb/MapRDBShellInterpreter.java new file mode 100644 index 00000000000..bbb214f216c --- /dev/null +++ b/maprdb/src/main/java/org/apache/zeppelin/maprdb/MapRDBShellInterpreter.java @@ -0,0 +1,146 @@ +/* + * 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.maprdb; + +import java.io.*; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.exec.CommandLine; +import org.apache.commons.exec.DefaultExecutor; +import org.apache.commons.exec.ExecuteException; +import org.apache.commons.exec.ExecuteWatchdog; +import org.apache.commons.exec.PumpStreamHandler; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Shell interpreter for Zeppelin. + */ +public class MapRDBShellInterpreter extends Interpreter { + private static final Logger LOGGER = LoggerFactory.getLogger(MapRDBShellInterpreter.class); + private static final String TIMEOUT_PROPERTY = "maprdb.shell.command.timeout.millisecs"; + private final String shell = "mapr dbshell --cmdfile ${cmdfile}"; + private final String shellSub = "cmdfile"; + ConcurrentHashMap executors; + + public MapRDBShellInterpreter(Properties property) { + super(property); + } + + @Override + public void open() { + LOGGER.info("Command timeout property: {}", getProperty(TIMEOUT_PROPERTY)); + executors = new ConcurrentHashMap<>(); + } + + @Override + public void close() {} + + + @Override + public InterpreterResult interpret(String cmd, InterpreterContext contextInterpreter) { + LOGGER.debug("Run shell command '" + cmd + "'"); + OutputStream outStream = new ByteArrayOutputStream(); + + File cmdfile = null; + try { + cmdfile = File.createTempFile("zeppelin_maprdb_shell_", ".cmd"); + FileWriter cmdfileWriter = new FileWriter(cmdfile); + cmdfileWriter.write(cmd); + cmdfileWriter.close(); + + Map cmdSubs = new HashMap<>(); + cmdSubs.put(shellSub, cmdfile.getAbsolutePath()); + + CommandLine cmdLine = CommandLine.parse(shell, cmdSubs); + + DefaultExecutor executor = new DefaultExecutor(); + executor.setStreamHandler(new PumpStreamHandler( + contextInterpreter.out, contextInterpreter.out)); + executor.setWatchdog(new ExecuteWatchdog(Long.valueOf(getProperty(TIMEOUT_PROPERTY)))); + executors.put(contextInterpreter.getParagraphId(), executor); + int exitVal = executor.execute(cmdLine); + LOGGER.info("Paragraph " + contextInterpreter.getParagraphId() + + " return with exit value: " + exitVal); + return new InterpreterResult(Code.SUCCESS, outStream.toString()); + } catch (ExecuteException e) { + int exitValue = e.getExitValue(); + LOGGER.error("Can not run " + cmd, e); + Code code = Code.ERROR; + String message = outStream.toString(); + if (exitValue == 143) { + code = Code.INCOMPLETE; + message += "Paragraph received a SIGTERM\n"; + LOGGER.info("The paragraph " + contextInterpreter.getParagraphId() + + " stopped executing: " + message); + } + message += "ExitValue: " + exitValue; + return new InterpreterResult(code, message); + } catch (IOException e) { + LOGGER.error("Can not run " + cmd, e); + return new InterpreterResult(Code.ERROR, e.getMessage()); + } finally { + if (cmdfile != null) { + cmdfile.delete(); + } + executors.remove(contextInterpreter.getParagraphId()); + } + } + + @Override + public void cancel(InterpreterContext context) { + DefaultExecutor executor = executors.remove(context.getParagraphId()); + if (executor != null) { + executor.getWatchdog().destroyProcess(); + } + } + + @Override + public FormType getFormType() { + return FormType.SIMPLE; + } + + @Override + public int getProgress(InterpreterContext context) { + return 0; + } + + @Override + public Scheduler getScheduler() { + return SchedulerFactory.singleton().createOrGetParallelScheduler( + MapRDBShellInterpreter.class.getName() + this.hashCode(), 10); + } + + @Override + public List completion(String buf, int cursor, + InterpreterContext interpreterContext) { + return null; + } + +} diff --git a/maprdb/src/main/resources/interpreter-setting.json b/maprdb/src/main/resources/interpreter-setting.json new file mode 100644 index 00000000000..4b56d2581fe --- /dev/null +++ b/maprdb/src/main/resources/interpreter-setting.json @@ -0,0 +1,17 @@ +[ + { + "group": "maprdb", + "name": "shell", + "className": "org.apache.zeppelin.maprdb.MapRDBShellInterpreter", + "properties": { + "maprdb.shell.command.timeout.millisecs": { + "propertyName": "maprdb.shell.command.timeout.millisecs", + "defaultValue": "300000", + "description": "MapR-DB Shell command time out in millisecs. Default = 300000 (5 min)" + } + }, + "editor": { + "editOnDblClick": false + } + } +] diff --git a/markdown/pom.xml b/markdown/pom.xml index 9675aa70957..0fea4cff9b5 100644 --- a/markdown/pom.xml +++ b/markdown/pom.xml @@ -20,19 +20,20 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-markdown jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Markdown interpreter + md 3.4 2.2-cj-1.0 1.6.0 @@ -85,54 +86,12 @@ maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/md - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/md - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin 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/main/resources/interpreter-setting.json b/markdown/src/main/resources/interpreter-setting.json index 98192108aae..d2a59c8b6c0 100644 --- a/markdown/src/main/resources/interpreter-setting.json +++ b/markdown/src/main/resources/interpreter-setting.json @@ -14,7 +14,8 @@ }, "editor": { "language": "markdown", - "editOnDblClick": true + "editOnDblClick": true, + "completionSupport": false } } ] 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); diff --git a/neo4j/pom.xml b/neo4j/pom.xml new file mode 100644 index 00000000000..1504d40684f --- /dev/null +++ b/neo4j/pom.xml @@ -0,0 +1,144 @@ + + + + + 4.0.0 + + + zeppelin + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + .. + + + org.apache.zeppelin + zeppelin-neo4j + jar + 0.8.2-mapr-1912-r2 + Zeppelin: Neo4j interpreter + + + 1.4.3 + 3.2.3 + 3.2.3 + 2.8.9 + + + + + ${project.groupId} + zeppelin-interpreter + ${project.version} + provided + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + org.neo4j.driver + neo4j-java-driver + ${neo4j.driver.version} + + + + org.slf4j + slf4j-api + + + + org.slf4j + slf4j-log4j12 + + + + junit + junit + test + + + + org.neo4j.test + neo4j-harness + ${neo4j.version} + test + + + + + + + maven-enforcer-plugin + 1.3.1 + + + enforce + none + + + + + + maven-dependency-plugin + 2.8 + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/../../interpreter/neo4j + false + false + true + runtime + + + + copy-artifact + package + + copy + + + ${project.build.directory}/../../interpreter/neo4j + false + false + true + runtime + + + ${project.groupId} + ${project.artifactId} + ${project.version} + ${project.packaging} + + + + + + + + + + diff --git a/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/Neo4jConnectionManager.java b/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/Neo4jConnectionManager.java new file mode 100644 index 00000000000..7cd504ef200 --- /dev/null +++ b/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/Neo4jConnectionManager.java @@ -0,0 +1,151 @@ +/* + * 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.graph.neo4j; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.resource.Resource; +import org.apache.zeppelin.resource.ResourcePool; +import org.neo4j.driver.v1.AuthToken; +import org.neo4j.driver.v1.AuthTokens; +import org.neo4j.driver.v1.Config; +import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.GraphDatabase; +import org.neo4j.driver.v1.Session; +import org.neo4j.driver.v1.StatementResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Neo4j connection manager for Zeppelin. + */ +public class Neo4jConnectionManager { + static final Logger LOGGER = LoggerFactory.getLogger(Neo4jConnectionManager.class); + + public static final String NEO4J_SERVER_URL = "neo4j.url"; + public static final String NEO4J_AUTH_TYPE = "neo4j.auth.type"; + public static final String NEO4J_AUTH_USER = "neo4j.auth.user"; + public static final String NEO4J_AUTH_PASSWORD = "neo4j.auth.password"; + public static final String NEO4J_MAX_CONCURRENCY = "neo4j.max.concurrency"; + + private static final Pattern PROPERTY_PATTERN = Pattern.compile("\\{\\w+\\}"); + private static final String REPLACE_CURLY_BRACKETS = "\\{|\\}"; + + private static final Pattern $_PATTERN = Pattern.compile("\\$\\w+\\}"); + private static final String REPLACE_$ = "\\$"; + + private Driver driver = null; + + private final String neo4jUrl; + + private final Config config; + + private final AuthToken authToken; + + /** + * + * Enum type for the AuthToken + * + */ + public enum Neo4jAuthType {NONE, BASIC} + + public Neo4jConnectionManager(Properties properties) { + this.neo4jUrl = properties.getProperty(NEO4J_SERVER_URL); + this.config = Config.build() + .withMaxIdleSessions(Integer.parseInt(properties.getProperty(NEO4J_MAX_CONCURRENCY))) + .toConfig(); + String authType = properties.getProperty(NEO4J_AUTH_TYPE); + switch (Neo4jAuthType.valueOf(authType.toUpperCase())) { + case BASIC: + String username = properties.getProperty(NEO4J_AUTH_USER); + String password = properties.getProperty(NEO4J_AUTH_PASSWORD); + this.authToken = AuthTokens.basic(username, password); + break; + case NONE: + LOGGER.debug("Creating NONE authentication"); + this.authToken = AuthTokens.none(); + break; + default: + throw new RuntimeException("Neo4j authentication type not supported"); + } + } + + private Driver getDriver() { + if (driver == null) { + driver = GraphDatabase.driver(this.neo4jUrl, this.authToken, this.config); + } + return driver; + } + + public void open() { + getDriver(); + } + + public void close() { + getDriver().close(); + } + + private Session getSession() { + return getDriver().session(); + } + + public StatementResult execute(String cypherQuery, + InterpreterContext interpreterContext) { + Map params = new HashMap<>(); + if (interpreterContext != null) { + ResourcePool resourcePool = interpreterContext.getResourcePool(); + Set keys = extractParams(cypherQuery, PROPERTY_PATTERN, REPLACE_CURLY_BRACKETS); + keys.addAll(extractParams(cypherQuery, $_PATTERN, REPLACE_$)); + for (String key : keys) { + Resource resource = resourcePool.get(key); + if (resource != null) { + params.put(key, resource.get()); + } + } + } + LOGGER.debug("Executing cypher query {} with params {}", cypherQuery, params); + StatementResult result; + try (Session session = getSession()) { + result = params.isEmpty() + ? getSession().run(cypherQuery) : getSession().run(cypherQuery, params); + } + return result; + } + + public StatementResult execute(String cypherQuery) { + return execute(cypherQuery, null); + } + + private Set extractParams(String cypherQuery, Pattern pattern, String replaceChar) { + Matcher matcher = pattern.matcher(cypherQuery); + Set keys = new HashSet<>(); + while (matcher.find()) { + keys.add(matcher.group().replaceAll(replaceChar, StringUtils.EMPTY)); + } + return keys; + } + +} diff --git a/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/Neo4jCypherInterpreter.java b/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/Neo4jCypherInterpreter.java new file mode 100644 index 00000000000..a6255225587 --- /dev/null +++ b/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/Neo4jCypherInterpreter.java @@ -0,0 +1,274 @@ +/* + * 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.graph.neo4j; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.graph.neo4j.utils.Neo4jConversionUtils; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResult.Code; +import org.apache.zeppelin.interpreter.graph.GraphResult; +import org.apache.zeppelin.scheduler.Scheduler; +import org.apache.zeppelin.scheduler.SchedulerFactory; +import org.neo4j.driver.internal.types.InternalTypeSystem; +import org.neo4j.driver.internal.util.Iterables; +import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.StatementResult; +import org.neo4j.driver.v1.Value; +import org.neo4j.driver.v1.types.Node; +import org.neo4j.driver.v1.types.Relationship; +import org.neo4j.driver.v1.types.TypeSystem; +import org.neo4j.driver.v1.util.Pair; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Neo4j interpreter for Zeppelin. + */ +public class Neo4jCypherInterpreter extends Interpreter { + private static final String TABLE = "%table"; + public static final String NEW_LINE = "\n"; + public static final String TAB = "\t"; + + private static final String MAP_KEY_TEMPLATE = "%s.%s"; + + private Map labels; + + private Set types; + + private final Neo4jConnectionManager neo4jConnectionManager; + + private final ObjectMapper jsonMapper = new ObjectMapper(); + + public Neo4jCypherInterpreter(Properties properties) { + super(properties); + this.neo4jConnectionManager = new Neo4jConnectionManager(properties); + } + + @Override + public void open() { + this.neo4jConnectionManager.open(); + } + + @Override + public void close() { + this.neo4jConnectionManager.close(); + } + + public Map getLabels(boolean refresh) { + if (labels == null || refresh) { + Map old = labels == null ? + new LinkedHashMap() : new LinkedHashMap<>(labels); + labels = new LinkedHashMap<>(); + StatementResult result = this.neo4jConnectionManager.execute("CALL db.labels()"); + Set colors = new HashSet<>(); + while (result.hasNext()) { + Record record = result.next(); + String label = record.get("label").asString(); + String color = old.get(label); + while (color == null || colors.contains(color)) { + color = Neo4jConversionUtils.getRandomLabelColor(); + } + colors.add(color); + labels.put(label, color); + } + } + return labels; + } + + private Set getTypes(boolean refresh) { + if (types == null || refresh) { + types = new HashSet<>(); + StatementResult result = this.neo4jConnectionManager.execute("CALL db.relationshipTypes()"); + while (result.hasNext()) { + Record record = result.next(); + types.add(record.get("relationshipType").asString()); + } + } + return types; + } + + @Override + public InterpreterResult interpret(String cypherQuery, InterpreterContext interpreterContext) { + logger.info("Opening session"); + if (StringUtils.isBlank(cypherQuery)) { + return new InterpreterResult(Code.SUCCESS); + } + try { + StatementResult result = this.neo4jConnectionManager.execute(cypherQuery, + interpreterContext); + Set nodes = new HashSet<>(); + Set relationships = new HashSet<>(); + List columns = new ArrayList<>(); + List> lines = new ArrayList>(); + while (result.hasNext()) { + Record record = result.next(); + List> fields = record.fields(); + List line = new ArrayList<>(); + for (Pair field : fields) { + if (field.value().hasType(InternalTypeSystem.TYPE_SYSTEM.NODE())) { + nodes.add(field.value().asNode()); + } else if (field.value().hasType(InternalTypeSystem.TYPE_SYSTEM.RELATIONSHIP())) { + relationships.add(field.value().asRelationship()); + } else if (field.value().hasType(InternalTypeSystem.TYPE_SYSTEM.PATH())) { + nodes.addAll(Iterables.asList(field.value().asPath().nodes())); + relationships.addAll(Iterables.asList(field.value().asPath().relationships())); + } else { + setTabularResult(field.key(), field.value(), columns, line, + InternalTypeSystem.TYPE_SYSTEM); + } + } + if (!line.isEmpty()) { + lines.add(line); + } + } + if (!nodes.isEmpty()) { + return renderGraph(nodes, relationships); + } else { + return renderTable(columns, lines); + } + } catch (Exception e) { + logger.error("Exception while interpreting cypher query", e); + return new InterpreterResult(Code.ERROR, e.getMessage()); + } + } + + private void setTabularResult(String key, Object obj, List columns, List line, + TypeSystem typeSystem) { + if (obj instanceof Value) { + Value value = (Value) obj; + if (value.hasType(typeSystem.MAP())) { + Map map = value.asMap(); + for (Entry entry : map.entrySet()) { + setTabularResult(String.format(MAP_KEY_TEMPLATE, key, entry.getKey()), entry.getValue(), + columns, line, typeSystem); + } + } else { + addValueToLine(key, columns, line, value); + } + } else if (obj instanceof Map) { + Map map = (Map) obj; + for (Entry entry : map.entrySet()) { + setTabularResult(String.format(MAP_KEY_TEMPLATE, key, entry.getKey()), entry.getValue(), + columns, line, typeSystem); + } + } else { + addValueToLine(key, columns, line, obj); + } + } + + private void addValueToLine(String key, List columns, List line, Object value) { + if (!columns.contains(key)) { + columns.add(key); + } + int position = columns.indexOf(key); + if (line.size() < columns.size()) { + for (int i = line.size(); i < columns.size(); i++) { + line.add(null); + } + } + if (value != null) { + if (value instanceof Value) { + Value val = (Value) value; + if (val.hasType(InternalTypeSystem.TYPE_SYSTEM.LIST())) { + value = val.asList(); + } else if (val.hasType(InternalTypeSystem.TYPE_SYSTEM.MAP())) { + value = val.asMap(); + } + } + if (value instanceof Collection) { + try { + value = jsonMapper.writer().writeValueAsString(value); + } catch (Exception ignored) {} + } + } + line.set(position, value == null ? null : value.toString()); + } + + private InterpreterResult renderTable(List cols, List> lines) { + logger.info("Executing renderTable method"); + StringBuilder msg = null; + if (cols.isEmpty()) { + msg = new StringBuilder(); + } else { + msg = new StringBuilder(TABLE); + msg.append(NEW_LINE); + msg.append(StringUtils.join(cols, TAB)); + msg.append(NEW_LINE); + for (List line : lines) { + if (line.size() < cols.size()) { + for (int i = line.size(); i < cols.size(); i++) { + line.add(null); + } + } + msg.append(StringUtils.join(line, TAB)); + msg.append(NEW_LINE); + } + } + return new InterpreterResult(Code.SUCCESS, msg.toString()); + } + + private InterpreterResult renderGraph(Set nodes, + Set relationships) { + logger.info("Executing renderGraph method"); + List nodesList = new ArrayList<>(); + List relsList = new ArrayList<>(); + for (Relationship rel : relationships) { + relsList.add(Neo4jConversionUtils.toZeppelinRelationship(rel)); + } + Map labels = getLabels(true); + for (Node node : nodes) { + nodesList.add(Neo4jConversionUtils.toZeppelinNode(node, labels)); + } + return new GraphResult(Code.SUCCESS, + new GraphResult.Graph(nodesList, relsList, labels, getTypes(true), true)); + } + + @Override + public Scheduler getScheduler() { + return SchedulerFactory.singleton() + .createOrGetParallelScheduler(Neo4jCypherInterpreter.class.getName() + this.hashCode(), + Integer.parseInt(getProperty(Neo4jConnectionManager.NEO4J_MAX_CONCURRENCY))); + } + + @Override + public int getProgress(InterpreterContext context) { + return 0; + } + + @Override + public FormType getFormType() { + return FormType.SIMPLE; + } + + @Override + public void cancel(InterpreterContext context) { + } + +} diff --git a/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/utils/Neo4jConversionUtils.java b/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/utils/Neo4jConversionUtils.java new file mode 100644 index 00000000000..484940198a7 --- /dev/null +++ b/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/utils/Neo4jConversionUtils.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.graph.neo4j.utils; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.neo4j.driver.v1.types.Node; +import org.neo4j.driver.v1.types.Relationship; + +/** + * Neo4jConversionUtils + */ +public class Neo4jConversionUtils { + private Neo4jConversionUtils() {} + + private static final String[] LETTERS = "0123456789ABCDEF".split(""); + + public static final String COLOR_GREY = "#D3D3D3"; + + public static org.apache.zeppelin.tabledata.Node toZeppelinNode(Node n, + Map graphLabels) { + Set labels = new LinkedHashSet<>(); + String firstLabel = null; + for (String label : n.labels()) { + if (firstLabel == null) { + firstLabel = label; + } + labels.add(label); + } + return new org.apache.zeppelin.tabledata.Node(n.id(), n.asMap(), + labels); + } + + public static org.apache.zeppelin.tabledata.Relationship + toZeppelinRelationship(Relationship r) { + return new org.apache.zeppelin.tabledata.Relationship(r.id(), r.asMap(), + r.startNodeId(), r.endNodeId(), r.type()); + } + + public static String getRandomLabelColor() { + char[] color = new char[7]; + color[0] = '#'; + for (int i = 1; i < color.length; i++) { + color[i] = LETTERS[(int) Math.floor(Math.random() * 16)].charAt(0); + } + return new String(color); + } + +} diff --git a/neo4j/src/main/resources/interpreter-setting.json b/neo4j/src/main/resources/interpreter-setting.json new file mode 100644 index 00000000000..8db4367cc3e --- /dev/null +++ b/neo4j/src/main/resources/interpreter-setting.json @@ -0,0 +1,42 @@ +[ + { + "group": "neo4j", + "name": "neo4j", + "className": "org.apache.zeppelin.graph.neo4j.Neo4jCypherInterpreter", + "properties": { + "neo4j.url": { + "envName": null, + "propertyName": "neo4j.url", + "defaultValue": "bolt://localhost:7687", + "description": "The Neo4j's BOLT url." + }, + "neo4j.auth.type": { + "envName": null, + "propertyName": "neo4j.auth.type", + "defaultValue": "BASIC", + "description": "The Neo4j's authentication type (NONE, BASIC)." + }, + "neo4j.auth.user": { + "envName": null, + "propertyName": "neo4j.auth.user", + "defaultValue": "", + "description": "The Neo4j user name." + }, + "neo4j.auth.password": { + "envName": null, + "propertyName": "neo4j.auth.password", + "defaultValue": "", + "description": "The Neo4j user password." + }, + "neo4j.max.concurrency": { + "envName": null, + "propertyName": "neo4j.max.concurrency", + "defaultValue": "50", + "description": "Max concurrency call from Zeppelin to Neo4j server." + } + }, + "editor": { + "editOnDblClick": false + } + } +] diff --git a/neo4j/src/test/java/org/apache/zeppelin/graph/neo4j/Neo4jCypherInterpreterTest.java b/neo4j/src/test/java/org/apache/zeppelin/graph/neo4j/Neo4jCypherInterpreterTest.java new file mode 100644 index 00000000000..97815047d3b --- /dev/null +++ b/neo4j/src/test/java/org/apache/zeppelin/graph/neo4j/Neo4jCypherInterpreterTest.java @@ -0,0 +1,250 @@ +/* + * 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.graph.neo4j; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.lang3.StringUtils; +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.graph.neo4j.Neo4jConnectionManager.Neo4jAuthType; +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.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResult.Code; +import org.apache.zeppelin.interpreter.graph.GraphResult; +import org.apache.zeppelin.resource.LocalResourcePool; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; +import org.neo4j.harness.ServerControls; +import org.neo4j.harness.TestServerBuilders; + +import com.google.gson.Gson; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class Neo4jCypherInterpreterTest { + + private Neo4jCypherInterpreter interpreter; + + private InterpreterContext context; + + private static ServerControls server; + + private static final Gson gson = new Gson(); + + private static final String LABEL_PERSON = "Person"; + private static final String REL_KNOWS = "KNOWS"; + + private static final String CYPHER_FOREACH = "FOREACH (x in range(1,1000) | CREATE (:%s{name: \"name\" + x, age: %s}))"; + private static final String CHPHER_UNWIND = "UNWIND range(1,1000) as x " + + "MATCH (n), (m) WHERE id(n) = x AND id(m) = toInt(rand() * 1000) " + + "CREATE (n)-[:%s]->(m)"; + + @BeforeClass + public static void setUpNeo4jServer() throws Exception { + server = TestServerBuilders.newInProcessBuilder() + .withConfig("dbms.security.auth_enabled","false") + .withFixture(String.format(CYPHER_FOREACH, LABEL_PERSON, "x % 10")) + .withFixture(String.format(CHPHER_UNWIND, REL_KNOWS)) + .newServer(); + } + + @AfterClass + public static void tearDownNeo4jServer() throws Exception { + server.close(); + } + + @Before + public void setUpZeppelin() { + Properties p = new Properties(); + p.setProperty(Neo4jConnectionManager.NEO4J_SERVER_URL, server.boltURI().toString()); + p.setProperty(Neo4jConnectionManager.NEO4J_AUTH_TYPE, Neo4jAuthType.NONE.toString()); + p.setProperty(Neo4jConnectionManager.NEO4J_MAX_CONCURRENCY, "50"); + interpreter = new Neo4jCypherInterpreter(p); + context = new InterpreterContext("note", "id", null, "title", "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new GUI(), + new AngularObjectRegistry(new InterpreterGroup().getId(), null), + new LocalResourcePool("id"), + new LinkedList(), + new InterpreterOutput(null)); + } + + @After + public void tearDownZeppelin() throws Exception { + interpreter.close(); + } + + @Test + public void testTableWithArray() { + interpreter.open(); + InterpreterResult result = interpreter.interpret("return 'a' as colA, 'b' as colB, [1, 2, 3] as colC", context); + assertEquals(Code.SUCCESS, result.code()); + final String tableResult = "colA\tcolB\tcolC\n\"a\"\t\"b\"\t[1,2,3]\n"; + assertEquals(tableResult, result.toString().replace("%table ", StringUtils.EMPTY)); + + result = interpreter.interpret("return 'a' as colA, 'b' as colB, [{key: \"value\"}, {key: 1}] as colC", context); + assertEquals(Code.SUCCESS, result.code()); + final String tableResultWithMap = "colA\tcolB\tcolC\n\"a\"\t\"b\"\t[{\"key\":\"value\"},{\"key\":1}]\n"; + assertEquals(tableResultWithMap, result.toString().replace("%table ", StringUtils.EMPTY)); + } + + @Test + public void testCreateIndex() { + interpreter.open(); + InterpreterResult result = interpreter.interpret("CREATE INDEX ON :Person(name)", context); + assertEquals(Code.SUCCESS, result.code()); + assertEquals(StringUtils.EMPTY, result.toString()); + } + + @Test + public void testRenderTable() { + interpreter.open(); + InterpreterResult result = interpreter.interpret("MATCH (n:Person) " + + "WHERE n.name IN ['name1', 'name2', 'name3'] " + + "RETURN n.name AS name, n.age AS age", context); + assertEquals(Code.SUCCESS, result.code()); + final String tableResult = "name\tage\n\"name1\"\t1\n\"name2\"\t2\n\"name3\"\t3\n"; + assertEquals(tableResult, result.toString().replace("%table ", StringUtils.EMPTY)); + } + + @Test + public void testRenderMap() { + interpreter.open(); + final String jsonQuery = "RETURN {key: \"value\", listKey: [{inner: \"Map1\"}, {inner: \"Map2\"}]} as object"; + final String objectKey = "object.key"; + final String objectListKey = "object.listKey"; + InterpreterResult result = interpreter.interpret(jsonQuery, context); + assertEquals(Code.SUCCESS, result.code()); + String[] rows = result.toString().replace("%table ", StringUtils.EMPTY).split(Neo4jCypherInterpreter.NEW_LINE); + assertEquals(rows.length, 2); + List header = Arrays.asList(rows[0].split(Neo4jCypherInterpreter.TAB)); + assertEquals(header.contains(objectKey), true); + assertEquals(header.contains(objectListKey), true); + List row = Arrays.asList(rows[1].split(Neo4jCypherInterpreter.TAB)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value"); + assertEquals(row.get(header.indexOf(objectListKey)), "[{\"inner\":\"Map1\"},{\"inner\":\"Map2\"}]"); + + final String query = "WITH [{key: \"value\", listKey: [{inner: \"Map1\"}, {inner: \"Map2\"}]}," + + "{key: \"value2\", listKey: [{inner: \"Map12\"}, {inner: \"Map22\"}]}] " + + "AS array UNWIND array AS object RETURN object"; + result = interpreter.interpret(query, context); + assertEquals(Code.SUCCESS, result.code()); + rows = result.toString().replace("%table ", StringUtils.EMPTY).split(Neo4jCypherInterpreter.NEW_LINE); + assertEquals(rows.length, 3); + header = Arrays.asList(rows[0].split(Neo4jCypherInterpreter.TAB)); + assertEquals(header.contains(objectKey), true); + assertEquals(header.contains(objectListKey), true); + row = Arrays.asList(rows[1].split(Neo4jCypherInterpreter.TAB)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value"); + assertEquals(row.get(header.indexOf(objectListKey)), "[{\"inner\":\"Map1\"},{\"inner\":\"Map2\"}]"); + row = Arrays.asList(rows[2].split(Neo4jCypherInterpreter.TAB)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value2"); + assertEquals(row.get(header.indexOf(objectListKey)), "[{\"inner\":\"Map12\"},{\"inner\":\"Map22\"}]"); + + final String jsonListWithNullQuery = "WITH [{key: \"value\", listKey: null}," + + "{key: \"value2\", listKey: [{inner: \"Map1\"}, {inner: \"Map2\"}]}] " + + "AS array UNWIND array AS object RETURN object"; + result = interpreter.interpret(jsonListWithNullQuery, context); + assertEquals(Code.SUCCESS, result.code()); + rows = result.toString().replace("%table ", StringUtils.EMPTY).split(Neo4jCypherInterpreter.NEW_LINE); + assertEquals(rows.length, 3); + header = Arrays.asList(rows[0].split(Neo4jCypherInterpreter.TAB, -1)); + assertEquals(header.contains(objectKey), true); + assertEquals(header.contains(objectListKey), true); + row = Arrays.asList(rows[1].split(Neo4jCypherInterpreter.TAB, -1)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value"); + assertEquals(row.get(header.indexOf(objectListKey)), StringUtils.EMPTY); + assertEquals(row.get(header.indexOf(objectListKey)), ""); + row = Arrays.asList(rows[2].split(Neo4jCypherInterpreter.TAB, -1)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value2"); + assertEquals(row.get(header.indexOf(objectListKey)), "[{\"inner\":\"Map1\"},{\"inner\":\"Map2\"}]"); + + final String jsonListWithoutListKeyQuery = "WITH [{key: \"value\"}," + + "{key: \"value2\", listKey: [{inner: \"Map1\"}, {inner: \"Map2\"}]}] " + + "AS array UNWIND array AS object RETURN object"; + result = interpreter.interpret(jsonListWithoutListKeyQuery, context); + assertEquals(Code.SUCCESS, result.code()); + rows = result.toString().replace("%table ", StringUtils.EMPTY).split(Neo4jCypherInterpreter.NEW_LINE); + assertEquals(rows.length, 3); + header = Arrays.asList(rows[0].split(Neo4jCypherInterpreter.TAB, -1)); + assertEquals(header.contains(objectKey), true); + assertEquals(header.contains(objectListKey), true); + row = Arrays.asList(rows[1].split(Neo4jCypherInterpreter.TAB, -1)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value"); + assertEquals(row.get(header.indexOf(objectListKey)), StringUtils.EMPTY); + row = Arrays.asList(rows[2].split(Neo4jCypherInterpreter.TAB, -1)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value2"); + assertEquals(row.get(header.indexOf(objectListKey)), "[{\"inner\":\"Map1\"},{\"inner\":\"Map2\"}]"); + } + + @Test + public void testRenderNetwork() { + interpreter.open(); + InterpreterResult result = interpreter.interpret("MATCH (n)-[r:KNOWS]-(m) RETURN n, r, m LIMIT 1", context); + GraphResult.Graph graph = gson.fromJson(result.toString().replace("%network ", StringUtils.EMPTY), GraphResult.Graph.class); + assertEquals(2, graph.getNodes().size()); + assertEquals(true, graph.getNodes().iterator().next().getLabel().equals(LABEL_PERSON)); + assertEquals(1, graph.getEdges().size()); + assertEquals(true, graph.getEdges().iterator().next().getLabel().equals(REL_KNOWS)); + assertEquals(1, graph.getLabels().size()); + assertEquals(1, graph.getTypes().size()); + assertEquals(true, graph.getLabels().containsKey(LABEL_PERSON)); + assertEquals(REL_KNOWS, graph.getTypes().iterator().next()); + assertEquals(Code.SUCCESS, result.code()); + } + + @Test + public void testFallingQuery() { + interpreter.open(); + final String ERROR_MSG_EMPTY = ""; + InterpreterResult result = interpreter.interpret(StringUtils.EMPTY, context); + assertEquals(Code.SUCCESS, result.code()); + assertEquals(ERROR_MSG_EMPTY, result.toString()); + + result = interpreter.interpret(null, context); + assertEquals(Code.SUCCESS, result.code()); + assertEquals(ERROR_MSG_EMPTY, result.toString()); + + result = interpreter.interpret("MATCH (n:Person{name: }) RETURN n.name AS name, n.age AS age", context); + assertEquals(Code.ERROR, result.code()); + } + +} 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,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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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\" 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,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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", - "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,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\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\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,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\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,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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,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" } ] }, - "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+i+jSY9J55FBj9CscTM3/heJiIh4rjNn4IknYNEiePFFGD/edCIzfL5ghYeH8/HHH//o9++9994f/PrFF1+86Occf/14BnQeQNqqNPq90o+cMTn0Dup92VlFRES82fbtkJwMISFQVgYdOphOZI7PXyJsKB0DOrLqjlVM6zONxKWJPFf0HPWOetOxxAmJiYmmI4iL6XvqW/T99B51dTB3LiQmwvTpsHKlvcsVgJ/D4bDfhdFL5Ofnx/n+59r7zV5S81Jp7NeYJWOW0C2wm/vDiYiIGFBVBamp4HDAkiXQvfv5H3ehNdRXaYLlAt3bdKcwtZARYSOIWxRHdmm2rd5EIiJiPw4HZGdDXByMGgXvv3/hcmVHmmA54WLad+XhSpJzk+ka2JWFIxcS3Or8n0YUERHxVocPQ1oa7N8Pr70GN9zw839GEyy5LOHB4WxL28YN7W8gMjOS3B25piOJiIi4zIoVEBkJERGwdevFlSs70gTLCc6276L9RaTkpdC/c39euPUFWrdo3YDpREREGs7x4zBjBhQVQU4O9Ovn3J/XBEtcpl/nfpTdW4Z/U38iMiN4b+97piOJiIg47b33rKlVQIC1/YKz5cqONMFywuW077V71jJ15VTGXz+ep4c8zRVNr3BxOhEREdf6+99h9mzrsuDixZCUdOnPpQmWNIika5OomFbBkdojxCyMobim2HQkERGRCyouhpgYOHoUKiour1zZkSZYTnBV+172yTJmFMxgWp9pPHzTwzRt3NQF6URERC7f2bPw5JOQmQnz58OECa55XrtNsFSwnODKN8eBvx3gnpX3cKT2CK+NfY1e7Xu55HlFREQu1Y4d1lE3QUHWJcFOnVz33HYrWLpEaEingE6svnM1aTFpJCxJYN6WeTpqR0REjKivh+efh4QESE+H1atdW67sSBMsJzRU+97z9R5S81Jp3rg5S8YsoUvrLi5/DRERkfPZtw8mTbIuDS5dCqGhDfM6mmCJ213b9lo2TtpIUmgSsQtjWVq21FZvQhERcb9/nB3Ypw/ceits2NBw5cqONMFygjvad/mhcpJzkwltG0rWyCyC/IMa9PVERMR+jhyBe++FvXuto24iIhr+NTXBEqMiO0RSnFZMj6t6EJkZSf5n+aYjiYiID8nPtzYN7dkTtm1zT7myI02wnODu9r3py02k5qUyqOsgnr/1ea5sfqXbXltERHzLiRPwwAPwwQfWvVYDBrj39TXBEo8xsMtAyjPKada4GREvR1BYVWg6koiIeKH337emVs2bW0fduLtc2ZEmWE4w2b7X7F5D2qo0JvaeyFNDnqJFkxZGcoiIiPc4dQoefhiWLbP2tRo2zFwWTbDEIw0PG05FRgXV31YTuzCWkgMlpiOJiIgHKymB2FioqbGOujFZruxIEywneEL7djgc/OWTvzCzYCb3972f2TfNpkmjJkYziYiI5zh7Fp5+Gl56CebNg9tvN53I4glrqDupYDnBk94cNd/WMGXlFI5/d5ycMTn0aNfDdCQRETFs507rqJs2bSA7G0JCTCf6J09aQ91Blwi9VMiVIRTcVUBqZCoDsgcwf+t8HbUjImJT9fXWwcwDB8LkyVBQ4Fnlyo40wXKCp7bv3cd2k5KXQqtmrcgenU3n1p1NRxIRETfZv98qVbW1kJMDYWGmE52fp66hDUUTLB8QdlUYH0z+gJu73Uzswlher3jdVm9iERE7cjisXdhjY2HIEGt/K08tV3akCZYTvKF9lx4sJTk3mZ7tepI5MpN2LduZjiQiIi721VeQkQG7dlklKyrKdKKf5w1rqCtpguVjojtG81H6R1wTeA0RL0fw1q63TEcSEREXWrXK2jQ0NBSKi72jXNmRJlhO8Lb2vXHfRiblTWLINUN4Luk5ApoHmI4kIiKX6Ntv4cEHYf1666ibm24yncg53raGXi5NsHxYQtcEyjPKAYjMjGTjvo2GE4mIyKXYuNGaWjVqBOXl3leu7EgTLCd4c/t+a9dbpK9K567wu3hi8BM6akdExAt89x385jfwxhuwcCGMHGk60aXz5jX0UmiCZRMjrxtJxbQKvjj+BX0W9qH0YKnpSCIi8hNKS6FPH6iqso668eZyZUcqWDbSrmU7lv9iOb8e+GuSXk/iqQ+e4lz9OdOxRETkX5w7B7/7HSQlwezZsHw5tNMHwr2OLhE6wZfGm/tP7Gdy/mRqz9aSMyaHsKu0eYqIiGm7dkFKCgQEWEfddPahfaN9aQ29GJpg2VTn1p15J/kd7gq/i/7Z/VlQvMBWb3wREU/icFiHM/fvD3ffDWvX+la5siNNsJzgq+1759GdpOSlENgikOzR2YRcqQOsRETcpboapkyBEyeso2569DCdqGH46hp6IZpgCT3a9WDzlM0M7DyQ6Kxo/lz5Z1v9JRARMcHhsD4dGBMDCQmwebPvlis70gTLCXZo3yUHSkjOTSY8OJwFwxdwVcurTEcSEfE5x47BtGmwfbt11E1MjOlEDc8Oa+i/0gRLfiC2Uywl6SVcHXA1EZkRrNm9xnQkERGfsno1RERY91iVlNijXNmRJlhOsFv7LqwqZFLeJJJCk5ibNJdWzVqZjiQi4rVOnoRZs+Cdd+DVVyEx0XQi97LbGqoJllxQYrdEKqZVcLb+LJGZkWz+crPpSCIiXmnTJuuom3PnrKNu7Fau7EgTLCfYrX3/q/zP8slYnUFqZCqPJz5O8ybNTUcSEfF4p0/Db39r3WeVmQmjR5tOZI7d1lBNsOSi3NbzNsozytl5bCdxi+IoP1RuOpKIiEcrL4e4ONi92/q5ncuVHalgyUUL8g9ixYQVzOo3i1teu4VnNj1DXX2d6VgiIh6lrg6eeQaGDoWHHoK//hXatzedStxNlwidYLfx5k/Zd3wfk/Mnc7ruNDljcghtG2o6koiIcXv2QGoqtGhh3cjepYvpRJ7DbmuoJlhySboGduXdlHeZcP0EbnzlRrI+yrLVXxwRkX/lcFj3WPXrBxMnwrp1Kld2pwmWE+zWvi/Wjq92kJybTHv/9rwy+hU6BXQyHUlExG0OHICpU+HoUeuom169TCfyTHZbQzXBksvWq30viqYWER8ST3RWNMs+WWY6koiIWyxbBtHRcOON8OGHKlfyT5pgOcFu7ftSFNcUk5ybTEzHGF4c/iJtr2hrOpKIiMt9/TVMnw5lZdYWDH36mE7k+ey2hmqCJS4VFxJH6b2lBPkHEfFyBGv3rDUdSUTEpQoKrKNuOnSAjz9WuZLz0wTLCXZr35dr/RfrmZw/mRFhI5gzdA7+zfxNRxIRuWS1tda2C2vWWJ8QHDzYdCLvYrc1VBMsaTCDrxlMRUYFtWdricqKomh/kelIIiKX5MMPISoKTp2CigqVK/l5mmA5wW7t25VW7FjBfavvY2r0VB5NfJRmjZuZjiQi8rPOnIHHHrMmVgsWwNixphN5L7utoZpgiVuM6zWO8oxyKo9U0ndRXyoPV5qOJCLykyoroW9f2L7dupld5UqcoYIlbhPcKpj82/OZGT+TwTmDmbN5jo7aERGPU1cHv/+9dRlw5kzIy4PgYNOpxNvoEqET7DbebEhVx6uYlDeJOkcdS8cspXub7qYjiYiwd6911E3jxrBkCXTrZjqR77DbGqoJlhjRLbAb61PXM7bnWOIXx7P448W2+osnIp7F4YBFiyA+HsaNg/XrVa7k8miC5QS7tW932X5kO8m5yXQK6MTi0Yvp0KqD6UgiYiMHD8I998ChQ9amoddfbzqRb7LbGqoJlhjXO6g3W+7ZQkzHGKIyo3jz0zdNRxIRm1i+3Np+ITYWtmxRuRLX0QTLCXZr3yZsrd5Kcm4yfUP6Mn/YfNpc0cZ0JBHxQd98A/ffD8XF1gHN8fGmE/k+u62hmmCJR4m/Op6yjDLatGhDZGYk6z5fZzqSiPiYdeuso27atoXSUpUraRiaYDnBbu3btHWfr2Pqyqnc1uM2nh36LC2btjQdSUS8WG0t/OpXsHIlZGfDLbeYTmQvdltDNcESjzU0dCjlGeUcP32c6KxotlZvNR1JRLzUli0QHQ0nTlhH3ahcSUPTBMsJdmvfnuTNT99k+prppMek88igR3TUjohclDNn4IknrC0YXnwRxo83nci+7LaGqmA5wW5vDk9z8G8HSVuVxsGTB8kZk0PvoN6mI4mIB9u+HZKTISTEKlgdtAOMUXZbQ3WJULxGx4COrLpjFdP6TCNxaSLPFT1HvaPedCwR8TB1dTB3LiQmwvTp1j1XKlfibppgOcFu7duT7f1mL6l5qTT2a8ySMUvoFtjNdCQR8QBVVdZRNw6HddRNd53C5THstoZqgiVeqXub7hSmFjIibARxi+LILs221V9cEfkhh8P6ZGBcHIwaBe+/r3IlZmmC5QS7tW9vUXm4kuTcZLoGdmXhyIUEt9Kx9yJ2cvgwpKXB/v3WUTc33GA6kZyP3dZQTbDE64UHh7MtbRs3tL+ByMxIcnfkmo4kIm6yYgVERlobh27dqnIlnkMTLCfYrX17o6L9RaTkpdC/c39euPUFWrdobTqSiDSA48dhxgwoKrKOuunXz3Qi+Tl2W0M1wRKf0q9zP8ruLcO/qT8RmRG8t/c905FExMXee8+aWgUEQFmZypV4Jp8vWNXV1QwePJjevXsTHh7OCy+88KPHbNiwgcDAQGJiYoiJieHJJ580kFRcxb+ZPwtGLGDhyIWk5qXyQMEDnDp7ynQsEblMf/87zJwJkybBwoXw0kvg7286lcj5+fwlwkOHDnHo0CGioqI4efIksbGx5Ofn07Nnz+8fs2HDBubOncvKlSt/8rnsNt70BV+f+ppfrvklpYdKyRmTQ1xInOlIInIJioutTUNjY60d2du0MZ1InGW3NdTnJ1gdOnQgKioKgFatWtGrVy9qamp+9Dg7fdPtpO0VbXnjP9/gsUGPMfLPI3ms8DHO1p01HUtELtLZs/DoozByJPzv/8Kf/qRyJd7B5wvWv6qqqqKsrIz4+Pgffa2oqIioqChGjBjBp59+aiCdNKSJN0yk9N5SttVso98r/djx1Q7TkUTkZ+zYYd1fVVwMpaUwYYLpRCIXzzYF6+TJk4wfP5558+bRqlWrH3wtNjaWL7/8krKyMn75y18yZswYQymlIXUK6MTqO1eTFpNGwpIE5m2Zp6N2RDxQfT08/zwkJEB6OqxeDZ06mU4l4hyfvwcL4Ny5c4wcOZJhw4Yxc+bMn338NddcQ0lJCW3btv3B7/v5+fHoo49+/+vExEQSExNdHVfcYM/Xe0jNS6V54+YsGbOELq27mI4kIsC+fdZN7GfPwtKlEBpqOpFcqsLCQgoLC7//9eOPP26r23FsUbBSUlJo164dzz333Hm/fvjwYYKDrd2/t23bxoQJE6iqqvrR4+x2g56vq6uv4w8f/oE/FP2BPwz9AymRKfj5+ZmOJWJLDodVqP77v+Ghh6wfjRubTiWuZLc11OcL1ubNm0lISCA8PBw/Pz/8/Px46qmn2LdvH35+fqSnp/PSSy/x8ssv07RpU6644gr++Mc/nvc+Lbu9Oeyi/FA5ybnJhLYNJWtkFkH+QaYjidjKkSNw772wd6911E1EhOlE0hDstob6fMFyJbu9Oezk9LnTPFr4KEvLl5I5IpPbet5mOpKILeTnQ0aGdVnwscegeXPTiaSh2G0NVcFygt3eHHa06ctNpOalMqjrIJ6/9XmubH6l6UgiPunECXjgAfjgA+vS4IABphNJQ7PbGmqbTxGKXIyBXQZSnlFOs8bNiHg5gsKqQtORRHzO++9bR900b24ddaNyJb5IEywn2K19292a3WtIW5XGxN4TeWrIU7Ro0sJ0JBGvduoUPPwwLFsGixfDsGGmE4k72W0N1QRL5AKGhw2nIqOC6m+riV0YS8mBEtORRLxWSYl1zE1NDVRUqFyJ79MEywl2a99icTgc/OWTvzCzYCb3972f2TfNpkmjJqZjiXiFs2fh6aetg5nnzYPbbzedSEyx2xqqguUEu7055Idqvq1hysopHP/uODljcujRrofpSCIebedO64DmNm0gOxtCQkwnEpPstobqEqHIRQq5MoSCuwpIjUxlQPYA5m+dr6N2RM6jvh7mz4eBA2HyZCgoULkS+9EEywl2a99yYbuP7SYlL4VWzVqRPTqbzq07m44k4hH277dKVW0t5ORAWJjpROIp7LaGaoIlcgnCrgrjg8kfcHO3m4ldGMvrFa/b6h8OkX/ncFi7sMfGwpAh1v5WKldiZ5pgOcFu7VsuTunBUpJzk+nZrieZIzNp17Kd6UgibvXVV9Zu7Lt2WSUrKsp0IvFEdltDNcESuUzRHaP5KP0jrgm8hoiXI3hr11umI4m4zapV1qahoaFQXKxyJfIPmmA5wW7tW5y3cd9GJuVNYsg1Q3gu6TkCmgeYjiTSIL79Fh58ENavt466uekm04nE09ltDdUES8SFEromUJ5RDkBkZiQb9200nEjE9TZutKZWjRpBebnKlcj5aILlBLu1b7k8b+16i/RV6dwVfhdPDH5CR+2I1/vuO/jNb+CNN2DhQhg50nQi8SZ2W0M1wRJpICOvG0nFtAq+OP4FfRb2ofRgqelIIpestBT69IGqKuuoG5UrkZ+mgiXSgNq1bMfyXyzn1wN/TdLrSTz1wVOcqz9nOpbIRTt3Dn73O0hKgtmzYflyaKcPyor8LF0idILdxpviWvtP7Gdy/mRqz9aSMyaHsKu0SZB4tl27ICUFAgKso246az9duQx2W0M1wRJxk86tO/NO8jvcFX4X/bP7s6B4ga3+sRHv4XBYhzP37w933w1r16pciThLEywn2K19S8PZeXQnKXkpBLYIJHt0NiFX6qA28QzV1TBlCpw4YR1100NnmouL2G0N1QRLxIAe7XqwecpmBnYeSHRWNH+u/LOt/uERz+NwWJ8OjImBhATYvFnlSuRyaILlBLu1b3GPkgMlJOcmEx4czoLhC7iq5VWmI4nNHDsG06bB9u3WUTcxMaYTiS+y2xqqCZaIYbGdYilJL+HqgKuJyIxgze41piOJjaxeDRER1j1WJSUqVyKuogmWE+zWvsX9CqsKmZQ3iaTQJOYmzaVVs1amI4mPOnkSZs2Cd96BV1+FxETTicTX2W0N1QRLxIMkdkukYloFZ+vPEpkZyeYvN5uOJD5o0ybrqJtz56yjblSuRFxPEywn2K19i1n5n+WTsTqD1MhUHk98nOZNmpuOJF7u9Gn47W+t+6wyM2H0aNOJxE7stoZqgiXioW7reRvlGeXsPLaTuEVxlB8qNx1JvFh5OcTFwe7d1s9VrkQalgqWiAcL8g9ixYQVzOo3i1teu4VnNj1DXX2d6VjiRerq4JlnYOhQeOgh+OtfoX1706lEfJ8uETrBbuNN8Sz7ju9jcv5kTtedJmdMDqFtQ01HEg+3Zw+kpkKLFtaN7F26mE4kdma3NVQTLBEv0TWwK++mvMuE6ydw4ys3kvVRlq3+sZKL53BY91j16wcTJ8K6dSpXIu6mCZYT7Na+xXPt+GoHybnJtPdvzyujX6FTQCfTkcRDHDgAU6fC0aPWUTe9eplOJGKx2xqqCZaIF+rVvhdFU4uID4knOiuaZZ8sMx1JPMCyZRAdDTfeCB9+qHIlYpImWE6wW/sW71BcU0xybjIxHWN4cfiLtL2irelI4mZffw3Tp0NZmbUFQ58+phOJ/Jjd1lBNsES8XFxIHKX3lhLkH0TEyxGs3bPWdCRxo4IC66ibDh3g449VrkQ8hSZYTrBb+xbvs/6L9UzOn8yIsBHMGToH/2b+piNJA6mttbZdWLPG+oTg4MGmE4n8NLutoZpgifiQwdcMpiKjgtqztURlRVG0v8h0JGkAH34IUVFw6hRUVKhciXgiTbCcYLf2Ld5txY4V3Lf6PqZGT+XRxEdp1riZ6Uhymc6cgccesyZWCxbA2LGmE4lcPLutoZpgifiocb3GUZ5RTuWRSvou6kvl4UrTkeQyVFZC376wfbt1M7vKlYhnU8ES8WHBrYLJvz2fmfEzGZwzmDmb5+ioHS9TVwe//711GXDmTMjLg+Bg06lE5OfoEqET7DbeFN9SdbyKSXmTqHPUsXTMUrq36W46kvyMvXuto24aN4YlS6BbN9OJRC6d3dZQTbBEbKJbYDfWp65nbM+xxC+OZ/HHi231j503cThg0SKIj4dx42D9epUrEW+jCZYT7Na+xXdtP7Kd5NxkOgV0YvHoxXRo1cF0JPk/Bw/CPffAoUPWpqHXX286kYhr2G0N1QRLxIZ6B/Vmyz1biOkYQ1RmFG9++qbpSAIsX25tvxAbC1u2qFyJeDNNsJxgt/Yt9rC1eivJucn0DenL/GHzaXNFG9ORbOebb+D++6G42DqgOT7edCIR17PbGqoJlojNxV8dT1lGGW1atCEyM5J1n68zHclW1q2zjrpp2xZKS1WuRHyFJlhOsFv7FvtZ9/k6pq6cym09buPZoc/SsmlL05F8Vm0t/OpXsHIlZGfDLbeYTiTSsOy2hmqCJSLfGxo6lPKMco6fPk50VjRbq7eajuSTtmyB6Gg4ccI66kblSsT3aILlBLu1b7G3Nz99k+lrppMek84jgx7RUTsucOYMPPGEtQXDiy/C+PGmE4m4j93WUBUsJ9jtzSFy8G8HSVuVxsGTB8kZk0PvoN6mI3mt7dshORlCQqyC1UE7Y4jN2G0N1SVCEbmgjgEdWXXHKqb1mUbi0kSeK3qOeke96Vhepa4O5s6FxESYPt2650rlSsT3aYLlBLu1b5F/tfebvaTmpdLYrzFLxiyhW2A305E8XlWVddSNw2EdddNdpxOJjdltDdUES0QuSvc23SlMLWRE2AjiFsWRXZptq38sneFwWJ8MjIuDUaPg/fdVrkTsRhMsJ9itfYtcSOXhSpJzk+ka2JWFIxcS3CrYdCSPcfgwpKXB/v3WUTc33GA6kYhnsNsaqgmWiDgtPDicbWnbuKH9DURmRpK7I9d0JI+wYgVERlobh27dqnIlYmeaYDnBbu1b5GIU7S8iJS+F/p3788KtL9C6RWvTkdzu+HGYMQOKiqyjbvr1M51IxPPYbQ3VBEtELku/zv0ou7cM/6b+RGRG8N7e90xHcqv33rOmVgEBUFamciUiFk2wnGC39i3irLV71jJ15VTGXz+ep4c8zRVNrzAdqcH8/e8we7Z1WXDxYkhKMp1IxLPZbQ3VBEtEXCbp2iQqplVwpPYIMQtjKK4pNh2pQRQXQ0wMHD1qHXWjciUi/04TLCfYrX2LXI5lnyxjRsEMpvWZxsM3PUzTxk1NR7psZ8/Ck09CZibMnw8TJphOJOI97LaGqmA5wW5vDpHLdeBvB7hn5T0cqT3Ca2Nfo1f7XqYjXbIdO6yjboKCrEuCnTqZTiTiXey2huoSoYg0mE4BnVh952rSYtJIWJLAvC3zvO6onfp6eP55SEiA9HRYvVrlSkR+niZYTrBb+xZxpT1f7yE1L5XmjZuzZMwSurTuYjrSz9q3DyZNsi4NLl0KoaGmE4l4L7utoZpgiYhbXNv2WjZO2khSaBKxC2NZWrbUY/+x/cfZgX36wK23woYNKlci4hxNsJxgt/Yt0lDKD5WTnJtMaNtQskZmEeQfZDrS944cgXvvhb17raNuIiJMJxLxDXZbQzXBEhG3i+wQSXFaMT2u6kFkZiT5n+WbjgRAfr61aWjPnrBtm8qViFw6TbCcYLf2LeIOm77cRGpeKoO6DuL5W5/nyuZXuj3DiRPwwAPwwQfWvVYDBrg9gojPs9saqgmWiBg1sMtAyjPKada4GREvR1BYVejW13//fWtq1by5ddSNypWIuIImWE6wW/sWcbc1u9eQtiqNib0n8tSQp2jRpEWDvdb0kcM5AAAgAElEQVSpU/Dww7BsmbWv1bBhDfZSIoL91lBNsETEYwwPG05FRgXV31YTuzCWkgMlDfI6JSUQGws1NdZRNypXIuJqmmA5wW7tW8QUh8PBXz75CzMLZnJ/3/uZfdNsmjRqctnPe/YsPP00vPQSzJsHt9/ugrAiclHstoaqYDnBbm8OEdNqvq1hysopHP/uODljcujRrsclP9fOndZRN23aQHY2hIS4MKiI/Cy7raG6RCgiHivkyhAK7iogNTKVAdkDmL91vtNH7dTXWwczDxwIkydDQYHKlYg0PJ8vWNXV1QwePJjevXsTHh7OCy+8cN7HzZgxg7CwMKKioigrK3NzShG5ED8/P+6Lu4+iqUW88ckbJL2exP4T+y/qz+7fD//xH/DGG/DhhzBtGvj5NXBgERE8qGDdcccd3HHHHWRlZbFjxw6XPW+TJk147rnn2L59O0VFRbz00kt89tlnP3jM22+/zeeff87u3bvJysoiIyPDZa8vIq4RdlUYH0z+gJu73Uzswlher3j9gpcbHA5rF/bYWBgyxNrfKizMzYFFxNYu/65RF4mLiyMnJ4fly5fjcDho164dCQkJDBo0iEGDBhEeHn5Jz9uhQwc6dOgAQKtWrejVqxc1NTX07Nnz+8fk5+eTkpICQHx8PCdOnODw4cMEBwdf/n+YiLhMk0ZN+H83/T+GXTuM5Nxk8j7LI3NkJu1atvv+MV99BRkZsGsXvPMOREUZDCwituUxE6wHH3yQsrIyjh07Rl5eHpMmTaK6uppZs2YRFRVFu3btuOeee9i7d+8lv0ZVVRVlZWXEx8f/4Pdramro3Lnz978OCQmhpqbmkl9HRBpWdMdoPkr/iGsCryHi5Qje2vUWAKtWWZuGhoZCcbHKlYiY4zETrH9o3bo1o0aNYtSoUQDU1tbyP//zP1RWVrJ69WreeOMNli5dyi9+8QunnvfkyZOMHz+eefPm0apVq4aILiJu1KJJC+b8xxxG9RhFyopJzFqUx+n8P7JsWQA33WQ6nYjYnccVrH/n7+/PSy+9xH//93+zYcMG8vLyeOihh+jWrRtxcXEX9Rznzp1j/PjxJCcnc9ttt/3o6yEhIezf/8+bZqurqwm5wMeMHnvsse9/npiYSGJiolP/PSLiYvsScCwoh7H/hd99kTi6LAESTKcSsb3CwkIKCwtNxzDH4SHeeOMNR2RkpOMXv/iFIz8/33HmzJkffP3+++///uc1NTWOu++++6KfOzk52fFf//VfF/z66tWrHcOHD3c4HA5HUVGRIz4+/ryP86D/uURs79Qph2PWLIejY0eHY9Uq6/dW7Vzl6PiHjo6H1j7kOHX2lNmAIvIDdltDPWaC9ac//YkpU6ZQUFDAuHHjCAgI4Oabb6ZHjx58/fXXP/hkYadOnb6/cf3nbN68mT/96U+Eh4cTHR2Nn58fTz31FPv27cPPz4/09HSGDx/OmjVruPbaa/H39+fVV19tqP9MEXGB0lJr09CePa2jbtr93z3uI68bScW0CjLeyqDPwj68NvY1ojtGmw0rIrbkMTu533///fzxj3+kSZMmHDhwgGXLlvHOO++wb98+unbtyrx587juuuuIiopi0KBBXHHFFTzzzDNuzWi3XWhFPM25c/Dss9YxN3/8I9x55/n3tXI4HPyp8k88uPZBHrjxAf5nwP+45KgdEbl0dltDPaZgffHFF8yZM4eEhAT+8z//k6ZNm573cXfccQfvvvsuWVlZjBs3zq0Z7fbmEPEku3ZBSgoEBFhH3fzLB38vaP+J/UzOn0zt2VpyxuQQdpU2wxIxxW5rqMcUrH/YvHkzYWFhBAUFmY7yI3Z7c4h4AocDFiyARx+Fxx6D++6DRk5sMFPvqGdB8QIe3/A4jyc+zrQ+0/DTdu4ibme3NdTjCpYns9ubQ8S06mqYMgVOnICcHOhx6Wc9s/PoTlLyUghsEUj26GxCrtSBhCLuZLc11GM2GhUR+QeHwzo/MCYGEhJg8+bLK1cAPdr1YPOUzQzsPJDorGj+XPlnW/1jLyLupQmWE+zWvkVMOHbMOpR5+3brPMGYGNe/RsmBEpJzkwkPDmfB8AVc1fIq17+IiPyA3dZQTbBExGOsXg0REdYN7CUlDVOuAGI7xVKSXsLVAVcTkRnBmt1rGuaFRMS2NMFygt3at4i7nDwJs2ZZhzO/+iq484CEwqpCJuVNIik0iblJc2nVTEdpiTQEu62hmmCJiFGbNlkHNJ87B+Xl7i1XAIndEqmYVsHZ+rNEZkay+cvN7g0gIj5JEywn2K19izSk06fht7+17rPKzITRo00ngvzP8slYnUFqZCqPJz5O8ybNTUcS8Rl2W0M1wRIRtysvh7g42L3b+rknlCuA23reRnlGOTuP7SRuURzlh8pNRxIRL6WCJSJuU1cHzzwDQ4fCQw/BX/8K7dubTvVDQf5BrJiwgln9ZnHLa7fwzKZnqKuvMx1LRLyMLhE6wW7jTRFX2rMHUlOhRQvrRvYuXUwn+nn7ju9jcv5kTtedJmdMDqFtQ01HEvFadltDNcESkQblcFj3WPXrBxMnwrp13lGuALoGduXdlHeZcP0EbnzlRrI+yrLVAiEil04TLCfYrX2LXK4DB2DqVDh61Drqplcv04ku3Y6vdpCcm0x7//a8MvoVOgV0Mh1JxKvYbQ3VBEtEGsSyZRAdDTfeCB9+6N3lCqBX+14UTS0iPiSe6Kxoln2yzHQkEfFgmmA5wW7tW+RSfP01TJ8OZWXWFgx9+phO5HrFNcUk5yYT0zGGF4e/SNsr2pqOJOLx7LaGaoIlIi5TUGAdddOhA3z8sW+WK4C4kDhK7y0lyD+IiJcjWLtnrelIIuJhNMFygt3at8jFqq21tl1Ys8b6hODgwaYTuc/6L9YzOX8yI8JGMGfoHPyb+ZuOJOKR7LaGaoIlIpflww8hKgpOnYKKCnuVK4DB1wymIqOC2rO1RGVFUbS/yHQkEfEAmmA5wW7tW+SnnDkDjz1mTawWLICxY00nMm/FjhXct/o+pkZP5dHER2nWuJnpSCIew25rqCZYIuK0ykro2xe2b7duZle5sozrNY7yjHIqj1TSd1FfKg9Xmo4kIoaoYInIRaurg9//3roMOHMm5OVBcLDpVJ4luFUw+bfnMzN+JoNzBjNn8xwdtSNiQ7pE6AS7jTdF/tXevdZRN40bw5Il0K2b6USer+p4FZPyJlHnqGPpmKV0b9PddCQRY+y2hmqCJSI/yeGARYsgPh7GjYP161WuLla3wG6sT13P2J5jiV8cz+KPF9tqgRGxM02wnGC39i1y8CDccw8cOmRtGnr99aYTea/tR7aTnJtMp4BOLB69mA6tOpiOJOJWdltDNcESkfNavtzafiE2FrZsUbm6XL2DerPlni3EdIwhKjOKNz9903QkEWlAmmA5wW7tW+zpm2/g/vuhuNg6oDk+3nQi37O1eivJucn0DenL/GHzaXNFG9ORRBqc3dZQTbBE5Hvr1llH3bRtC6WlKlcNJf7qeMoyymjTog2RmZGs+3yd6Ugi4mKaYDnBbu1b7KO2Fn71K1i5ErKz4ZZbTCeyj3Wfr2Pqyqnc1uM2nh36LC2btjQdSaRB2G0N1QRLxOa2bIHoaDhxwjrqRuXKvYaGDqU8o5zjp48TnRXN1uqtpiOJiAtoguUEu7Vv8W1nzsATT1hbMLz4IowfbzqRvPnpm0xfM530mHQeGfSIjtoRn2K3NVQFywl2e3OI79q+HZKTISTEKlgdtGOAxzj4t4OkrUrj4MmD5IzJoXdQb9ORRFzCbmuoLhGK2EhdHcydC4mJMH26dc+VypVn6RjQkVV3rGJan2kkLk3kuaLnqHfUm44lIk7SBMsJdmvf4luqqqyjbhwO66ib7jq1xePt/WYvqXmpNPZrzJIxS+gW2M10JJFLZrc1VBMsER/ncFifDIyLg1Gj4P33Va68Rfc23SlMLWRE2AjiFsWRXZptqwVKxJtpguUEu7Vv8X6HD0NaGuzfbx11c8MNphPJpao8XElybjJdA7uycORCglsFm44k4hS7raGaYIn4qBUrIDLS2jh061aVK28XHhzOtrRt3ND+BiIzI8ndkWs6koj8BE2wnGC39i3e6fhxmDEDioqso2769TOdSFytaH8RKXkp9O/cnxdufYHWLVqbjiTys+y2hmqCJeJD3nvPmloFBEBZmcqVr+rXuR9l95bh39SfiMwI3tv7nulIIvJvNMFygt3at3iPv/8dZs+2LgsuXgxJSaYTibus3bOWqSunMv768Tw95GmuaHqF6Ugi52W3NVQTLBEvV1wMMTFw9Kh11I3Klb0kXZtExbQKjtQeIWZhDMU1xaYjiQiaYDnFbu1bPNvZs/Dkk5CZCfPnw4QJphOJacs+WcaMghlM6zONh296mKaNm5qOJPI9u62hKlhOsNubQzzXjh3WUTdBQdYlwU6dTCcST3Hgbwe4Z+U9HKk9wmtjX6NX+16mI4kA9ltDdYlQxIvU18Pzz0NCAqSnw+rVKlfyQ50COrH6ztWkxaSRsCSBeVvm6agdEQM0wXKC3dq3eJZ9+2DSJOvS4NKlEBpqOpF4uj1f7yE1L5XmjZuzZMwSurTuYjqS2Jjd1lBNsEQ83D/ODuzTB269FTZsULmSi3Nt22vZOGkjSaFJxC6MZWnZUlstcCImaYLlBLu1bzHvyBG4917Yu9c66iYiwnQi8Vblh8pJzk0mtG0oWSOzCPIPMh1JbMZua6gmWCIeKj/f2jS0Z0/Ytk3lSi5PZIdIitOK6XFVDyIzI8n/LN90JBGfpgmWE+zWvsWMEyfggQfggw+se60GDDCdSHzNpi83kZqXyqCug3j+1ue5svmVpiOJDdhtDdUES8SDvP++NbVq3tw66kblShrCwC4DKc8op1njZkS8HEFhVaHpSCI+RxMsJ9itfYv7nDoFDz8My5ZZ+1oNG2Y6kdjFmt1rSFuVxsTeE3lqyFO0aNLCdCTxUXZbQzXBEjGspARiY6GmxjrqRuVK3Gl42HAqMiqo/raa2IWxlBwoMR1JxCdoguUEu7VvaVhnz8LTT8NLL8G8eXD77aYTiZ05HA7+8slfmFkwk/v73s/sm2bTpFET07HEh9htDVXBcoLd3hzScHbutI66adMGsrMhJMR0IhFLzbc1TFk5hePfHSdnTA492vUwHUl8hN3WUF0iFHGj+nrrYOaBA2HyZCgoULkSzxJyZQgFdxWQGpnKgOwBzN86X0ftiFwCTbCcYLf2La61f79VqmprIScHwsJMJxL5abuP7SYlL4VWzVqRPTqbzq07m44kXsxua6gmWCINzOGwdmGPjYUhQ6z9rVSuxBuEXRXGB5M/4OZuNxO7MJbXK1631QIpcjk0wXKC3dq3XL6vvoKMDNi1yypZUVGmE4lcmtKDpSTnJtOzXU8yR2bSrmU705HEy9htDdUES6SBrFplbRoaGgrFxSpX4t2iO0bzUfpHXBN4DREvR/DWrrdMRxLxaJpgOcFu7VsuzbffwoMPwvr11lE3N91kOpGIa23ct5FJeZMYcs0Qnkt6joDmAaYjiRew2xqqCZaIC23caE2tGjWC8nKVK/FNCV0TKM8oByAyM5KN+zYaTiTieTTBcoLd2rdcvO++g9/8Bt54AxYuhJEjTScScY+3dr1F+qp07gq/iycGP6GjduSC7LaGaoIlcplKS6FPH6iqso66UbkSOxl53UgqplXwxfEv6LOwD6UHS01HEvEIKlgil+jcOfjd7yApCWbPhuXLoZ0+WCU21K5lO5b/Yjm/Hvhrkl5P4qkPnuJc/TnTsUSM0iVCJ9htvCkXtmsXpKRAQIB11E1n7b8oAsD+E/uZnD+Z2rO15IzJIewqbfomFrutoZpgiTjB4bAOZ+7fH+6+G9auVbkS+VedW3fmneR3uCv8Lvpn92dB8QJbLaoi/6AJlhPs1r7lh6qrYcoUOHHCOuqmh87AFflJO4/uJCUvhcAWgWSPzibkSh28aWd2W0M1wRL5GQ6H9enAmBhISIDNm1WuRC5Gj3Y92DxlMwM7DyQ6K5o/V/7ZVgus2JsmWE6wW/sWOHYMpk2D7duto25iYkwnEvFOJQdKSM5NJjw4nAXDF3BVy6tMRxI3s9saqgmWyAWsXg0REdY9ViUlKlcilyO2Uywl6SVcHXA1EZkRrNm9xnQkkQalCZYT7Na+7erkSZg1C955B159FRITTScS8S2FVYVMyptEUmgSc5Pm0qpZK9ORxA3stob6/ARr6tSpBAcHExERcd6vb9iwgcDAQGJiYoiJieHJJ590c0LxJJs2WUfdnDtnHXWjciXieondEqmYVsHZ+rNEZkay+cvNpiOJuJzPT7A2bdpEq1atSElJoaKi4kdf37BhA3PnzmXlypU/+1x2a992cvo0/Pa31n1WmZkwerTpRCL2kP9ZPhmrM0iNTOXxxMdp3qS56UjSQOy2hvr8BGvgwIG0adPmJx9jp2+4/Fh5OcTFwe7d1s9VrkTc57aet1GeUc7OYzuJWxRH+aFy05FEXMLnC9bFKCoqIioqihEjRvDpp5+ajiNuUlcHzzwDQ4fCQw/BX/8K7dubTiViP0H+QayYsIJZ/WZxy2u38MymZ6irrzMdS+Sy+PwlQoB9+/YxatSo814iPHnyJI0aNaJly5a8/fbbzJw5k127dp33eew23vRle/ZAaiq0aGHdyN6li+lEIgKw7/g+JudP5nTdaXLG5BDaNtR0JHERu62hTUwHMK1Vq39+emXYsGHcd999fP3117Rt2/a8j3/ssce+/3liYiKJugvaqzgckJUFjzxi/fjlL6GR5rgiHqNrYFfeTXmX+Vvnc+MrN/LkzU+SHpuOn5+f6WjipMLCQgoLC03HMMYWE6yqqipGjRpFZWXlj752+PBhgoODAdi2bRsTJkygqqrqvM9jt/btaw4cgKlT4ehR66ibXr1MJxKRn7Ljqx0k5ybT3r89r4x+hU4BnUxHkstgtzXU5/+/+5133kn//v3ZtWsXXbp04dVXXyUrK4uFCxcC8Oabb3LDDTcQHR3NAw88wLJlywwnloawbBlER8ONN8KHH6pciXiDXu17UTS1iPiQeKKzoln2if59Fu9hiwmWq9itffuCr7+G6dOhrMzagqFPH9OJRORSFNcUk5ybTEzHGF4c/iJtrzj/bRziuey2hvr8BEvsq6DAOuqmQwf4+GOVKxFvFhcSR+m9pQT5BxHxcgRr96w1HUnkJ2mC5QS7tW9vVVtrbbuwZo31CcHBg00nEhFXWv/FeibnT2ZE2AjmDJ2DfzN/05HkIthtDdUES3zKhx9CVBScOgUVFSpXIr5o8DWDqciooPZsLVFZURTtLzIdSeRHNMFygt3atzc5cwYee8yaWC1YAGPHmk4kIu6wYscK7lt9H1Ojp/Jo4qM0a9zMdCS5ALutoZpgiderrIS+fWH7dutmdpUrEfsY12sc5RnlVB6ppO+ivlQe/vF2PCImqGCJ16qrg9//3roMOHMm5OXB/21pJiI2EtwqmPzb85kZP5PBOYOZs3mOjtoR43SJ0Al2G296sr17raNuGjeGJUugWzfTiUTEE1Qdr2JS3iTqHHUsHbOU7m26m44k/8dua6gmWOJVHA5YtAji42HcOFi/XuVKRP6pW2A31qeuZ2zPscQvjmfxx4tttaiL59AEywl2a9+e5uBBuOceOHTI2jT0+utNJxIRT7b9yHaSc5PpFNCJxaMX06FVB9ORbM1ua6gmWOIVli+3tl+IjYUtW1SuROTn9Q7qzZZ7thDTMYaozCje/PRN05HERjTBcoLd2rcn+OYbuP9+KC62DmiOjzedSES80dbqrSTnJtM3pC/zh82nzRVtTEeyHbutoZpgicdat8466qZtWygtVbkSkUsXf3U8ZRlltGnRhsjMSNZ9vs50JPFxmmA5wW7t25TaWvjVr2DlSsjOhltuMZ1IRHzJus/XMXXlVG7rcRvPDn2Wlk1bmo5kC3ZbQzXBEo+yZQtER8OJE9ZRNypXIuJqQ0OHUp5RzvHTx4nOimZr9VbTkcQHaYLlBLu1b3c6cwaeeMLaguHFF2H8eNOJRMQO3vz0TaavmU56TDqPDHpER+00ILutoSpYTrDbm8Ndtm+H5GQICbEKVgd9klpE3Ojg3w6StiqNgycPkjMmh95BvU1H8kl2W0N1iVCMqauDuXMhMRGmT7fuuVK5EhF36xjQkVV3rGJan2kkLk3kuaLnqHfUm44lXk4TLCfYrX03pKoq66gbh8M66qa7TrMQEQ+w95u9pOal0tivMUvGLKFbYDfTkXyG3dZQTbDErRwO65OBcXEwahS8/77KlYh4ju5tulOYWsiIsBHELYojuzTbVqVAXEcTLCfYrX272uHDkJYG+/dbR93ccIPpRCIiF1Z5uJLk3GS6BnZl4ciFBLcKNh3Jq9ltDdUES9xixQqIjLQ2Dt26VeVKRDxfeHA429K2cUP7G4jMjCR3R67pSOJFNMFygt3atyscPw4zZkBRkXXUTb9+phOJiDivaH8RKXkp9O/cnxdufYHWLVqbjuR17LaGaoIlDea996ypVUAAlJWpXImI9+rXuR9l95bh39SfiMwI3tv7nulI4uE0wXKC3dr3pfr732H2bOuy4OLFkJRkOpGIiOus3bOWqSunMv768Tw95GmuaHqF6UhewW5rqCZY4lLFxRATA0ePWkfdqFyJiK9JujaJimkVHKk9QszCGIprik1HEg+kCZYT7Na+nXH2LDz5JGRmwvz5MGGC6UQiIg1v2SfLmFEwg2l9pvHwTQ/TtHFT05E8lt3WUBUsJ9jtzXGxduywjroJCrIuCXbqZDqRiIj7HPjbAe5ZeQ9Hao/w2tjX6NW+l+lIHslua6guEcolq6+H55+HhARIT4fVq1WuRMR+OgV0YvWdq0mLSSNhSQLztszTUTuiCZYz7Na+f8q+fTBpknVpcOlSCA01nUhExLw9X+8hNS+V5o2bs2TMErq07mI6ksew2xqqCZY45R9nB/bpA7feChs2qFyJiPzDtW2vZeOkjSSFJhG7MJalZUttVSrknzTBcoLd2ve/O3IE7r0X9u61jrqJiDCdSETEc5UfKic5N5nQtqFkjcwiyD/IdCSj7LaGaoIlFyU/39o0tGdP2LZN5UpE5OdEdoikOK2YHlf1IDIzkvzP8k1HEjfSBMsJdmvfACdOwAMPwAcfWPdaDRhgOpGIiPfZ9OUmUvNSGdR1EM/f+jxXNr/SdCS3s9saqgmWXND771tTq+bNraNuVK5ERC7NwC4DKc8op1njZkS8HEFhVaHpSNLANMFygl3a96lT8PDDsGyZta/VsGGmE4mI+I41u9eQtiqNib0n8tSQp2jRpIXpSG5hlzX0HzTBkh8oKYHYWKipsY66UbkSEXGt4WHDqciooPrbamIXxlJyoMR0JGkAmmA5wZfb99mz8PTT8NJLMG8e3H676UQiIr7N4XDwl0/+wsyCmdzf935m3zSbJo2amI7VYHx5DT0fFSwn+OqbY+dO66ibNm0gOxtCQkwnEhGxj5pva5iycgrHvztOzpgcerTrYTpSg/DVNfRCdInQxurrrYOZBw6EyZOhoEDlSkTE3UKuDKHgrgJSI1MZkD2A+Vvn66gdH6AJlhN8qX3v32+VqtpayMmBsDDTiUREZPex3aTkpdCqWSuyR2fTuXVn05FcxpfW0IuhCZbNOBzWLuyxsTBkiLW/lcqViIhnCLsqjA8mf8DN3W4mdmEsr1e8bqtS4ks0wXKCt7fvr76CjAzYtcsqWVFRphOJiMiFlB4sJTk3mZ7tepI5MpN2LduZjnRZvH0NdZYmWDaxapW1aWhoKBQXq1yJiHi66I7RfJT+EdcEXkPEyxG8test05HECZpgOcEb2/e338KDD8L69dZRNzfdZDqRiIg4a+O+jUzKm8SQa4bwXNJzBDQPMB3Jad64hl4OTbB82MaN1tSqUSMoL1e5EhHxVgldEyjPKAcgMjOSjfs2Gk4kP0cTLCd4S/v+7jv4zW/gjTdg4UIYOdJ0IhERcZW3dr1F+qp07gq/iycGP+E1R+14yxrqKppg+ZjSUujTB6qqrKNuVK5ERHzLyOtGUjGtgi+Of0GfhX0oPVhqOpKchwqWjzh3Dn73O0hKgtmzYflyaOfdHzgREZELaNeyHct/sZxfD/w1Sa8n8dQHT3Gu/pzpWPIvdInQCZ463ty1C1JSICDAOuqms+/sSyciIj9j/4n9TM6fTO3ZWnLG5BB2lWdubuipa2hD0QTLizkc1uHM/fvD3XfD2rUqVyIidtO5dWfeSX6Hu8Lvon92fxYUL7BVkfFUmmA5wZPad3U1TJkCJ05YR9308M2zQUVExAk7j+4kJS+FwBaBZI/OJuRKzzlg1pPWUHfQBMvLOBzWpwNjYiAhATZvVrkSERFLj3Y92DxlMwM7DyQ6K5o/V/7ZVqXGk2iC5QTT7fvYMZg2DbZvt466iYkxFkVERDxcyYESknOTCQ8OZ8HwBVzV8iqjeUyvoe6mCZaXWL0aIiKse6xKSlSuRETkp8V2iqUkvYSrA64mIjOCNbvXmI5kK5pgOcFE+z55EmbNgnfegVdfhcREt768iIj4gMKqQiblTSIpNIm5SXNp1ayV2zNogiUeY9Mm66ibc+eso25UrkRE5FIkdkukYloFZ+vPEpkZyeYvN5uO5PM0wXKCu9r36dPw299a91llZsLo0Q3+kiIiYhP5n+WTsTqD1MhUHk98nOZNmrvldTXBEqPKyyEuDnbvtn6uciUiIq50W8/bKM8oZ+exncQtiqP8ULnpSD5JBctD1NXBM8/A0KHw0EPw179C+/amU4mIiC8K8g9ixYQVzOo3i1teu4VnNj1DXX2d6Vg+RZcIndBQ4809eyA1FVq0sG5k79LF5S8hIiJyXvuO72Ny/mRO150mZ0wOoW1DG+R1dIlQ3MbhsO6x6tcPJk6EdetUrkRExL26Bg66C94AAAhxSURBVHbl3ZR3mXD9BG585UayPsqyVRFqKJpgOcGV7fvAAZg6FY4etY666dXLJU8rIiJyyXZ8tYPk3GTa+7fnldGv0Cmgk8ueWxMsaXDLlkF0NNx4I3z4ocqViIh4hl7te1E0tYj4kHiis6JZ9sky05G8liZYTrjc9v311zB9OpSVWVsw9OnjwnAiIiIuVFxTTHJuMjEdY3hx+Iu0vaLtZT2fJljSIAoKrKNuOnSAjz9WuRIREc8WFxJH6b2lBPkHEfFyBGv3rDUdyatoguWES2nftbXWtgtr1lifEBw8uIHCiYiINJD1X6xncv5kRoSNYM7QOfg383f6OTTBEpf58EOIioJTp6CiQuVKRES80+BrBlORUUHt2VqisqIo2l9kOpLH0wTLCRfbvs+cgccesyZWCxbA2LENn01ERMQdVuxYwX2r72Nq9FQeTXyUZo2bXdSf0wRLLktlJfTtC9u3Wzezq1yJiIgvGddrHOUZ5VQeqaTvor5UHq40HckjqWC5SF0d/P731mXAmTMhLw+Cg02nEhERcb3gVsHk357PzPiZDM4ZzJzNc3TUzr/RJUInXGi8uXevddRN48awZAl06+b2aCIiIkZUHa9iUt4k6hx1LB2zlO5tup/3cbpE6GOmTp1KcHAwERERF3zMjBkzCAsLIyoqirKysot+bocDFi2C+HgYNw7Wr1e5EhERe+kW2I31qesZ23Ms8YvjWfzxYlsVqQvx+YI1efJk1q698N4db7/9Np9//jm7d+8mKyuLjIyMi3regwdh5EjrLMENG+C//gsa+fz/mr6lsLDQdARxMX1PfYu+n96jkV8jHuz3IIWphSwoXsCoP4/i0MlDpmMZ5fOVYODAgbRp0+aCX8/PzyclJQWA+Ph4Tpw4weHDh3/yOZcvt7ZfiI2FLVvg+utdGlncRP94+x59T32Lvp/ep3dQb7bcs4WYjjFEZUbx5qdvmo5kTBPTAUyrqamhc+fO3/86JCSEmpoagi9wh/rdd0NxMaxcaV0aFBERkX9q1rgZ/3vz/zIibATJucnkfZbH/GHzTcdyO5+fYLlamzZQWqpyJSIi8lPir46nLKOMNi3aEJkZaTqO29niU4T79u1j1KhRVFRU/OhrGRkZ3HzzzUycOBGAnj17smHDhvNOsPz8/Bo8q4iIiK+yQeX4ni0uETocjgt+U0ePHs1LL73ExIkT2bJlC4GBgRe8PGinN4aIiIhcOp8vWHfeeSeFhYUcO3aMLl268Pjjj3PmzBn8/PxIT09n+PDhrFmzhmuvvRZ/f39effVV05FFRETEy9niEqGIiIiIO+km9/MoKCigZ8+eXHfddTz77LPnfcylbk4q7vdz388NGzYQGBhITEwMMTExPPnkkwZSysVqyM2Dxf1+7vupv5/epbq6msGDB9O7d2/Cw8N54YX/3879gza1h2Ecf4KNxpBB0kVIRUQUIqSmBmkQHIogqBgdMrgoxm4uRTpklUpdzGAuhRKcUlpQcHGJi4MgomQwLmopQfPXThWhFSSt/d1B6LWxNqmc6/H0fD/b6fkNL7x56UPOyfvPhudcMaMG63z79s0cPHjQVCoV02q1zNGjR827d+/WnSkUCubs2bPGGGNevnxpBgcH7SgVXeimn0+fPjXnz5+3qUJs1bNnz0ypVDKRSGTD+8yns3TqJ/PpLPPz86ZUKhljjFlcXDSHDx927f9QvsFqUywWdejQIe3fv19er1eXLl3So0eP1p35neWksEc3/ZT4AYOT/B/Lg2GfTv2UmE8n2bt3r6LRqCQpEAgoHA6r2WyuO+OWGSVgtWlfPNrX1/fTh+NXy0nx9+mmn5L04sULRaNRnTt3Tm/fvv2TJcJizOf2w3w6U6VS0evXrzXYtjjSLTO67X9FCHQSi8VUq9Xk9/v1+PFjXbx4UXNzc3aXBUDMp1MtLS0pmUwqm80qEAjYXY4t+AarTSgUUq1WW7tuNBoKhUI/nanX65uewd+hm34GAgH5/X5J0pkzZ7S8vKxPnz790TphHeZze2E+nWdlZUXJZFKXL1/WhQsXfrrvlhklYLU5fvy4yuWyqtWqWq2W7t+/r0Qise5MIpHQ1NSUJHVcTgp7ddPPH5/9F4tFGWMUDAb/dKnYAtNheTDz6Syb9ZP5dJ5r167pyJEjGhkZ2fC+W2aUR4RtduzYoYmJCZ0+fVqrq6saHh5WOBxWLpdjOakDddPPhw8fanJyUl6vV7t379aDBw/sLhubYHnw9tKpn8ynszx//lwzMzOKRCIaGBiQx+PR7du3Va1WXTejLBoFAACwGI8IAQAALEbAAgAAsBgBCwAAwGIELAAAAIsRsAAAACxGwAIAALAYAQsAAMBiBCwAAACLEbAAAAAsRsACAACwGAELAADAYgQsAAAAi/XYXQAA/K5Xr15penpaHo9H1WpV9+7dUy6X0+fPn9VsNjU2NqYDBw7YXSYAFyJgAXCkcrmsfD6vbDYrSUqlUorH48rn81pdXdXJkyd17Ngx3bhxw+ZKAbgRAQuAI929e1d37txZu/7y5YuCwaDi8bgajYZGR0d19epV+woE4GoeY4yxuwgA2Kp6va59+/atXff19SmVSunWrVs2VgUA3/GSOwBH+jFczc7O6uPHjxoaGrKxIgD4DwELgOM9efJEu3bt0okTJ9b+9uHDBxsrAuB2BCwAjvP161el02m9efNG0veA1d/fL5/PJ0kyxiiTydhZIgCX4yV3AI5TKBSUyWQUi8XU09Oj9+/fa8+ePWv3x8fHdeXKFRsrBOB2vOQOwHEWFhaUTqfV29srSbp586auX78un8+nnTt3KpFI6NSpUzZXCcDNCFgAAAAW4x0sAAAAixGwAAAALEbAAgAAsBgBCwAAwGIELAAAAIv9C+N/6/TYtMaiAAAAAElFTkSuQmCC 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": "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\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": {} +} diff --git a/pig/pom.xml b/pig/pom.xml index f76a3f90ccc..8c5728ecce5 100644 --- a/pig/pom.xml +++ b/pig/pom.xml @@ -22,22 +22,24 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-pig jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Apache Pig Interpreter Zeppelin interpreter for Apache Pig http://zeppelin.apache.org - 0.17.0 - 2.6.0 + pig + 0.16.0-mapr-1912 + 2.7.0-mapr-1808 0.7.0 1.6.3 2.10 @@ -96,8 +98,10 @@ org.apache.hadoop hadoop-client ${hadoop.version} + provided + + + + + + + + junit @@ -175,58 +194,13 @@ maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/pig - - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/pig - - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - - + + maven-resources-plugin + maven-surefire-plugin diff --git a/pig/src/main/java/org/apache/zeppelin/pig/PigInterpreter.java b/pig/src/main/java/org/apache/zeppelin/pig/PigInterpreter.java index 893741651fc..2bd6fc8022d 100644 --- a/pig/src/main/java/org/apache/zeppelin/pig/PigInterpreter.java +++ b/pig/src/main/java/org/apache/zeppelin/pig/PigInterpreter.java @@ -18,7 +18,6 @@ package org.apache.zeppelin.pig; import org.apache.commons.io.output.ByteArrayOutputStream; -import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.pig.PigServer; import org.apache.pig.impl.logicalLayer.FrontendException; @@ -60,7 +59,7 @@ public void open() { } try { pigServer = new PigServer(execType); - for (Map.Entry entry : getProperty().entrySet()) { + for (Map.Entry entry : getProperties().entrySet()) { if (!entry.getKey().toString().startsWith("zeppelin.")) { pigServer.getPigContext().getProperties().setProperty(entry.getKey().toString(), entry.getValue().toString()); @@ -117,7 +116,7 @@ public InterpreterResult interpret(String cmd, InterpreterContext contextInterpr } PigStats stats = PigStats.get(); if (stats != null) { - String errorMsg = stats.getDisplayString(); + String errorMsg = PigUtils.extactJobStats(stats); if (errorMsg != null) { LOGGER.error("Fail to run pig script, " + errorMsg); return new InterpreterResult(Code.ERROR, errorMsg); @@ -135,7 +134,7 @@ public InterpreterResult interpret(String cmd, InterpreterContext contextInterpr StringBuilder outputBuilder = new StringBuilder(); PigStats stats = PigStats.get(); if (stats != null && includeJobStats) { - String jobStats = stats.getDisplayString(); + String jobStats = PigUtils.extactJobStats(stats); if (jobStats != null) { outputBuilder.append(jobStats); } diff --git a/pig/src/main/java/org/apache/zeppelin/pig/PigQueryInterpreter.java b/pig/src/main/java/org/apache/zeppelin/pig/PigQueryInterpreter.java index d3bc4325c45..6f9a91e2825 100644 --- a/pig/src/main/java/org/apache/zeppelin/pig/PigQueryInterpreter.java +++ b/pig/src/main/java/org/apache/zeppelin/pig/PigQueryInterpreter.java @@ -55,7 +55,7 @@ public PigQueryInterpreter(Properties properties) { } @Override - public void open() { + public void open() throws InterpreterException { pigServer = getPigInterpreter().getPigServer(); maxResult = Integer.parseInt(getProperty(MAX_RESULTS)); } @@ -141,7 +141,7 @@ public InterpreterResult interpret(String st, InterpreterContext context) { } PigStats stats = PigStats.get(); if (stats != null) { - String errorMsg = stats.getDisplayString(); + String errorMsg = PigUtils.extactJobStats(stats); if (errorMsg != null) { return new InterpreterResult(Code.ERROR, errorMsg); } @@ -159,7 +159,7 @@ public PigServer getPigServer() { return this.pigServer; } - private PigInterpreter getPigInterpreter() { + private PigInterpreter getPigInterpreter() throws InterpreterException { LazyOpenInterpreter lazy = null; PigInterpreter pig = null; Interpreter p = getInterpreterInTheSameSessionByClassName(PigInterpreter.class.getName()); diff --git a/pig/src/main/java/org/apache/zeppelin/pig/PigUtils.java b/pig/src/main/java/org/apache/zeppelin/pig/PigUtils.java index 8fc69ed4013..d1a5ddcd06b 100644 --- a/pig/src/main/java/org/apache/zeppelin/pig/PigUtils.java +++ b/pig/src/main/java/org/apache/zeppelin/pig/PigUtils.java @@ -22,19 +22,19 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.pig.PigRunner; -import org.apache.pig.backend.hadoop.executionengine.spark.plan.SparkOperator; -import org.apache.pig.backend.hadoop.executionengine.tez.TezExecType; +// import org.apache.pig.backend.hadoop.executionengine.spark.plan.SparkOperator; +// import org.apache.pig.backend.hadoop.executionengine.tez.TezExecType; import org.apache.pig.tools.pigstats.InputStats; import org.apache.pig.tools.pigstats.JobStats; import org.apache.pig.tools.pigstats.OutputStats; import org.apache.pig.tools.pigstats.PigStats; import org.apache.pig.tools.pigstats.mapreduce.MRJobStats; import org.apache.pig.tools.pigstats.mapreduce.SimplePigStats; -import org.apache.pig.tools.pigstats.spark.SparkJobStats; -import org.apache.pig.tools.pigstats.spark.SparkPigStats; -import org.apache.pig.tools.pigstats.spark.SparkScriptState; -import org.apache.pig.tools.pigstats.tez.TezDAGStats; -import org.apache.pig.tools.pigstats.tez.TezPigScriptStats; +// import org.apache.pig.tools.pigstats.spark.SparkJobStats; +// import org.apache.pig.tools.pigstats.spark.SparkPigStats; +// import org.apache.pig.tools.pigstats.spark.SparkScriptState; +// import org.apache.pig.tools.pigstats.tez.TezDAGStats; +// import org.apache.pig.tools.pigstats.tez.TezPigScriptStats; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,4 +67,246 @@ public static File createTempPigScript(List lines) throws IOException { return createTempPigScript(StringUtils.join(lines, "\n")); } + public static String extactJobStats(PigStats stats) { + return extractFromSimplePigStats((SimplePigStats) stats); + /* + if (stats instanceof SimplePigStats) { + return extractFromSimplePigStats((SimplePigStats) stats); + } else if (stats instanceof TezPigScriptStats) { + return extractFromTezPigStats((TezPigScriptStats) stats); + } else { + throw new RuntimeException("Unrecognized stats type:" + stats.getClass().getSimpleName()); + } + */ + } + + public static String extractFromSimplePigStats(SimplePigStats stats) { + + try { + Field userIdField = PigStats.class.getDeclaredField("userId"); + userIdField.setAccessible(true); + String userId = (String) (userIdField.get(stats)); + Field startTimeField = PigStats.class.getDeclaredField("startTime"); + startTimeField.setAccessible(true); + long startTime = (Long) (startTimeField.get(stats)); + Field endTimeField = PigStats.class.getDeclaredField("endTime"); + endTimeField.setAccessible(true); + long endTime = (Long) (endTimeField.get(stats)); + + if (stats.getReturnCode() == PigRunner.ReturnCode.UNKNOWN) { + LOGGER.warn("unknown return code, can't display the results"); + return null; + } + if (stats.getPigContext() == null) { + LOGGER.warn("unknown exec type, don't display the results"); + return null; + } + + SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); + StringBuilder sb = new StringBuilder(); + sb.append("\nHadoopVersion\tPigVersion\tUserId\tStartedAt\tFinishedAt\tFeatures\n"); + sb.append(stats.getHadoopVersion()).append("\t").append(stats.getPigVersion()).append("\t") + .append(userId).append("\t") + .append(sdf.format(new Date(startTime))).append("\t") + .append(sdf.format(new Date(endTime))).append("\t") + .append(stats.getFeatures()).append("\n"); + sb.append("\n"); + if (stats.getReturnCode() == PigRunner.ReturnCode.SUCCESS) { + sb.append("Success!\n"); + } else if (stats.getReturnCode() == PigRunner.ReturnCode.PARTIAL_FAILURE) { + sb.append("Some jobs have failed! Stop running all dependent jobs\n"); + } else { + sb.append("Failed!\n"); + } + sb.append("\n"); + + Field jobPlanField = PigStats.class.getDeclaredField("jobPlan"); + jobPlanField.setAccessible(true); + PigStats.JobGraph jobPlan = (PigStats.JobGraph) jobPlanField.get(stats); + + if (stats.getReturnCode() == PigRunner.ReturnCode.SUCCESS + || stats.getReturnCode() == PigRunner.ReturnCode.PARTIAL_FAILURE) { + sb.append("Job Stats (time in seconds):\n"); + sb.append(MRJobStats.SUCCESS_HEADER).append("\n"); + List arr = jobPlan.getSuccessfulJobs(); + for (JobStats js : arr) { + sb.append(js.getDisplayString()); + } + sb.append("\n"); + } + if (stats.getReturnCode() == PigRunner.ReturnCode.FAILURE + || stats.getReturnCode() == PigRunner.ReturnCode.PARTIAL_FAILURE) { + sb.append("Failed Jobs:\n"); + sb.append(MRJobStats.FAILURE_HEADER).append("\n"); + List arr = jobPlan.getFailedJobs(); + for (JobStats js : arr) { + sb.append(js.getDisplayString()); + } + sb.append("\n"); + } + sb.append("Input(s):\n"); + for (InputStats is : stats.getInputStats()) { + sb.append(is.getDisplayString()); + } + sb.append("\n"); + sb.append("Output(s):\n"); + for (OutputStats ds : stats.getOutputStats()) { + sb.append(ds.getDisplayString()); + } + + sb.append("\nCounters:\n"); + sb.append("Total records written : " + stats.getRecordWritten()).append("\n"); + sb.append("Total bytes written : " + stats.getBytesWritten()).append("\n"); + sb.append("Spillable Memory Manager spill count : " + + stats.getSMMSpillCount()).append("\n"); + sb.append("Total bags proactively spilled: " + + stats.getProactiveSpillCountObjects()).append("\n"); + sb.append("Total records proactively spilled: " + + stats.getProactiveSpillCountRecords()).append("\n"); + sb.append("\nJob DAG:\n").append(jobPlan.toString()); + + return "Script Statistics: \n" + sb.toString(); + } catch (Exception e) { + LOGGER.error("Can not extract message from SimplePigStats", e); + return "Can not extract message from SimpelPigStats," + ExceptionUtils.getStackTrace(e); + } + } + + /* + private static String extractFromTezPigStats(TezPigScriptStats stats) { + + try { + if (stats.getReturnCode() == PigRunner.ReturnCode.UNKNOWN) { + LOGGER.warn("unknown return code, can't display the results"); + return null; + } + if (stats.getPigContext() == null) { + LOGGER.warn("unknown exec type, don't display the results"); + return null; + } + + Field userIdField = PigStats.class.getDeclaredField("userId"); + userIdField.setAccessible(true); + String userId = (String) (userIdField.get(stats)); + Field startTimeField = PigStats.class.getDeclaredField("startTime"); + startTimeField.setAccessible(true); + long startTime = (Long) (startTimeField.get(stats)); + Field endTimeField = PigStats.class.getDeclaredField("endTime"); + endTimeField.setAccessible(true); + long endTime = (Long) (endTimeField.get(stats)); + + SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + sb.append(String.format("%1$20s: %2$-100s%n", "HadoopVersion", stats.getHadoopVersion())); + sb.append(String.format("%1$20s: %2$-100s%n", "PigVersion", stats.getPigVersion())); + sb.append(String.format("%1$20s: %2$-100s%n", "TezVersion", TezExecType.getTezVersion())); + sb.append(String.format("%1$20s: %2$-100s%n", "UserId", userId)); + sb.append(String.format("%1$20s: %2$-100s%n", "FileName", stats.getFileName())); + sb.append(String.format("%1$20s: %2$-100s%n", "StartedAt", sdf.format(new Date(startTime)))); + sb.append(String.format("%1$20s: %2$-100s%n", "FinishedAt", sdf.format(new Date(endTime)))); + sb.append(String.format("%1$20s: %2$-100s%n", "Features", stats.getFeatures())); + sb.append("\n"); + if (stats.getReturnCode() == PigRunner.ReturnCode.SUCCESS) { + sb.append("Success!\n"); + } else if (stats.getReturnCode() == PigRunner.ReturnCode.PARTIAL_FAILURE) { + sb.append("Some tasks have failed! Stop running all dependent tasks\n"); + } else { + sb.append("Failed!\n"); + } + sb.append("\n"); + + // Print diagnostic info in case of failure + if (stats.getReturnCode() == PigRunner.ReturnCode.FAILURE + || stats.getReturnCode() == PigRunner.ReturnCode.PARTIAL_FAILURE) { + if (stats.getErrorMessage() != null) { + String[] lines = stats.getErrorMessage().split("\n"); + for (int i = 0; i < lines.length; i++) { + String s = lines[i].trim(); + if (i == 0 || !org.apache.commons.lang.StringUtils.isEmpty(s)) { + sb.append(String.format("%1$20s: %2$-100s%n", i == 0 ? "ErrorMessage" : "", s)); + } + } + sb.append("\n"); + } + } + + Field tezDAGStatsMapField = TezPigScriptStats.class.getDeclaredField("tezDAGStatsMap"); + tezDAGStatsMapField.setAccessible(true); + Map tezDAGStatsMap = + (Map) tezDAGStatsMapField.get(stats); + int count = 0; + for (TezDAGStats dagStats : tezDAGStatsMap.values()) { + sb.append("\n"); + sb.append("DAG " + count++ + ":\n"); + sb.append(dagStats.getDisplayString()); + sb.append("\n"); + } + + sb.append("Input(s):\n"); + for (InputStats is : stats.getInputStats()) { + sb.append(is.getDisplayString().trim()).append("\n"); + } + sb.append("\n"); + sb.append("Output(s):\n"); + for (OutputStats os : stats.getOutputStats()) { + sb.append(os.getDisplayString().trim()).append("\n"); + } + return "Script Statistics:\n" + sb.toString(); + } catch (Exception e) { + LOGGER.error("Can not extract message from SimplePigStats", e); + return "Can not extract message from SimpelPigStats," + ExceptionUtils.getStackTrace(e); + } + } + */ + + public static List extractJobIds(PigStats stat) { + return extractJobIdsFromSimplePigStats((SimplePigStats) stat); + /* + if (stat instanceof SimplePigStats) { + return extractJobIdsFromSimplePigStats((SimplePigStats) stat); + } else if (stat instanceof TezPigScriptStats) { + return extractJobIdsFromTezPigStats((TezPigScriptStats) stat); + } else { + throw new RuntimeException("Unrecognized stats type:" + stat.getClass().getSimpleName()); + } + */ + } + + public static List extractJobIdsFromSimplePigStats(SimplePigStats stat) { + List jobIds = new ArrayList<>(); + try { + Field jobPlanField = PigStats.class.getDeclaredField("jobPlan"); + jobPlanField.setAccessible(true); + PigStats.JobGraph jobPlan = (PigStats.JobGraph) jobPlanField.get(stat); + List arr = jobPlan.getJobList(); + for (JobStats js : arr) { + jobIds.add(js.getJobId()); + } + return jobIds; + } catch (Exception e) { + LOGGER.error("Can not extract jobIds from SimpelPigStats", e); + throw new RuntimeException("Can not extract jobIds from SimpelPigStats", e); + } + } + + /* + public static List extractJobIdsFromTezPigStats(TezPigScriptStats stat) { + List jobIds = new ArrayList<>(); + try { + Field tezDAGStatsMapField = TezPigScriptStats.class.getDeclaredField("tezDAGStatsMap"); + tezDAGStatsMapField.setAccessible(true); + Map tezDAGStatsMap = + (Map) tezDAGStatsMapField.get(stat); + for (TezDAGStats dagStats : tezDAGStatsMap.values()) { + LOGGER.debug("Tez JobId:" + dagStats.getJobId()); + jobIds.add(dagStats.getJobId()); + } + return jobIds; + } catch (Exception e) { + LOGGER.error("Can not extract jobIds from TezPigScriptStats", e); + throw new RuntimeException("Can not extract jobIds from TezPigScriptStats", e); + } + } + */ } diff --git a/pig/src/main/resources/interpreter-setting.json b/pig/src/main/resources/interpreter-setting.json index 058e71ba706..3fa265bcafa 100644 --- a/pig/src/main/resources/interpreter-setting.json +++ b/pig/src/main/resources/interpreter-setting.json @@ -8,7 +8,7 @@ "envName": null, "propertyName": "zeppelin.pig.execType", "defaultValue": "mapreduce", - "description": "local | mapreduce | tez_local | tez | spark_local | spark", + "description": "local | mapreduce", "type": "string" }, "zeppelin.pig.includeJobStats": { @@ -17,20 +17,6 @@ "defaultValue": false, "description": "flag to include job stats in output", "type": "checkbox" - }, - "SPARK_MASTER": { - "envName": "SPARK_MASTER", - "propertyName": "SPARK_MASTER", - "defaultValue": "local", - "description": "local | yarn-client", - "type": "string" - }, - "SPARK_JAR": { - "envName": "SPARK_JAR", - "propertyName": "SPARK_JAR", - "defaultValue": "", - "description": "spark assembly jar uploaded in hdfs", - "type": "textarea" } }, "editor": { diff --git a/pig/src/test/java/org/apache/zeppelin/pig/PigInterpreterSparkTest.java b/pig/src/test/java/org/apache/zeppelin/pig/PigInterpreterSparkTest.java index e821bfea3d4..43ade16990d 100644 --- a/pig/src/test/java/org/apache/zeppelin/pig/PigInterpreterSparkTest.java +++ b/pig/src/test/java/org/apache/zeppelin/pig/PigInterpreterSparkTest.java @@ -44,8 +44,8 @@ public void setUpSpark(boolean includeJobStats) { properties.put("zeppelin.pig.includeJobStats", includeJobStats + ""); pigInterpreter = new PigInterpreter(properties); pigInterpreter.open(); - context = new InterpreterContext(null, "paragraph_id", null, null, null, null, null, null, null, null, - null, null); + context = new InterpreterContext(null, "paragraph_id", null, null, null, null, null, null, null, + null, null, null, null); } @After diff --git a/pig/src/test/java/org/apache/zeppelin/pig/PigInterpreterTest.java b/pig/src/test/java/org/apache/zeppelin/pig/PigInterpreterTest.java index efcbb588691..ac1339068ad 100644 --- a/pig/src/test/java/org/apache/zeppelin/pig/PigInterpreterTest.java +++ b/pig/src/test/java/org/apache/zeppelin/pig/PigInterpreterTest.java @@ -47,8 +47,8 @@ private void setUpLocal(boolean includeJobStats) { properties.put("zeppelin.pig.includeJobStats", includeJobStats + ""); pigInterpreter = new PigInterpreter(properties); pigInterpreter.open(); - context = new InterpreterContext(null, "paragraph_id", null, null, null, null, null, null, null, null, - null, null); + context = new InterpreterContext(null, "paragraph_id", null, null, null, + null, null, null, null, null, null,null, null); } @After diff --git a/pig/src/test/java/org/apache/zeppelin/pig/PigInterpreterTezTest.java b/pig/src/test/java/org/apache/zeppelin/pig/PigInterpreterTezTest.java index 964b31c9ba3..48f07bf147a 100644 --- a/pig/src/test/java/org/apache/zeppelin/pig/PigInterpreterTezTest.java +++ b/pig/src/test/java/org/apache/zeppelin/pig/PigInterpreterTezTest.java @@ -48,8 +48,8 @@ public void setUpTez(boolean includeJobStats) { properties.put("tez.queue.name", "test"); pigInterpreter = new PigInterpreter(properties); pigInterpreter.open(); - context = new InterpreterContext(null, "paragraph_id", null, null, null, null, null, null, null, null, - null, null); + context = new InterpreterContext(null, "paragraph_id", null, null, null, null, null, null, null, + null, null, null, null); } @After diff --git a/pig/src/test/java/org/apache/zeppelin/pig/PigQueryInterpreterTest.java b/pig/src/test/java/org/apache/zeppelin/pig/PigQueryInterpreterTest.java index de297c75e99..ad395b5814d 100644 --- a/pig/src/test/java/org/apache/zeppelin/pig/PigQueryInterpreterTest.java +++ b/pig/src/test/java/org/apache/zeppelin/pig/PigQueryInterpreterTest.java @@ -21,6 +21,7 @@ import org.apache.commons.io.IOUtils; 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.InterpreterResult; import org.junit.After; @@ -48,7 +49,7 @@ public class PigQueryInterpreterTest { private InterpreterContext context; @Before - public void setUp() { + public void setUp() throws InterpreterException { Properties properties = new Properties(); properties.put("zeppelin.pig.execType", "local"); properties.put("zeppelin.pig.maxResult", "20"); @@ -65,8 +66,8 @@ public void setUp() { pigInterpreter.open(); pigQueryInterpreter.open(); - context = new InterpreterContext(null, "paragraph_id", null, null, null, null, null, null, null, null, - null, null); + context = new InterpreterContext(null, "paragraph_id", null, null, null, null, null, null, null, + null, null, null, null); } @After diff --git a/pom.xml b/pom.xml index b85645417fa..2beaa79a4dd 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.zeppelin zeppelin pom - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin Zeppelin project http://zeppelin.apache.org @@ -52,19 +52,22 @@ 2013 + interpreter-parent zeppelin-interpreter zeppelin-zengine zeppelin-display - spark-dependencies groovy spark markdown angular shell + maprdb livy - hbase + pig jdbc + drill + hive file flink ignite @@ -76,6 +79,8 @@ bigquery alluxio scio + neo4j + sap zeppelin-web zeppelin-server zeppelin-jupyter @@ -84,16 +89,16 @@ - 2.10.5 - 2.10 + 1.7 + 2.11.7 + 2.11 2.2.4 1.12.5 - v6.9.1 - v0.18.1 - 4.2.0 - 1.3 + v8.9.3 + 5.5.1 + 1.4 1.7.10 @@ -101,8 +106,7 @@ 0.9.2 2.2 0.2.1 - 15.0 - 9.2.15.v20160210 + 9.4.14.v20181114 4.4.1 4.5.1 4.0.2 @@ -110,7 +114,7 @@ 1.9 1.5 2.4 - 3.2.1 + 3.2.2 1.1.1 1.3.1 1.3.2 @@ -246,12 +250,6 @@ ${commons.cli.version} - - com.google.guava - guava - ${guava.version} - - org.apache.shiro @@ -326,6 +324,19 @@ test + + + joda-time + joda-time + 2.10.3 + + + + org.apache.zookeeper + zookeeper + 3.4.11-mapr-1808 + provided + @@ -335,8 +346,8 @@ maven-compiler-plugin ${plugin.compiler.version} - 1.7 - 1.7 + ${java.version} + ${java.version} @@ -404,7 +415,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 +425,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/* @@ -446,26 +457,6 @@ - - maven-dependency-plugin - - - copy-dependencies - process-test-resources - - copy-dependencies - - - ${project.build.directory}/lib - false - false - true - runtime - - - - - maven-jar-plugin ${plugin.jar.version} @@ -519,6 +510,7 @@ true + true enforce @@ -586,6 +578,22 @@ maven-dependency-plugin ${plugin.dependency.version} + + + copy-dependencies + process-test-resources + + copy-dependencies + + + ${project.build.directory}/lib + false + false + true + runtime + + + @@ -749,7 +757,7 @@ scala-2.11 - 2.11.7 + 2.11.8 2.11 @@ -761,6 +769,30 @@ cloudera https://repository.cloudera.com/artifactory/cloudera-repos/ + + hortonworks + http://repo.hortonworks.com/content/groups/public/ + + + + + + vendor-repo-mapr + + + mapr-maven-repo + ${env.MAPR_MAVEN_REPO} + + true + + + true + + + + repository-mapr-com + http://repository.mapr.com/maven/ + @@ -772,6 +804,13 @@ + + integration + + zeppelin-integration + + + r @@ -886,6 +925,9 @@ jar + + -Xdoclint:none + @@ -1015,6 +1057,7 @@ **/src/fonts/source-code-pro* **/src/**/**.test.js **/e2e/**/**.spec.js + package-lock.json **/src/main/java/org/apache/zeppelin/jdbc/SqlCompleter.java @@ -1065,16 +1108,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 @@ -1082,6 +1120,11 @@ **/R/rzeppelin/DESCRIPTION **/R/rzeppelin/NAMESPACE + + python/src/main/resources/grpc/**/* + + + docs/assets/themes/zeppelin/note/**/note.json diff --git a/python/README.md b/python/README.md index cd8a0ca1c72..1b9e91b9311 100644 --- a/python/README.md +++ b/python/README.md @@ -17,12 +17,7 @@ mvn -Dpython.test.exclude='' test -pl python -am - **Py4j support** [Py4j](https://www.py4j.org/) enables Python programs to dynamically access Java objects in a JVM. - It is required in order to use Zeppelin [dynamic forms](http://zeppelin.apache.org/docs/0.6.0-SNAPSHOT/manual/dynamicform.html) feature. - - - bootstrap process - - Interpreter environment is setup with thex [bootstrap.py](https://github.com/apache/zeppelin/blob/master/python/src/main/resources/bootstrap.py) - It defines `help()` and `z` convenience functions + It is required in order to use Zeppelin [dynamic forms](https://zeppelin.apache.org/docs/latest/manual/dynamicform.html) feature. ### Dev prerequisites @@ -50,3 +45,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..a31499015ba 100644 --- a/python/pom.xml +++ b/python/pom.xml @@ -20,27 +20,25 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-python jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Python interpreter + python 0.9.2 - - **/PythonInterpreterWithPythonInstalledTest.java, - **/PythonInterpreterPandasSqlTest.java, - **/PythonInterpreterMatplotlibTest.java - https://pypi.python.org/packages /64/5c/01e13b68e8caafece40d549f232c9b5677ad1016071a48d04cc3895acaa3 + 1.15.0 + 2.4.1 @@ -73,6 +71,22 @@ slf4j-log4j12 + + io.grpc + grpc-netty + ${grpc.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + junit @@ -88,14 +102,32 @@ + + + + kr.motd.maven + os-maven-plugin + 1.4.1.Final + + + + - maven-enforcer-plugin - 1.3.1 + 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} + - enforce - none + + compile + compile-custom + @@ -136,6 +168,19 @@ + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + + test-jar + + + + org.apache.maven.plugins @@ -147,47 +192,14 @@ + + maven-enforcer-plugin + maven-dependency-plugin - 2.8 - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/python - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/python - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin 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..b3bc7fd7d2c --- /dev/null +++ b/python/src/main/java/org/apache/zeppelin/python/IPythonClient.java @@ -0,0 +1,219 @@ +/* +* 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.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; +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.security.SecureRandom; +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 SecureRandom random = new SecureRandom(); + + /** + * 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%text ".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().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 + 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..ba2a9395273 --- /dev/null +++ b/python/src/main/java/org/apache/zeppelin/python/IPythonInterpreter.java @@ -0,0 +1,425 @@ +/* +* 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.ManagedChannelBuilder; +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.httpclient.util.ExceptionUtil; +import org.apache.commons.io.FileUtils; +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; +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; + + protected BaseZeppelinContext zeppelinContext; + private String pythonExecutable; + private long ipythonLaunchTimeout; + private String additionalPythonPath; + private String additionalPythonInitFile; + private boolean useBuiltinPy4j = true; + private boolean useAuth = false; + private String secret; + private volatile boolean pythonProcessFailed = false; + + 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) { + LOGGER.info("setAdditionalPythonPath: " + 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; + } + + 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 { + if (ipythonClient != null) { + // IPythonInterpreter might already been opened by PythonInterpreter + return; + } + pythonExecutable = getProperty("zeppelin.python", "python"); + LOGGER.info("Python Exec: " + pythonExecutable); + String checkPrerequisiteResult = checkIPythonPrerequisite(pythonExecutable); + if (!StringUtils.isEmpty(checkPrerequisiteResult)) { + throw new InterpreterException("IPython prerequisite is not meet: " + + checkPrerequisiteResult); + } + ipythonLaunchTimeout = Long.parseLong( + getProperty("zeppelin.ipython.launch.timeout", "30000")); + this.zeppelinContext = buildZeppelinContext(); + int ipythonPort = RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces(); + int jvmGatewayPort = RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces(); + LOGGER.info("Launching IPython Kernel at port: " + ipythonPort); + LOGGER.info("Launching JVM Gateway at port: " + jvmGatewayPort); + 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(message_size)); + this.useAuth = Boolean.parseBoolean(getProperty("zeppelin.py4j.useAuth", "false")); + this.secret = Py4JUtils.createSecret(256); + launchIPythonKernel(ipythonPort); + setupJVMGateway(jvmGatewayPort, secret, useAuth); + } catch (Exception e) { + throw new RuntimeException("Fail to open IPythonInterpreter", e); + } + } + + /** + * non-empty return value mean the errors when checking ipython prerequisite. + * empty value mean IPython prerequisite is meet. + * + * @param pythonExec + * @return + */ + public String checkIPythonPrerequisite(String pythonExec) { + ProcessBuilder processBuilder = new ProcessBuilder(pythonExec, "-m", "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) { + return "Fail to run pip freeze.\n" + + IOUtils.toString(new FileInputStream(stderrFile)); + } + String freezeOutput = IOUtils.toString(new FileInputStream(stdoutFile)); + LOGGER.debug("Installed python packages:\n" + freezeOutput); + if (!freezeOutput.contains("jupyter-client=")) { + return "jupyter-client is not installed."; + } + if (!freezeOutput.contains("ipykernel=")) { + return "ipkernel is not installed"; + } + if (!freezeOutput.contains("ipython=")) { + return "ipython is not installed"; + } + if (!freezeOutput.contains("grpcio=")) { + return "grpcio is not installed"; + } + if (!freezeOutput.contains("protobuf=")) { + return "protobuf is not installed"; + } + LOGGER.info("IPython prerequisite is met"); + + } catch (Exception e) { + LOGGER.warn("Fail to checkIPythonPrerequisite", e); + return "Fail to checkIPythonPrerequisite: " + ExceptionUtils.getStackTrace(e); + } + return ""; + } + + private void setupJVMGateway(int jvmGatewayPort, String secret, boolean useAuth) + throws IOException { + gatewayServer = + Py4JUtils.createGatewayServer(this, "127.0.0.1", jvmGatewayPort, secret, useAuth); + 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); + + if (useBuiltinPy4j) { + 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 = setupIPythonEnv(); + executor.execute(cmd, envs, this); + + // wait until IPython kernel is started or timeout + long startTime = System.currentTimeMillis(); + while (!pythonProcessFailed) { + 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"); + } + } + if (pythonProcessFailed) { + throw new IOException("Fail to launch IPython Kernel as the python process is failed"); + } + } + + protected Map setupIPythonEnv() throws IOException { + Map envs = EnvironmentUtils.getProcEnvironment(); + if (envs.containsKey("PYTHONPATH")) { + if (additionalPythonPath != null) { + envs.put("PYTHONPATH", additionalPythonPath + ":" + envs.get("PYTHONPATH")); + } + } else { + envs.put("PYTHONPATH", additionalPythonPath); + } + if (useAuth) { + envs.put("PY4J_GATEWAY_SECRET", secret); + } + LOGGER.info("PYTHONPATH:" + envs.get("PYTHONPATH")); + return envs; + } + + @Override + public void close() throws InterpreterException { + if (watchDog != null) { + LOGGER.debug("Kill IPython Process"); + ipythonClient.stop(StopRequest.newBuilder().build()); + try { + ipythonClient.shutdown(); + } catch (InterruptedException e) { + LOGGER.warn("Fail to shutdown IPythonClient"); + } + watchDog.destroyProcess(); + gatewayServer.shutdown(); + } + } + + @Override + 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(), + 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) throws InterpreterException { + ipythonClient.cancel(CancelRequest.newBuilder().build()); + } + + @Override + public FormType getFormType() { + return FormType.SIMPLE; + } + + @Override + public int getProgress(InterpreterContext context) throws InterpreterException { + return 0; + } + + @Override + public List completion(String buf, int cursor, + InterpreterContext interpreterContext) { + LOGGER.debug("Call completion for: " + buf); + List completions = new ArrayList<>(); + CompletionResponse response = + ipythonClient.complete( + CompletionRequest.getDefaultInstance().newBuilder().setCode(buf) + .setCursor(cursor).build()); + for (int i = 0; i < response.getMatchesCount(); i++) { + String match = response.getMatches(i); + int lastIndexOfDot = match.lastIndexOf("."); + if (lastIndexOfDot != -1) { + match = match.substring(lastIndexOfDot + 1); + } + completions.add(new InterpreterCompletion(match, match, "")); + } + return completions; + } + + public BaseZeppelinContext 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); + pythonProcessFailed = true; + } + + 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/Py4JUtils.java b/python/src/main/java/org/apache/zeppelin/python/Py4JUtils.java new file mode 100644 index 00000000000..1f164792e9a --- /dev/null +++ b/python/src/main/java/org/apache/zeppelin/python/Py4JUtils.java @@ -0,0 +1,91 @@ +/* + * 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.codec.binary.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import py4j.GatewayServer; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.SecureRandom; +import java.util.Properties; + +/** + * Utils class for Py4J related stuff + */ +public class Py4JUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(Py4JUtils.class); + + public static GatewayServer createGatewayServer(Object entryPoint, + String serverAddress, + int port, + String secretKey, + boolean useAuth) throws IOException { + LOGGER.info("Launching GatewayServer at " + serverAddress + ":" + port); + if (useAuth) { + try { + Class clz = Class.forName("py4j.GatewayServer$GatewayServerBuilder", true, + Thread.currentThread().getContextClassLoader()); + Object builder = clz.getConstructor(Object.class).newInstance(entryPoint); + builder.getClass().getMethod("authToken", String.class).invoke(builder, secretKey); + builder.getClass().getMethod("javaPort", int.class).invoke(builder, port); + builder.getClass().getMethod("javaAddress", InetAddress.class).invoke(builder, + InetAddress.getByName(serverAddress)); + builder.getClass() + .getMethod("callbackClient", int.class, InetAddress.class, String.class) + .invoke(builder, port, InetAddress.getByName(serverAddress), secretKey); + return + (GatewayServer) builder.getClass().getMethod("build").invoke(builder); + } catch (Exception e) { + throw new IOException("Fail to create GatewayServer", e); + } + } else { + return new GatewayServer(entryPoint, port); + } + } + + public static String getLocalIP(Properties properties) { + // zeppelin.python.gatewayserver_address is only for unit test on travis. + // Because the FQDN would fail unit test on travis ci. + String gatewayserver_address = + properties.getProperty("zeppelin.python.gatewayserver_address"); + if (gatewayserver_address != null) { + return gatewayserver_address; + } + + try { + return Inet4Address.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + LOGGER.warn("can't get local IP", e); + } + // fall back to loopback addreess + return "127.0.0.1"; + } + + public static String createSecret(int secretBitLength) { + SecureRandom rnd = new SecureRandom(); + byte[] secretBytes = new byte[secretBitLength / java.lang.Byte.SIZE]; + rnd.nextBytes(secretBytes); + return Base64.encodeBase64String(secretBytes); + } +} diff --git a/python/src/main/java/org/apache/zeppelin/python/PythonCondaInterpreter.java b/python/src/main/java/org/apache/zeppelin/python/PythonCondaInterpreter.java index 455d7866957..887beb8ce7a 100644 --- a/python/src/main/java/org/apache/zeppelin/python/PythonCondaInterpreter.java +++ b/python/src/main/java/org/apache/zeppelin/python/PythonCondaInterpreter.java @@ -50,6 +50,8 @@ public class PythonCondaInterpreter extends Interpreter { public static final Pattern PATTERN_COMMAND_HELP = Pattern.compile("help"); public static final Pattern PATTERN_COMMAND_INFO = Pattern.compile("info"); + private String currentCondaEnvName = StringUtils.EMPTY; + public PythonCondaInterpreter(Properties property) { super(property); } @@ -65,7 +67,8 @@ public void close() { } @Override - public InterpreterResult interpret(String st, InterpreterContext context) { + public InterpreterResult interpret(String st, InterpreterContext context) + throws InterpreterException { InterpreterOutput out = context.out; Matcher activateMatcher = PATTERN_COMMAND_ACTIVATE.matcher(st); Matcher createMatcher = PATTERN_COMMAND_CREATE.matcher(st); @@ -112,8 +115,19 @@ public InterpreterResult interpret(String st, InterpreterContext context) { } } + public String getCurrentCondaEnvName() { + return currentCondaEnvName; + } + + public void setCurrentCondaEnvName(String currentCondaEnvName) { + if (currentCondaEnvName == null) { + currentCondaEnvName = StringUtils.EMPTY; + } + this.currentCondaEnvName = currentCondaEnvName; + } + private void changePythonEnvironment(String envName) - throws IOException, InterruptedException { + throws IOException, InterruptedException, InterpreterException { PythonInterpreter python = getPythonInterpreter(); String binPath = null; if (envName == null) { @@ -130,16 +144,17 @@ private void changePythonEnvironment(String envName) } } } + setCurrentCondaEnvName(envName); python.setPythonCommand(binPath); } - private void restartPythonProcess() { + private void restartPythonProcess() throws InterpreterException { PythonInterpreter python = getPythonInterpreter(); python.close(); python.open(); } - protected PythonInterpreter getPythonInterpreter() { + protected PythonInterpreter getPythonInterpreter() throws InterpreterException { LazyOpenInterpreter lazy = null; PythonInterpreter python = null; Interpreter p = @@ -199,7 +214,7 @@ private String runCondaEnv(List restArgs) } private InterpreterResult runCondaActivate(String envName) - throws IOException, InterruptedException { + throws IOException, InterruptedException, InterpreterException { if (null == envName || envName.isEmpty()) { return new InterpreterResult(Code.ERROR, "Env name should be specified"); @@ -212,7 +227,7 @@ private InterpreterResult runCondaActivate(String envName) } private InterpreterResult runCondaDeactivate() - throws IOException, InterruptedException { + throws IOException, InterruptedException, InterpreterException { changePythonEnvironment(null); restartPythonProcess(); @@ -221,8 +236,12 @@ private InterpreterResult runCondaDeactivate() private String runCondaList() throws IOException, InterruptedException { List commands = new ArrayList(); - commands.add("conda"); - commands.add("list"); + commands.add(0, "conda"); + commands.add(1, "list"); + if (!getCurrentCondaEnvName().isEmpty()) { + commands.add(2, "-n"); + commands.add(3, getCurrentCondaEnvName()); + } return runCondaCommandForTableOutput("Installed Package List", commands); } @@ -259,6 +278,10 @@ private String runCondaInstall(List restArgs) restArgs.add(0, "conda"); restArgs.add(1, "install"); restArgs.add(2, "--yes"); + if (!getCurrentCondaEnvName().isEmpty()) { + restArgs.add(3, "-n"); + restArgs.add(4, getCurrentCondaEnvName()); + } return runCondaCommandForTextOutput("Package Installation", restArgs); } @@ -269,6 +292,10 @@ private String runCondaUninstall(List restArgs) restArgs.add(0, "conda"); restArgs.add(1, "uninstall"); restArgs.add(2, "--yes"); + if (!getCurrentCondaEnvName().isEmpty()) { + restArgs.add(3, "-n"); + restArgs.add(4, getCurrentCondaEnvName()); + } return runCondaCommandForTextOutput("Package Uninstallation", restArgs); } @@ -349,10 +376,16 @@ public int getProgress(InterpreterContext context) { */ @Override public Scheduler getScheduler() { - PythonInterpreter pythonInterpreter = getPythonInterpreter(); - if (pythonInterpreter != null) { - return pythonInterpreter.getScheduler(); - } else { + PythonInterpreter pythonInterpreter = null; + try { + pythonInterpreter = getPythonInterpreter(); + if (pythonInterpreter != null) { + return pythonInterpreter.getScheduler(); + } else { + return null; + } + } catch (InterpreterException e) { + e.printStackTrace(); return null; } } diff --git a/python/src/main/java/org/apache/zeppelin/python/PythonDockerInterpreter.java b/python/src/main/java/org/apache/zeppelin/python/PythonDockerInterpreter.java index cb0f62078f2..22f6c2ee994 100644 --- a/python/src/main/java/org/apache/zeppelin/python/PythonDockerInterpreter.java +++ b/python/src/main/java/org/apache/zeppelin/python/PythonDockerInterpreter.java @@ -56,7 +56,8 @@ public void close() { } @Override - public InterpreterResult interpret(String st, InterpreterContext context) { + public InterpreterResult interpret(String st, InterpreterContext context) + throws InterpreterException { File pythonScript = new File(getPythonInterpreter().getScriptPath()); InterpreterOutput out = context.out; @@ -105,7 +106,7 @@ public InterpreterResult interpret(String st, InterpreterContext context) { } - public void setPythonCommand(String cmd) { + public void setPythonCommand(String cmd) throws InterpreterException { PythonInterpreter python = getPythonInterpreter(); python.setPythonCommand(cmd); } @@ -140,21 +141,27 @@ public int getProgress(InterpreterContext context) { */ @Override public Scheduler getScheduler() { - PythonInterpreter pythonInterpreter = getPythonInterpreter(); - if (pythonInterpreter != null) { - return pythonInterpreter.getScheduler(); - } else { + PythonInterpreter pythonInterpreter = null; + try { + pythonInterpreter = getPythonInterpreter(); + if (pythonInterpreter != null) { + return pythonInterpreter.getScheduler(); + } else { + return null; + } + } catch (InterpreterException e) { + e.printStackTrace(); return null; } } - private void restartPythonProcess() { + private void restartPythonProcess() throws InterpreterException { PythonInterpreter python = getPythonInterpreter(); python.close(); python.open(); } - protected PythonInterpreter getPythonInterpreter() { + protected PythonInterpreter getPythonInterpreter() throws InterpreterException { LazyOpenInterpreter lazy = null; PythonInterpreter python = null; Interpreter p = getInterpreterInTheSameSessionByClassName(PythonInterpreter.class.getName()); @@ -173,7 +180,7 @@ protected PythonInterpreter getPythonInterpreter() { return python; } - public boolean pull(InterpreterOutput out, String image) { + public boolean pull(InterpreterOutput out, String image) throws InterpreterException { int exit = 0; try { exit = runCommand(out, "docker", "pull", image); 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..cac743df3ec 100644 --- a/python/src/main/java/org/apache/zeppelin/python/PythonInterpreter.java +++ b/python/src/main/java/org/apache/zeppelin/python/PythonInterpreter.java @@ -44,6 +44,7 @@ import org.apache.commons.exec.PumpStreamHandler; import org.apache.commons.exec.environment.EnvironmentUtils; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; import org.apache.zeppelin.display.GUI; import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.InterpreterResult.Code; @@ -57,7 +58,6 @@ import org.slf4j.LoggerFactory; import py4j.GatewayServer; -import py4j.commands.Command; /** * Python interpreter for Zeppelin. @@ -70,6 +70,7 @@ public class PythonInterpreter extends Interpreter implements ExecuteResultHandl public static final String DEFAULT_ZEPPELIN_PYTHON = "python"; public static final String MAX_RESULT = "zeppelin.python.maxResult"; + private PythonZeppelinContext zeppelinContext; private InterpreterContext context; private Pattern errorInLastLine = Pattern.compile(".*(Error|Exception): .*$"); private String pythonPath; @@ -91,6 +92,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); @@ -100,7 +102,7 @@ public PythonInterpreter(Properties property) { File scriptFile = File.createTempFile("zeppelin_python-", ".py", new File("/tmp")); scriptPath = scriptFile.getAbsolutePath(); } catch (IOException e) { - throw new InterpreterException(e); + throw new RuntimeException(e); } } @@ -115,7 +117,7 @@ private String workingDir() { return path; } - private void createPythonScript() { + private void createPythonScript() throws InterpreterException { File out = new File(scriptPath); if (out.exists() && out.isDirectory()) { @@ -130,7 +132,7 @@ public String getScriptPath() { return scriptPath; } - private void copyFile(File out, String sourceFile) { + private void copyFile(File out, String sourceFile) throws InterpreterException { ClassLoader classLoader = getClass().getClassLoader(); try { FileOutputStream outStream = new FileOutputStream(out); @@ -143,7 +145,8 @@ private void copyFile(File out, String sourceFile) { } } - private void createGatewayServerAndStartScript() throws UnknownHostException { + private void createGatewayServerAndStartScript() + throws UnknownHostException, InterpreterException { createPythonScript(); if (System.getenv("ZEPPELIN_HOME") != null) { py4jLibPath = System.getenv("ZEPPELIN_HOME") + File.separator + ZEPPELIN_PY4JPATH; @@ -218,11 +221,36 @@ private void createGatewayServerAndStartScript() throws UnknownHostException { } @Override - public void open() { + public void open() throws InterpreterException { + // try IPythonInterpreter first. If it is not available, we will fallback to the original + // python interpreter implementation. + iPythonInterpreter = getIPythonInterpreter(); + this.zeppelinContext = new PythonZeppelinContext( + getInterpreterGroup().getInterpreterHookRegistry(), + Integer.parseInt(getProperty("zeppelin.python.maxResult", "1000"))); + if (getProperty("zeppelin.python.useIPython", "true").equals("true") && + StringUtils.isEmpty(iPythonInterpreter.checkIPythonPrerequisite(getPythonBindPath()))) { + try { + iPythonInterpreter.open(); + LOG.info("IPython is available, Use IPythonInterpreter to replace PythonInterpreter"); + return; + } catch (Exception e) { + iPythonInterpreter = null; + LOG.warn("Fail to open IPythonInterpreter", e); + } + } + + // reset iPythonInterpreter to null as it is not available + iPythonInterpreter = null; + LOG.info("IPython is not available, use the native PythonInterpreter"); // Add matplotlib display hook InterpreterGroup intpGroup = getInterpreterGroup(); if (intpGroup != null && intpGroup.getInterpreterHookRegistry() != null) { - registerHook(HookType.POST_EXEC_DEV, "__zeppelin__._displayhook()"); + try { + registerHook(HookType.POST_EXEC_DEV.getName(), "__zeppelin__._displayhook()"); + } catch (InvalidHookException e) { + throw new InterpreterException(e); + } } // Add matplotlib display hook try { @@ -232,8 +260,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() { + public void close() throws InterpreterException { + if (iPythonInterpreter != null) { + iPythonInterpreter.close(); + return; + } pythonscriptRunning = false; pythonScriptInitialized = false; @@ -318,13 +365,22 @@ public void appendOutput(String message) throws IOException { } @Override - public InterpreterResult interpret(String cmd, InterpreterContext contextInterpreter) { + public InterpreterResult interpret(String cmd, InterpreterContext contextInterpreter) + throws InterpreterException { + if (iPythonInterpreter != null) { + return iPythonInterpreter.interpret(cmd, contextInterpreter); + } + if (cmd == null || cmd.isEmpty()) { return new InterpreterResult(Code.SUCCESS, ""); } this.context = contextInterpreter; + zeppelinContext.setGui(context.getGui()); + zeppelinContext.setNoteGui(context.getNoteGui()); + zeppelinContext.setInterpreterContext(context); + if (!pythonscriptRunning) { return new InterpreterResult(Code.ERROR, "python process not running" + outputStream.toString()); @@ -399,7 +455,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); @@ -410,7 +466,10 @@ public void interrupt() throws IOException { } @Override - public void cancel(InterpreterContext context) { + public void cancel(InterpreterContext context) throws InterpreterException { + if (iPythonInterpreter != null) { + iPythonInterpreter.cancel(context); + } try { interrupt(); } catch (IOException e) { @@ -424,12 +483,18 @@ public FormType getFormType() { } @Override - public int getProgress(InterpreterContext context) { + public int getProgress(InterpreterContext context) throws InterpreterException { + 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 +502,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; } @@ -485,11 +553,15 @@ void bootStrapInterpreter(String file) throws IOException { bootstrapCode += line + "\n"; } - interpret(bootstrapCode, context); + try { + interpret(bootstrapCode, InterpreterContext.get()); + } catch (InterpreterException e) { + throw new IOException(e); + } } - public GUI getGui() { - return context.getGui(); + public PythonZeppelinContext getZeppelinContext() { + return zeppelinContext; } String getLocalIp() { diff --git a/python/src/main/java/org/apache/zeppelin/python/PythonInterpreterPandasSql.java b/python/src/main/java/org/apache/zeppelin/python/PythonInterpreterPandasSql.java index e73d7b36bd2..ea05ce7eb4d 100644 --- a/python/src/main/java/org/apache/zeppelin/python/PythonInterpreterPandasSql.java +++ b/python/src/main/java/org/apache/zeppelin/python/PythonInterpreterPandasSql.java @@ -22,6 +22,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; @@ -42,7 +43,7 @@ public PythonInterpreterPandasSql(Properties property) { super(property); } - PythonInterpreter getPythonInterpreter() { + PythonInterpreter getPythonInterpreter() throws InterpreterException { LazyOpenInterpreter lazy = null; PythonInterpreter python = null; Interpreter p = getInterpreterInTheSameSessionByClassName(PythonInterpreter.class.getName()); @@ -62,7 +63,7 @@ PythonInterpreter getPythonInterpreter() { } @Override - public void open() { + public void open() throws InterpreterException { LOG.info("Open Python SQL interpreter instance: {}", this.toString()); try { @@ -76,19 +77,20 @@ public void open() { } @Override - public void close() { + public void close() throws InterpreterException { LOG.info("Close Python SQL interpreter instance: {}", this.toString()); Interpreter python = getPythonInterpreter(); python.close(); } @Override - public InterpreterResult interpret(String st, InterpreterContext context) { + public InterpreterResult interpret(String st, InterpreterContext context) + throws InterpreterException { LOG.info("Running SQL query: '{}' over Pandas DataFrame", st); Interpreter python = getPythonInterpreter(); return python.interpret( - "__zeppelin__.show(pysqldf('" + st + "'))\n__zeppelin__._displayhook()", context); + "__zeppelin__.show(pysqldf('" + st + "'))", context); } @Override 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..36e0a13099d --- /dev/null +++ b/python/src/main/resources/grpc/python/ipython_server.py @@ -0,0 +1,175 @@ +# 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 os +import sys +import threading +import time +from concurrent import futures + +import grpc +import ipython_pb2 +import ipython_pb2_grpc + +is_py2 = sys.version[0] == '2' +if is_py2: + import Queue as queue +else: + import queue as queue + + +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:\n") + print(request.code.encode('utf-8')) + 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=None) + payload_reply.append(reply) + + t = threading.Thread(name="ConsumerThread", target=execute_worker) + t.start() + + # We want to ensure that the kernel is alive because in case of OOM or other errors + # Execution might be stuck there: + # https://github.com/jupyter/jupyter_client/blob/master/jupyter_client/blocking/client.py#L32 + while t.is_alive() and self.isKernelAlive(): + 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 kernel is not alive (should be same as thread is still alive), means that we face + # an unexpected issue. + if not self.isKernelAlive() or t.is_alive(): + yield ipython_pb2.ExecuteResponse(status=ipython_pb2.ERROR, + type=ipython_pb2.TEXT, + output="Ipython kernel has been stopped. Please check logs. It might be because of an out of memory issue.") + return + + 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=None) + return ipython_pb2.CompletionResponse(matches=reply['content']['matches']) + + def status(self, request, context): + return ipython_pb2.StatusResponse(status = self._status) + + def isKernelAlive(self): + return self._km.is_alive() + + def terminate(self): + self._km.shutdown_kernel() + + def stop(self, request, context): + self.terminate() + return ipython_pb2.StopResponse() + + +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 ipython.isKernelAlive(): + time.sleep(5) + except KeyboardInterrupt: + print("interrupted") + finally: + print("shutdown") + # we let 2 sc for all request to be complete + server.stop(2) + ipython.terminate() + os._exit(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..0b2bd24644c --- /dev/null +++ b/python/src/main/resources/grpc/python/zeppelin_python.py @@ -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. +# + +from py4j.java_gateway import java_import, JavaGateway, GatewayClient +import os +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 getInterpreterContext(self): + return self.z.getInterpreterContext() + + def input(self, name, defaultValue=""): + return self.z.input(name, defaultValue) + + def textbox(self, name, defaultValue=""): + return self.z.textbox(name, defaultValue) + + def noteTextbox(self, name, defaultValue=""): + return self.z.noteTextbox(name, defaultValue) + + def select(self, name, options, defaultValue=""): + return self.z.select(name, defaultValue, self.getParamOptions(options)) + + def noteSelect(self, name, options, defaultValue=""): + return self.z.noteSelect(name, defaultValue, self.getParamOptions(options)) + + def checkbox(self, name, options, defaultChecked=[]): + return self.z.checkbox(name, self.getDefaultChecked(defaultChecked), self.getParamOptions(options)) + + def noteCheckbox(self, name, options, defaultChecked=[]): + return self.z.noteCheckbox(name, self.getDefaultChecked(defaultChecked), self.getParamOptions(options)) + + def getParamOptions(self, options): + 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 javaOptions + + def getDefaultChecked(self, defaultChecked): + javaDefaultChecked = self.javaList() + for check in defaultChecked: + javaDefaultChecked.append(check) + return javaDefaultChecked + + 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() + + def registerHook(self, event, cmd, replName=None): + if replName is None: + self.z.registerHook(event, cmd) + else: + self.z.registerHook(event, cmd, replName) + + def unregisterHook(self, event, replName=None): + if replName is None: + self.z.unregisterHook(event) + else: + self.z.unregisterHook(event, replName) + + def registerNoteHook(self, event, cmd, noteId, replName=None): + if replName is None: + self.z.registerNoteHook(event, cmd, noteId) + else: + self.z.registerNoteHook(event, cmd, noteId, replName) + + def unregisterNoteHook(self, event, noteId, replName=None): + if replName is None: + self.z.unregisterNoteHook(event, noteId) + else: + self.z.unregisterNoteHook(event, noteId, replName) + +# start JVM gateway +if "PY4J_GATEWAY_SECRET" in os.environ: + from py4j.java_gateway import GatewayParameters + gateway_secret = os.environ["PY4J_GATEWAY_SECRET"] + gateway = JavaGateway(gateway_parameters=GatewayParameters( + port=${JVM_GATEWAY_PORT}, auth_token=gateway_secret, auto_convert=True)) +else: + gateway = JavaGateway(GatewayClient(port=${JVM_GATEWAY_PORT}), auto_convert=True) + +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..6c02e9487bd 100644 --- a/python/src/main/resources/interpreter-setting.json +++ b/python/src/main/resources/interpreter-setting.json @@ -17,11 +17,43 @@ "defaultValue": "1000", "description": "Max number of dataframe rows to display.", "type": "number" + }, + "zeppelin.python.useIPython": { + "propertyName": "zeppelin.python.useIPython", + "defaultValue": false, + "description": "whether use IPython when it is available", + "type": "checkbox" + } + }, + "editor": { + "language": "python", + "editOnDblClick": false, + "completionSupport": true + } + }, + { + "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" + }, + "zeppelin.ipython.grpc.message_size": { + "propertyName": "zeppelin.ipython.grpc.message_size", + "defaultValue": "33554432", + "description": "grpc message size, default is 32M", + "type": "number" } }, "editor": { "language": "python", - "editOnDblClick": false + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": true } }, { @@ -32,7 +64,9 @@ }, "editor":{ "language": "sql", - "editOnDblClick": false + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": false } }, { @@ -43,7 +77,8 @@ }, "editor": { "language": "sh", - "editOnDblClick": false + "editOnDblClick": false, + "completionSupport": false } }, { @@ -54,7 +89,8 @@ }, "editor":{ "language": "sh", - "editOnDblClick": false + "editOnDblClick": false, + "completionSupport": false } } ] diff --git a/python/src/main/resources/python/zeppelin_python.py b/python/src/main/resources/python/zeppelin_python.py index eff88249ee4..a1941ce5e45 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: @@ -60,29 +61,66 @@ def __init__(self, z): self._setup_matplotlib() def getInterpreterContext(self): - return self.z.getCurrentInterpreterContext() + return self.z.getInterpreterContext() def input(self, name, defaultValue=""): - return self.z.getGui().input(name, defaultValue) + return self.z.input(name, defaultValue) + + def textbox(self, name, defaultValue=""): + return self.z.textbox(name, defaultValue) + + def noteTextbox(self, name, defaultValue=""): + return self.z.noteTextbox(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) + return self.z.select(name, defaultValue, self.getParamOptions(options)) + + def noteSelect(self, name, options, defaultValue=""): + return self.z.noteSelect(name, defaultValue, self.getParamOptions(options)) def checkbox(self, name, options, defaultChecked=[]): + return self.z.checkbox(name, self.getDefaultChecked(defaultChecked), self.getParamOptions(options)) + + def noteCheckbox(self, name, options, defaultChecked=[]): + return self.z.noteCheckbox(name, self.getDefaultChecked(defaultChecked), self.getParamOptions(options)) + + def registerHook(self, event, cmd, replName=None): + if replName is None: + self.z.registerHook(event, cmd) + else: + self.z.registerHook(event, cmd, replName) + + def unregisterHook(self, event, replName=None): + if replName is None: + self.z.unregisterHook(event) + else: + self.z.unregisterHook(event, replName) + + def registerNoteHook(self, event, cmd, noteId, replName=None): + if replName is None: + self.z.registerNoteHook(event, cmd, noteId) + else: + self.z.registerNoteHook(event, cmd, noteId, replName) + + def unregisterNoteHook(self, event, noteId, replName=None): + if replName is None: + self.z.unregisterNoteHook(event, noteId) + else: + self.z.unregisterNoteHook(event, noteId, replName) + + def getParamOptions(self, options): 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() + return javaOptions + + def getDefaultChecked(self, defaultChecked): + javaDefaultChecked = self.javaList() for check in defaultChecked: - javaDefaultCheck.append(check) - return self.z.getGui().checkbox(name, javaDefaultCheck, javaOptions) + javaDefaultChecked.append(check) + return javaDefaultChecked def show(self, p, **kwargs): if hasattr(p, '__name__') and p.__name__ == "matplotlib.pyplot": @@ -205,7 +243,7 @@ def handler_stop_signals(sig, frame): intp.onPythonScriptInitialized(os.getpid()) java_import(gateway.jvm, "org.apache.zeppelin.display.Input") -z = __zeppelin__ = PyZeppelinContext(intp) +z = __zeppelin__ = PyZeppelinContext(intp.getZeppelinContext()) __zeppelin__._setup_matplotlib() _zcUserQueryNameSpace["__zeppelin__"] = __zeppelin__ @@ -261,8 +299,7 @@ def handler_stop_signals(sig, frame): to_run_hooks = code.body[-nhooks:] to_run_exec, to_run_single = (code.body[:-(nhooks + 1)], - [code.body[-(nhooks + 1)]]) - + [code.body[-(nhooks + 1)]] if len(code.body) > nhooks else []) try: for node in to_run_exec: mod = ast.Module([node]) 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..5e256f9556b --- /dev/null +++ b/python/src/test/java/org/apache/zeppelin/python/IPythonInterpreterTest.java @@ -0,0 +1,527 @@ +/* + * 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.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.InterpreterResult.Code; +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; + + public void startInterpreter(Properties properties) throws InterpreterException { + interpreter = new IPythonInterpreter(properties); + InterpreterGroup mockInterpreterGroup = mock(InterpreterGroup.class); + interpreter.setInterpreterGroup(mockInterpreterGroup); + interpreter.open(); + } + + @After + public void close() throws InterpreterException { + interpreter.close(); + } + + + @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.message_size", "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.toInterpreterResultMessage(); + assertEquals(1, interpreterResultMessages.size()); + assertTrue(interpreterResultMessages.get(0).getData().contains("exceeds maximum size 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.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()); + 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()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + 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.toInterpreterResultMessage(); + assertEquals(1, interpreterResultMessages.size()); + boolean isPython2 = interpreterResultMessages.get(0).getData().equals("2\n"); + + // single output without print + context = getInterpreterContext(); + result = interpreter.interpret("'hello world'", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.toInterpreterResultMessage(); + 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.toInterpreterResultMessage(); + 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); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.toInterpreterResultMessage(); + 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.toInterpreterResultMessage(); + 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.toInterpreterResultMessage(); + 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.toInterpreterResultMessage(); + 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.toInterpreterResultMessage(); + 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.toInterpreterResultMessage(); + 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.toInterpreterResultMessage(); + 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.toInterpreterResultMessage(); + 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.toInterpreterResultMessage(); + 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.toInterpreterResultMessage(); + 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); + 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()); + assertEquals("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.toInterpreterResultMessage(); + 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("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("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.toInterpreterResultMessage(); + 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.toInterpreterResultMessage(); + 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(); + } + try { + interpreter.cancel(context2); + } catch (InterpreterException e) { + e.printStackTrace(); + } + } + }.start(); + result = interpreter.interpret("import time\ntime.sleep(10)", context2); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.ERROR, result.code()); + interpreterResultMessages = context2.out.toInterpreterResultMessage(); + 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.toInterpreterResultMessage(); + // the order of IMAGE and TEXT is not determined + // check there must be one IMAGE output + boolean hasImageOutput = false; + boolean hasLineText = false; + boolean hasFigureText = false; + for (InterpreterResultMessage msg : interpreterResultMessages) { + if (msg.getType() == InterpreterResult.Type.IMG) { + hasImageOutput = true; + } + if (msg.getType() == InterpreterResult.Type.TEXT + && msg.getData().contains("matplotlib.lines.Line2D")) { + hasLineText = true; + } + if (msg.getType() == InterpreterResult.Type.TEXT + && msg.getData().contains("matplotlib.figure.Figure")) { + hasFigureText = true; + } + } + assertTrue("No Image Output", hasImageOutput); + assertTrue("No Line Text", hasLineText); + assertTrue("No Figure Text", hasFigureText); + + // bokeh + // bokeh initialization + 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()); + interpreterResultMessages = context.out.toInterpreterResultMessage(); + for (InterpreterResultMessage message : interpreterResultMessages) { + LOGGER.info("Data:" + message.getData()); + } + 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.toInterpreterResultMessage(); + 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(1).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.toInterpreterResultMessage(); + // 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.toInterpreterResultMessage(); + 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.toInterpreterResultMessage(); + assertEquals(InterpreterResult.Type.TABLE, interpreterResultMessages.get(0).getType()); + assertEquals("id\tname\n1\ta\n2\tb\n3\tc\n", interpreterResultMessages.get(0).getData()); + + // clear output + context = getInterpreterContext(); + result = interpreter.interpret("import time\nprint(\"Hello\")\ntime.sleep(0.5)\nz.getInterpreterContext().out().clear()\nprint(\"world\")\n", context); + assertEquals("%text world\n", context.out.getCurrentOutput().toString()); + } + + public void testIpythonKernelCrash_shouldNotHangExecution() + throws InterpreterException, IOException { + // The goal of this test is to ensure that we handle case when the kernel die. + // In order to do so, we will kill the kernel process from the python code. + // A real example of that could be a out of memory by the code we execute. + String codeDep = "!pip install psutil"; + String codeFindPID = "from os import getpid\n" + + "import psutil\n" + + "pids = psutil.pids()\n" + + "my_pid = getpid()\n" + + "pidToKill = []\n" + + "for pid in pids:\n" + + " try:\n" + + " p = psutil.Process(pid)\n" + + " cmd = p.cmdline()\n" + + " for arg in cmd:\n" + + " if arg.count('ipykernel'):\n" + + " pidToKill.append(pid)\n" + + " except:\n" + + " continue\n" + + "len(pidToKill)"; + String codeKillKernel = "from os import kill\n" + + "import signal\n" + + "for pid in pidToKill:\n" + + " kill(pid, signal.SIGKILL)"; + InterpreterContext context = getInterpreterContext(); + InterpreterResult result = interpreter.interpret(codeDep, context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + context = getInterpreterContext(); + result = interpreter.interpret(codeFindPID, context); + assertEquals(Code.SUCCESS, result.code()); + InterpreterResultMessage output = context.out.toInterpreterResultMessage().get(0); + int numberOfPID = Integer.parseInt(output.getData()); + assertTrue(numberOfPID > 0); + context = getInterpreterContext(); + result = interpreter.interpret(codeKillKernel, context); + assertEquals(Code.ERROR, result.code()); + output = context.out.toInterpreterResultMessage().get(0); + assertTrue(output.getData().equals("Ipython kernel has been stopped. Please check logs. " + + "It might be because of an out of memory issue.")); + } + + private static InterpreterContext getInterpreterContext() { + return new InterpreterContext( + "noteId", + "paragraphId", + "replName", + "paragraphTitle", + "paragraphText", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new GUI(), + null, + null, + null, + new InterpreterOutput(null)); + } +} diff --git a/python/src/test/java/org/apache/zeppelin/python/PythonCondaInterpreterTest.java b/python/src/test/java/org/apache/zeppelin/python/PythonCondaInterpreterTest.java index 28d47e048ec..c750352a81c 100644 --- a/python/src/test/java/org/apache/zeppelin/python/PythonCondaInterpreterTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/PythonCondaInterpreterTest.java @@ -37,7 +37,7 @@ public class PythonCondaInterpreterTest { private PythonInterpreter python; @Before - public void setUp() { + public void setUp() throws InterpreterException { conda = spy(new PythonCondaInterpreter(new Properties())); python = mock(PythonInterpreter.class); @@ -57,7 +57,7 @@ private void setMockCondaEnvList() throws IOException, InterruptedException { } @Test - public void testListEnv() throws IOException, InterruptedException { + public void testListEnv() throws IOException, InterruptedException, InterpreterException { setMockCondaEnvList(); // list available env @@ -72,23 +72,25 @@ public void testListEnv() throws IOException, InterruptedException { } @Test - public void testActivateEnv() throws IOException, InterruptedException { + public void testActivateEnv() throws IOException, InterruptedException, InterpreterException { setMockCondaEnvList(); - + String envname = "env1"; InterpreterContext context = getInterpreterContext(); - conda.interpret("activate env1", context); + conda.interpret("activate " + envname, context); verify(python, times(1)).open(); verify(python, times(1)).close(); verify(python).setPythonCommand("/path1/bin/python"); + assertTrue(envname.equals(conda.getCurrentCondaEnvName())); } @Test - public void testDeactivate() { + public void testDeactivate() throws InterpreterException { InterpreterContext context = getInterpreterContext(); conda.interpret("deactivate", context); verify(python, times(1)).open(); verify(python, times(1)).close(); verify(python).setPythonCommand("python"); + assertTrue(conda.getCurrentCondaEnvName().isEmpty()); } @Test @@ -129,6 +131,7 @@ private InterpreterContext getInterpreterContext() { new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), null, null, null, diff --git a/python/src/test/java/org/apache/zeppelin/python/PythonDockerInterpreterTest.java b/python/src/test/java/org/apache/zeppelin/python/PythonDockerInterpreterTest.java index 566b5e0b35a..56346304530 100644 --- a/python/src/test/java/org/apache/zeppelin/python/PythonDockerInterpreterTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/PythonDockerInterpreterTest.java @@ -41,7 +41,7 @@ public class PythonDockerInterpreterTest { private PythonInterpreter python; @Before - public void setUp() { + public void setUp() throws InterpreterException { docker = spy(new PythonDockerInterpreter(new Properties())); python = mock(PythonInterpreter.class); @@ -58,7 +58,7 @@ public void setUp() { } @Test - public void testActivateEnv() { + public void testActivateEnv() throws InterpreterException { InterpreterContext context = getInterpreterContext(); docker.interpret("activate env", context); verify(python, times(1)).open(); @@ -68,7 +68,7 @@ public void testActivateEnv() { } @Test - public void testDeactivate() { + public void testDeactivate() throws InterpreterException { InterpreterContext context = getInterpreterContext(); docker.interpret("deactivate", context); verify(python, times(1)).open(); @@ -86,6 +86,7 @@ private InterpreterContext getInterpreterContext() { new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), null, null, 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..1ab9cf197a8 100644 --- a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterMatplotlibTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterMatplotlibTest.java @@ -22,6 +22,7 @@ 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; @@ -53,6 +54,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(); @@ -69,6 +71,7 @@ public void setUp() throws Exception { new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), new AngularObjectRegistry(intpGroup.getId(), null), new LocalResourcePool("id"), new LinkedList(), @@ -77,12 +80,12 @@ public void setUp() throws Exception { } @After - public void afterTest() throws IOException { + public void afterTest() throws IOException, InterpreterException { python.close(); } @Test - public void dependenciesAreInstalled() { + public void dependenciesAreInstalled() throws InterpreterException { // matplotlib InterpreterResult ret = python.interpret("import matplotlib", context); assertEquals(ret.message().toString(), InterpreterResult.Code.SUCCESS, ret.code()); @@ -93,7 +96,7 @@ public void dependenciesAreInstalled() { } @Test - public void showPlot() throws IOException { + public void showPlot() throws IOException, InterpreterException { // Simple plot test InterpreterResult ret; ret = python.interpret("import matplotlib.pyplot as plt", context); @@ -110,7 +113,7 @@ public void showPlot() throws IOException { @Test // Test for when configuration is set to auto-close figures after show(). - public void testClose() throws IOException { + public void testClose() throws IOException, InterpreterException { InterpreterResult ret; InterpreterResult ret1; InterpreterResult ret2; @@ -144,7 +147,7 @@ public void testClose() throws IOException { @Test // Test for when configuration is set to not auto-close figures after show(). - public void testNoClose() throws IOException { + public void testNoClose() throws IOException, InterpreterException { InterpreterResult ret; InterpreterResult ret1; InterpreterResult ret2; 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..c462d2e2c41 100644 --- a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterPandasSqlTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterPandasSqlTest.java @@ -33,11 +33,13 @@ 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.InterpreterResult.Type; +import org.apache.zeppelin.interpreter.InterpreterResultMessage; import org.apache.zeppelin.interpreter.InterpreterResultMessageOutput; import org.apache.zeppelin.resource.LocalResourcePool; import org.apache.zeppelin.user.AuthenticationInfo; @@ -73,9 +75,22 @@ 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 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 +100,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); @@ -104,18 +110,18 @@ public void setUp() throws Exception { } @After - public void afterTest() throws IOException { + public void afterTest() throws IOException, InterpreterException { sql.close(); } @Test - public void dependenciesAreInstalled() { + public void dependenciesAreInstalled() throws InterpreterException { InterpreterResult ret = python.interpret("import pandas\nimport pandasql\nimport numpy\n", context); assertEquals(ret.message().toString(), InterpreterResult.Code.SUCCESS, ret.code()); } @Test - public void errorMessageIfDependenciesNotInstalled() { + public void errorMessageIfDependenciesNotInstalled() throws InterpreterException { InterpreterResult ret; ret = sql.interpret("SELECT * from something", context); @@ -125,7 +131,7 @@ public void errorMessageIfDependenciesNotInstalled() { } @Test - public void sqlOverTestDataPrintsTable() throws IOException { + public void sqlOverTestDataPrintsTable() throws IOException, InterpreterException { InterpreterResult ret; // given //String expectedTable = "name\tage\n\nmoon\t33\n\npark\t34"; @@ -140,27 +146,28 @@ 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(InterpreterResult.Code.SUCCESS, ret.code()); + List outputs = out.toInterpreterResultMessage(); + assertEquals(1, outputs.size()); + assertEquals(Type.TABLE, outputs.get(0).getType()); + assertTrue(outputs.get(0).getData().contains("moon\t33")); + assertTrue(outputs.get(0).getData().contains("park\t34")); assertEquals(InterpreterResult.Code.SUCCESS, sql.interpret("select case when name==\"aa\" then name else name end from df2", context).code()); } @Test - public void badSqlSyntaxFails() throws IOException { + public void badSqlSyntaxFails() throws IOException, InterpreterException { //when InterpreterResult ret = sql.interpret("select wrong syntax", context); //then assertNotNull("Interpreter returned 'null'", ret); assertEquals(ret.toString(), InterpreterResult.Code.ERROR, ret.code()); - assertTrue(out.toInterpreterResultMessage().size() == 0); } @Test - public void showDataFrame() throws IOException { + public void showDataFrame() throws IOException, InterpreterException { InterpreterResult ret; ret = python.interpret("import pandas as pd", context); ret = python.interpret("import numpy as np", context); @@ -176,10 +183,12 @@ 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")); + List outputs = out.toInterpreterResultMessage(); + assertEquals(1, outputs.size()); + assertEquals(Type.TABLE, outputs.get(0).getType()); + assertTrue(outputs.get(0).getData().contains("index_name")); + assertTrue(outputs.get(0).getData().contains("nan")); + assertTrue(outputs.get(0).getData().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..c0beccbd9da 100644 --- a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java +++ b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java @@ -38,6 +38,7 @@ 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; @@ -59,11 +60,12 @@ 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; } @Before - public void beforeTest() throws IOException { + public void beforeTest() throws IOException, InterpreterException { cmdHistory = ""; // python interpreter @@ -81,33 +83,35 @@ public void beforeTest() throws IOException { new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), new AngularObjectRegistry(group.getId(), null), new LocalResourcePool("id"), new LinkedList(), out); + InterpreterContext.set(context); pythonInterpreter.open(); } @After - public void afterTest() throws IOException { + public void afterTest() throws IOException, InterpreterException { pythonInterpreter.close(); } @Test - public void testInterpret() throws InterruptedException, IOException { + public void testInterpret() throws InterruptedException, IOException, InterpreterException { InterpreterResult result = pythonInterpreter.interpret("print (\"hi\")", context); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); } @Test - public void testInterpretInvalidSyntax() throws IOException { + public void testInterpretInvalidSyntax() throws IOException, InterpreterException { InterpreterResult result = pythonInterpreter.interpret("for x in range(0,3): print (\"hi\")\n", context); assertEquals(InterpreterResult.Code.SUCCESS, result.code()); assertTrue(new String(out.getOutputAt(0).toByteArray()).contains("hi\nhi\nhi")); } @Test - public void testRedefinitionZeppelinContext() { + public void testRedefinitionZeppelinContext() throws InterpreterException { String pyRedefinitionCode = "z = 1\n"; String pyRestoreCode = "z = __zeppelin__\n"; String pyValidCode = "z.input(\"test\")\n"; @@ -119,6 +123,12 @@ public void testRedefinitionZeppelinContext() { assertEquals(InterpreterResult.Code.SUCCESS, pythonInterpreter.interpret(pyValidCode, context).code()); } + @Test + public void testOutputClear() throws InterpreterException { + InterpreterResult result = pythonInterpreter.interpret("print(\"Hello\")\nz.getInterpreterContext().out().clear()\nprint(\"world\")\n", context); + assertEquals("%text world\n", out.getCurrentOutput().toString()); + } + @Override public void onUpdateAll(InterpreterOutput out) { 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/r/pom.xml b/r/pom.xml index 4c1b218335d..81bbdce4bc6 100644 --- a/r/pom.xml +++ b/r/pom.xml @@ -21,16 +21,19 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent - zeppelin-zrinterpreter_2.10 + org.apache.zeppelin + zeppelin-zrinterpreter_${scala.binary.version} jar + 0.8.2-mapr-1912-r2 Zeppelin: R Interpreter R Interpreter for Zeppelin + http://zeppelin.apache.org .sh @@ -68,13 +71,6 @@ provided - - ${project.groupId} - zeppelin-spark-dependencies_${scala.binary.version} - ${project.version} - provided - - ${project.groupId} zeppelin-interpreter @@ -84,7 +80,7 @@ ${project.groupId} - zeppelin-spark_${scala.binary.version} + spark-interpreter ${project.version} provided @@ -359,7 +355,6 @@ - scala-2.10 diff --git a/r/src/main/java/org/apache/zeppelin/rinterpreter/KnitR.java b/r/src/main/java/org/apache/zeppelin/rinterpreter/KnitR.java index bdc5b868605..ab29efe9b7b 100644 --- a/r/src/main/java/org/apache/zeppelin/rinterpreter/KnitR.java +++ b/r/src/main/java/org/apache/zeppelin/rinterpreter/KnitR.java @@ -34,12 +34,12 @@ public class KnitR extends Interpreter implements WrappedInterpreter { KnitRInterpreter intp; - public KnitR(Properties property, Boolean startSpark) { - super(property); - intp = new KnitRInterpreter(property, startSpark); + public KnitR(Properties properties, Boolean startSpark) { + super(properties); + intp = new KnitRInterpreter(properties, startSpark); } - public KnitR(Properties property) { - this(property, true); + public KnitR(Properties properties) { + this(properties, true); } public KnitR() { @@ -47,38 +47,39 @@ public KnitR() { } @Override - public void open() { + public void open() throws InterpreterException { intp.open(); } @Override - public void close() { + public void close() throws InterpreterException { intp.close(); } @Override - public InterpreterResult interpret(String s, InterpreterContext interpreterContext) { + public InterpreterResult interpret(String s, InterpreterContext interpreterContext) + throws InterpreterException { return intp.interpret(s, interpreterContext); } @Override - public void cancel(InterpreterContext interpreterContext) { + public void cancel(InterpreterContext interpreterContext) throws InterpreterException { intp.cancel(interpreterContext); } @Override - public FormType getFormType() { + public FormType getFormType() throws InterpreterException { return intp.getFormType(); } @Override - public int getProgress(InterpreterContext interpreterContext) { + public int getProgress(InterpreterContext interpreterContext) throws InterpreterException { return intp.getProgress(interpreterContext); } @Override public List completion(String s, int i, - InterpreterContext interpreterContext) { + InterpreterContext interpreterContext) throws InterpreterException { List completion = intp.completion(s, i, interpreterContext); return completion; } @@ -94,14 +95,14 @@ public Scheduler getScheduler() { } @Override - public void setProperty(Properties property) { - super.setProperty(property); - intp.setProperty(property); + public void setProperties(Properties properties) { + super.setProperties(properties); + intp.setProperties(properties); } @Override - public Properties getProperty() { - return intp.getProperty(); + public Properties getProperties() { + return intp.getProperties(); } @Override diff --git a/r/src/main/java/org/apache/zeppelin/rinterpreter/RRepl.java b/r/src/main/java/org/apache/zeppelin/rinterpreter/RRepl.java index 81891f80ce7..bdf7dae8765 100644 --- a/r/src/main/java/org/apache/zeppelin/rinterpreter/RRepl.java +++ b/r/src/main/java/org/apache/zeppelin/rinterpreter/RRepl.java @@ -34,12 +34,12 @@ public class RRepl extends Interpreter implements WrappedInterpreter { RReplInterpreter intp; - public RRepl(Properties property, Boolean startSpark) { - super(property); - intp = new RReplInterpreter(property, startSpark); + public RRepl(Properties properties, Boolean startSpark) { + super(properties); + intp = new RReplInterpreter(properties, startSpark); } - public RRepl(Properties property) { - this(property, true); + public RRepl(Properties properties) { + this(properties, true); } public RRepl() { @@ -47,38 +47,39 @@ public RRepl() { } @Override - public void open() { + public void open() throws InterpreterException { intp.open(); } @Override - public void close() { + public void close() throws InterpreterException { intp.close(); } @Override - public InterpreterResult interpret(String s, InterpreterContext interpreterContext) { + public InterpreterResult interpret(String s, InterpreterContext interpreterContext) + throws InterpreterException { return intp.interpret(s, interpreterContext); } @Override - public void cancel(InterpreterContext interpreterContext) { + public void cancel(InterpreterContext interpreterContext) throws InterpreterException { intp.cancel(interpreterContext); } @Override - public FormType getFormType() { + public FormType getFormType() throws InterpreterException { return intp.getFormType(); } @Override - public int getProgress(InterpreterContext interpreterContext) { + public int getProgress(InterpreterContext interpreterContext) throws InterpreterException { return intp.getProgress(interpreterContext); } @Override public List completion(String s, int i, - InterpreterContext interpreterContext) { + InterpreterContext interpreterContext) throws InterpreterException { List completion = intp.completion(s, i, interpreterContext); return completion; } @@ -94,14 +95,14 @@ public Scheduler getScheduler() { } @Override - public void setProperty(Properties property) { - super.setProperty(property); - intp.setProperty(property); + public void setProperties(Properties properties) { + super.setProperties(properties); + intp.setProperties(properties); } @Override - public Properties getProperty() { - return intp.getProperty(); + public Properties getProperties() { + return intp.getProperties(); } @Override diff --git a/r/src/main/resources/interpreter-setting.json b/r/src/main/resources/interpreter-setting.json index b7dcaf7fafc..c5997a3441c 100644 --- a/r/src/main/resources/interpreter-setting.json +++ b/r/src/main/resources/interpreter-setting.json @@ -24,6 +24,11 @@ "defaultValue": "60", "type": "number" } + }, + "editor": { + "language": "r", + "editOnDblClick": false, + "completionKey": "TAB" } }, { diff --git a/r/src/main/scala/org/apache/zeppelin/rinterpreter/KnitRInterpreter.scala b/r/src/main/scala/org/apache/zeppelin/rinterpreter/KnitRInterpreter.scala index bc779c7a4f1..64b1d2686e5 100644 --- a/r/src/main/scala/org/apache/zeppelin/rinterpreter/KnitRInterpreter.scala +++ b/r/src/main/scala/org/apache/zeppelin/rinterpreter/KnitRInterpreter.scala @@ -27,9 +27,9 @@ import org.apache.zeppelin.interpreter.InterpreterResult import org.apache.zeppelin.rinterpreter.rscala.RException -class KnitRInterpreter(property: Properties, startSpark : Boolean = true) extends RInterpreter(property, startSpark) { - def this(property : Properties) = { - this(property, true) +class KnitRInterpreter(properties: Properties, startSpark : Boolean = true) extends RInterpreter(properties, startSpark) { + def this(properties : Properties) = { + this(properties, true) } override def open: Unit = { diff --git a/r/src/main/scala/org/apache/zeppelin/rinterpreter/RInterpreter.scala b/r/src/main/scala/org/apache/zeppelin/rinterpreter/RInterpreter.scala index 9f5181d1bf7..0783f6c38c2 100644 --- a/r/src/main/scala/org/apache/zeppelin/rinterpreter/RInterpreter.scala +++ b/r/src/main/scala/org/apache/zeppelin/rinterpreter/RInterpreter.scala @@ -41,7 +41,7 @@ abstract class RInterpreter(properties : Properties, startSpark : Boolean = true def getrContext: RContext = rContext - protected lazy val rContext : RContext = synchronized{ RContext(property, this.getInterpreterGroup().getId()) } + protected lazy val rContext : RContext = synchronized{ RContext(properties, this.getInterpreterGroup().getId()) } def open: Unit = rContext.synchronized { logger.trace("RInterpreter opening") diff --git a/r/src/main/scala/org/apache/zeppelin/rinterpreter/RReplInterpreter.scala b/r/src/main/scala/org/apache/zeppelin/rinterpreter/RReplInterpreter.scala index 63be30240c0..013ccd8dd88 100644 --- a/r/src/main/scala/org/apache/zeppelin/rinterpreter/RReplInterpreter.scala +++ b/r/src/main/scala/org/apache/zeppelin/rinterpreter/RReplInterpreter.scala @@ -26,12 +26,12 @@ import org.apache.zeppelin.interpreter.InterpreterContext import org.apache.zeppelin.interpreter.InterpreterResult import org.apache.zeppelin.rinterpreter.rscala.RException -class RReplInterpreter(property: Properties, startSpark : Boolean = true) extends RInterpreter(property, startSpark) { +class RReplInterpreter(properties: Properties, startSpark : Boolean = true) extends RInterpreter(properties, startSpark) { - // protected val rContext : RContext = RContext(property) + // protected val rContext : RContext = RContext(properties) - def this(property : Properties) = { - this(property, true) + def this(properties : Properties) = { + this(properties, true) } private var firstCell : Boolean = true def interpret(st: String, context: InterpreterContext): InterpreterResult = { diff --git a/r/src/test/scala/org/apache/zeppelin/rinterpreter/RInterpreterTest.scala b/r/src/test/scala/org/apache/zeppelin/rinterpreter/RInterpreterTest.scala index 72085162aad..443394c4083 100644 --- a/r/src/test/scala/org/apache/zeppelin/rinterpreter/RInterpreterTest.scala +++ b/r/src/test/scala/org/apache/zeppelin/rinterpreter/RInterpreterTest.scala @@ -85,7 +85,7 @@ class RInterpreterTest extends FlatSpec { it should "have persistent properties" in { val props = new Properties() props.setProperty("hello", "world") - rint.setProperty(props) + rint.setProperties(props) assertResult("world") { rint.getProperty("hello") } diff --git a/sap/pom.xml b/sap/pom.xml new file mode 100644 index 00000000000..0f7a14cd474 --- /dev/null +++ b/sap/pom.xml @@ -0,0 +1,86 @@ + + + + + 4.0.0 + + + interpreter-parent + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + ../interpreter-parent + + + org.apache.zeppelin + sap + jar + 0.8.2-mapr-1912-r2 + Zeppelin: Sap + Zeppelin SAP support + + + sap + + + + + ${project.groupId} + zeppelin-interpreter + ${project.version} + + + + org.slf4j + slf4j-api + + + + org.slf4j + slf4j-log4j12 + + + + junit + junit + test + + + + org.mockito + mockito-all + test + + + + + + + maven-enforcer-plugin + + + maven-dependency-plugin + + + maven-resources-plugin + + + + + diff --git a/sap/src/main/java/org/apache/zeppelin/sap/UniverseInterpreter.java b/sap/src/main/java/org/apache/zeppelin/sap/UniverseInterpreter.java new file mode 100644 index 00000000000..17da1c95d37 --- /dev/null +++ b/sap/src/main/java/org/apache/zeppelin/sap/UniverseInterpreter.java @@ -0,0 +1,248 @@ +/* + * 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.sap; + +import org.apache.commons.lang.StringUtils; +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.sap.universe.*; +import org.apache.zeppelin.scheduler.Scheduler; +import org.apache.zeppelin.scheduler.SchedulerFactory; + + +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * SAP Universe interpreter for Zeppelin. + */ +public class UniverseInterpreter extends Interpreter { + + public UniverseInterpreter(Properties properties) { + super(properties); + } + + private UniverseClient client; + private UniverseUtil universeUtil; + private UniverseCompleter universeCompleter; + + private static final String EMPTY_COLUMN_VALUE = StringUtils.EMPTY; + private static final char WHITESPACE = ' '; + private static final char NEWLINE = '\n'; + private static final char TAB = '\t'; + private static final String TABLE_MAGIC_TAG = "%table "; + private static final String EMPTY_DATA_MESSAGE = "%html\n" + + "

    No Data Available

    "; + + private static final String CONCURRENT_EXECUTION_KEY = "universe.concurrent.use"; + private static final String CONCURRENT_EXECUTION_COUNT = "universe.concurrent.maxConnection"; + + @Override + public void open() throws InterpreterException { + String user = getProperty("universe.user"); + String password = getProperty("universe.password"); + String apiUrl = getProperty("universe.api.url"); + String authType = getProperty("universe.authType"); + final int queryTimeout = Integer.parseInt( + StringUtils.defaultIfEmpty(getProperty("universe.queryTimeout"), "7200000")); + this.client = + new UniverseClient(user, password, apiUrl, authType, queryTimeout); + this.universeUtil = new UniverseUtil(); + } + + @Override + public void close() throws InterpreterException { + try { + client.close(); + } catch (Exception e) { + throw new InterpreterException(e.getCause()); + } + } + + @Override + public InterpreterResult interpret(String originalSt, InterpreterContext context) + throws InterpreterException { + final String st = Boolean.parseBoolean(getProperty("universe.interpolation", "false")) ? + interpolate(originalSt, context.getResourcePool()) : originalSt; + try { + InterpreterResult interpreterResult = new InterpreterResult(InterpreterResult.Code.SUCCESS); + String paragraphId = context.getParagraphId(); + String token = client.getToken(paragraphId); + client.loadUniverses(token); + UniverseQuery universeQuery = universeUtil.convertQuery(st, client, token); + String queryId = client.createQuery(token, universeQuery); + // process parameters + List parameters = client.getParameters(token, queryId); + + for (UniverseQueryPrompt parameter : parameters) { + Object value = context.getGui().getParams().get(parameter.getName()); + if (value != null) { + parameter.setValue(value.toString()); + } + context.getGui().textbox(parameter.getName(), StringUtils.EMPTY); + } + + if (!parameters.isEmpty() && parameters.size() != context.getGui().getParams().size()) { + client.deleteQuery(token, queryId); + interpreterResult.add("Set parameters"); + return interpreterResult; + } + + if (!parameters.isEmpty()) { + client.setParametersValues(token, queryId, parameters); + } + + // get results + List> results = client.getResults(token, queryId); + String table = formatResults(results); + // remove query + client.deleteQuery(token, queryId); + interpreterResult.add(table); + return interpreterResult; + } catch (Exception e) { + throw new InterpreterException(e.getMessage(), e); + } finally { + try { + client.closeSession(context.getParagraphId()); + } catch (Exception e) { + logger.error("Error close SAP session", e ); + } + } + } + + @Override + public void cancel(InterpreterContext context) throws InterpreterException { + try { + client.closeSession(context.getParagraphId()); + } catch (Exception e) { + logger.error("Error close SAP session", e ); + } + } + + @Override + public FormType getFormType() throws InterpreterException { + return FormType.NATIVE; + } + + @Override + public int getProgress(InterpreterContext context) throws InterpreterException { + return 0; + } + + @Override + public List completion(String buf, int cursor, + InterpreterContext interpreterContext) + throws InterpreterException { + List candidates = new ArrayList<>(); + + try { + universeCompleter = createOrUpdateUniverseCompleter(interpreterContext, buf, cursor); + universeCompleter.complete(buf, cursor, candidates); + } catch (UniverseException e) { + logger.error("Error update completer", e ); + } + + return candidates; + } + + @Override + public Scheduler getScheduler() { + String schedulerName = UniverseInterpreter.class.getName() + this.hashCode(); + return isConcurrentExecution() ? + SchedulerFactory.singleton().createOrGetParallelScheduler(schedulerName, + getMaxConcurrentConnection()) + : SchedulerFactory.singleton().createOrGetFIFOScheduler(schedulerName); + } + + private boolean isConcurrentExecution() { + return Boolean.valueOf(getProperty(CONCURRENT_EXECUTION_KEY, "true")); + } + + private int getMaxConcurrentConnection() { + return Integer.valueOf( + StringUtils.defaultIfEmpty(getProperty(CONCURRENT_EXECUTION_COUNT), "10")); + } + + private String formatResults(List> results) { + StringBuilder msg = new StringBuilder(); + if (results != null) { + if (results.isEmpty()) { + return EMPTY_DATA_MESSAGE; + } + msg.append(TABLE_MAGIC_TAG); + for (int i = 0; i < results.size(); i++) { + List items = results.get(i); + for (int j = 0; j < items.size(); j++) { + if (j > 0) { + msg.append(TAB); + } + msg.append(replaceReservedChars(items.get(j))); + } + msg.append(NEWLINE); + } + } + + return msg.toString(); + } + + private String replaceReservedChars(String str) { + if (str == null) { + return EMPTY_COLUMN_VALUE; + } + return str.replace(TAB, WHITESPACE).replace(NEWLINE, WHITESPACE); + } + + private UniverseCompleter createOrUpdateUniverseCompleter(InterpreterContext interpreterContext, + final String buf, final int cursor) + throws UniverseException { + final UniverseCompleter completer; + if (universeCompleter == null) { + completer = new UniverseCompleter(3600); + } else { + completer = universeCompleter; + } + try { + final String token = client.getToken(interpreterContext.getParagraphId()); + ExecutorService executorService = Executors.newFixedThreadPool(1); + executorService.execute(new Runnable() { + @Override + public void run() { + completer.createOrUpdate(client, token, buf, cursor); + } + }); + + executorService.shutdown(); + + executorService.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.warn("Completion timeout", e); + } finally { + try { + client.closeSession(interpreterContext.getParagraphId()); + } catch (Exception e) { + logger.error("Error close SAP session", e ); + } + } + return completer; + } +} diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseClient.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseClient.java new file mode 100644 index 00000000000..c397f45902d --- /dev/null +++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseClient.java @@ -0,0 +1,799 @@ +/* + * 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.sap.universe; + +import com.sun.org.apache.xpath.internal.NodeSet; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.exception.ExceptionUtils; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.*; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.message.BasicHeader; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * Client for API SAP Universe + */ +public class UniverseClient { + + private static Logger logger = LoggerFactory.getLogger(UniverseClient.class); + private static final String TOKEN_HEADER = "X-SAP-LogonToken"; + private static final String EL_FOLDER = "folder"; + private static final String EL_ITEM = "item"; + private static final String EL_NAME = "name"; + private static final String EL_PATH = "path"; + private static final String EL_ID = "id"; + private static final String EL_TECH_NAME = "technicalName"; + private static final String EL_ANSWER = "answer"; + private static final String EL_INFO = "info"; + private Map tokens = new HashMap(); + private static final long DAY = 1000 * 60 * 60 * 24; + private CloseableHttpClient httpClient; + private String user; + private String password; + private String apiUrl; + private String authType; + private Header[] commonHeaders = { + new BasicHeader("Accept", "application/xml"), + new BasicHeader("Content-Type", "application/xml") + }; + // + private final Map universesMap = new ConcurrentHashMap(); + private final Map> universeInfosMap = + new ConcurrentHashMap(); + // for update the data (which was not updated a long time) + private long universesUpdated = 0; + private Map universesInfoUpdatedMap = new HashMap<>(); + + private final String loginRequestTemplate = "\n" + + "%s\n" + + "%s\n" + + "%s\n" + ""; + private final String createQueryRequestTemplate = + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " " + + " \n%s\n" + + " %s\n" + + " \n" + + "\n" + + "\n"; + private final String filterPartTemplate = "%s\n"; + private final String errorMessageTemplate = "%s\n\n%s"; + private final String parameterTemplate = "\n" + + "%s\n" + + "%s\n" + + "%s\n" + + "%s\n" + + "\n"; + private final String parameterAnswerTemplate = "\n" + + " \n" + + " \n" + " " + + " %s\n" + + " \n" + + " \n"; + + public UniverseClient(String user, String password, String apiUrl, String authType, + int queryTimeout) { + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(queryTimeout) + .setSocketTimeout(queryTimeout) + .build(); + PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); + cm.setMaxTotal(100); + cm.setDefaultMaxPerRoute(100); + cm.closeIdleConnections(10, TimeUnit.MINUTES); + httpClient = HttpClientBuilder.create() + .setConnectionManager(cm) + .setDefaultRequestConfig(requestConfig) + .build(); + + this.user = user; + this.password = password; + this.authType = authType; + if (StringUtils.isNotBlank(apiUrl)) { + this.apiUrl = apiUrl.replaceAll("/$", ""); + } + } + + public void close() throws UniverseException { + for (String s : tokens.keySet()) { + closeSession(s); + } + try { + httpClient.close(); + } catch (Exception e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(close all): Error close HTTP client", ExceptionUtils.getStackTrace(e))); + } + + } + + public String createQuery(String token, UniverseQuery query) throws UniverseException { + try { + HttpPost httpPost = new HttpPost(String.format("%s%s", apiUrl, "/sl/v1/queries")); + setHeaders(httpPost, token); + String where = StringUtils.isNotBlank(query.getWhere()) ? + String.format(filterPartTemplate, query.getWhere()) : StringUtils.EMPTY; + httpPost.setEntity(new StringEntity( + String.format(createQueryRequestTemplate, query.getUniverseInfo().getType(), + query.getUniverseInfo().getId(), query.getDuplicatedRows(), + query.getMaxRowsRetrieved().isPresent(), query.getMaxRowsRetrieved().orElse(0), + query.getSelect(), where), "UTF-8")); + HttpResponse response = httpClient.execute(httpPost); + + if (response.getStatusLine().getStatusCode() == 200) { + return getValue(EntityUtils.toString(response.getEntity()), "//success/id"); + } + + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(create query): Request failed\n", EntityUtils.toString(response.getEntity()))); + } catch (IOException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(create query): Request failed", ExceptionUtils.getStackTrace(e))); + } catch (ParserConfigurationException | SAXException | XPathExpressionException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(create query): Response processing failed", ExceptionUtils.getStackTrace(e))); + } + } + + public void deleteQuery(String token, String queryId) throws UniverseException { + try { + if (StringUtils.isNotBlank(queryId)) { + HttpDelete httpDelete = new HttpDelete(String.format("%s%s%s", apiUrl, "/sl/v1/queries/", + queryId)); + setHeaders(httpDelete, token); + httpClient.execute(httpDelete); + } + } catch (Exception e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(delete query): Request failed", ExceptionUtils.getStackTrace(e))); + } + } + + public List> getResults(String token, String queryId) throws UniverseException { + HttpGet httpGet = new HttpGet(String.format("%s%s%s%s", apiUrl, "/sl/v1/queries/", + queryId, "/data.svc/Flows0")); + setHeaders(httpGet, token); + HttpResponse response = null; + try { + response = httpClient.execute(httpGet); + if (response.getStatusLine().getStatusCode() != 200) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get results): Request failed\n", EntityUtils.toString(response.getEntity()))); + } + } catch (IOException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get results): Request failed", ExceptionUtils.getStackTrace(e))); + } + + try (InputStream xmlStream = response.getEntity().getContent()) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(xmlStream); + XPathFactory xPathfactory = XPathFactory.newInstance(); + XPath xpath = xPathfactory.newXPath(); + XPathExpression expr = xpath.compile("//feed/entry/content/properties"); + NodeList resultsNodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET); + if (resultsNodes != null) { + return parseResults(resultsNodes); + } else { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get results): Response processing failed")); + } + } catch (IOException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get results): Request failed", ExceptionUtils.getStackTrace(e))); + } catch (ParserConfigurationException | SAXException | XPathExpressionException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get results): Response processing failed", ExceptionUtils.getStackTrace(e))); + } + } + + public String getToken(String paragraphId) throws UniverseException { + try { + if (tokens.containsKey(paragraphId)) { + return tokens.get(paragraphId); + } + HttpPost httpPost = new HttpPost(String.format("%s%s", apiUrl, "/logon/long")); + setHeaders(httpPost); + + httpPost.setEntity(new StringEntity( + String.format(loginRequestTemplate, user, password, authType), "UTF-8")); + HttpResponse response = httpClient.execute(httpPost); + String result = null; + if (response.getStatusLine().getStatusCode() == 200) { + result = getValue(EntityUtils.toString(response.getEntity()), + "//content/attrs/attr[@name=\"logonToken\"]"); + tokens.put(paragraphId, result); + } + + return result; + } catch (IOException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get token): Request failed", ExceptionUtils.getStackTrace(e))); + } catch (ParserConfigurationException | SAXException | XPathExpressionException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get token): Response processing failed", ExceptionUtils.getStackTrace(e))); + } + } + + public boolean closeSession(String paragraphId) throws UniverseException { + try { + if (tokens.containsKey(paragraphId)) { + HttpPost httpPost = new HttpPost(String.format("%s%s", apiUrl, "/logoff")); + setHeaders(httpPost, tokens.get(paragraphId)); + HttpResponse response = httpClient.execute(httpPost); + if (response.getStatusLine().getStatusCode() == 200) { + return true; + } + } + + return false; + } catch (Exception e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(close session): Request failed", ExceptionUtils.getStackTrace(e))); + } finally { + tokens.remove(paragraphId); + } + } + + public UniverseInfo getUniverseInfo(String universeName) { + return universesMap.get(universeName); + } + + public Map getUniverseNodesInfo(String token, String universeName) + throws UniverseException { + UniverseInfo universeInfo = universesMap.get(universeName); + if (universeInfo != null && StringUtils.isNotBlank(universeInfo.getId())) { + Map universeNodeInfoMap = universeInfosMap.get(universeName); + if (universeNodeInfoMap != null && universesInfoUpdatedMap.containsKey(universeName) && + !isExpired(universesInfoUpdatedMap.get(universeName))) { + return universeNodeInfoMap; + } else { + universeNodeInfoMap = new HashMap<>(); + } + try { + HttpGet httpGet = + new HttpGet(String.format("%s%s%s", apiUrl, "/sl/v1/universes/", universeInfo.getId())); + setHeaders(httpGet, token); + HttpResponse response = httpClient.execute(httpGet); + + if (response.getStatusLine().getStatusCode() == 200) { + try (InputStream xmlStream = response.getEntity().getContent()) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(xmlStream); + XPathFactory xPathfactory = XPathFactory.newInstance(); + XPath xpath = xPathfactory.newXPath(); + XPathExpression expr = xpath.compile("//outline/folder"); + XPathExpression exprRootItems = xpath.compile("//outline/item"); + NodeList universeInfoNodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET); + NodeList universeRootInfoNodes = + (NodeList) exprRootItems.evaluate(doc, XPathConstants.NODESET); + if (universeInfoNodes != null) { + parseUniverseInfo(universeInfoNodes, universeNodeInfoMap); + } + if (universeRootInfoNodes != null) { + parseUniverseInfo(universeRootInfoNodes, universeNodeInfoMap); + } + } catch (Exception e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get universe nodes info): Response processing failed", + ExceptionUtils.getStackTrace(e))); + } + } + } catch (IOException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get universe nodes info): Request failed", ExceptionUtils.getStackTrace(e))); + } + universeInfosMap.put(universeName, universeNodeInfoMap); + universesInfoUpdatedMap.put(universeName, System.currentTimeMillis()); + + return universeNodeInfoMap; + } + return Collections.emptyMap(); + + } + + public void loadUniverses(String token) throws UniverseException { + if (universesMap.isEmpty() || universesUpdated == 0 || isExpired(universesUpdated)) { + Map universes = new ConcurrentHashMap(); + loadUniverses(token, 0, universes); + universesMap.clear(); + universesMap.putAll(universes); + universesUpdated = System.currentTimeMillis(); + } + } + + public void cleanUniverses() { + universesMap.clear(); + } + + public void removeUniverseInfo(String universe) { + universeInfosMap.remove(universe); + } + + public Map getUniversesMap() { + return universesMap; + } + + public List getParameters(String token, String queryId) + throws UniverseException { + HttpGet httpGet = new HttpGet(String.format("%s%s%s%s", apiUrl, "/sl/v1/queries/", + queryId, "/parameters")); + setHeaders(httpGet, token); + HttpResponse response = null; + try { + response = httpClient.execute(httpGet); + if (response.getStatusLine().getStatusCode() != 200) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get parameters): Request failed\n", EntityUtils.toString(response.getEntity()))); + } + } catch (IOException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get parameters): Request failed", ExceptionUtils.getStackTrace(e))); + } + + try (InputStream xmlStream = response.getEntity().getContent()) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(xmlStream); + XPathFactory xPathfactory = XPathFactory.newInstance(); + XPath xpath = xPathfactory.newXPath(); + XPathExpression expr = xpath.compile("//parameters/parameter"); + NodeList parametersNodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET); + if (parametersNodes != null) { + return parseParameters(parametersNodes); + } else { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get parameters): Response processing failed")); + } + } catch (IOException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get parameters): Response processing failed", ExceptionUtils.getStackTrace(e))); + } catch (ParserConfigurationException | SAXException | XPathExpressionException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get parameters): Response processing failed", ExceptionUtils.getStackTrace(e))); + } + } + + public void setParametersValues(String token, String queryId, + List parameters) throws UniverseException { + HttpPut httpPut = new HttpPut(String.format("%s%s%s%s", apiUrl, "/sl/v1/queries/", + queryId, "/parameters")); + setHeaders(httpPut, token); + HttpResponse response = null; + try { + StringBuilder request = new StringBuilder(); + request.append("\n"); + for (UniverseQueryPrompt parameter : parameters) { + String answer = String.format(parameterAnswerTemplate, parameter.getConstrained(), + parameter.getType(), parameter.getCardinality(), parameter.getKeepLastValues(), + parameter.getValue()); + String id = parameter.getId() != null ? String.format("%s\n", parameter.getId()) : + StringUtils.EMPTY; + String technicalName = parameter.getTechnicalName() != null ? + String.format("%s\n", parameter.getTechnicalName()) : + StringUtils.EMPTY; + String name = parameter.getTechnicalName() != null ? + String.format("%s\n", parameter.getName()) : + StringUtils.EMPTY; + request.append(String.format(parameterTemplate, id, technicalName, name, answer)); + } + request.append("\n"); + + httpPut.setEntity(new StringEntity(request.toString(), "UTF-8")); + + response = httpClient.execute(httpPut); + if (response.getStatusLine().getStatusCode() != 200) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(set parameters): Request failed\n", EntityUtils.toString(response.getEntity()))); + } + } catch (IOException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(set parameters): Request failed", ExceptionUtils.getStackTrace(e))); + } + } + + private void loadUniverses(String token, int offset, Map universesMap) + throws UniverseException { + int limit = 50; + HttpGet httpGet = new HttpGet(String.format("%s%s?offset=%s&limit=%s", apiUrl, + "/sl/v1/universes", + offset, limit)); + setHeaders(httpGet, token); + HttpResponse response = null; + try { + response = httpClient.execute(httpGet); + } catch (Exception e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get universes): Request failed", ExceptionUtils.getStackTrace(e))); + } + if (response != null && response.getStatusLine().getStatusCode() == 200) { + try (InputStream xmlStream = response.getEntity().getContent()) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(xmlStream); + XPathFactory xPathfactory = XPathFactory.newInstance(); + XPath xpath = xPathfactory.newXPath(); + XPathExpression expr = xpath.compile("//universe"); + NodeList universesNodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET); + if (universesNodes != null) { + int count = universesNodes.getLength(); + for (int i = 0; i < count; i++) { + Node universe = universesNodes.item(i); + if (universe.hasChildNodes()) { + NodeList universeParameters = universe.getChildNodes(); + int parapetersCount = universeParameters.getLength(); + String id = null; + String name = null; + String type = null; + for (int j = 0; j < parapetersCount; j++) { + Node parameterNode = universeParameters.item(j); + parameterNode.getNodeName(); + if (parameterNode.getNodeType() == Node.ELEMENT_NODE) { + if (parameterNode.getNodeName().equalsIgnoreCase("id")) { + id = parameterNode.getTextContent(); + continue; + } + if (parameterNode.getNodeName().equalsIgnoreCase("name")) { + name = parameterNode.getTextContent(); + continue; + } + if (parameterNode.getNodeName().equalsIgnoreCase("type")) { + type = parameterNode.getTextContent(); + continue; + } + } + } + if (StringUtils.isNotBlank(type)) { + name = name.replaceAll(String.format("\\.%s$", type), StringUtils.EMPTY); + } + universesMap.put(name, new UniverseInfo(id, name, type)); + } + } + if (count == limit) { + offset += limit; + loadUniverses(token, offset, universesMap); + } + } + } catch (IOException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get universes): Response processing failed", ExceptionUtils.getStackTrace(e))); + } catch (ParserConfigurationException | SAXException | XPathExpressionException e) { + throw new UniverseException(String.format(errorMessageTemplate, "UniverseClient " + + "(get universes): Response processing failed", ExceptionUtils.getStackTrace(e))); + } + } + } + + private boolean isExpired(Long lastUpdated) { + if (lastUpdated == null || System.currentTimeMillis() - lastUpdated > DAY) { + return true; + } + + return false; + } + + private void setHeaders(HttpRequestBase request) { + setHeaders(request, null); + } + + private void setHeaders(HttpRequestBase request, String token) { + request.setHeaders(commonHeaders); + if (StringUtils.isNotBlank(token)) { + request.addHeader(TOKEN_HEADER, token); + } + } + + private String getValue(String response, String xPathString) throws ParserConfigurationException, + IOException, SAXException, XPathExpressionException { + try (InputStream xmlStream = new ByteArrayInputStream(response.getBytes())) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(xmlStream); + XPathFactory xPathfactory = XPathFactory.newInstance(); + XPath xpath = xPathfactory.newXPath(); + XPathExpression expr = xpath.compile(xPathString); + Node tokenNode = (Node) expr.evaluate(doc, XPathConstants.NODE); + if (tokenNode != null) { + return tokenNode.getTextContent(); + } + } + return null; + } + + private List parseParameters(NodeList parametersNodeList) { + List parameters = new ArrayList<>(); + if (parametersNodeList != null) { + int count = parametersNodeList.getLength(); + for (int i = 0; i < count; i++) { + Node parameterNode = parametersNodeList.item(i); + Node type = parameterNode.getAttributes().getNamedItem("type"); + if (type != null && type.getTextContent().equalsIgnoreCase("prompt") && + parameterNode.hasChildNodes()) { + NodeList parameterInfoNodes = parameterNode.getChildNodes(); + int childNodesCount = parameterInfoNodes.getLength(); + String name = null; + Integer id = null; + String cardinality = null; + String constrained = null; + String valueType = null; + String technicalName = null; + String keepLastValues = null; + for (int j = 0; j < childNodesCount; j++) { + Node childNode = parameterInfoNodes.item(j); + String childNodeName = childNode.getNodeName(); + if (childNodeName.equalsIgnoreCase(EL_NAME)) { + name = childNode.getTextContent(); + continue; + } + if (childNodeName.equalsIgnoreCase(EL_ID)) { + id = Integer.parseInt(childNode.getTextContent()); + continue; + } + if (childNodeName.equalsIgnoreCase(EL_TECH_NAME)) { + technicalName = childNode.getTextContent(); + continue; + } + if (childNodeName.equalsIgnoreCase(EL_ANSWER)) { + NamedNodeMap answerAttributes = childNode.getAttributes(); + if (answerAttributes.getNamedItem("constrained") != null) { + constrained = answerAttributes.getNamedItem("constrained").getTextContent(); + } + if (answerAttributes.getNamedItem("type") != null) { + valueType = answerAttributes.getNamedItem("type").getTextContent(); + } + NodeList answerNodes = childNode.getChildNodes(); + int answerCount = answerNodes.getLength(); + for (int k = 0; k < answerCount; k++) { + Node answerChildNode = answerNodes.item(k); + String answerChildNodeName = answerChildNode.getNodeName(); + if (answerChildNodeName.equalsIgnoreCase(EL_INFO)) { + NamedNodeMap infoAttributes = answerChildNode.getAttributes(); + if (infoAttributes.getNamedItem("cardinality") != null) { + cardinality = infoAttributes.getNamedItem("cardinality").getTextContent(); + } + if (infoAttributes.getNamedItem("keepLastValues") != null) { + keepLastValues = infoAttributes.getNamedItem("keepLastValues").getTextContent(); + } + break; + } + } + continue; + } + } + if (name != null && id != null && cardinality != null) { + parameters.add(new UniverseQueryPrompt(id, name, cardinality, constrained, valueType, + technicalName, keepLastValues)); + break; + } + } + } + } + + return parameters; + } + + private List> parseResults(NodeList resultsNodeList) { + List> results = new ArrayList<>(); + if (resultsNodeList != null) { + int count = resultsNodeList.getLength(); + for (int i = 0; i < count; i++) { + Node node = resultsNodeList.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE && node.hasChildNodes()) { + NodeList properties = node.getChildNodes(); + if (properties != null) { + int countProperties = properties.getLength(); + List headers = new ArrayList<>(); + List row = new ArrayList<>(); + // first property is id + for (int j = 1; j < countProperties; j++) { + Node propertyNode = properties.item(j); + if (i == 0) { + headers.add(propertyNode.getNodeName().replaceAll("^\\w*:", StringUtils.EMPTY)); + } + row.add(propertyNode.getTextContent()); + } + if (i == 0) { + results.add(headers); + } + results.add(row); + } + } + } + } + + return results; + } + + private void addAttributesToDimension(Node universeNode, Map nodes) { + final NodeList attributeNodes = universeNode.getChildNodes(); + final int attributeNodesCount = attributeNodes.getLength(); + + for (int i = 0; i < attributeNodesCount; ++i) { + final Node attributeNode = attributeNodes.item(i); + + if (attributeNode.getNodeName().equalsIgnoreCase(EL_ITEM)) { + final NodeList childNodes = attributeNode.getChildNodes(); + final int childNodesCount = childNodes.getLength(); + + String nodeId = null; + String nodeName = null; + String nodePath = null; + for (int j = 0; j < childNodesCount; j++) { + Node childNode = childNodes.item(j); + if (childNode.getNodeType() == Node.ELEMENT_NODE) { + switch (childNode.getNodeName().toLowerCase()) { + case EL_NAME: + nodeName = childNode.getTextContent(); + break; + case EL_ID: + nodeId = childNode.getTextContent(); + break; + case EL_PATH: + nodePath = childNode.getTextContent(); + break; + } + } + } + StringBuilder key = new StringBuilder(); + if (StringUtils.isNotBlank(nodeName)) { + String nodeType = null; + String[] parts = nodePath.split("\\\\"); + List path = new ArrayList(); + for (String part : parts) { + String[] p = part.split("\\|"); + if (p.length == 2) { + if (p[1].equalsIgnoreCase("folder") || p[1].equalsIgnoreCase("dimension")) { + path.add(p[0]); + } else { + nodeName = p[0]; + nodeType = p[1]; + } + } + } + final String folder = StringUtils.join(path, "\\"); + key.append("["); + key.append(StringUtils.join(path, "].[")); + key.append(String.format("].[%s]", nodeName)); + nodes.put(key.toString(), + new UniverseNodeInfo(nodeId, nodeName, nodeType, folder, nodePath)); + } + } + } + } + + private void parseUniverseInfo(NodeList universeInfoNodes, Map nodes) { + if (universeInfoNodes != null) { + int count = universeInfoNodes.getLength(); + for (int i = 0; i < count; i++) { + Node node = universeInfoNodes.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE && node.hasChildNodes()) { + String name = node.getNodeName(); + NodeList childNodes = node.getChildNodes(); + int childNodesCount = childNodes.getLength(); + if (name.equalsIgnoreCase(EL_FOLDER)) { + NodeSet list = new NodeSet(); + for (int j = 0; j < childNodesCount; j++) { + Node childNode = childNodes.item(j); + if (childNode.getNodeType() == Node.ELEMENT_NODE && childNode.hasChildNodes()) { + String childNodeName = childNode.getNodeName(); + if (childNodeName.equalsIgnoreCase(EL_FOLDER) + || childNodeName.equalsIgnoreCase(EL_ITEM)) { + list.addNode(childNode); + } + } + } + if (list.getLength() > 0) { + parseUniverseInfo(list, nodes); + } + } else if (name.equalsIgnoreCase(EL_ITEM)) { + String nodeId = null; + String nodeName = null; + String nodePath = null; + for (int j = 0; j < childNodesCount; j++) { + Node childNode = childNodes.item(j); + if (childNode.getNodeType() == Node.ELEMENT_NODE) { + String childNodeName = childNode.getNodeName(); + if (childNodeName.equalsIgnoreCase(EL_NAME)) { + nodeName = childNode.getTextContent(); + continue; + } + if (childNodeName.equalsIgnoreCase(EL_ID)) { + nodeId = childNode.getTextContent(); + continue; + } + if (childNodeName.equalsIgnoreCase(EL_PATH)) { + nodePath = childNode.getTextContent(); + continue; + } + } + } + String folder = null; + StringBuilder key = new StringBuilder(); + if (StringUtils.isNotBlank(nodeName)) { + String nodeType = null; + if (StringUtils.isNotBlank(nodePath)) { + String[] parts = nodePath.split("\\\\"); + List path = new ArrayList(); + for (String part : parts) { + String[] p = part.split("\\|"); + if (p.length == 2) { + if (p[1].equalsIgnoreCase("folder")) { + path.add(p[0]); + } else { + nodeName = p[0]; + nodeType = p[1]; + if (p[1].equalsIgnoreCase("dimension")) { + addAttributesToDimension(node, nodes); + } + } + } + } + folder = StringUtils.join(path, "\\"); + if (path.isEmpty()) { + key.append(String.format("[%s]", nodeName)); + } else { + key.append("["); + key.append(StringUtils.join(path, "].[")); + key.append(String.format("].[%s]", nodeName)); + } + } + nodes.put(key.toString(), + new UniverseNodeInfo(nodeId, nodeName, nodeType, folder, nodePath)); + } + } + } + } + } + } +} diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseCompleter.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseCompleter.java new file mode 100644 index 00000000000..e67011b5dc4 --- /dev/null +++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseCompleter.java @@ -0,0 +1,344 @@ +/* + * 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.sap.universe; + +import jline.console.completer.ArgumentCompleter.ArgumentList; +import jline.console.completer.ArgumentCompleter.WhitespaceArgumentDelimiter; +import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.completer.CachedCompleter; +import org.apache.zeppelin.completer.CompletionType; +import org.apache.zeppelin.completer.StringsCompleter; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.*; +import java.util.regex.Pattern; + +/** + * SAP Universe auto complete functionality. + */ +public class UniverseCompleter { + + private static Logger logger = LoggerFactory.getLogger(UniverseCompleter.class); + + private static final String KEYWORD_SPLITERATOR = ","; + public static final String CLEAN_NAME_REGEX = "\\[|\\]"; + public static final Character START_NAME = '['; + public static final Character END_NAME = ']'; + public static final String KW_UNIVERSE = "universe"; + public static final String TYPE_FOLDER = "folder"; + + private static final Comparator nodeInfoComparator = new Comparator() { + @Override + public int compare(UniverseNodeInfo o1, UniverseNodeInfo o2) { + if (o1.getType().equalsIgnoreCase(TYPE_FOLDER) + && o2.getType().equalsIgnoreCase(TYPE_FOLDER)) { + return o1.getName().compareToIgnoreCase(o2.getName()); + } + if (o1.getType().equalsIgnoreCase(TYPE_FOLDER)) { + return -1; + } + if (o2.getType().equalsIgnoreCase(TYPE_FOLDER)) { + return 1; + } + if (!o1.getType().equalsIgnoreCase(o2.getType())) { + return o1.getType().compareToIgnoreCase(o2.getType()); + } else { + + return o1.getName().compareToIgnoreCase(o2.getName()); + } + } + }; + + /** + * Delimiter that can split keyword list + */ + private WhitespaceArgumentDelimiter sqlDelimiter = new WhitespaceArgumentDelimiter() { + + private Pattern pattern = Pattern.compile(",|;"); + + @Override + public boolean isDelimiterChar(CharSequence buffer, int pos) { + char c = buffer.charAt(pos); + boolean endName = false; + for (int i = pos; i > 0; i--) { + char ch = buffer.charAt(i); + if (ch == '\n') { + break; + } + if (ch == START_NAME && !endName) { + return false; + } + if (ch == END_NAME) { + break; + } + } + return pattern.matcher(StringUtils.EMPTY + buffer.charAt(pos)).matches() + || super.isDelimiterChar(buffer, pos); + } + }; + + /** + * Universe completer + */ + private CachedCompleter universeCompleter; + + /** + * Keywords completer + */ + private CachedCompleter keywordCompleter; + + /** + * UniverseInfo completers + */ + private Map universeInfoCompletersMap = new HashMap<>(); + + private int ttlInSeconds; + + public UniverseCompleter(int ttlInSeconds) { + this.ttlInSeconds = ttlInSeconds; + } + + public int complete(String buffer, int cursor, List candidates) { + CursorArgument cursorArgument = parseCursorArgument(buffer, cursor); + + String argument = cursorArgument.getCursorArgumentPartForComplete(); + if (cursorArgument.isUniverseNamePosition()) { + List universeCandidates = new ArrayList<>(); + universeCompleter.getCompleter().complete(argument, argument.length(), + universeCandidates); + addCompletions(candidates, universeCandidates, CompletionType.universe.name()); + return universeCandidates.size(); + } + + if (cursorArgument.isUniverseNodePosition()) { + List universeNodeCandidates = new ArrayList(); + CachedCompleter completer = universeInfoCompletersMap.get(cursorArgument.getUniverse()); + if (completer != null) { + completer.getCompleter().complete(argument, argument.length(), universeNodeCandidates); + } + Collections.sort(universeNodeCandidates, nodeInfoComparator); + addCompletions(candidates, universeNodeCandidates); + return universeNodeCandidates.size(); + } + + List keywordCandidates = new ArrayList<>(); + keywordCompleter.getCompleter().complete(argument, + argument.length() > 0 ? argument.length() : 0, keywordCandidates); + addCompletions(candidates, keywordCandidates, CompletionType.keyword.name()); + + return keywordCandidates.size(); + } + + public void createOrUpdate(UniverseClient client, String token, String buffer, int cursor) { + try { + CursorArgument cursorArgument = parseCursorArgument(buffer, cursor); + if (keywordCompleter == null || keywordCompleter.getCompleter() == null + || keywordCompleter.isExpired()) { + Set keywords = getKeywordsCompletions(); + if (keywords != null && !keywords.isEmpty()) { + keywordCompleter = new CachedCompleter(new StringsCompleter(keywords), 0); + } + } + if (cursorArgument.needLoadUniverses() || (universeCompleter == null + || universeCompleter.getCompleter() == null || universeCompleter.isExpired())) { + client.cleanUniverses(); + client.loadUniverses(token); + if (client.getUniversesMap().size() > 0) { + universeCompleter = new CachedCompleter( + new StringsCompleter(client.getUniversesMap().keySet()), ttlInSeconds); + } + } + if (cursorArgument.needLoadUniverseInfo() && + (!universeInfoCompletersMap.containsKey(cursorArgument.getUniverse()) || + universeInfoCompletersMap.get(cursorArgument.getUniverse()).getCompleter() == null || + universeInfoCompletersMap.get(cursorArgument.getUniverse()).isExpired())) { + if (StringUtils.isNotBlank(cursorArgument.getUniverse())) { + client.removeUniverseInfo(cursorArgument.getUniverse()); + Map info = client.getUniverseNodesInfo(token, cursorArgument + .getUniverse()); + CachedCompleter completer = new CachedCompleter( + new UniverseNodeInfoCompleter(info.values()), ttlInSeconds); + universeInfoCompletersMap.put(cursorArgument.getUniverse(), completer); + } + } + } catch (Exception e) { + logger.error("Failed to update completions", e); + } + } + + private Set getKeywordsCompletions() throws IOException { + String keywords = + new BufferedReader(new InputStreamReader( + UniverseCompleter.class.getResourceAsStream("/universe.keywords"))).readLine(); + + Set completions = new TreeSet<>(); + + if (StringUtils.isNotBlank(keywords)) { + String[] words = keywords.split(KEYWORD_SPLITERATOR); + for (String word : words) { + completions.add(word); + } + } + + return completions; + } + + private CursorArgument parseCursorArgument(String buffer, int cursor) { + CursorArgument result = new CursorArgument(); + if (buffer != null && buffer.length() >= cursor) { + String buf = buffer.substring(0, cursor); + if (StringUtils.isNotBlank(buf)) { + ArgumentList argList = sqlDelimiter.delimit(buf, cursor); + int argIndex = argList.getCursorArgumentIndex(); + if (argIndex == 0) { + result.setCursorArgumentPartForComplete(argList.getCursorArgument()); + return result; + } + + if (argIndex > 0 && argList.getArguments()[argIndex - 1].equalsIgnoreCase(KW_UNIVERSE)) { + result.setUniverseNamePosition(true); + result.setCursorArgumentPartForComplete(cleanName(argList.getCursorArgument() + .substring(0, argList.getArgumentPosition()))); + return result; + } + if (argIndex > 1) { + for (int i = argIndex - 2; i >= 0; i--) { + if (argList.getArguments()[i].equalsIgnoreCase(KW_UNIVERSE)) { + result.setUniverse(cleanName(argList.getArguments()[i + 1])); + break; + } + } + + if (StringUtils.isNotBlank(result.getUniverse()) + && argList.getCursorArgument().startsWith(START_NAME.toString())) { + result.setCursorArgumentPartForComplete( + argList.getCursorArgument().substring(0, argList.getArgumentPosition())); + result.setUniverseNodePosition(true); + return result; + } else { + result.setCursorArgumentPartForComplete(argList.getCursorArgument() + .substring(0, argList.getArgumentPosition())); + } + } + } + } + + if (result.getCursorArgumentPartForComplete() == null) { + result.setCursorArgumentPartForComplete(StringUtils.EMPTY); + } + + return result; + } + + private String cleanName(String name) { + return name.replaceAll(CLEAN_NAME_REGEX, StringUtils.EMPTY); + } + + private void addCompletions(List interpreterCompletions, + List candidates, String meta) { + for (CharSequence candidate : candidates) { + String value; + if (meta.equalsIgnoreCase(CompletionType.universe.name())) { + value = String.format("%s%s;\n", candidate.toString(), END_NAME); + } else { + value = candidate.toString(); + } + interpreterCompletions.add(new InterpreterCompletion(candidate.toString(), value, meta)); + } + } + + private void addCompletions(List interpreterCompletions, + List candidates) { + for (UniverseNodeInfo candidate : candidates) { + String value; + if (candidate.getType().equalsIgnoreCase(TYPE_FOLDER)) { + value = String.format("%s%s.%s", candidate.getName(), END_NAME, START_NAME); + } else { + value = String.format("%s%s", candidate.getName(), END_NAME); + } + interpreterCompletions.add(new InterpreterCompletion(candidate.getName(), value, + candidate.getType())); + } + } + + public CachedCompleter getUniverseCompleter() { + return universeCompleter; + } + + public Map getUniverseInfoCompletersMap() { + return universeInfoCompletersMap; + } + + private class CursorArgument { + private boolean universeNamePosition = false; + private boolean universeNodePosition = false; + private String universe; + private String cursorArgumentPartForComplete; + + public boolean isUniverseNamePosition() { + return universeNamePosition; + } + + public void setUniverseNamePosition(boolean universeNamePosition) { + this.universeNamePosition = universeNamePosition; + } + + public boolean isUniverseNodePosition() { + return universeNodePosition; + } + + public void setUniverseNodePosition(boolean universeNodePosition) { + this.universeNodePosition = universeNodePosition; + } + + public String getCursorArgumentPartForComplete() { + return cursorArgumentPartForComplete; + } + + public void setCursorArgumentPartForComplete(String cursorArgumentPartForComplete) { + this.cursorArgumentPartForComplete = cursorArgumentPartForComplete; + } + + public String getUniverse() { + return universe; + } + + public void setUniverse(String universe) { + this.universe = universe; + } + + public boolean needLoadUniverses() { + if (universe == null) { + return true; + } + return false; + } + + public boolean needLoadUniverseInfo() { + if (universe != null && universeNodePosition) { + return true; + } + return false; + } + } +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseException.java similarity index 65% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java rename to sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseException.java index 975d6ea3c76..8086f94419a 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java +++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseException.java @@ -15,20 +15,24 @@ * limitations under the License. */ -package org.apache.zeppelin.interpreter.remote; +package org.apache.zeppelin.sap.universe; -import static org.junit.Assert.assertTrue; -import java.io.IOException; +/** + * Runtime Exception for SAP universe + */ +public class UniverseException extends Exception { -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterUtils; -import org.junit.Test; + public UniverseException(Throwable e) { + super(e); + } -public class RemoteInterpreterUtilsTest { + public UniverseException(String m) { + super(m); + } - @Test - public void testFindRandomAvailablePortOnAllLocalInterfaces() throws IOException { - assertTrue(RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces() > 0); + public UniverseException(String msg, Throwable t) { + super(msg, t); } } diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseInfo.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseInfo.java new file mode 100644 index 00000000000..4f40dced2bc --- /dev/null +++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseInfo.java @@ -0,0 +1,60 @@ +/* + * 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.sap.universe; + +/** + * Info about of universe node + */ +public class UniverseInfo { + private String id; + private String name; + private String type; + + public UniverseInfo() { + } + + public UniverseInfo(String id, String name, String type) { + this.id = id; + this.name = name; + this.type = type; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfo.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfo.java new file mode 100644 index 00000000000..fe0c97e0ab7 --- /dev/null +++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfo.java @@ -0,0 +1,85 @@ +/* + * 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.sap.universe; + +/** + * Info about of universe item + */ +public class UniverseNodeInfo { + private String id; + private String name; + private String type; + private String folder; + private String nodePath; + + public UniverseNodeInfo() { + } + + public UniverseNodeInfo(String id, String name, String type, String folder, String nodePath) { + this.id = id; + this.name = name; + this.type = type; + this.folder = folder; + this.nodePath = nodePath; + } + + public UniverseNodeInfo(String name, String type) { + this.name = name; + this.type = type; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getFolder() { + return folder; + } + + public void setFolder(String folder) { + this.folder = folder; + } + + public String getNodePath() { + return nodePath; + } + + public void setNodePath(String nodePath) { + this.nodePath = nodePath; + } +} diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfoCompleter.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfoCompleter.java new file mode 100644 index 00000000000..af46b4627a1 --- /dev/null +++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseNodeInfoCompleter.java @@ -0,0 +1,187 @@ +/* + * 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.sap.universe; + +import jline.console.completer.Completer; +import jline.internal.Preconditions; +import org.apache.commons.lang.StringUtils; + +import java.util.*; + +/** + * Case-insensitive completer. + */ +public class UniverseNodeInfoCompleter implements Completer { + private final UniverseInfoTreeNode tree = new UniverseInfoTreeNode(); + + public UniverseNodeInfoCompleter() { + } + + public UniverseNodeInfoCompleter(final Collection nodes) { + Preconditions.checkNotNull(nodes); + for (UniverseNodeInfo node : nodes) { + String folder = node.getFolder(); + if (StringUtils.isBlank(folder)) { + tree.putInfo(node); + } else { + String[] path = folder.split("\\\\"); + UniverseInfoTreeNode universeInfoTreeNode = tree; + for (String s : path) { + if (!universeInfoTreeNode.contains(s)) { + universeInfoTreeNode = universeInfoTreeNode.putFolder(s); + } else { + universeInfoTreeNode = universeInfoTreeNode.getFolder(s); + } + } + universeInfoTreeNode.putInfo(node); + } + } + } + + public int complete(final String buffer, final int cursor, final List candidates) { + return completeCollection(buffer, cursor, candidates); + } + + private int completeCollection(final String buffer, final int cursor, + final Collection candidates) { + Preconditions.checkNotNull(candidates); + if (buffer == null) { + candidates.addAll(tree.getNodesInfo()); + } else { + String part = buffer.substring(0, cursor); + List path = new ArrayList<>(); + path.addAll(Arrays.asList(part.split("\\]\\.\\["))); + if (part.endsWith(UniverseCompleter.START_NAME.toString())) { + path.add(StringUtils.EMPTY); + } + + UniverseInfoTreeNode treeNode = tree; + for (int i = 0; i < path.size() - 1; i++) { + String folder = cleanName(path.get(i)); + if (treeNode.contains(folder)) { + treeNode = treeNode.getFolder(folder); + if (treeNode == null) { + break; + } + } + } + String p = cleanName(path.get(path.size() - 1)).toUpperCase(); + if (treeNode != null && treeNode.getChildren() != null) { + if (p.isEmpty()) { + candidates.addAll(treeNode.getNodesInfo()); + } else { + for (UniverseNodeInfo universeNodeInfo : treeNode.getNodesInfo()) { + if (universeNodeInfo.getName().toUpperCase().startsWith(p)) { + candidates.add(universeNodeInfo); + } + } + } + } + } + + return candidates.isEmpty() ? -1 : 0; + } + + private String cleanName(String name) { + return name.replaceAll(UniverseCompleter.CLEAN_NAME_REGEX, StringUtils.EMPTY); + } + + private class UniverseInfoTreeNode { + private String name; + private boolean isFolder; + private Map children; + + public UniverseInfoTreeNode() { + this.name = "/"; + this.isFolder = true; + this.children = new HashMap<>(); + } + + public UniverseInfoTreeNode(String name) { + this.name = name; + this.isFolder = true; + this.children = new HashMap<>(); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isFolder() { + return isFolder; + } + + public void setFolder(boolean folder) { + isFolder = folder; + } + + public Map getChildren() { + return children; + } + + public void setChildren(Map children) { + this.children = children; + } + + public boolean contains(String name) { + return children.containsKey(name); + } + + public UniverseInfoTreeNode getFolder(String name) { + Object child = children.get(name); + if (child instanceof UniverseInfoTreeNode) { + return (UniverseInfoTreeNode) children.get(name); + } + + return null; + } + + public UniverseInfoTreeNode putFolder(String name) { + UniverseInfoTreeNode newNode = new UniverseInfoTreeNode(name); + children.put(name, newNode); + return newNode; + } + + public void putInfo(UniverseNodeInfo info) { + children.put(info.getId(), info); + } + + public Collection getNodesInfo() { + HashMap map = new HashMap<>(); + if (children != null) { + for (Object o : children.values()) { + if (o instanceof UniverseNodeInfo) { + final UniverseNodeInfo nodeInfo = (UniverseNodeInfo) o; + map.put(nodeInfo.getName(), nodeInfo); + } else { + final UniverseInfoTreeNode treeNode = (UniverseInfoTreeNode) o; + final UniverseNodeInfo nodeInfo = + new UniverseNodeInfo(treeNode.getName(), UniverseCompleter.TYPE_FOLDER); + if (!map.containsKey(nodeInfo.getName())) { + map.put(nodeInfo.getName(), nodeInfo); + } + } + } + } + return map.values(); + } + } +} diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQuery.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQuery.java new file mode 100644 index 00000000000..43894b2dc26 --- /dev/null +++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQuery.java @@ -0,0 +1,68 @@ +/* + * 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.sap.universe; + +import org.apache.commons.lang.StringUtils; + +import java.util.OptionalInt; + +/** + * Data of universe query + */ +public class UniverseQuery { + private String select; + private String where; + private UniverseInfo universeInfo; + private boolean duplicatedRows = false; + private OptionalInt maxRowsRetrieved; + + public UniverseQuery(String select, String where, UniverseInfo universeInfo, + boolean duplicatedRows, OptionalInt maxRowsRetrieved) { + this.select = select; + this.where = where; + this.universeInfo = universeInfo; + this.duplicatedRows = duplicatedRows; + this.maxRowsRetrieved = maxRowsRetrieved; + } + + public boolean isCorrect() { + return StringUtils.isNotBlank(select) && universeInfo != null && + StringUtils.isNotBlank(universeInfo.getId()) + && StringUtils.isNotBlank(universeInfo.getName()); + } + + public String getSelect() { + return select; + } + + public String getWhere() { + return where; + } + + public UniverseInfo getUniverseInfo() { + return universeInfo; + } + + public boolean getDuplicatedRows() { + return duplicatedRows; + } + + public OptionalInt getMaxRowsRetrieved() { + return maxRowsRetrieved; + } +} diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQueryPrompt.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQueryPrompt.java new file mode 100644 index 00000000000..04b2b49b63a --- /dev/null +++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseQueryPrompt.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.sap.universe; + +/** + * Info about parameter of universe query + */ +public class UniverseQueryPrompt { + private Integer id; + private String name; + private String cardinality; + private String constrained; + private String type; + private String value; + private String technicalName; + private String keepLastValues; + + public UniverseQueryPrompt() { + } + + public UniverseQueryPrompt(Integer id, String name, String cardinality, String constrained, + String type, String technicalName, String keepLastValues) { + this.id = id; + this.name = name; + this.cardinality = cardinality; + this.constrained = constrained; + this.type = type; + this.technicalName = technicalName; + this.keepLastValues = keepLastValues; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCardinality() { + return cardinality; + } + + public void setCardinality(String cardinality) { + this.cardinality = cardinality; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getConstrained() { + return constrained; + } + + public void setConstrained(String constrained) { + this.constrained = constrained; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getTechnicalName() { + return technicalName; + } + + public void setTechnicalName(String technicalName) { + this.technicalName = technicalName; + } + + public String getKeepLastValues() { + return keepLastValues; + } + + public void setKeepLastValues(String keepLastValues) { + this.keepLastValues = keepLastValues; + } +} diff --git a/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseUtil.java b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseUtil.java new file mode 100644 index 00000000000..5ccf331860b --- /dev/null +++ b/sap/src/main/java/org/apache/zeppelin/sap/universe/UniverseUtil.java @@ -0,0 +1,678 @@ +/* + * 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.sap.universe; + +import jline.console.completer.ArgumentCompleter.WhitespaceArgumentDelimiter; +import org.apache.commons.lang.StringUtils; + +import java.util.*; + +/** + * Util class for convert request from Zeppelin to SAP + */ +public class UniverseUtil { + + private static final String COMPRASION_START_TEMPLATE = "\n"; + private static final String COMPRASION_END_TEMPLATE = "\n"; + private static final String COMPARISON_FILTER = "\n"; + private static final String CONST_OPERAND_START_TEMPLATE = "\n"; + private static final String CONST_OPERAND_END_TEMPLATE = "\n"; + private static final String CONST_OPERAND_VALUE_TEMPLATE = "\n" + + "%s\n\n"; + private static final String PREDEFINED_FILTER_TEMPLATE = "\n"; + private static final String OBJECT_OPERAND_TEMPLATE = "\n"; + private static final String RESULT_START_TEMPLATE = "\n"; + private static final String RESULT_END_TEMPLATE = "\n"; + private static final String RESULT_OBJ_TEMPLATE = "\n"; + + private static final String MARKER_EQUAL = "#EqualTo#"; + private static final String MARKER_LESS_EQUAL = "#LessThanOrEqualTo#"; + private static final String MARKER_NOT_EQUAL = "#NotEqualTo#"; + private static final String MARKER_LESS = "#LessThan#"; + private static final String MARKER_GREATER_EQUALS = "#GreaterThanOrEqualTo#"; + private static final String MARKER_GREATER = "#GreaterThan#"; + private static final String MARKER_IN = "#InList#"; + private static final String MARKER_NOT_IN = "#NotInList#"; + private static final String MARKER_NULL = "#IsNull#"; + private static final String MARKER_NOT_NULL = "#IsNotNull#"; + private static final String MARKER_FILTER = "#filter#"; + private static final String MARKER_AND = "#and#"; + private static final String MARKER_OR = "#or#"; + private static final String MARKER_BACKSPACE = "#backspace#"; + private static final String MARKER_LEFT_BRACE = "#left_brace#"; + private static final String MARKER_RIGHT_BRACE = "#right_brace#"; + + + private static final String LEFT_BRACE = "("; + private static final String RIGHT_BRACE = ")"; + + public static final Map OPERATIONS; + private static final WhitespaceArgumentDelimiter delimiter = new WhitespaceArgumentDelimiter(); + + static { + OPERATIONS = new HashMap<>(); + OPERATIONS.put(MARKER_EQUAL, 1); + OPERATIONS.put(MARKER_LESS_EQUAL, 1); + OPERATIONS.put(MARKER_NOT_EQUAL, 1); + OPERATIONS.put(MARKER_LESS, 1); + OPERATIONS.put(MARKER_GREATER_EQUALS, 1); + OPERATIONS.put(MARKER_GREATER, 1); + OPERATIONS.put(MARKER_IN, 1); + OPERATIONS.put(MARKER_NOT_IN, 1); + OPERATIONS.put(MARKER_NULL, 1); + OPERATIONS.put(MARKER_NOT_NULL, 1); + OPERATIONS.put(MARKER_FILTER, 1); + OPERATIONS.put(MARKER_AND, 2); + OPERATIONS.put(MARKER_OR, 3); + } + + public static OptionalInt parseInt(String toParse) { + try { + return OptionalInt.of(Integer.parseInt(toParse)); + } catch (NumberFormatException e) { + return OptionalInt.empty(); + } + } + + public UniverseQuery convertQuery(String text, UniverseClient client, String token) + throws UniverseException { + StringBuilder select = new StringBuilder(); + StringBuilder universe = new StringBuilder(); + StringBuilder buf = new StringBuilder(); + StringBuilder resultObj = new StringBuilder(); + StringBuilder whereBuf = new StringBuilder(); + UniverseInfo universeInfo = null; + String where = null; + boolean singleQuoteClosed = true; + boolean pathClosed = true; + boolean universePart = false; + boolean selectPart = false; + boolean wherePart = false; + boolean listOperator = false; + boolean operatorPosition = false; + boolean duplicatedRows = true; + Map nodeInfos = null; + OptionalInt limit = OptionalInt.empty(); + + final int limitIndex = text.lastIndexOf("limit"); + if (limitIndex != -1) { + final String[] arguments = delimiter.delimit(text, 0).getArguments(); + final int length = arguments.length; + if (arguments[length - 3].equals("limit")) { + limit = parseInt(arguments[length - 2]); + } else if (arguments[length - 2].equals("limit")) { + final String toParse = arguments[length - 1]; + limit = parseInt(toParse.endsWith(";") ? + toParse.substring(0, toParse.length() - 1) : toParse); + } + text = text.substring(0, limitIndex); + } + + if (!text.endsWith(";")) { + text += ";"; + } + + char[] array = text.toCharArray(); + for (int i = 0; i < array.length; i++) { + char c = array[i]; + buf.append(c); + if (c == '\'') { + if (i == 0 || array[i - 1] != '\\') { + singleQuoteClosed = !singleQuoteClosed; + } + } + if (c == '[' && pathClosed && singleQuoteClosed) { + pathClosed = false; + if (wherePart) { + operatorPosition = false; + } + } + if (c == ']' && !pathClosed && singleQuoteClosed) { + pathClosed = true; + if (wherePart) { + operatorPosition = true; + if (i + 1 == array.length || (array[i + 1] != '.' + && isFilter(String.format("%s]", whereBuf.toString()), text.substring(i + 1)))) { + whereBuf.append(c); + whereBuf.append(MARKER_FILTER); + if (i + 1 == array.length) { + wherePart = false; + where = parseWhere(whereBuf.toString(), nodeInfos); + } + continue; + } + } + } + if (c == '(' && wherePart && pathClosed && singleQuoteClosed) { + if (listOperator) { + whereBuf.append(MARKER_LEFT_BRACE); + continue; + } else { + whereBuf.append(c); + continue; + } + } + if (c == ')' && wherePart && pathClosed && singleQuoteClosed) { + if (listOperator) { + whereBuf.append(MARKER_RIGHT_BRACE); + listOperator = false; + continue; + } else { + whereBuf.append(c); + continue; + } + } + + if (!universePart && singleQuoteClosed + && buf.toString().toLowerCase().endsWith("universe")) { + universePart = true; + continue; + } + + if (universePart) { + if (c == ';' && singleQuoteClosed) { + universePart = false; + if (universe.toString().trim().length() > 2) { + String universeName = + universe.toString().trim().substring(1, universe.toString().trim().length() - 1); + universeInfo = client.getUniverseInfo(universeName); + nodeInfos = client.getUniverseNodesInfo(token, universeName); + } + } else { + universe.append(c); + } + continue; + } + + if (!selectPart && pathClosed && singleQuoteClosed + && buf.toString().toLowerCase().endsWith("select")) { + if (StringUtils.isBlank(universe.toString())) { + throw new UniverseException("Not found universe name"); + } + selectPart = true; + select.append(RESULT_START_TEMPLATE); + continue; + } + + if (!wherePart && pathClosed && singleQuoteClosed) { + if (buf.toString().toLowerCase().endsWith("where")) { + wherePart = true; + } + if (buf.toString().toLowerCase().endsWith("where") || i == array.length - 1) { + selectPart = false; + select.append(parseResultObj(resultObj.toString() + .replaceAll("(?i)wher$", "").replaceAll("(?i)distinc", ""), + nodeInfos)); + select.append(RESULT_END_TEMPLATE); + continue; + } + } + + if (selectPart) { + if (pathClosed && singleQuoteClosed && buf.toString().toLowerCase().endsWith("distinct")) { + duplicatedRows = false; + continue; + } + if (pathClosed && singleQuoteClosed && c == ',') { + select.append(parseResultObj(resultObj.toString().replaceAll("(?i)distinc", ""), + nodeInfos)); + resultObj = new StringBuilder(); + } else { + resultObj.append(c); + } + continue; + } + + if (wherePart) { + if (c == ';' && pathClosed && singleQuoteClosed) { // todo: check for limit + wherePart = false; + where = parseWhere(whereBuf.toString(), nodeInfos); + } else { + if (!singleQuoteClosed || !pathClosed) { + switch (c) { + case ' ': + case '\n': + whereBuf.append(MARKER_BACKSPACE); + break; + case '(': + whereBuf.append(MARKER_LEFT_BRACE); + break; + case ')': + whereBuf.append(MARKER_RIGHT_BRACE); + break; + default: + whereBuf.append(c); + } + } else if (pathClosed) { + if ((c == 'a' || c == 'A') && i < array.length - 2 && + text.substring(i, i + 3).equalsIgnoreCase("and")) { + i += 2; + whereBuf.append(MARKER_AND); + operatorPosition = false; + continue; + } + if ((c == 'o' || c == 'O') && i < array.length - 1 && + text.substring(i, i + 2).equalsIgnoreCase("or")) { + i += 1; + whereBuf.append(MARKER_OR); + operatorPosition = false; + continue; + } + if (operatorPosition) { + switch (c) { + case '=': + whereBuf.append(MARKER_EQUAL); + operatorPosition = false; + break; + case '<': + if (i + 1 < array.length) { + if (array[i + 1] == '=') { + whereBuf.append(MARKER_LESS_EQUAL); + operatorPosition = false; + i++; + break; + } else if (array[i + 1] == '>') { + whereBuf.append(MARKER_NOT_EQUAL); + operatorPosition = false; + i++; + break; + } + } + operatorPosition = false; + whereBuf.append(MARKER_LESS); + break; + case '>': + if (i + 1 < array.length) { + if (array[i + 1] == '=') { + whereBuf.append(MARKER_GREATER_EQUALS); + operatorPosition = false; + i++; + break; + } + } + operatorPosition = false; + whereBuf.append(MARKER_GREATER); + break; + case 'i': + case 'I': + boolean whileI = true; + StringBuilder operI = new StringBuilder(); + operI.append(c); + while (whileI) { + i++; + if (i >= array.length) { + whileI = false; + } + + if (array[i] != ' ' && array[i] != '\n') { + operI.append(array[i]); + } else { + continue; + } + String tmp = operI.toString().toLowerCase(); + if (tmp.equals("in")) { + whereBuf.append(MARKER_IN); + listOperator = true; + whileI = false; + operatorPosition = false; + } else if (tmp.equals("isnull")) { + whereBuf.append(MARKER_NULL); + whileI = false; + operatorPosition = false; + } else if (tmp.equals("isnotnull")) { + whereBuf.append(MARKER_NOT_NULL); + whileI = false; + operatorPosition = false; + } + // longest 9 - isnotnull + if (tmp.length() > 8) { + whileI = false; + } + } + break; + case 'n': + case 'N': + boolean whileN = true; + StringBuilder operN = new StringBuilder(); + operN.append(c); + while (whileN) { + i++; + if (i >= array.length) { + whileN = false; + } + + if (array[i] != ' ' && array[i] != '\n') { + operN.append(array[i]); + } else { + continue; + } + + String tmp = operN.toString().toLowerCase(); + + if (tmp.equals("notin")) { + whereBuf.append(MARKER_NOT_IN); + listOperator = true; + whileN = false; + operatorPosition = false; + } + + // longest 5 - notin + if (tmp.length() > 4) { + whileN = false; + } + } + break; + default: + whereBuf.append(c); + } + } else { + whereBuf.append(c); + } + } else { + whereBuf.append(c); + } + } + } + } + + if (wherePart && StringUtils.isBlank(where)) { + throw new UniverseException("Incorrect block where"); + } + + UniverseQuery universeQuery = new UniverseQuery(select.toString().trim(), + where, universeInfo, duplicatedRows, limit); + + if (!universeQuery.isCorrect()) { + throw new UniverseException("Incorrect query"); + } + + return universeQuery; + } + + private String parseWhere(String where, Map nodeInfos) + throws UniverseException { + List out = new ArrayList<>(); + Stack stack = new Stack<>(); + + where = where.replaceAll("\\s*", ""); + + Set operationSymbols = new HashSet<>(OPERATIONS.keySet()); + operationSymbols.add(LEFT_BRACE); + operationSymbols.add(RIGHT_BRACE); + + int index = 0; + + boolean findNext = true; + while (findNext) { + int nextOperationIndex = where.length(); + String nextOperation = ""; + for (String operation : operationSymbols) { + int i = where.indexOf(operation, index); + if (i >= 0 && i < nextOperationIndex) { + nextOperation = operation; + nextOperationIndex = i; + } + } + if (nextOperationIndex == where.length()) { + findNext = false; + } else { + if (index != nextOperationIndex) { + out.add(where.substring(index, nextOperationIndex)); + } + if (nextOperation.equals(LEFT_BRACE)) { + stack.push(nextOperation); + } else if (nextOperation.equals(RIGHT_BRACE)) { + while (!stack.peek().equals(LEFT_BRACE)) { + out.add(stack.pop()); + if (stack.empty()) { + throw new UniverseException("Unmatched brackets"); + } + } + stack.pop(); + } else { + while (!stack.empty() && !stack.peek().equals(LEFT_BRACE) && + (OPERATIONS.get(nextOperation) >= OPERATIONS.get(stack.peek()))) { + out.add(stack.pop()); + } + stack.push(nextOperation); + } + index = nextOperationIndex + nextOperation.length(); + } + } + if (index != where.length()) { + out.add(where.substring(index)); + } + while (!stack.empty()) { + out.add(stack.pop()); + } + StringBuffer result = new StringBuffer(); + if (!out.isEmpty()) + result.append(out.remove(0)); + while (!out.isEmpty()) + result.append(" ").append(out.remove(0)); + + // result contains the reverse polish notation + return convertWhereToXml(result.toString(), nodeInfos); + } + + private String parseResultObj(String resultObj, Map nodeInfos) + throws UniverseException { + if (StringUtils.isNotBlank(resultObj)) { + UniverseNodeInfo nodeInfo = nodeInfos.get(resultObj.trim()); + if (nodeInfo != null) { + return String.format(RESULT_OBJ_TEMPLATE, nodeInfo.getNodePath(), nodeInfo.getId()); + } + throw new UniverseException(String.format("Not found information about: \"%s\"", + resultObj.trim())); + } + + return StringUtils.EMPTY; + } + + private String convertWhereToXml(String rpn, Map nodeInfos) + throws UniverseException { + StringTokenizer tokenizer = new StringTokenizer(rpn, " "); + + Stack stack = new Stack(); + + while (tokenizer.hasMoreTokens()) { + StringBuilder tmp = new StringBuilder(); + String token = tokenizer.nextToken(); + if (!OPERATIONS.keySet().contains(token)) { + stack.push(token.trim()); + } else { + String rightOperand = revertReplace(stack.pop()); + String operator = token.replaceAll("^#|#$", ""); + + if (token.equalsIgnoreCase(MARKER_NOT_NULL) || token.equalsIgnoreCase(MARKER_NULL)) { + UniverseNodeInfo rightOperandInfo = nodeInfos.get(rightOperand); + stack.push(String.format(COMPARISON_FILTER, rightOperandInfo.getId(), + rightOperandInfo.getNodePath(), operator)); + continue; + } + + if (token.equalsIgnoreCase(MARKER_FILTER)) { + UniverseNodeInfo rightOperandInfo = nodeInfos.get(rightOperand); + stack.push(String.format(PREDEFINED_FILTER_TEMPLATE, rightOperandInfo.getNodePath(), + rightOperandInfo.getId())); + continue; + } + + String leftOperand = stack.empty() ? null : revertReplace(stack.pop()); + + if (token.equalsIgnoreCase(MARKER_AND) || token.equalsIgnoreCase(MARKER_OR)) { + if (rightOperand.matches("^\\[.*\\]$")) { + UniverseNodeInfo rightOperandInfo = nodeInfos.get(rightOperand); + if (rightOperandInfo == null) { + throw new UniverseException(String.format("Not found information about: \"%s\"", + rightOperand)); + } + rightOperand = String.format(PREDEFINED_FILTER_TEMPLATE, + rightOperandInfo.getNodePath(), rightOperandInfo.getId()); + } + if (leftOperand.matches("^\\[.*\\]$")) { + UniverseNodeInfo leftOperandInfo = nodeInfos.get(leftOperand); + if (leftOperandInfo == null) { + throw new UniverseException(String.format("Not found information about: \"%s\"", + leftOperand)); + } + leftOperand = String.format(PREDEFINED_FILTER_TEMPLATE, leftOperandInfo.getNodePath(), + leftOperandInfo.getId()); + } + tmp.append(String.format("<%s>\n", operator)); + tmp.append(leftOperand); + tmp.append("\n"); + tmp.append(rightOperand); + tmp.append("\n"); + tmp.append(String.format("\n", operator)); + stack.push(tmp.toString()); + continue; + } + + UniverseNodeInfo leftOperandInfo = nodeInfos.get(leftOperand); + if (leftOperandInfo == null) { + throw new UniverseException(String.format("Not found information about: \"%s\"", + leftOperand)); + } + if (token.equalsIgnoreCase(MARKER_IN) || token.equalsIgnoreCase(MARKER_NOT_IN)) { + String listValues = rightOperand.replaceAll("^\\(|\\)$", "").trim(); + boolean startItem = false; + List values = new ArrayList<>(); + StringBuilder value = new StringBuilder(); + boolean isNumericList = false; + if (listValues.charAt(0) != '\'') { + isNumericList = true; + } + if (isNumericList) { + String[] nums = listValues.split(","); + for (String num : nums) { + values.add(num.trim()); + } + } else { + for (int i = 0; i < listValues.length(); i++) { + char c = listValues.charAt(i); + if (c == '\'' && (i == 0 || listValues.charAt(i - 1) != '\\')) { + startItem = !startItem; + if (!startItem) { + values.add(value.toString()); + value = new StringBuilder(); + } + continue; + } + if (startItem) { + value.append(c); + } + } + } + + if (!values.isEmpty()) { + tmp.append(String.format(COMPRASION_START_TEMPLATE, leftOperandInfo.getNodePath(), + operator, leftOperandInfo.getId())); + tmp.append(CONST_OPERAND_START_TEMPLATE); + String type = isNumericList ? "Numeric" : "String"; + for (String v : values) { + tmp.append(String.format(CONST_OPERAND_VALUE_TEMPLATE, type, v)); + } + tmp.append(CONST_OPERAND_END_TEMPLATE); + tmp.append(COMPRASION_END_TEMPLATE); + stack.push(tmp.toString()); + } + continue; + } + + // EqualTo, LessThanOrEqualTo, NotEqualTo, LessThan, GreaterThanOrEqualTo, GreaterThan + UniverseNodeInfo rightOperandInfo = null; + if (rightOperand.startsWith("[") && rightOperand.endsWith("]")) { + rightOperandInfo = nodeInfos.get(rightOperand); + if (rightOperandInfo == null) { + throw new UniverseException(String.format("Not found information about: \"%s\"", + rightOperand)); + } + } + if (OPERATIONS.containsKey(token)) { + if (rightOperandInfo != null) { + tmp.append(String.format(COMPRASION_START_TEMPLATE, leftOperandInfo.getNodePath(), + operator, leftOperandInfo.getId())); + tmp.append(String.format(OBJECT_OPERAND_TEMPLATE, rightOperandInfo.getId(), + rightOperandInfo.getNodePath())); + tmp.append(COMPRASION_END_TEMPLATE); + } else { + String type = rightOperand.startsWith("'") ? "String" : "Numeric"; + String value = rightOperand.replaceAll("^'|'$", ""); + tmp.append(String.format(COMPRASION_START_TEMPLATE, leftOperandInfo.getNodePath(), + operator, leftOperandInfo.getId())); + tmp.append(CONST_OPERAND_START_TEMPLATE); + tmp.append(String.format(CONST_OPERAND_VALUE_TEMPLATE, type, value)); + tmp.append(CONST_OPERAND_END_TEMPLATE); + tmp.append(COMPRASION_END_TEMPLATE); + } + stack.push(tmp.toString()); + continue; + } + throw new UniverseException(String.format("Incorrect syntax after: \"%s\"", leftOperand)); + } + } + + return stack.pop(); + } + + private String revertReplace(String s) { + return s.replaceAll(MARKER_BACKSPACE, " ") + .replaceAll(MARKER_LEFT_BRACE, "(") + .replaceAll(MARKER_RIGHT_BRACE, ")"); + } + + private boolean isFilter(String buf, String after) { + boolean result = false; + String[] parts = buf.trim().split("\\s"); + if (parts[parts.length - 1].matches("^\\[.*\\]$")) { + // check before + if (parts.length == 1) { + result = true; + } else { + int count = parts.length - 2; + Set operations = new HashSet(OPERATIONS.keySet()); + operations.remove(MARKER_AND); + operations.remove(MARKER_OR); + while (count >= 0) { + String p = parts[count]; + if (StringUtils.isNotBlank(p)) { + if (!operations.contains(p)) { + result = true; + break; + } else { + return false; + } + } + count--; + } + } + after = after.trim(); + // check after + if (result && !after.startsWith("and") && !after.startsWith("or") && + !after.startsWith(";") && StringUtils.isNotBlank(after)) { + result = false; + } + } + + return result; + } +} diff --git a/sap/src/main/resources/interpreter-setting.json b/sap/src/main/resources/interpreter-setting.json new file mode 100644 index 00000000000..548d545b7a7 --- /dev/null +++ b/sap/src/main/resources/interpreter-setting.json @@ -0,0 +1,71 @@ +[ + { + "group": "sap", + "name": "universe", + "className": "org.apache.zeppelin.sap.UniverseInterpreter", + "defaultInterpreter": true, + "properties": { + "universe.api.url": { + "envName": null, + "propertyName": "universe.api.url", + "defaultValue": "http://localhost:6405/biprws", + "description": "API url of Universe", + "type": "url" + }, + "universe.user": { + "envName": null, + "propertyName": "universe.user", + "defaultValue": "", + "description": "Username for API of Universe", + "type": "string" + }, + "universe.password": { + "envName": null, + "propertyName": "universe.password", + "defaultValue": "", + "description": "Password for API of Universe", + "type": "password" + }, + "universe.authType": { + "envName": null, + "propertyName": "universe.password", + "defaultValue": "secEnterprise", + "description": "Type of authentication for API of Universe. Available values: secEnterprise, secLDAP, secWinAD, secSAPR3", + "type": "string" + }, + "universe.queryTimeout": { + "envName": null, + "propertyName": "universe.queryTimeout", + "defaultValue": "7200000", + "description": "Query timeout for API of Universe", + "type": "number" + }, + "universe.interpolation": { + "envName": null, + "propertyName": "universe.interpolation", + "defaultValue": false, + "description": "Enable ZeppelinContext variable interpolation into paragraph text", + "type": "checkbox" + }, + "universe.concurrent.use": { + "envName": null, + "propertyName": "universe.concurrent.use", + "defaultValue": true, + "description": "Use parallel scheduler", + "type": "checkbox" + }, + "universe.concurrent.maxConnection": { + "envName": null, + "propertyName": "universe.concurrent.maxConnection", + "defaultValue": "10", + "description": "Number of concurrent execution", + "type": "number" + } + }, + "editor": { + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": false + } + } +] diff --git a/sap/src/main/resources/universe.keywords b/sap/src/main/resources/universe.keywords new file mode 100644 index 00000000000..0811bfbe3bd --- /dev/null +++ b/sap/src/main/resources/universe.keywords @@ -0,0 +1 @@ +universe,select,where,and,or,is null,is not null,in \ No newline at end of file diff --git a/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseCompleterTest.java b/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseCompleterTest.java new file mode 100644 index 00000000000..91a42179cd6 --- /dev/null +++ b/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseCompleterTest.java @@ -0,0 +1,134 @@ +/** + * 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.sap.universe; + +import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.completer.CachedCompleter; +import org.junit.Before; +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Universe completer unit tests + */ +public class UniverseCompleterTest { + + private UniverseCompleter universeCompleter; + private UniverseUtil universeUtil; + private UniverseClient universeClient; + + @Before + public void beforeTest() throws UniverseException { + universeCompleter = new UniverseCompleter(0); + universeUtil = new UniverseUtil(); + Map universes = new HashMap<>(); + + universes.put("testUniverse", new UniverseInfo("1", "testUniverse", "uvx")); + universes.put("test with space", new UniverseInfo("2", "test with space", "uvx")); + universes.put("(GLOBAL) universe", new UniverseInfo("3", "(GLOBAL) universe", "uvx")); + UniverseInfo universeInfo = new UniverseInfo("1", "testUniverse", "uvx"); + Map testUniverseNodes = new HashMap<>(); + testUniverseNodes.put("[Dimension].[Test].[name1]", + new UniverseNodeInfo("name1id", "name1", "dimension", "Dimension\\Test", + "Dimension|folder\\Test|folder\\name1|dimension")); + testUniverseNodes.put("[Dimension].[Test].[name2]", + new UniverseNodeInfo("name2id", "name2", "dimension", "Dimension\\Test", + "Dimension|folder\\Test|folder\\name2|dimension")); + testUniverseNodes.put("[Filter].[name3]", + new UniverseNodeInfo("name3id", "name3", "filter", "Filter", + "Filter|folder\\name3|filter")); + testUniverseNodes.put("[Filter].[name4]", + new UniverseNodeInfo("name4id", "name4", "filter", "Filter", + "Filter|folder\\name4|filter")); + testUniverseNodes.put("[Measure].[name5]", + new UniverseNodeInfo("name5id", "name5", "measure", "Measure", + "Measure|folder\\name5|measure")); + + universeClient = mock(UniverseClient.class); + when(universeClient.getUniverseInfo(anyString())).thenReturn(universeInfo); + when(universeClient.getUniverseNodesInfo(anyString(), anyString())) + .thenReturn(testUniverseNodes); + when(universeClient.getUniversesMap()).thenReturn(universes); + } + + @Test + public void testCreateUniverseNameCompleter() { + String buffer = "universe ["; + List candidates = new ArrayList<>(); + universeCompleter.createOrUpdate(universeClient, null, buffer, 9); + CachedCompleter completer = universeCompleter.getUniverseCompleter(); + assertNull(completer); + universeCompleter.createOrUpdate(universeClient, null, buffer, 10); + completer = universeCompleter.getUniverseCompleter(); + assertNotNull(completer); + + completer.getCompleter().complete(StringUtils.EMPTY, 0, candidates); + assertEquals(3, candidates.size()); + } + + @Test + public void testCreateUniverseNodesCompleter() { + String buffer = "universe [testUniverse]; select ["; + List candidates = new ArrayList<>(); + universeCompleter.createOrUpdate(universeClient, null, buffer, 32); + Map completerMap = universeCompleter.getUniverseInfoCompletersMap(); + assertFalse(completerMap.containsKey("testUniverse")); + universeCompleter.createOrUpdate(universeClient, null, buffer, 33); + completerMap = universeCompleter.getUniverseInfoCompletersMap(); + assertTrue(completerMap.containsKey("testUniverse")); + CachedCompleter completer = completerMap.get("testUniverse"); + + completer.getCompleter().complete(StringUtils.EMPTY, 0, candidates); + assertEquals(3, candidates.size()); + List candidatesStrings = new ArrayList<>(); + for (Object o : candidates) { + UniverseNodeInfo info = (UniverseNodeInfo) o; + candidatesStrings.add(info.getName()); + } + List expected = Arrays.asList("Filter", "Measure", "Dimension"); + Collections.sort(candidatesStrings); + Collections.sort(expected); + assertEquals(expected, candidatesStrings); + } + + @Test + public void testNestedUniverseNodes() { + String buffer = "universe [testUniverse]; select [Dimension].[Test].[n"; + List candidates = new ArrayList<>(); + + universeCompleter.createOrUpdate(universeClient, null, buffer, 53); + Map completerMap = universeCompleter.getUniverseInfoCompletersMap(); + assertTrue(completerMap.containsKey("testUniverse")); + CachedCompleter completer = completerMap.get("testUniverse"); + + completer.getCompleter().complete("[Dimension].[Test].[n", 21, candidates); + assertEquals(2, candidates.size()); + List candidatesStrings = new ArrayList<>(); + for (Object o : candidates) { + UniverseNodeInfo info = (UniverseNodeInfo) o; + candidatesStrings.add(info.getName()); + } + List expected = Arrays.asList("name1", "name2"); + Collections.sort(candidatesStrings); + Collections.sort(expected); + assertEquals(expected, candidatesStrings); + } +} diff --git a/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseUtilTest.java b/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseUtilTest.java new file mode 100644 index 00000000000..81a027e1ced --- /dev/null +++ b/sap/src/test/java/org/apache/zeppelin/sap/universe/UniverseUtilTest.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.sap.universe; + +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +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.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class UniverseUtilTest { + + private UniverseClient universeClient; + private UniverseUtil universeUtil; + + @Before + public void beforeTest() throws UniverseException { + universeUtil = new UniverseUtil(); + UniverseInfo universeInfo = new UniverseInfo("1", "testUniverse", "uvx"); + Map testUniverseNodes = new HashMap<>(); + testUniverseNodes.put("[Dimension].[Test].[name1]", + new UniverseNodeInfo("name1id", "name1", "dimension", "Dimension\\Test", + "Dimension|folder\\Test|folder\\name1|dimension")); + testUniverseNodes.put("[Dimension].[Test].[name2]", + new UniverseNodeInfo("name2id", "name2", "dimension", "Filter\\Test", + "Dimension|folder\\Test|folder\\name2|dimension")); + testUniverseNodes.put("[Filter].[name3]", + new UniverseNodeInfo("name3id", "name3", "filter", "Filter", + "Filter|folder\\name3|filter")); + testUniverseNodes.put("[Filter].[name4]", + new UniverseNodeInfo("name4id", "name4", "filter", "Filter", + "Filter|folder\\name4|filter")); + testUniverseNodes.put("[Measure].[name5]", + new UniverseNodeInfo("name5id", "name5", "measure", "Measure", + "Measure|folder\\name5|measure")); + + universeClient = mock(UniverseClient.class); + when(universeClient.getUniverseInfo(anyString())).thenReturn(universeInfo); + when(universeClient.getUniverseNodesInfo(anyString(), anyString())) + .thenReturn(testUniverseNodes); + } + + @Test + public void testForConvert() throws UniverseException { + String request = "universe [testUniverse];\n" + + "select [Measure].[name5]\n" + + "where [Filter].[name3] and [Dimension].[Test].[name2] > 1;"; + UniverseQuery universeQuery = universeUtil.convertQuery(request, universeClient, null); + assertNotNull(universeQuery); + assertNotNull(universeQuery.getUniverseInfo()); + assertEquals("\n" + + "\n" + + "", universeQuery.getSelect()); + assertEquals("\n" + + "\n" + + "\n\n" + + "\n" + + "\n" + + "1\n" + + "\n" + + "\n" + + "\n\n" + + "\n", universeQuery.getWhere()); + assertEquals("testUniverse", universeQuery.getUniverseInfo().getName()); + } + + @Test + public void testConvertConditions() throws UniverseException { + String request = "universe [testUniverse];\n" + + "select [Measure].[name5]\n" + + "where [Filter].[name3] " + + "and [Dimension].[Test].[name2] >= 1 " + + "and [Dimension].[Test].[name2] < 20 " + + "and [Dimension].[Test].[name1] <> 'test' " + + "and [Dimension].[Test].[name1] is not null " + + "and [Measure].[name5] is null" + + "and [Dimension].[Test].[name1] in ('var1', 'v a r 2') " + + "and [Dimension].[Test].[name1] in ('var1','withoutspaces')" + + "and [Dimension].[Test].[name1] in ('one value')" + + "and [Dimension].[Test].[name2] in (1,3,4);"; + UniverseQuery universeQuery = universeUtil.convertQuery(request, universeClient, null); + assertNotNull(universeQuery); + assertEquals("\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n\n" + + "\n" + + "\n" + + "\n" + + "1\n" + + "\n" + + "\n" + + "\n\n" + + "\n\n" + + "\n" + + "\n" + + "\n" + + "20\n" + + "\n" + + "\n" + + "\n\n" + + "\n\n" + + "\n" + + "\n" + + "\n" + + "test\n" + + "\n" + + "\n" + + "\n\n" + + "\n\n" + + "\n\n" + + "\n\n" + + "\n\n" + + "\n\n" + + "\n" + + "\n" + + "\n" + + "var1\n" + + "\n" + + "\n" + + "v a r 2\n" + + "\n" + + "\n" + + "\n\n" + + "\n\n" + + "\n" + + "\n" + + "\n" + + "var1\n" + + "\n" + + "\n" + + "withoutspaces\n" + + "\n" + + "\n" + + "\n\n" + + "\n\n" + + "\n" + + "\n" + + "\n" + + "one value\n" + + "\n" + + "\n" + + "\n\n" + + "\n\n" + + "\n" + + "\n" + + "\n" + + "1\n" + + "\n" + + "\n" + + "3\n" + + "\n" + + "\n" + + "4\n" + + "\n" + + "\n" + + "\n\n" + + "\n", + universeQuery.getWhere()); + } + + @Test(expected = UniverseException.class) + public void testFailConvertWithoutUniverse() throws UniverseException { + String request = "universe ;\n" + + "select [Measure].[name5]\n" + + "where [Filter].[name3] and [Dimension].[Test].[name2] > 1;"; + universeUtil.convertQuery(request, universeClient, null); + } + + @Test(expected = UniverseException.class) + public void testFailConvertWithIncorrectSelect() throws UniverseException { + String request = "universe [testUniverse];\n" + + "select [not].[exist];"; + universeUtil.convertQuery(request, universeClient, null); + } + + + @Test(expected = UniverseException.class) + public void testFailConvertWithIncorrectCondition() throws UniverseException { + String request = "universe [testUniverse];\n" + + "select [Measure].[name5]\n" + + "where [Filter].[name;"; + universeUtil.convertQuery(request, universeClient, null); + } + + @Test + public void testFiltersConditions() throws UniverseException { + String request1 = "universe [testUniverse];\n" + + "select [Measure].[name5]\n" + + "where [Filter].[name3];"; + String request2 = "universe [testUniverse];\n" + + "select [Measure].[name5]\n" + + "where [Measure].[name5] > 2 and [Filter].[name3];"; + String request3 = "universe [testUniverse];\n" + + "select [Measure].[name5]\n" + + "where [Filter].[name3] or [Measure].[name5];"; + String request4 = "universe [testUniverse];\n" + + "select [Measure].[name5]\n" + + "where [Filter].[name3] and [Measure].[name5] is null;"; + UniverseQuery universeQuery = universeUtil.convertQuery(request1, universeClient, null); + assertEquals("\n", + universeQuery.getWhere()); + universeQuery = universeUtil.convertQuery(request2, universeClient, null); + assertEquals("\n" + + "\n" + + "\n" + + "\n" + + "2\n" + + "\n" + + "\n" + + "\n\n" + + "\n\n" + + "\n", + universeQuery.getWhere()); + universeQuery = universeUtil.convertQuery(request3, universeClient, null); + assertEquals("\n" + + "\n\n" + + "\n\n" + + "\n", + universeQuery.getWhere()); + universeQuery = universeUtil.convertQuery(request4, universeClient, null); + assertEquals("\n" + + "\n\n" + + "\n\n" + + "\n", + universeQuery.getWhere()); + } + + @Test + public void testNestedConditions() throws UniverseException { + String request = "universe [testUniverse];\n" + + "select [Dimension].[Test].[name2]\n" + + "where ([Measure].[name5] = 'text' or ([Dimension].[Test].[name1] in ('1','2', '3') and\n" + + "[Dimension].[Test].[name2] is not null)) and ([Filter].[name4] or [Measure].[name5] >=12)\n" + + "or [Dimension].[Test].[name2] not in (31, 65, 77);"; + UniverseQuery universeQuery = universeUtil.convertQuery(request, universeClient, null); + assertEquals("\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "text\n" + + "\n" + + "\n" + + "\n\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "1\n" + + "\n" + + "\n" + + "2\n" + + "\n" + + "\n" + + "3\n" + + "\n" + + "\n" + + "\n\n" + + "\n\n" + + "\n\n" + + "\n\n" + + "\n" + + "\n\n" + + "\n" + + "\n" + + "\n" + + "12\n" + + "\n" + + "\n" + + "\n\n" + + "\n\n" + + "\n\n" + + "\n" + + "\n" + + "\n" + + "31\n" + + "\n" + + "\n" + + "65\n" + + "\n" + + "\n" + + "77\n" + + "\n" + + "\n" + + "\n\n" + + "\n", + universeQuery.getWhere()); + } + + @Test + public void testWithoutConditions() throws UniverseException { + String request = "universe [testUniverse];\n" + + "select [Dimension].[Test].[name2], [Measure].[name5],\n" + + "[Dimension].[Test].[name1] ;"; + UniverseQuery universeQuery = universeUtil.convertQuery(request, universeClient, null); + assertNull(universeQuery.getWhere()); + assertEquals("\n" + + "\n" + + "\n" + + "\n" + + "", + universeQuery.getSelect()); + } + + @Test + public void testCaseSensitive() throws UniverseException { + String request = "uniVersE [testUniverse];\n" + + "seLEct [Dimension].[Test].[name2], [Measure].[name5]\n" + + "whERE [Dimension].[Test].[name2] Is NULl Or [Measure].[name5] IN (1,2) aNd [Measure].[name5] is NOT nUll;"; + UniverseQuery universeQuery = universeUtil.convertQuery(request, universeClient, null); + assertEquals("\n" + + "\n" + + "\n" + + "", + universeQuery.getSelect()); + assertEquals("\n" + + "\n\n" + + "\n" + + "\n" + + "\n" + "\n" + "1\n" + + "\n" + + "\n" + + "2\n" + + "\n" + + "\n" + + "\n\n" + + "\n\n" + + "\n\n" + + "\n", + universeQuery.getWhere()); + } +} diff --git a/scalding/pom.xml b/scalding/pom.xml index cc1015f21b1..228750cb5db 100644 --- a/scalding/pom.xml +++ b/scalding/pom.xml @@ -20,19 +20,20 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin - zeppelin-scalding_2.10 + zeppelin-scalding_2.11 jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Scalding interpreter + scalding 2.6.0 0.16.1-RC1 @@ -147,55 +148,14 @@ maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/scalding - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/scalding - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + maven-resources-plugin + + org.scala-tools diff --git a/scalding/src/main/java/org/apache/zeppelin/scalding/ScaldingInterpreter.java b/scalding/src/main/java/org/apache/zeppelin/scalding/ScaldingInterpreter.java index 7156c37e974..d3ebadaf057 100644 --- a/scalding/src/main/java/org/apache/zeppelin/scalding/ScaldingInterpreter.java +++ b/scalding/src/main/java/org/apache/zeppelin/scalding/ScaldingInterpreter.java @@ -69,7 +69,7 @@ public ScaldingInterpreter(Properties property) { @Override public void open() { numOpenInstances = numOpenInstances + 1; - String maxOpenInstancesStr = property.getProperty(MAX_OPEN_INSTANCES, + String maxOpenInstancesStr = getProperty(MAX_OPEN_INSTANCES, MAX_OPEN_INSTANCES_DEFAULT); int maxOpenInstances = 50; try { @@ -83,8 +83,8 @@ public void open() { return; } logger.info("Opening instance {}", numOpenInstances); - logger.info("property: {}", property); - String argsString = property.getProperty(ARGS_STRING, ARGS_STRING_DEFAULT); + logger.info("property: {}", getProperties()); + String argsString = getProperty(ARGS_STRING, ARGS_STRING_DEFAULT); String[] args; if (argsString == null) { args = new String[0]; @@ -121,7 +121,7 @@ public InterpreterResult interpret(String cmd, InterpreterContext contextInterpr return new InterpreterResult(Code.SUCCESS); } InterpreterResult interpreterResult = new InterpreterResult(Code.ERROR); - if (property.getProperty(ARGS_STRING).contains("hdfs")) { + if (getProperty(ARGS_STRING).contains("hdfs")) { UserGroupInformation ugi = null; try { ugi = UserGroupInformation.createProxyUser(user, UserGroupInformation.getLoginUser()); diff --git a/scalding/src/test/java/org/apache/zeppelin/scalding/ScaldingInterpreterTest.java b/scalding/src/test/java/org/apache/zeppelin/scalding/ScaldingInterpreterTest.java index 8a23c421384..e5b1e90539b 100644 --- a/scalding/src/test/java/org/apache/zeppelin/scalding/ScaldingInterpreterTest.java +++ b/scalding/src/test/java/org/apache/zeppelin/scalding/ScaldingInterpreterTest.java @@ -65,7 +65,7 @@ public void setUp() throws Exception { InterpreterGroup intpGroup = new InterpreterGroup(); context = new InterpreterContext("note", "id", null, "title", "text", new AuthenticationInfo(), - new HashMap(), new GUI(), new AngularObjectRegistry( + new HashMap(), new GUI(), new GUI(), new AngularObjectRegistry( intpGroup.getId(), null), null, new LinkedList(), null); } diff --git a/scio/pom.xml b/scio/pom.xml index 3d17dc88efc..093ad763155 100644 --- a/scio/pom.xml +++ b/scio/pom.xml @@ -20,20 +20,21 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin - zeppelin-scio_2.10 + zeppelin-scio_2.11 jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Scio Zeppelin Scio support + scio 0.2.4 14.0.1 @@ -106,12 +107,12 @@ maven-enforcer-plugin - - - enforce - none - - + + + maven-dependency-plugin + + + maven-resources-plugin @@ -156,34 +157,6 @@ - - org.apache.maven.plugins - maven-dependency-plugin - - - package - - copy - - - ${project.build.directory}/../../interpreter/scio - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - - - org.scala-tools maven-scala-plugin 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/java/org/apache/zeppelin/scio/ScioInterpreterTest.java b/scio/src/test/java/org/apache/zeppelin/scio/ScioInterpreterTest.java index ec17879654a..91b5fa16ee7 100644 --- a/scio/src/test/java/org/apache/zeppelin/scio/ScioInterpreterTest.java +++ b/scio/src/test/java/org/apache/zeppelin/scio/ScioInterpreterTest.java @@ -44,6 +44,7 @@ private InterpreterContext getNewContext() { new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), new AngularObjectRegistry(intpGroup.getId(), null), new LocalResourcePool("id"), new LinkedList(), 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 { diff --git a/scripts/docker/spark-cluster-managers/spark_mesos/Dockerfile b/scripts/docker/spark-cluster-managers/spark_mesos/Dockerfile index f47b16d8f16..0afda575c12 100644 --- a/scripts/docker/spark-cluster-managers/spark_mesos/Dockerfile +++ b/scripts/docker/spark-cluster-managers/spark_mesos/Dockerfile @@ -15,7 +15,7 @@ FROM centos:centos6 ENV SPARK_PROFILE 2.1 -ENV SPARK_VERSION 2.1.1 +ENV SPARK_VERSION 2.1.2 ENV HADOOP_PROFILE 2.7 ENV HADOOP_VERSION 2.7.0 diff --git a/scripts/docker/spark-cluster-managers/spark_standalone/Dockerfile b/scripts/docker/spark-cluster-managers/spark_standalone/Dockerfile index 19391d0ed9b..8bf0f8d360f 100644 --- a/scripts/docker/spark-cluster-managers/spark_standalone/Dockerfile +++ b/scripts/docker/spark-cluster-managers/spark_standalone/Dockerfile @@ -15,7 +15,7 @@ FROM centos:centos6 ENV SPARK_PROFILE 2.1 -ENV SPARK_VERSION 2.1.1 +ENV SPARK_VERSION 2.1.2 ENV HADOOP_PROFILE 2.7 ENV SPARK_HOME /usr/local/spark diff --git a/scripts/docker/spark-cluster-managers/spark_yarn_cluster/Dockerfile b/scripts/docker/spark-cluster-managers/spark_yarn_cluster/Dockerfile index 116d4c3f88e..cbbda20dc0c 100644 --- a/scripts/docker/spark-cluster-managers/spark_yarn_cluster/Dockerfile +++ b/scripts/docker/spark-cluster-managers/spark_yarn_cluster/Dockerfile @@ -15,7 +15,7 @@ FROM centos:centos6 ENV SPARK_PROFILE 2.1 -ENV SPARK_VERSION 2.1.1 +ENV SPARK_VERSION 2.1.2 ENV HADOOP_PROFILE 2.7 ENV HADOOP_VERSION 2.7.0 diff --git a/scripts/docker/zeppelin/bin/Dockerfile b/scripts/docker/zeppelin/bin/Dockerfile index 9fb1aff70f4..3122a34c503 100644 --- a/scripts/docker/zeppelin/bin/Dockerfile +++ b/scripts/docker/zeppelin/bin/Dockerfile @@ -17,7 +17,7 @@ FROM ubuntu:16.04 MAINTAINER Apache Software Foundation # `Z_VERSION` will be updated by `dev/change_zeppelin_version.sh` -ENV Z_VERSION="0.8.0-SNAPSHOT" +ENV Z_VERSION="0.8.1" ENV LOG_TAG="[ZEPPELIN_${Z_VERSION}]:" \ Z_HOME="/zeppelin" \ LANG=en_US.UTF-8 \ @@ -33,20 +33,17 @@ RUN echo "$LOG_TAG update and install basic packages" && \ apt-get install -y build-essential RUN echo "$LOG_TAG install tini related packages" && \ - apt-get install -y curl grep sed dpkg && \ + apt-get install -y wget curl grep sed dpkg && \ TINI_VERSION=`curl https://github.com/krallin/tini/releases/latest | grep -o "/v.*\"" | sed 's:^..\(.*\).$:\1:'` && \ curl -L "https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini_${TINI_VERSION}.deb" > tini.deb && \ 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" && \ @@ -55,7 +52,7 @@ RUN echo "$LOG_TAG Install miniconda2 related packages" && \ libglib2.0-0 libxext6 libsm6 libxrender1 \ git mercurial subversion && \ echo 'export PATH=/opt/conda/bin:$PATH' > /etc/profile.d/conda.sh && \ - wget --quiet https://repo.continuum.io/miniconda/Miniconda2-4.3.11-Linux-x86_64.sh -O ~/miniconda.sh && \ + wget --quiet https://repo.continuum.io/miniconda/Miniconda2-4.2.12-Linux-x86_64.sh -O ~/miniconda.sh && \ /bin/bash ~/miniconda.sh -b -p /opt/conda && \ rm ~/miniconda.sh ENV PATH /opt/conda/bin:$PATH @@ -70,15 +67,19 @@ RUN echo "$LOG_TAG Install python related packages" && \ apt-get install -y libpng-dev libfreetype6-dev libxft-dev && \ # for tkinter apt-get install -y python-tk libxml2-dev libxslt-dev zlib1g-dev && \ - pip install numpy && \ - pip install matplotlib + conda config --set always_yes yes --set changeps1 no && \ + conda update -q conda && \ + conda info -a && \ + conda config --add channels conda-forge && \ + conda install -q numpy=1.12.1 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 RUN echo "$LOG_TAG Install R related packages" && \ echo "deb http://cran.rstudio.com/bin/linux/ubuntu xenial/" | tee -a /etc/apt/sources.list && \ gpg --keyserver keyserver.ubuntu.com --recv-key E084DAB9 && \ gpg -a --export E084DAB9 | apt-key add - && \ apt-get -y update && \ - apt-get -y install r-base r-base-dev && \ + apt-get -y --allow-unauthenticated install r-base r-base-dev && \ R -e "install.packages('knitr', repos='http://cran.us.r-project.org')" && \ R -e "install.packages('ggplot2', repos='http://cran.us.r-project.org')" && \ R -e "install.packages('googleVis', repos='http://cran.us.r-project.org')" && \ diff --git a/scripts/mapr-dsr/build.sh b/scripts/mapr-dsr/build.sh new file mode 100755 index 00000000000..ffd0837f7c3 --- /dev/null +++ b/scripts/mapr-dsr/build.sh @@ -0,0 +1,137 @@ +#!/bin/sh + +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"} + +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-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 + 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:-"gcr.io/mapr-252711/zeppelin-0.8.2"} +KUBEFLOW_IMAGE_VERSION=${KUBEFLOW_IMAGE_VERSION:-"$IMAGE_VERSION"} +SPARK_REPO="gcr.io/mapr-252711/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..6278b036c06 --- /dev/null +++ b/scripts/mapr-dsr/centos7/Dockerfile @@ -0,0 +1,64 @@ +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-r2" +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 \ + 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 git less nano patch vim && \ + 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..2b06653368f --- /dev/null +++ b/scripts/mapr-dsr/kubeflow/Dockerfile @@ -0,0 +1,77 @@ +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-r2" +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 \ + 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 git less nano patch vim && \ + 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..8ea6612237e --- /dev/null +++ b/scripts/mapr-dsr/spark/Dockerfile @@ -0,0 +1,33 @@ +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-r2" +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 \ + 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 gcr.io/mapr-252711/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..b61e00fa0a3 --- /dev/null +++ b/scripts/mapr-dsr/ubuntu16/Dockerfile @@ -0,0 +1,70 @@ +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-r2" +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 \ + 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 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 + +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/shell/pom.xml b/shell/pom.xml index 58d890084fc..ca1b2ba83eb 100644 --- a/shell/pom.xml +++ b/shell/pom.xml @@ -20,22 +20,25 @@ 4.0.0 - zeppelin + interpreter-parent org.apache.zeppelin - 0.8.0-SNAPSHOT - .. + 0.8.2-mapr-1912-r2 + ../interpreter-parent org.apache.zeppelin zeppelin-shell jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Shell interpreter + sh + 3.4 1.3 + sh @@ -43,7 +46,6 @@ ${project.groupId} zeppelin-interpreter ${project.version} - provided
    @@ -79,54 +81,12 @@ maven-enforcer-plugin - - - enforce - none - - - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/sh - false - false - true - runtime - - - - copy-artifact - package - - copy - - - ${project.build.directory}/../../interpreter/sh - false - false - true - runtime - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - + + + maven-resources-plugin 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..b1a2fa7d46a 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; @@ -46,7 +47,11 @@ */ public class ShellInterpreter extends KerberosInterpreter { private static final Logger LOGGER = LoggerFactory.getLogger(ShellInterpreter.class); + private static final String TIMEOUT_PROPERTY = "shell.command.timeout.millisecs"; + private String DEFAULT_TIMEOUT_PROPERTY = "60000"; + + private static final String DIRECTORY_USER_HOME = "shell.working.directory.user.home"; private final boolean isWindows = System.getProperty("os.name").startsWith("Windows"); private final String shell = isWindows ? "cmd /c" : "bash -c"; ConcurrentHashMap executors; @@ -79,7 +84,9 @@ public void close() { @Override - public InterpreterResult interpret(String cmd, InterpreterContext contextInterpreter) { + public InterpreterResult interpret(String originalCmd, InterpreterContext contextInterpreter) { + String cmd = Boolean.parseBoolean(getProperty("zeppelin.shell.interpolation")) ? + interpolate(originalCmd, contextInterpreter.getResourcePool()) : originalCmd; LOGGER.debug("Run shell command '" + cmd + "'"); OutputStream outStream = new ByteArrayOutputStream(); @@ -96,8 +103,14 @@ public InterpreterResult interpret(String cmd, InterpreterContext contextInterpr DefaultExecutor executor = new DefaultExecutor(); executor.setStreamHandler(new PumpStreamHandler( contextInterpreter.out, contextInterpreter.out)); - executor.setWatchdog(new ExecuteWatchdog(Long.valueOf(getProperty(TIMEOUT_PROPERTY)))); + + executor.setWatchdog(new ExecuteWatchdog( + Long.valueOf(getProperty(TIMEOUT_PROPERTY, DEFAULT_TIMEOUT_PROPERTY)))); executors.put(contextInterpreter.getParagraphId(), executor); + if (Boolean.valueOf(getProperty(DIRECTORY_USER_HOME))) { + executor.setWorkingDirectory(new File(System.getProperty("user.home"))); + } + int exitVal = executor.execute(cmdLine); LOGGER.info("Paragraph " + contextInterpreter.getParagraphId() + " return with exit value: " + exitVal); @@ -168,8 +181,8 @@ protected boolean runKerberosLogin() { return false; } - public void createSecureConfiguration() { - Properties properties = getProperty(); + public void createSecureConfiguration() throws InterpreterException { + Properties properties = getProperties(); CommandLine cmdLine = CommandLine.parse(shell); cmdLine.addArgument("-c", false); String kinitCommand = String.format("kinit -k -t %s %s", diff --git a/shell/src/main/resources/interpreter-setting.json b/shell/src/main/resources/interpreter-setting.json index 7728d5fe125..b6784b9ab46 100644 --- a/shell/src/main/resources/interpreter-setting.json +++ b/shell/src/main/resources/interpreter-setting.json @@ -11,6 +11,13 @@ "description": "Shell command time out in millisecs. Default = 60000", "type": "number" }, + "shell.working.directory.user.home": { + "envName": "SHELL_WORKING_DIRECTORY_USER_HOME", + "propertyName": "shell.working.directory.user.home", + "defaultValue": false, + "description": "If this set to true, the shell's working directory will be set to user home", + "type": "checkbox" + }, "zeppelin.shell.auth.type": { "envName": null, "propertyName": "zeppelin.shell.auth.type", @@ -31,11 +38,19 @@ "defaultValue": "", "description": "Kerberos principal", "type": "string" + }, + "zeppelin.shell.interpolation": { + "envName": null, + "propertyName": "zeppelin.shell.interpolation", + "defaultValue": false, + "description": "Enable ZeppelinContext variable interpolation into paragraph text", + "type": "checkbox" } }, "editor": { "language": "sh", - "editOnDblClick": false + "editOnDblClick": false, + "completionSupport": false } } ] diff --git a/shell/src/test/java/org/apache/zeppelin/shell/ShellInterpreterTest.java b/shell/src/test/java/org/apache/zeppelin/shell/ShellInterpreterTest.java index b369f2d0969..b67170c14a2 100644 --- a/shell/src/test/java/org/apache/zeppelin/shell/ShellInterpreterTest.java +++ b/shell/src/test/java/org/apache/zeppelin/shell/ShellInterpreterTest.java @@ -41,7 +41,8 @@ public void setUp() throws Exception { p.setProperty("shell.command.timeout.millisecs", "2000"); shell = new ShellInterpreter(p); - context = new InterpreterContext("", "1", null, "", "", null, null, null, null, null, null, null); + context = new InterpreterContext("", "1", null, "", "", null, null, null, null, null, null, + null, null); shell.open(); } diff --git a/spark-dependencies/pom.xml b/spark-dependencies/pom.xml deleted file mode 100644 index b7904c091fa..00000000000 --- a/spark-dependencies/pom.xml +++ /dev/null @@ -1,1042 +0,0 @@ - - - - - 4.0.0 - - - zeppelin - org.apache.zeppelin - 0.8.0-SNAPSHOT - .. - - - org.apache.zeppelin - zeppelin-spark-dependencies_2.10 - jar - 0.8.0-SNAPSHOT - Zeppelin: Spark dependencies - Zeppelin spark support - - - - - 1.4.1 - 2.3.0 - ${hadoop.version} - 1.7.7 - - 0.7.1 - 2.4.1 - - 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 - - 0.8.2.1 - - - 2.3 - - - - - - org.apache.avro - avro - ${avro.version} - - - org.apache.avro - avro-ipc - ${avro.version} - - - io.netty - netty - - - org.mortbay.jetty - jetty - - - org.mortbay.jetty - jetty-util - - - org.mortbay.jetty - servlet-api - - - org.apache.velocity - velocity - - - - - org.apache.avro - avro-mapred - ${avro.version} - ${avro.mapred.classifier} - - - io.netty - netty - - - org.mortbay.jetty - jetty - - - org.mortbay.jetty - jetty-util - - - org.mortbay.jetty - servlet-api - - - org.apache.velocity - velocity - - - - - - - net.java.dev.jets3t - jets3t - ${jets3t.version} - runtime - - - commons-logging - commons-logging - - - - - org.apache.hadoop - hadoop-yarn-api - ${yarn.version} - - - asm - asm - - - org.ow2.asm - asm - - - org.jboss.netty - netty - - - commons-logging - commons-logging - - - - - - org.apache.hadoop - hadoop-yarn-common - ${yarn.version} - - - asm - asm - - - org.ow2.asm - asm - - - org.jboss.netty - netty - - - javax.servlet - servlet-api - - - commons-logging - commons-logging - - - - - - org.apache.hadoop - hadoop-yarn-server-web-proxy - ${yarn.version} - - - asm - asm - - - org.ow2.asm - asm - - - org.jboss.netty - netty - - - javax.servlet - servlet-api - - - commons-logging - commons-logging - - - - - - org.apache.hadoop - hadoop-yarn-client - ${yarn.version} - - - asm - asm - - - org.ow2.asm - asm - - - org.jboss.netty - netty - - - javax.servlet - servlet-api - - - commons-logging - commons-logging - - - - - - - - - - org.apache.spark - spark-core_${scala.binary.version} - ${spark.version} - - - org.apache.hadoop - hadoop-client - - - - - - org.apache.spark - spark-repl_${scala.binary.version} - ${spark.version} - - - - org.apache.spark - spark-sql_${scala.binary.version} - ${spark.version} - - - - org.apache.spark - spark-hive_${scala.binary.version} - ${spark.version} - - - - org.apache.spark - spark-streaming_${scala.binary.version} - ${spark.version} - - - - org.apache.spark - spark-catalyst_${scala.binary.version} - ${spark.version} - - - - - org.apache.hadoop - 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 - spark-yarn_${scala.binary.version} - ${spark.version} - - - - org.apache.hadoop - hadoop-yarn-api - ${yarn.version} - - - - - - - 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 - - - - - - - - - - - maven-enforcer-plugin - - - enforce - none - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - 1 - false - -Xmx1024m -XX:MaxPermSize=256m - - - - - com.googlecode.maven-download-plugin - download-maven-plugin - ${plugin.download.version} - - - - org.apache.maven.plugins - maven-shade-plugin - ${plugin.shade.version} - - - - *:* - - org/datanucleus/** - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - reference.conf - - - - - - package - - shade - - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - ${project.build.directory}/../../interpreter/spark/dep - false - false - true - org.datanucleus - - - - package - - copy - - - ${project.build.directory}/../../interpreter/spark/dep - false - false - true - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - - - - - - - - com.googlecode.maven-download-plugin - download-maven-plugin - - - download-pyspark-files - validate - - wget - - - 60000 - 5 - true - ${spark.src.download.url} - ${project.build.directory} - - - - - - - maven-clean-plugin - - - - ${basedir}/../python/build - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - zip-pyspark-files - generate-resources - - run - - - - - - - - - - - - - - - com.googlecode.maven-download-plugin - download-maven-plugin - - - download-sparkr-files - validate - - wget - - - 60000 - 5 - ${spark.bin.download.url} - true - ${project.build.directory} - - - - - - maven-resources-plugin - 2.7 - - - copy-sparkr-files - generate-resources - - copy-resources - - - ${project.build.directory}/../../interpreter/spark/R/lib - - - - ${project.build.directory}/spark-${spark.version}-bin-without-hadoop/R/lib - - - - - - - - - - diff --git a/spark/interpreter/figure/unnamed-chunk-1-1.png b/spark/interpreter/figure/unnamed-chunk-1-1.png new file mode 100644 index 00000000000..6f03c95aff3 Binary files /dev/null and b/spark/interpreter/figure/unnamed-chunk-1-1.png differ diff --git a/spark/interpreter/pom.xml b/spark/interpreter/pom.xml new file mode 100644 index 00000000000..7b41f9a7112 --- /dev/null +++ b/spark/interpreter/pom.xml @@ -0,0 +1,585 @@ + + + + + 4.0.0 + + + spark-parent + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + ../pom.xml + + + org.apache.zeppelin + spark-interpreter + jar + 0.8.2-mapr-1912-r2 + 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.* + + + + + org.apache.zeppelin + zeppelin-display + ${project.version} + + + + + + org.apache.zeppelin + spark-scala-2.11 + ${project.version} + + + + org.apache.zeppelin + zeppelin-interpreter + ${project.version} + + + + + + org.apache.zeppelin + spark2-shims + ${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.hadoop + hadoop-client + + + + + + org.apache.hadoop + hadoop-client + 2.7.0-mapr-1808 + 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 + + + + + + + 1.7 + + + + + + + + + org.scalatest + scalatest-maven-plugin + + + + + + 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 + + + com.facebook.fb303 + org.apache.zeppelin.com.facebook.fb303 + + + + + + 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..aa1343aae50 --- /dev/null +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/AbstractSparkInterpreter.java @@ -0,0 +1,67 @@ +/* + * 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 { + + private SparkInterpreter parentSparkInterpreter; + + 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(); + + public void setParentSparkInterpreter(SparkInterpreter parentSparkInterpreter) { + this.parentSparkInterpreter = parentSparkInterpreter; + } + + public SparkInterpreter getParentSparkInterpreter() { + return parentSparkInterpreter; + } +} 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/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java new file mode 100644 index 00000000000..3a625c5405a --- /dev/null +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/IPySparkInterpreter.java @@ -0,0 +1,152 @@ +/* + * 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.BaseZeppelinContext; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Map; +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() throws InterpreterException { + setProperty("zeppelin.python", + PySparkInterpreter.getPythonExec(getProperties())); + sparkInterpreter = getSparkInterpreter(); + SparkConf conf = sparkInterpreter.getSparkContext().getConf(); + // only set PYTHONPATH in embedded, local or yarn-client mode. + // yarn-cluster will setup PYTHONPATH automatically. + if (!conf.contains("spark.submit.deployMode") || + !conf.get("spark.submit.deployMode").equals("cluster")) { + setAdditionalPythonPath(PythonUtils.sparkPythonPath()); + setAddBulitinPy4j(false); + } + setAdditionalPythonInitFile("python/zeppelin_ipyspark.py"); + setProperty("zeppelin.py4j.useAuth", + sparkInterpreter.getSparkVersion().isSecretSocketSupported() + ""); + super.open(); + } + + @Override + protected Map setupIPythonEnv() throws IOException { + Map env = super.setupIPythonEnv(); + // set PYSPARK_PYTHON + SparkConf conf = sparkInterpreter.getSparkContext().getConf(); + if (conf.contains("spark.pyspark.python")) { + env.put("PYSPARK_PYTHON", conf.get("spark.pyspark.python")); + } + return env; + } + + private SparkInterpreter getSparkInterpreter() throws InterpreterException { + 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 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 + "')"; + 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 + public void cancel(InterpreterContext context) throws InterpreterException { + super.cancel(context); + sparkInterpreter.cancel(context); + } + + @Override + public void close() throws InterpreterException { + super.close(); + if (sparkInterpreter != null) { + sparkInterpreter.close(); + } + } + + @Override + public int getProgress(InterpreterContext context) throws InterpreterException { + 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/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..f4cd411ffac --- /dev/null +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/NewSparkInterpreter.java @@ -0,0 +1,287 @@ +/* + * 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.sql.SQLContext; +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.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.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 SparkShims sparkShims; + + 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); + 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, Boolean.class) + .newInstance(conf, getDependencyFiles(), + Boolean.parseBoolean(getProperty("zeppelin.spark.printREPLOutput", "true"))); + 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(); + String sparkUrlProp = getProperty("zeppelin.spark.uiWebUrl", ""); + if (!StringUtils.isBlank(sparkUrlProp)) { + sparkUrl = sparkUrlProp; + } + sparkShims = SparkShims.getInstance(sc.version(), getProperties()); + sparkShims.setupSparkListener(sc.master(), sparkUrl); + 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("Fail to open SparkInterpreter", ExceptionUtils.getStackTrace(e)); + throw new InterpreterException("Fail to open SparkInterpreter", e); + } + } + + @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); + } + + 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 = getParentSparkInterpreter() + .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 74% 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 df410146d4d..d497a497e51 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/OldSparkInterpreter.java @@ -26,17 +26,16 @@ import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; -import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Properties; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; 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; @@ -46,12 +45,27 @@ 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.DefaultInterpreterProperty; import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterException; @@ -72,7 +86,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.base.Joiner; import scala.Console; import scala.Enumeration.Value; import scala.None; @@ -101,8 +114,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; @@ -117,7 +130,7 @@ public class SparkInterpreter extends Interpreter { 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); @@ -138,17 +151,19 @@ public class SparkInterpreter extends Interpreter { private JavaSparkContext jsc; private boolean enableSupportedVersionCheck; - public SparkInterpreter(Properties property) { + private SparkShims sparkShims; + + 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; env = SparkEnv.get(); - sparkListener = setupListeners(this.sc); + sparkShims = SparkShims.getInstance(sc.version(), getProperties()); + sparkShims.setupSparkListener(sc.master(), sparkUrl); } public SparkContext getSparkContext() { @@ -156,7 +171,6 @@ public SparkContext getSparkContext() { if (sc == null) { sc = createSparkContext(); env = SparkEnv.get(); - sparkListener = setupListeners(sc); } return sc; } @@ -177,78 +191,6 @@ public boolean isSparkContextInitialized() { } } - static JobProgressListener setupListeners(SparkContext context) { - JobProgressListener pl = new JobProgressListener(context.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 = 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(JobProgressListener.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")); } @@ -339,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; } @@ -351,7 +294,11 @@ private DepInterpreter getDepInterpreter() { } public boolean isYarnMode() { - return getProperty("master").startsWith("yarn"); + String master = getProperty("master"); + if (master == null) { + master = getProperty("spark.master", "local[*]"); + } + return master.startsWith("yarn"); } /** @@ -371,24 +318,24 @@ public Object createSparkSession() { conf.set("spark.executor.uri", execUri); } conf.set("spark.scheduler.mode", "FAIR"); - conf.setMaster(getProperty("master")); - if (isYarnMode()) { - conf.set("master", "yarn"); - conf.set("spark.submit.deployMode", "client"); - } - Properties intpProperty = getProperty(); + Properties intpProperty = getProperties(); for (Object k : intpProperty.keySet()) { String key = (String) k; String val = toString(intpProperty.get(key)); - if (key.startsWith("spark.") && !val.trim().isEmpty()) { - logger.debug(String.format("SparkConf: key = [%s], value = [%s]", key, val)); - conf.set(key, val); + if (!val.trim().isEmpty()) { + if (key.startsWith("spark.")) { + logger.debug(String.format("SparkConf: key = [%s], value = [%s]", key, val)); + conf.set(key, val); + } + if (key.startsWith("zeppelin.spark.")) { + String sparkPropertyKey = key.substring("zeppelin.spark.".length()); + logger.debug(String.format("SparkConf: key = [%s], value = [%s]", sparkPropertyKey, val)); + conf.set(sparkPropertyKey, val); + } } } - setupConfForPySpark(conf); - setupConfForSparkR(conf); Class SparkSession = Utils.findClass("org.apache.spark.sql.SparkSession"); Object builder = Utils.invokeStaticMethod(SparkSession, "builder"); Utils.invokeMethod(builder, "config", new Class[]{ SparkConf.class }, new Object[]{ conf }); @@ -443,7 +390,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 +414,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 +424,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 } } @@ -505,105 +452,27 @@ public SparkContext createSparkContext_1() { } conf.set("spark.scheduler.mode", "FAIR"); - Properties intpProperty = getProperty(); + Properties intpProperty = getProperties(); for (Object k : intpProperty.keySet()) { String key = (String) k; String val = toString(intpProperty.get(key)); - if (key.startsWith("spark.") && !val.trim().isEmpty()) { - logger.debug(String.format("SparkConf: key = [%s], value = [%s]", key, val)); - conf.set(key, val); + if (!val.trim().isEmpty()) { + if (key.startsWith("spark.")) { + logger.debug(String.format("SparkConf: key = [%s], value = [%s]", key, val)); + conf.set(key, val); + } + + if (key.startsWith("zeppelin.spark.")) { + String sparkPropertyKey = key.substring("zeppelin.spark.".length()); + logger.debug(String.format("SparkConf: key = [%s], value = [%s]", sparkPropertyKey, val)); + conf.set(sparkPropertyKey, val); + } } } - setupConfForPySpark(conf); - setupConfForSparkR(conf); SparkContext sparkContext = new SparkContext(conf); return sparkContext; } - private void setupConfForPySpark(SparkConf conf) { - Object pysparkBaseProperty = - new DefaultInterpreterProperty("SPARK_HOME", null, null).getValue(); - String pysparkBasePath = pysparkBaseProperty != null ? pysparkBaseProperty.toString() : null; - File pysparkPath; - if (null == pysparkBasePath) { - pysparkBasePath = - new DefaultInterpreterProperty("ZEPPELIN_HOME", "zeppelin.home", "../") - .getValue().toString(); - pysparkPath = new File(pysparkBasePath, - "interpreter" + File.separator + "spark" + File.separator + "pyspark"); - } else { - pysparkPath = new File(pysparkBasePath, - "python" + File.separator + "lib"); - } - - //Only one of py4j-0.9-src.zip and py4j-0.8.2.1-src.zip should exist - //TODO(zjffdu), this is not maintainable when new version is added. - String[] pythonLibs = new String[]{"pyspark.zip", "py4j-0.9-src.zip", "py4j-0.8.2.1-src.zip", - "py4j-0.10.1-src.zip", "py4j-0.10.3-src.zip", "py4j-0.10.4-src.zip"}; - ArrayList pythonLibUris = new ArrayList<>(); - for (String lib : pythonLibs) { - File libFile = new File(pysparkPath, lib); - if (libFile.exists()) { - pythonLibUris.add(libFile.toURI().toString()); - } - } - pythonLibUris.trimToSize(); - - // Distribute two libraries(pyspark.zip and py4j-*.zip) to workers - // when spark version is less than or equal to 1.4.1 - if (pythonLibUris.size() == 2) { - try { - String confValue = conf.get("spark.yarn.dist.files"); - conf.set("spark.yarn.dist.files", confValue + "," + Joiner.on(",").join(pythonLibUris)); - } catch (NoSuchElementException e) { - conf.set("spark.yarn.dist.files", Joiner.on(",").join(pythonLibUris)); - } - if (!useSparkSubmit()) { - conf.set("spark.files", conf.get("spark.yarn.dist.files")); - } - conf.set("spark.submit.pyArchives", Joiner.on(":").join(pythonLibs)); - conf.set("spark.submit.pyFiles", Joiner.on(",").join(pythonLibUris)); - } - - // Distributes needed libraries to workers - // when spark version is greater than or equal to 1.5.0 - if (isYarnMode()) { - conf.set("spark.yarn.isPython", "true"); - } - } - - private void setupConfForSparkR(SparkConf conf) { - Object sparkRBaseProperty = - new DefaultInterpreterProperty("SPARK_HOME", null, null).getValue(); - String sparkRBasePath = sparkRBaseProperty != null ? sparkRBaseProperty.toString() : null; - File sparkRPath; - if (null == sparkRBasePath) { - sparkRBasePath = - new DefaultInterpreterProperty("ZEPPELIN_HOME", "zeppelin.home", "../") - .getValue().toString(); - sparkRPath = new File(sparkRBasePath, - "interpreter" + File.separator + "spark" + File.separator + "R"); - } else { - sparkRPath = new File(sparkRBasePath, "R" + File.separator + "lib"); - } - - sparkRPath = new File(sparkRPath, "sparkr.zip"); - if (sparkRPath.exists() && sparkRPath.isFile()) { - String archives = null; - if (conf.contains("spark.yarn.dist.archives")) { - archives = conf.get("spark.yarn.dist.archives"); - } - if (archives != null) { - archives = archives + "," + sparkRPath + "#sparkr"; - } else { - archives = sparkRPath + "#sparkr"; - } - conf.set("spark.yarn.dist.archives", archives); - } else { - logger.warn("sparkr.zip is not found, sparkr may not work."); - } - } - static final String toString(Object o) { return (o instanceof String) ? (String) o : ""; } @@ -617,19 +486,19 @@ public boolean printREPLOutput() { } @Override - public void open() { + public void open() throws InterpreterException { this.enableSupportedVersionCheck = java.lang.Boolean.parseBoolean( - property.getProperty("zeppelin.spark.enableSupportedVersionCheck", "true")); + getProperty("zeppelin.spark.enableSupportedVersionCheck", "true")); // set properties and do login before creating any spark stuff for secured cluster if (isYarnMode()) { System.setProperty("SPARK_YARN_MODE", "true"); } - if (getProperty().containsKey("spark.yarn.keytab") && - getProperty().containsKey("spark.yarn.principal")) { + if (getProperties().containsKey("spark.yarn.keytab") && + getProperties().containsKey("spark.yarn.principal")) { try { - String keytab = getProperty().getProperty("spark.yarn.keytab"); - String principal = getProperty().getProperty("spark.yarn.principal"); + String keytab = getProperties().getProperty("spark.yarn.keytab"); + String principal = getProperties().getProperty("spark.yarn.principal"); UserGroupInformation.loginUserFromKeytab(principal, keytab); } catch (IOException e) { throw new RuntimeException("Can not pass kerberos authentication", e); @@ -803,7 +672,7 @@ public void open() { * * 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')); @@ -883,11 +752,11 @@ public void open() { 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; @@ -905,13 +774,13 @@ public void open() { } 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 = " @@ -1002,6 +871,10 @@ public void open() { } } + sparkUrl = getSparkUIUrl(); + sparkShims = SparkShims.getInstance(sc.version(), getProperties()); + sparkShims.setupSparkListener(sc.master(), sparkUrl); + numReferenceOfSparkContext.incrementAndGet(); } @@ -1010,6 +883,11 @@ public String getSparkUIUrl() { return sparkUrl; } + String sparkUrlProp = getProperty("zeppelin.spark.uiWebUrl", ""); + if (!StringUtils.isBlank(sparkUrlProp)) { + return sparkUrlProp; + } + if (sparkVersion.newerThanEquals(SparkVersion.SPARK_2_0_0)) { Option uiWebUrlOption = (Option) Utils.invokeMethod(sc, "uiWebUrl"); if (uiWebUrlOption.isDefined()) { @@ -1037,9 +915,9 @@ public void populateSparkWebUrl(InterpreterContext ctx) { sparkUrl = getSparkUIUrl(); Map infos = new java.util.HashMap<>(); infos.put("url", sparkUrl); - String uiEnabledProp = property.getProperty("spark.ui.enabled", "true"); + 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 { @@ -1087,7 +965,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<>(); @@ -1096,23 +974,33 @@ public List completion(String buf, int cursor, if (buf.length() < cursor) { cursor = buf.length(); } - String completionText = getCompletionTargetString(buf, cursor); - if (completionText == null) { - completionText = ""; - cursor = completionText.length(); - } ScalaCompleter c = (ScalaCompleter) Utils.invokeMethod(completer, "completer"); - Candidates ret = c.complete(completionText, cursor); - List candidates = WrapAsJava$.MODULE$.seqAsJavaList(ret.candidates()); - List completions = new LinkedList<>(); + if (Utils.isScala2_10() || !Utils.isCompilerAboveScala2_11_7()) { + String singleToken = getCompletionTargetString(buf, cursor); + Candidates ret = c.complete(singleToken, singleToken.length()); - for (String candidate : candidates) { - completions.add(new InterpreterCompletion(candidate, candidate, StringUtils.EMPTY)); - } + List candidates = WrapAsJava$.MODULE$.seqAsJavaList(ret.candidates()); + List completions = new LinkedList<>(); - return completions; + 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; + } } private String getCompletionTargetString(String text, int cursor) { @@ -1151,7 +1039,7 @@ private String getCompletionTargetString(String text, int cursor) { completionStartPosition = completionEndPosition - completionStartPosition; } resultCompletionText = completionScriptText.substring( - completionStartPosition , completionEndPosition); + completionStartPosition , completionEndPosition); return resultCompletionText; } @@ -1162,7 +1050,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; @@ -1183,7 +1071,7 @@ public Object getLastObject() { return obj; } - boolean isUnsupportedSparkVersion() { + public boolean isUnsupportedSparkVersion() { return enableSupportedVersionCheck && sparkVersion.isUnsupportedVersion(); } @@ -1207,6 +1095,7 @@ public InterpreterResult interpret(String line, InterpreterContext context) { public InterpreterResult interpret(String[] lines, InterpreterContext context) { synchronized (this) { z.setGui(context.getGui()); + z.setNoteGui(context.getNoteGui()); String jobDesc = "Started by: " + Utils.getUserName(context.getAuthenticationInfo()); sc.setJobGroup(Utils.buildJobGroupId(context), jobDesc, false); InterpreterResult r = interpretInput(lines, context); @@ -1237,9 +1126,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; @@ -1248,9 +1137,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; @@ -1336,114 +1225,7 @@ 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; - } - - private int[] getProgressFromStage_1_0x(JobProgressListener 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(JobProgressListener 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}; + return JobProgressUtil.progress(sc, jobGroup); } private Code getResultCode(scala.tools.nsc.interpreter.Results.Result r) { @@ -1483,14 +1265,10 @@ public FormType getFormType() { return FormType.NATIVE; } - public JobProgressListener getJobProgressListener() { - return sparkListener; - } - @Override public Scheduler getScheduler() { return SchedulerFactory.singleton().createOrGetFIFOScheduler( - SparkInterpreter.class.getName() + this.hashCode()); + OldSparkInterpreter.class.getName() + this.hashCode()); } public SparkZeppelinContext getZeppelinContext() { @@ -1506,18 +1284,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/interpreter/src/main/java/org/apache/zeppelin/spark/Py4JUtils.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/Py4JUtils.java new file mode 100644 index 00000000000..c8bdd3ba8c6 --- /dev/null +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/Py4JUtils.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.spark; + +import org.apache.commons.codec.binary.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import py4j.GatewayServer; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.SecureRandom; +import java.util.Properties; + +/** + * Utils class for Py4J related stuff + */ +public class Py4JUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(Py4JUtils.class); + + public static GatewayServer createGatewayServer(Object entryPoint, + String serverAddress, + int port, + String secretKey, + boolean useAuth) throws IOException { + LOGGER.info("Launching GatewayServer at " + serverAddress + ":" + port); + if (useAuth) { + try { + Class clz = Class.forName("py4j.GatewayServer$GatewayServerBuilder", true, + Thread.currentThread().getContextClassLoader()); + Object builder = clz.getConstructor(Object.class).newInstance(entryPoint); + builder.getClass().getMethod("authToken", String.class).invoke(builder, secretKey); + builder.getClass().getMethod("javaPort", int.class).invoke(builder, port); + builder.getClass().getMethod("javaAddress", InetAddress.class).invoke(builder, + InetAddress.getByName(serverAddress)); + builder.getClass() + .getMethod("callbackClient", int.class, InetAddress.class, String.class) + .invoke(builder, port, InetAddress.getByName(serverAddress), secretKey); + return (GatewayServer) builder.getClass().getMethod("build").invoke(builder); + } catch (Exception e) { + throw new IOException("Fail to create GatewayServer", e); + } + } else { + return new GatewayServer(entryPoint, port); + } + } + + public static String getLocalIP(Properties properties) { + // zeppelin.python.gatewayserver_address is only for unit test on travis. + // Because the FQDN would fail unit test on travis ci. + String gatewayserver_address = + properties.getProperty("zeppelin.python.gatewayserver_address"); + if (gatewayserver_address != null) { + return gatewayserver_address; + } + + try { + return Inet4Address.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + LOGGER.warn("can't get local IP", e); + } + // fall back to loopback addreess + return "127.0.0.1"; + } + + public static String createSecret(int secretBitLength) { + SecureRandom rnd = new SecureRandom(); + byte[] secretBytes = new byte[secretBitLength / Byte.SIZE]; + rnd.nextBytes(secretBytes); + return Base64.encodeBase64String(secretBytes); + } +} 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 77% 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 28910b2546b..c051a37d43f 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,41 @@ 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.InvalidHookException; +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; + /** * */ @@ -75,6 +81,9 @@ public class PySparkInterpreter extends Interpreter implements ExecuteResultHand boolean pythonscriptRunning = false; private static final int MAX_TIMEOUT_SEC = 10; private long pythonPid; + private String secret; + + private IPySparkInterpreter iPySparkInterpreter; public PySparkInterpreter(Properties property) { super(property); @@ -84,11 +93,11 @@ public PySparkInterpreter(Properties property) { File scriptFile = File.createTempFile("zeppelin_pyspark-", ".py"); scriptPath = scriptFile.getAbsolutePath(); } catch (IOException e) { - throw new InterpreterException(e); + throw new RuntimeException(e); } } - private void createPythonScript() { + private void createPythonScript() throws InterpreterException { ClassLoader classLoader = getClass().getClassLoader(); File out = new File(scriptPath); @@ -110,11 +119,33 @@ private void createPythonScript() { } @Override - public void open() { + public void open() throws InterpreterException { + // try IPySparkInterpreter first + iPySparkInterpreter = getIPySparkInterpreter(); + if (getProperty("zeppelin.pyspark.useIPython", "true").equals("true") && + StringUtils.isEmpty( + iPySparkInterpreter.checkIPythonPrerequisite(getPythonExec(getProperties())))) { + try { + iPySparkInterpreter.open(); + LOGGER.info("IPython is available, Use IPySparkInterpreter to replace PySparkInterpreter"); + return; + } catch (Exception e) { + iPySparkInterpreter = null; + LOGGER.warn("Fail to open IPySparkInterpreter", e); + } + } + + // reset iPySparkInterpreter to null as it is not available + iPySparkInterpreter = null; + LOGGER.info("IPython is not available, use the native PySparkInterpreter\n"); // Add matplotlib display hook InterpreterGroup intpGroup = getInterpreterGroup(); if (intpGroup != null && intpGroup.getInterpreterHookRegistry() != null) { - registerHook(HookType.POST_EXEC_DEV, "__zeppelin__._displayhook()"); + try { + registerHook(HookType.POST_EXEC_DEV.getName(), "__zeppelin__._displayhook()"); + } catch (InvalidHookException e) { + throw new InterpreterException(e); + } } DepInterpreter depInterpreter = getDepInterpreter(); @@ -169,53 +200,76 @@ public void open() { } } - private Map setupPySparkEnv() throws IOException{ + private Map setupPySparkEnv() throws IOException, InterpreterException { Map env = EnvironmentUtils.getProcEnvironment(); - if (!env.containsKey("PYTHONPATH")) { - SparkConf conf = getSparkConf(); - env.put("PYTHONPATH", conf.get("spark.submit.pyFiles").replaceAll(",", ":") + - ":../interpreter/lib/python"); + // only set PYTHONPATH in local or yarn-client mode. + // yarn-cluster will setup PYTHONPATH automatically. + SparkConf conf = null; + try { + conf = getSparkConf(); + } catch (InterpreterException e) { + throw new IOException(e); + } + if (!conf.get("spark.submit.deployMode", "client").equals("cluster")) { + if (!env.containsKey("PYTHONPATH")) { + env.put("PYTHONPATH", PythonUtils.sparkPythonPath()); + } else { + env.put("PYTHONPATH", PythonUtils.sparkPythonPath() + ":" + env.get("PYTHONPATH")); + } } // get additional class paths when using SPARK_SUBMIT and not using YARN-CLIENT // also, add all packages to PYTHONPATH since there might be transitive dependencies if (SparkInterpreter.useSparkSubmit() && !getSparkInterpreter().isYarnMode()) { - - String sparkSubmitJars = getSparkConf().get("spark.jars").replace(",", ":"); - - if (!"".equals(sparkSubmitJars)) { - env.put("PYTHONPATH", env.get("PYTHONPATH") + sparkSubmitJars); + String sparkSubmitJars = conf.get("spark.jars").replace(",", ":"); + if (!StringUtils.isEmpty(sparkSubmitJars)) { + env.put("PYTHONPATH", env.get("PYTHONPATH") + ":" + sparkSubmitJars); } } + // set PYSPARK_PYTHON + if (conf.contains("spark.pyspark.python")) { + env.put("PYSPARK_PYTHON", conf.get("spark.pyspark.python")); + } + LOGGER.info("PYTHONPATH: " + env.get("PYTHONPATH")); return env; } - private void createGatewayServerAndStartScript() { - // create python script - createPythonScript(); - - port = findRandomOpenPortOnAllLocalInterfaces(); - - 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"); + // 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() throws InterpreterException, IOException { + // create python script + createPythonScript(); + + port = findRandomOpenPortOnAllLocalInterfaces(); + secret = Py4JUtils.createSecret(256); + boolean useAuth = getSparkInterpreter().getSparkVersion().isSecretSocketSupported(); + gatewayServer = Py4JUtils.createGatewayServer(this, "127.0.0.1", port, secret, useAuth); + gatewayServer.start(); + + String pythonExec = getPythonExec(getProperties()); + LOGGER.info("pythonExec: " + pythonExec); CommandLine cmd = CommandLine.parse(pythonExec); cmd.addArgument(scriptPath, false); cmd.addArgument(Integer.toString(port), false); cmd.addArgument(Integer.toString(getSparkInterpreter().getSparkVersion().toNumber()), false); + if (useAuth) { + cmd.addArgument(secret, false); + } executor = new DefaultExecutor(); outputStream = new InterpreterOutputStream(LOGGER); PipedOutputStream ps = new PipedOutputStream(); @@ -250,7 +304,7 @@ private void createGatewayServerAndStartScript() { } } - private int findRandomOpenPortOnAllLocalInterfaces() { + private int findRandomOpenPortOnAllLocalInterfaces() throws InterpreterException { int port; try (ServerSocket socket = new ServerSocket(0);) { port = socket.getLocalPort(); @@ -262,7 +316,11 @@ private int findRandomOpenPortOnAllLocalInterfaces() { } @Override - public void close() { + public void close() throws InterpreterException { + if (iPySparkInterpreter != null) { + iPySparkInterpreter.close(); + return; + } executor.getWatchdog().destroyProcess(); new File(scriptPath).delete(); gatewayServer.shutdown(); @@ -345,13 +403,18 @@ public void appendOutput(String message) throws IOException { } @Override - public InterpreterResult interpret(String st, InterpreterContext context) { + 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"); } + sparkInterpreter.populateSparkWebUrl(context); if (!pythonscriptRunning) { return new InterpreterResult(Code.ERROR, "python process not running" @@ -403,9 +466,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; @@ -436,7 +503,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); @@ -447,7 +514,11 @@ public void interrupt() throws IOException { } @Override - public void cancel(InterpreterContext context) { + public void cancel(InterpreterContext context) throws InterpreterException { + if (iPySparkInterpreter != null) { + iPySparkInterpreter.cancel(context); + return; + } SparkInterpreter sparkInterpreter = getSparkInterpreter(); sparkInterpreter.cancel(context); try { @@ -463,7 +534,10 @@ public FormType getFormType() { } @Override - public int getProgress(InterpreterContext context) { + public int getProgress(InterpreterContext context) throws InterpreterException { + if (iPySparkInterpreter != null) { + return iPySparkInterpreter.getProgress(context); + } SparkInterpreter sparkInterpreter = getSparkInterpreter(); return sparkInterpreter.getProgress(context); } @@ -471,7 +545,11 @@ public int getProgress(InterpreterContext context) { @Override public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { + InterpreterContext interpreterContext) + throws InterpreterException { + if (iPySparkInterpreter != null) { + return iPySparkInterpreter.completion(buf, cursor, interpreterContext); + } if (buf.length() < cursor) { cursor = buf.length(); } @@ -569,7 +647,7 @@ private String getCompletionTargetString(String text, int cursor) { } - private SparkInterpreter getSparkInterpreter() { + private SparkInterpreter getSparkInterpreter() throws InterpreterException { LazyOpenInterpreter lazy = null; SparkInterpreter spark = null; Interpreter p = getInterpreterInTheSameSessionByClassName(SparkInterpreter.class.getName()); @@ -588,7 +666,22 @@ private SparkInterpreter getSparkInterpreter() { return spark; } - public SparkZeppelinContext getZeppelinContext() { + 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() throws InterpreterException { SparkInterpreter sparkIntp = getSparkInterpreter(); if (sparkIntp != null) { return getSparkInterpreter().getZeppelinContext(); @@ -597,7 +690,7 @@ public SparkZeppelinContext getZeppelinContext() { } } - public JavaSparkContext getJavaSparkContext() { + public JavaSparkContext getJavaSparkContext() throws InterpreterException { SparkInterpreter intp = getSparkInterpreter(); if (intp == null) { return null; @@ -606,7 +699,7 @@ public JavaSparkContext getJavaSparkContext() { } } - public Object getSparkSession() { + public Object getSparkSession() throws InterpreterException { SparkInterpreter intp = getSparkInterpreter(); if (intp == null) { return null; @@ -615,7 +708,7 @@ public Object getSparkSession() { } } - public SparkConf getSparkConf() { + public SparkConf getSparkConf() throws InterpreterException { JavaSparkContext sc = getJavaSparkContext(); if (sc == null) { return null; @@ -624,7 +717,7 @@ public SparkConf getSparkConf() { } } - public SQLContext getSQLContext() { + public SQLContext getSQLContext() throws InterpreterException { SparkInterpreter intp = getSparkInterpreter(); if (intp == null) { return null; diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/PythonUtils.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/PythonUtils.java new file mode 100644 index 00000000000..81826900526 --- /dev/null +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/PythonUtils.java @@ -0,0 +1,96 @@ +/* + * 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.commons.lang3.StringUtils; + +import java.io.File; +import java.io.FilenameFilter; +import java.util.ArrayList; +import java.util.List; + +/** + * Util class for PySpark + */ +public class PythonUtils { + + /** + * Get the PYTHONPATH for PySpark, either from SPARK_HOME, if it is set, or from ZEPPELIN_HOME + * when it is embedded mode. + * + * This method will called in zeppelin server process and spark driver process when it is + * local or yarn-client mode. + */ + public static String sparkPythonPath() { + List pythonPath = new ArrayList(); + String sparkHome = System.getenv("SPARK_HOME"); + String zeppelinHome = System.getenv("ZEPPELIN_HOME"); + if (zeppelinHome == null) { + zeppelinHome = new File("..").getAbsolutePath(); + } + if (sparkHome != null) { + // non-embedded mode when SPARK_HOME is specified. + File pyspark = new File(sparkHome, "python/lib/pyspark.zip"); + if (!pyspark.exists()) { + throw new RuntimeException("No pyspark.zip found under " + sparkHome + "/python/lib"); + } + pythonPath.add(pyspark.getAbsolutePath()); + File[] py4j = new File(sparkHome + "/python/lib").listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.startsWith("py4j"); + } + }); + if (py4j.length == 0) { + throw new RuntimeException("No py4j files found under " + sparkHome + "/python/lib"); + } else if (py4j.length > 1) { + throw new RuntimeException("Multiple py4j files found under " + sparkHome + "/python/lib"); + } else { + pythonPath.add(py4j[0].getAbsolutePath()); + } + } else { + // embedded mode + File pyspark = new File(zeppelinHome, "interpreter/spark/pyspark/pyspark.zip"); + if (!pyspark.exists()) { + throw new RuntimeException("No pyspark.zip found: " + pyspark.getAbsolutePath()); + } + pythonPath.add(pyspark.getAbsolutePath()); + File[] py4j = new File(zeppelinHome, "interpreter/spark/pyspark").listFiles( + new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.startsWith("py4j"); + } + }); + if (py4j.length == 0) { + throw new RuntimeException("No py4j files found under " + zeppelinHome + + "/interpreter/spark/pyspark"); + } else if (py4j.length > 1) { + throw new RuntimeException("Multiple py4j files found under " + sparkHome + + "/interpreter/spark/pyspark"); + } else { + pythonPath.add(py4j[0].getAbsolutePath()); + } + } + + // add ${ZEPPELIN_HOME}/interpreter/lib/python for all the cases + pythonPath.add(zeppelinHome + "/interpreter/lib/python"); + return StringUtils.join(pythonPath, ":"); + } +} 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..7df1bc95aa6 --- /dev/null +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkInterpreter.java @@ -0,0 +1,164 @@ +/* + * 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); + } + delegation.setParentSparkInterpreter(this); + } + + @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 76% 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 ca52f790f6a..6583bb00455 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/SparkRInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkRInterpreter.java @@ -27,6 +27,7 @@ import org.apache.spark.api.java.JavaSparkContext; import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.apache.zeppelin.interpreter.util.InterpreterOutputStream; import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; import org.slf4j.Logger; @@ -36,6 +37,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; /** * R and SparkR interpreter with visualization support. @@ -43,83 +46,87 @@ public class SparkRInterpreter extends Interpreter { private static final Logger logger = LoggerFactory.getLogger(SparkRInterpreter.class); - private static String renderOptions; + private String renderOptions; private SparkInterpreter sparkInterpreter; + private boolean isSpark2; private ZeppelinR zeppelinR; + private AtomicBoolean rbackendDead = new AtomicBoolean(false); private SparkContext sc; private JavaSparkContext jsc; + private String secret; public SparkRInterpreter(Properties property) { super(property); } @Override - public void open() { - String rCmdPath = getProperty("zeppelin.R.cmd"); + public void open() throws InterpreterException { + String rCmdPath = getProperty("zeppelin.R.cmd", "R"); String sparkRLibPath; if (System.getenv("SPARK_HOME") != null) { + // local or yarn-client mode when SPARK_HOME is specified sparkRLibPath = System.getenv("SPARK_HOME") + "/R/lib"; - } else { + } else if (System.getenv("ZEPPELIN_HOME") != null){ + // embedded mode when SPARK_HOME is not specified sparkRLibPath = System.getenv("ZEPPELIN_HOME") + "/interpreter/spark/R/lib"; // workaround to make sparkr work without SPARK_HOME System.setProperty("spark.test.home", System.getenv("ZEPPELIN_HOME") + "/interpreter/spark"); + } else { + // yarn-cluster mode + 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(); } } + this.isSpark2 = sparkVersion.newerThanEquals(SparkVersion.SPARK_2_0_0); + int timeout = this.sc.getConf().getInt("spark.r.backendConnectionTimeout", 6000); - int port = SparkRBackend.port(); - - this.sparkInterpreter = getSparkInterpreter(); - this.sc = sparkInterpreter.getSparkContext(); - this.jsc = sparkInterpreter.getJavaSparkContext(); - SparkVersion sparkVersion = new SparkVersion(sc.version()); ZeppelinRContext.setSparkContext(sc); ZeppelinRContext.setJavaSparkContext(jsc); - if (Utils.isSpark2()) { + if (isSpark2) { ZeppelinRContext.setSparkSession(sparkInterpreter.getSparkSession()); } ZeppelinRContext.setSqlContext(sparkInterpreter.getSQLContext()); ZeppelinRContext.setZeppelinContext(sparkInterpreter.getZeppelinContext()); - zeppelinR = new ZeppelinR(rCmdPath, sparkRLibPath, port, sparkVersion); + zeppelinR = + new ZeppelinR(rCmdPath, sparkRLibPath, SparkRBackend.port(), sparkVersion, timeout, this); try { zeppelinR.open(); } catch (IOException e) { - logger.error("Exception while opening SparkRInterpreter", e); - throw new InterpreterException(e); + throw new InterpreterException("Exception while opening SparkRInterpreter", e); } if (useKnitr()) { zeppelinR.eval("library('knitr')"); } - renderOptions = getProperty("zeppelin.R.render.options"); - } - - String getJobGroup(InterpreterContext context){ - return "zeppelin-" + context.getParagraphId(); + renderOptions = getProperty("zeppelin.R.render.options", + "out.format = 'html', comment = NA, echo = FALSE, results = 'asis', message = F, " + + "warning = F, fig.retina = 2"); } @Override - public InterpreterResult interpret(String lines, InterpreterContext interpreterContext) { + public InterpreterResult interpret(String lines, InterpreterContext interpreterContext) + throws InterpreterException { - SparkInterpreter sparkInterpreter = getSparkInterpreter(); sparkInterpreter.populateSparkWebUrl(interpreterContext); - if (sparkInterpreter.isUnsupportedSparkVersion()) { - return new InterpreterResult(InterpreterResult.Code.ERROR, "Spark " - + sparkInterpreter.getSparkVersion().toString() + " is not supported"); - } - String jobGroup = Utils.buildJobGroupId(interpreterContext); String jobDesc = "Started by: " + Utils.getUserName(interpreterContext.getAuthenticationInfo()); sparkInterpreter.getSparkContext().setJobGroup(jobGroup, jobDesc, false); - String imageWidth = getProperty("zeppelin.R.image.width"); + String imageWidth = getProperty("zeppelin.R.image.width", "100%"); String[] sl = lines.split("\n"); if (sl[0].contains("{") && sl[0].contains("}")) { @@ -140,18 +147,21 @@ public InterpreterResult interpret(String lines, InterpreterContext interpreterC String setJobGroup = ""; // assign setJobGroup to dummy__, otherwise it would print NULL for this statement - if (Utils.isSpark2()) { + if (isSpark2) { setJobGroup = "dummy__ <- setJobGroup(\"" + jobGroup + "\", \" +" + jobDesc + "\", TRUE)"; } else if (getSparkInterpreter().getSparkVersion().newerThanEquals(SparkVersion.SPARK_1_5_0)) { setJobGroup = "dummy__ <- setJobGroup(sc, \"" + jobGroup + "\", \"" + jobDesc + "\", TRUE)"; } - logger.debug("set JobGroup:" + setJobGroup); lines = setJobGroup + "\n" + lines; try { // render output with knitr + if (rbackendDead.get()) { + return new InterpreterResult(InterpreterResult.Code.ERROR, + "sparkR backend is dead, please try to increase spark.r.backendConnectionTimeout"); + } if (useKnitr()) { zeppelinR.setInterpreterOutput(null); zeppelinR.set(".zcmd", "\n```{r " + renderOptions + "}\n" + lines + "\n```"); @@ -174,11 +184,6 @@ public InterpreterResult interpret(String lines, InterpreterContext interpreterC } catch (Exception e) { logger.error("Exception while connecting to R", e); return new InterpreterResult(InterpreterResult.Code.ERROR, e.getMessage()); - } finally { - try { - } catch (Exception e) { - // Do nothing... - } } } @@ -190,7 +195,7 @@ public void close() { @Override public void cancel(InterpreterContext context) { if (this.sc != null) { - sc.cancelJobGroup(getJobGroup(context)); + sc.cancelJobGroup(Utils.buildJobGroupId(context)); } } @@ -200,7 +205,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 { @@ -216,11 +221,11 @@ public Scheduler getScheduler() { @Override public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { + InterpreterContext interpreterContext) { return new ArrayList<>(); } - private SparkInterpreter getSparkInterpreter() { + private SparkInterpreter getSparkInterpreter() throws InterpreterException { LazyOpenInterpreter lazy = null; SparkInterpreter spark = null; Interpreter p = getInterpreterInTheSameSessionByClassName(SparkInterpreter.class.getName()); @@ -240,10 +245,10 @@ private SparkInterpreter getSparkInterpreter() { } private boolean useKnitr() { - try { - return Boolean.parseBoolean(getProperty("zeppelin.R.knitr")); - } catch (Exception e) { - return false; - } + return Boolean.parseBoolean(getProperty("zeppelin.R.knitr", "true")); + } + + public AtomicBoolean getRbackendDead() { + return rbackendDead; } } 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 88% 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 index 134a65f39c7..cedb353dc1e 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/SparkSqlInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkSqlInterpreter.java @@ -17,12 +17,13 @@ package org.apache.zeppelin.spark; + import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; import java.util.Properties; import java.util.concurrent.atomic.AtomicInteger; - +import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.spark.SparkContext; import org.apache.spark.sql.SQLContext; import org.apache.zeppelin.interpreter.Interpreter; @@ -59,7 +60,7 @@ public void open() { this.maxResult = Integer.parseInt(getProperty(MAX_RESULTS)); } - private SparkInterpreter getSparkInterpreter() { + private SparkInterpreter getSparkInterpreter() throws InterpreterException { LazyOpenInterpreter lazy = null; SparkInterpreter spark = null; Interpreter p = getInterpreterInTheSameSessionByClassName(SparkInterpreter.class.getName()); @@ -86,7 +87,8 @@ public boolean concurrentSQL() { public void close() {} @Override - public InterpreterResult interpret(String st, InterpreterContext context) { + public InterpreterResult interpret(String st, InterpreterContext context) + throws InterpreterException { SQLContext sqlc = null; SparkInterpreter sparkInterpreter = getSparkInterpreter(); @@ -114,13 +116,16 @@ public InterpreterResult interpret(String st, InterpreterContext context) { // to def sql(sqlText: String): DataFrame (1.3 and later). // Therefore need to use reflection to keep binary compatibility for all spark versions. Method sqlMethod = sqlc.getClass().getMethod("sql", String.class); - rdd = sqlMethod.invoke(sqlc, st); - } catch (InvocationTargetException ite) { + String effectiveString = + Boolean.parseBoolean(getProperty("zeppelin.spark.sql.interpolation")) ? + interpolate(st, context.getResourcePool()) : st; + rdd = sqlMethod.invoke(sqlc, effectiveString); + } catch (InvocationTargetException e) { if (Boolean.parseBoolean(getProperty("zeppelin.spark.sql.stacktrace"))) { - throw new InterpreterException(ite); + return new InterpreterResult(Code.ERROR, ExceptionUtils.getStackTrace(e)); } - logger.error("Invocation target exception", ite); - String msg = ite.getTargetException().getMessage() + logger.error("Invocation target exception", e); + String msg = e.getCause().getMessage() + "\nset zeppelin.spark.sql.stacktrace = true to see full stacktrace"; return new InterpreterResult(Code.ERROR, msg); } catch (NoSuchMethodException | SecurityException | IllegalAccessException @@ -134,7 +139,7 @@ public InterpreterResult interpret(String st, InterpreterContext context) { } @Override - public void cancel(InterpreterContext context) { + public void cancel(InterpreterContext context) throws InterpreterException { SparkInterpreter sparkInterpreter = getSparkInterpreter(); SQLContext sqlc = sparkInterpreter.getSQLContext(); SparkContext sc = sqlc.sparkContext(); @@ -149,7 +154,7 @@ public FormType getFormType() { @Override - public int getProgress(InterpreterContext context) { + public int getProgress(InterpreterContext context) throws InterpreterException { SparkInterpreter sparkInterpreter = getSparkInterpreter(); return sparkInterpreter.getProgress(context); } 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 77% 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 index 4b027989e01..d0f3f795519 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/SparkVersion.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkVersion.java @@ -34,12 +34,18 @@ 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_0 = SparkVersion.fromVersionString("2.3.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 SPARK_3_0_0 = SparkVersion.fromVersionString("3.0.0"); public static final SparkVersion MIN_SUPPORTED_VERSION = SPARK_1_0_0; - public static final SparkVersion UNSUPPORTED_FUTURE_VERSION = SPARK_2_3_0; + public static final SparkVersion UNSUPPORTED_FUTURE_VERSION = SPARK_3_0_0; + private int version; + private int majorVersion; + private int minorVersion; + private int patchVersion; private String versionString; SparkVersion(String versionString) { @@ -54,11 +60,12 @@ public class SparkVersion { } String versions[] = numberPart.split("\\."); - int major = Integer.parseInt(versions[0]); - int minor = Integer.parseInt(versions[1]); - int patch = Integer.parseInt(versions[2]); + this.majorVersion = Integer.parseInt(versions[0]); + this.minorVersion = Integer.parseInt(versions[1]); + this.patchVersion = Integer.parseInt(versions[2]); // version is always 5 digits. (e.g. 2.0.0 -> 20000, 1.6.2 -> 10602) - version = Integer.parseInt(String.format("%d%02d%02d", major, minor, patch)); + version = Integer.parseInt(String.format("%d%02d%02d", majorVersion, minorVersion, + patchVersion)); } catch (Exception e) { logger.error("Can not recognize Spark version " + versionString + ". Assume it's a future release", e); @@ -108,6 +115,12 @@ public boolean oldSqlContextImplicits() { return this.olderThan(SPARK_1_3_0); } + public boolean isSecretSocketSupported() { + return this.newerThanEquals(SparkVersion.SPARK_2_4_0) || + this.newerThanEqualsPatchVersion(SPARK_2_3_1) || + this.newerThanEqualsPatchVersion(SparkVersion.fromVersionString("2.2.2")) || + this.newerThanEqualsPatchVersion(SparkVersion.fromVersionString("2.1.3")); + } public boolean equals(Object versionToCompare) { return version == ((SparkVersion) versionToCompare).version; } @@ -120,6 +133,12 @@ public boolean newerThanEquals(SparkVersion versionToCompare) { return version >= versionToCompare.version; } + public boolean newerThanEqualsPatchVersion(SparkVersion versionToCompare) { + return majorVersion == versionToCompare.majorVersion && + minorVersion == versionToCompare.minorVersion && + patchVersion >= versionToCompare.patchVersion; + } + public boolean olderThan(SparkVersion versionToCompare) { return version < versionToCompare.version; } 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 81% 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 413c690711a..979a1409ecf 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/SparkZeppelinContext.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkZeppelinContext.java @@ -42,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"); @@ -79,7 +76,7 @@ public SparkZeppelinContext( } if (supportedClasses.isEmpty()) { - throw new InterpreterException("Can not load Dataset/DataFrame/SchemaRDD class"); + throw new RuntimeException("Can not load Dataset/DataFrame/SchemaRDD class"); } } @@ -112,7 +109,7 @@ public String showData(Object df) { } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | ClassCastException e) { sc.clearJobGroup(); - throw new InterpreterException(e); + throw new RuntimeException(e); } List columns = null; @@ -129,7 +126,11 @@ public String showData(Object df) { .asJava(); } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - throw new InterpreterException(e); + throw new RuntimeException(e); + } + // DDL will empty DataFrame + if (columns.isEmpty()) { + return ""; } StringBuilder msg = new StringBuilder(); @@ -165,7 +166,7 @@ public String showData(Object df) { } } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - throw new InterpreterException(e); + throw new RuntimeException(e); } if (rows.length > maxResult) { @@ -173,7 +174,8 @@ public String showData(Object df) { msg.append(ResultMessages.getExceedsLimitRowsMessage(maxResult, SparkSqlInterpreter.MAX_RESULTS)); } - + // append %text at the end, otherwise the following output will be put in table as well. + msg.append("\n%text "); sc.clearJobGroup(); return msg.toString(); } @@ -205,9 +207,44 @@ public scala.collection.Seq checkbox( String name, scala.collection.Iterable defaultChecked, scala.collection.Iterable> options) { - return scala.collection.JavaConversions.asScalaBuffer( - gui.checkbox(name, asJavaCollection(defaultChecked), - tuplesToParamOptions(options))).toSeq(); + List defaultCheckedList = Lists.newArrayList(asJavaIterable(defaultChecked).iterator()); + Collection checkbox = checkbox(name, defaultCheckedList, tuplesToParamOptions(options)); + List checkboxList = Arrays.asList(checkbox.toArray()); + return scala.collection.JavaConversions.asScalaBuffer(checkboxList).toSeq(); + } + + @ZeppelinApi + public Object noteSelect(String name, scala.collection.Iterable> options) { + return noteSelect(name, "", options); + } + + @ZeppelinApi + public Object noteSelect(String name, Object defaultValue, + scala.collection.Iterable> options) { + return noteSelect(name, defaultValue, tuplesToParamOptions(options)); + } + + @ZeppelinApi + public scala.collection.Seq noteCheckbox( + String name, + scala.collection.Iterable> options) { + List allChecked = new LinkedList<>(); + for (Tuple2 option : asJavaIterable(options)) { + allChecked.add(option._1()); + } + return noteCheckbox(name, collectionAsScalaIterable(allChecked), options); + } + + @ZeppelinApi + public scala.collection.Seq noteCheckbox( + String name, + scala.collection.Iterable defaultChecked, + scala.collection.Iterable> options) { + List defaultCheckedList = Lists.newArrayList(asJavaIterable(defaultChecked).iterator()); + Collection checkbox = noteCheckbox(name, defaultCheckedList, + tuplesToParamOptions(options)); + List checkboxList = Arrays.asList(checkbox.toArray()); + return scala.collection.JavaConversions.asScalaBuffer(checkboxList).toSeq(); } private OptionInput.ParamOption[] tuplesToParamOptions( 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 59% 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 index 6448c97c78d..b1dd02dd0c1 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/Utils.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/Utils.java @@ -24,18 +24,22 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Utility and helper functions for the Spark Interpreter */ class Utils { public static Logger logger = LoggerFactory.getLogger(Utils.class); + private static final String SCALA_COMPILER_VERSION = evaluateScalaCompilerVersion(); static Object invokeMethod(Object o, String name) { return invokeMethod(o, name, new Class[]{}, new Object[]{}); } - static Object invokeMethod(Object o, String name, Class[] argTypes, Object[] params) { + static Object invokeMethod(Object o, String name, Class[] argTypes, Object[] params) { try { return o.getClass().getMethod(name, argTypes).invoke(o, params); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { @@ -44,7 +48,7 @@ static Object invokeMethod(Object o, String name, Class[] argTypes, Object[] par return null; } - static Object invokeStaticMethod(Class c, String name, Class[] argTypes, Object[] params) { + static Object invokeStaticMethod(Class c, String name, Class[] argTypes, Object[] params) { try { return c.getMethod(name, argTypes).invoke(null, params); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { @@ -53,17 +57,17 @@ static Object invokeStaticMethod(Class c, String name, Class[] argTypes, Object[ return null; } - static Object invokeStaticMethod(Class c, String name) { + static Object invokeStaticMethod(Class c, String name) { return invokeStaticMethod(c, name, new Class[]{}, new Object[]{}); } - static Class findClass(String name) { + static Class findClass(String name) { return findClass(name, false); } - static Class findClass(String name, boolean silence) { + static Class findClass(String name, boolean silence) { try { - return Utils.class.forName(name); + return Class.forName(name); } catch (ClassNotFoundException e) { if (!silence) { logger.error(e.getMessage(), e); @@ -72,7 +76,7 @@ static Class findClass(String name, boolean silence) { } } - static Object instantiateClass(String name, Class[] argTypes, Object[] params) { + static Object instantiateClass(String name, Class[] argTypes, Object[] params) { try { Constructor constructor = Utils.class.getClassLoader() .loadClass(name).getConstructor(argTypes); @@ -87,7 +91,7 @@ static Object instantiateClass(String name, Class[] argTypes, Object[] params) { // function works after intp is initialized static boolean isScala2_10() { try { - Utils.class.forName("org.apache.spark.repl.SparkIMain"); + Class.forName("org.apache.spark.repl.SparkIMain"); return true; } catch (ClassNotFoundException e) { return false; @@ -99,10 +103,45 @@ static boolean isScala2_10() { static boolean isScala2_11() { return !isScala2_10(); } + + static boolean isCompilerAboveScala2_11_7() { + if (isScala2_10() || SCALA_COMPILER_VERSION == null) { + return false; + } + Pattern p = Pattern.compile("([0-9]+)[.]([0-9]+)[.]([0-9]+)"); + Matcher m = p.matcher(SCALA_COMPILER_VERSION); + if (m.matches()) { + int major = Integer.parseInt(m.group(1)); + int minor = Integer.parseInt(m.group(2)); + int bugfix = Integer.parseInt(m.group(3)); + return (major > 2 || (major == 2 && minor > 11) || (major == 2 && minor == 11 && bugfix > 7)); + } + return false; + } + + private static String evaluateScalaCompilerVersion() { + String version = null; + try { + Properties p = new Properties(); + Class completionClass = findClass("scala.tools.nsc.interpreter.JLineCompletion"); + if (completionClass != null) { + try (java.io.InputStream in = completionClass.getClass() + .getResourceAsStream("/compiler.properties")) { + p.load(in); + version = p.getProperty("version.number"); + } catch (java.io.IOException e) { + logger.error("Failed to evaluate Scala compiler version", e); + } + } + } catch (RuntimeException e) { + logger.error("Failed to evaluate Scala compiler version", e); + } + return version; + } static boolean isSpark2() { try { - Utils.class.forName("org.apache.spark.sql.SparkSession"); + Class.forName("org.apache.spark.sql.SparkSession"); return true; } catch (ClassNotFoundException e) { return false; @@ -110,19 +149,11 @@ static boolean isSpark2() { } public static String buildJobGroupId(InterpreterContext context) { - return "zeppelin-" + 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()); + String uName = "anonymous"; + if (context.getAuthenticationInfo() != null) { + uName = getUserName(context.getAuthenticationInfo()); + } + return "zeppelin|" + uName + "|" + context.getNoteId() + "|" + context.getParagraphId(); } public static String getUserName(AuthenticationInfo info) { 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 84% 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 index b46001aa0e0..addddc8a0eb 100644 --- a/spark/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; @@ -36,9 +37,12 @@ * R repl interaction */ public class ZeppelinR implements ExecuteResultHandler { - Logger logger = LoggerFactory.getLogger(ZeppelinR.class); + private static Logger logger = LoggerFactory.getLogger(ZeppelinR.class); + + private final SparkRInterpreter sparkRInterpreter; private final String rCmdPath; private final SparkVersion sparkVersion; + private final int timeout; private DefaultExecutor executor; private InterpreterOutputStream outputStream; private PipedOutputStream input; @@ -108,16 +112,18 @@ public Object getValue() { * @param libPath sparkr library path */ public ZeppelinR(String rCmdPath, String libPath, int sparkRBackendPort, - SparkVersion sparkVersion) { + SparkVersion sparkVersion, int timeout, SparkRInterpreter sparkRInterpreter) { this.rCmdPath = rCmdPath; this.libPath = libPath; this.sparkVersion = sparkVersion; this.port = sparkRBackendPort; + this.timeout = timeout; + this.sparkRInterpreter = sparkRInterpreter; try { File scriptFile = File.createTempFile("zeppelin_sparkr-", ".R"); scriptPath = scriptFile.getAbsolutePath(); } catch (IOException e) { - throw new InterpreterException(e); + throw new RuntimeException(e); } } @@ -125,7 +131,7 @@ public ZeppelinR(String rCmdPath, String libPath, int sparkRBackendPort, * Start R repl * @throws IOException */ - public void open() throws IOException { + public void open() throws IOException, InterpreterException { createRScript(); zeppelinR.put(hashCode(), this); @@ -140,12 +146,15 @@ public void open() throws IOException { cmd.addArgument(Integer.toString(port)); 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()); executor = new DefaultExecutor(); - outputStream = new InterpreterOutputStream(logger); + outputStream = new SparkRInterpreterOutputStream(logger, sparkRInterpreter); input = new PipedOutputStream(); PipedInputStream in = new PipedInputStream(input); @@ -170,7 +179,7 @@ public void open() throws IOException { * @param expr * @return */ - public Object eval(String expr) { + public Object eval(String expr) throws InterpreterException { synchronized (this) { rRequestObject = new Request("eval", expr, null); return request(); @@ -182,7 +191,7 @@ public Object eval(String expr) { * @param key * @param value */ - public void set(String key, Object value) { + public void set(String key, Object value) throws InterpreterException { synchronized (this) { rRequestObject = new Request("set", key, value); request(); @@ -194,7 +203,7 @@ public void set(String key, Object value) { * @param key * @return */ - public Object get(String key) { + public Object get(String key) throws InterpreterException { synchronized (this) { rRequestObject = new Request("get", key, null); return request(); @@ -206,7 +215,7 @@ public Object get(String key) { * @param key * @return */ - public String getS0(String key) { + public String getS0(String key) throws InterpreterException { synchronized (this) { rRequestObject = new Request("getS", key, null); return (String) request(); @@ -217,7 +226,7 @@ public String getS0(String key) { * Send request to r repl and return response * @return responseValue */ - private Object request() throws RuntimeException { + private Object request() throws RuntimeException, InterpreterException { if (!rScriptRunning) { throw new RuntimeException("r repl is not running"); } @@ -332,7 +341,7 @@ public void onScriptInitialized() { /** * Create R script in tmp dir */ - private void createRScript() { + private void createRScript() throws InterpreterException { ClassLoader classLoader = getClass().getClassLoader(); File out = new File(scriptPath); @@ -391,4 +400,27 @@ public void onProcessFailed(ExecuteException e) { logger.error(e.getMessage(), e); rScriptRunning = false; } + + + /** + * InterpreterOutptStream for SparkInterpreter, used for checking R process status + */ + public static class SparkRInterpreterOutputStream extends InterpreterOutputStream { + + private SparkRInterpreter sparkRInterpreter; + + public SparkRInterpreterOutputStream(Logger logger, SparkRInterpreter sparkRInterpreter) { + super(logger); + this.sparkRInterpreter = sparkRInterpreter; + } + + @Override + protected void processLine(String s, int i) { + super.processLine(s, i); + if (s.contains("Java SparkR backend might have failed") // spark 2.x + || s.contains("Execution halted")) { // spark 1.x + sparkRInterpreter.getRbackendDead().set(true); + } + } + } } 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 93% rename from spark/src/main/resources/R/zeppelin_sparkr.R rename to spark/interpreter/src/main/resources/R/zeppelin_sparkr.R index 525c6c5c40b..5f64dfe148f 100644 --- a/spark/src/main/resources/R/zeppelin_sparkr.R +++ b/spark/interpreter/src/main/resources/R/zeppelin_sparkr.R @@ -22,6 +22,12 @@ hashCode <- as.integer(args[1]) 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))) @@ -30,8 +36,11 @@ print(paste("LibPath ", libPath)) .libPaths(c(file.path(libPath), .libPaths())) library(SparkR) - -SparkR:::connectBackend("localhost", port, 6000) +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/src/main/sparkr-resources/interpreter-setting.json b/spark/interpreter/src/main/resources/interpreter-setting.json similarity index 69% rename from spark/src/main/sparkr-resources/interpreter-setting.json rename to spark/interpreter/src/main/resources/interpreter-setting.json index d0fbd3ec2e2..2d9a605a6f0 100644 --- a/spark/src/main/sparkr-resources/interpreter-setting.json +++ b/spark/interpreter/src/main/resources/interpreter-setting.json @@ -17,7 +17,7 @@ "propertyName": null, "defaultValue": "", "description": "spark commandline args", - "type": "string" + "type": "textarea" }, "zeppelin.spark.useHiveContext": { "envName": "ZEPPELIN_SPARK_USEHIVECONTEXT", @@ -29,7 +29,7 @@ "spark.app.name": { "envName": "SPARK_APP_NAME", "propertyName": "spark.app.name", - "defaultValue": "Zeppelin", + "defaultValue": "Zeppelin Spark Session", "description": "The name of spark application.", "type": "string" }, @@ -47,6 +47,13 @@ "description": "Total number of cores to use. Empty value uses all available core.", "type": "number" }, + "spark.yarn.dist.archives": { + "envName": "ZEPPELIN_SPARK_YARN_DIST_ARCHIVES", + "propertyName": "spark.yarn.dist.archives", + "defaultValue": "", + "description": "Sets additional archives for spark", + "type": "string" + }, "zeppelin.spark.maxResult": { "envName": "ZEPPELIN_SPARK_MAXRESULT", "propertyName": "zeppelin.spark.maxResult", @@ -57,20 +64,44 @@ "master": { "envName": "MASTER", "propertyName": "spark.master", - "defaultValue": "local[*]", + "defaultValue": "yarn-cluster", "description": "Spark master uri. ex) spark://masterhost:7077", "type": "string" }, - "zeppelin.spark.unSupportedVersionCheck": { + "zeppelin.spark.enableSupportedVersionCheck": { "envName": null, "propertyName": "zeppelin.spark.enableSupportedVersionCheck", "defaultValue": true, "description": "Do not change - developer only setting, not for production use", "type": "checkbox" + }, + "zeppelin.spark.uiWebUrl": { + "envName": null, + "propertyName": "zeppelin.spark.uiWebUrl", + "defaultValue": "", + "description": "Override Spark UI default URL", + "type": "string" + }, + "zeppelin.spark.useNew": { + "envName": null, + "propertyName": "zeppelin.spark.useNew", + "defaultValue": true, + "description": "Whether use new spark interpreter implementation", + "type": "checkbox" + }, + "zeppelin.spark.ui.hidden": { + "envName": null, + "propertyName": "zeppelin.spark.ui.hidden", + "defaultValue": false, + "description": "Whether to hide spark ui in zeppelin ui", + "type": "checkbox" } }, "editor": { - "language": "scala" + "language": "scala", + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": true } }, { @@ -92,6 +123,13 @@ "description": "Show full exception stacktrace for SQL queries if set to true.", "type": "checkbox" }, + "zeppelin.spark.sql.interpolation": { + "envName": null, + "propertyName": "zeppelin.spark.sql.interpolation", + "defaultValue": false, + "description": "Enable ZeppelinContext variable interpolation into paragraph text", + "type": "checkbox" + }, "zeppelin.spark.maxResult": { "envName": "ZEPPELIN_SPARK_MAXRESULT", "propertyName": "zeppelin.spark.maxResult", @@ -108,7 +146,10 @@ } }, "editor": { - "language": "sql" + "language": "sql", + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": true } }, { @@ -132,7 +173,10 @@ } }, "editor": { - "language": "scala" + "language": "scala", + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": true } }, { @@ -146,10 +190,31 @@ "defaultValue": "python", "description": "Python command to run pyspark with", "type": "string" + }, + "zeppelin.pyspark.useIPython": { + "envName": null, + "propertyName": "zeppelin.pyspark.useIPython", + "defaultValue": false, + "description": "whether use IPython when it is available", + "type": "checkbox" } }, "editor": { - "language": "python" + "language": "python", + "editOnDblClick": false, + "completionKey": "TAB", + "completionSupport": true + } + }, + { + "group": "spark", + "name": "ipyspark", + "className": "org.apache.zeppelin.spark.IPySparkInterpreter", + "properties": {}, + "editor": { + "language": "python", + "editOnDblClick": false, + "completionSupport": true } }, { @@ -181,13 +246,15 @@ "zeppelin.R.render.options": { "envName": "ZEPPELIN_R_RENDER_OPTIONS", "propertyName": "zeppelin.R.render.options", - "defaultValue": "out.format = 'html', comment = NA, echo = FALSE, results = 'asis', message = F, warning = F", + "defaultValue": "out.format = 'html', comment = NA, echo = FALSE, results = 'asis', message = F, warning = F, fig.retina = 2", "description": "", "type": "textarea" } }, "editor": { - "language": "r" + "language": "r", + "editOnDblClick": false, + "completionSupport": false } } ] diff --git a/spark/interpreter/src/main/resources/python/zeppelin_ipyspark.py b/spark/interpreter/src/main/resources/python/zeppelin_ipyspark.py new file mode 100644 index 00000000000..b15ac7b5475 --- /dev/null +++ b/spark/interpreter/src/main/resources/python/zeppelin_ipyspark.py @@ -0,0 +1,73 @@ +# +# 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 +import os + +# start JVM gateway +if "PY4J_GATEWAY_SECRET" in os.environ: + from py4j.java_gateway import GatewayParameters + gateway_secret = os.environ["PY4J_GATEWAY_SECRET"] + gateway = JavaGateway(gateway_parameters=GatewayParameters( + port=${JVM_GATEWAY_PORT}, auth_token=gateway_secret, auto_convert=True)) +else: + gateway = JavaGateway(GatewayClient(port=${JVM_GATEWAY_PORT}), auto_convert=True) + +# for back compatibility +from pyspark.sql import SQLContext + +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()) + +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/src/main/resources/python/zeppelin_pyspark.py b/spark/interpreter/src/main/resources/python/zeppelin_pyspark.py similarity index 84% rename from spark/src/main/resources/python/zeppelin_pyspark.py rename to spark/interpreter/src/main/resources/python/zeppelin_pyspark.py index 347b543dcab..20cb539f5df 100644 --- a/spark/src/main/resources/python/zeppelin_pyspark.py +++ b/spark/interpreter/src/main/resources/python/zeppelin_pyspark.py @@ -81,24 +81,46 @@ def getInterpreterContext(self): def input(self, name, defaultValue=""): return self.z.input(name, defaultValue) + def textbox(self, name, defaultValue=""): + return self.z.textbox(name, defaultValue) + + def noteTextbox(self, name, defaultValue=""): + return self.z.noteTextbox(name, defaultValue) + def select(self, name, options, defaultValue=""): # auto_convert to ArrayList doesn't match the method signature on JVM side - tuples = list(map(lambda items: self.__tupleToScalaTuple2(items), options)) - iterables = gateway.jvm.scala.collection.JavaConversions.collectionAsScalaIterable(tuples) - return self.z.select(name, defaultValue, iterables) + return self.z.select(name, defaultValue, self.getParamOptions(options)) + + def noteSelect(self, name, options, defaultValue=""): + return self.z.noteSelect(name, defaultValue, self.getParamOptions(options)) def checkbox(self, name, options, defaultChecked=None): - if defaultChecked is None: - defaultChecked = [] - optionTuples = list(map(lambda items: self.__tupleToScalaTuple2(items), options)) - optionIterables = gateway.jvm.scala.collection.JavaConversions.collectionAsScalaIterable(optionTuples) - defaultCheckedIterables = gateway.jvm.scala.collection.JavaConversions.collectionAsScalaIterable(defaultChecked) - checkedItems = gateway.jvm.scala.collection.JavaConversions.seqAsJavaList(self.z.checkbox(name, defaultCheckedIterables, optionIterables)) + optionsIterable = self.getParamOptions(options) + defaultCheckedIterables = self.getDefaultChecked(defaultChecked) + checkedItems = gateway.jvm.scala.collection.JavaConversions.seqAsJavaList(self.z.checkbox(name, defaultCheckedIterables, optionsIterable)) result = [] for checkedItem in checkedItems: result.append(checkedItem) return result; + def noteCheckbox(self, name, options, defaultChecked=None): + optionsIterable = self.getParamOptions(options) + defaultCheckedIterables = self.getDefaultChecked(defaultChecked) + checkedItems = gateway.jvm.scala.collection.JavaConversions.seqAsJavaList(self.z.noteCheckbox(name, defaultCheckedIterables, optionsIterable)) + result = [] + for checkedItem in checkedItems: + result.append(checkedItem) + return result; + + def getParamOptions(self, options): + tuples = list(map(lambda items: self.__tupleToScalaTuple2(items), options)) + return gateway.jvm.scala.collection.JavaConversions.collectionAsScalaIterable(tuples) + + def getDefaultChecked(self, defaultChecked): + if defaultChecked is None: + defaultChecked = [] + return gateway.jvm.scala.collection.JavaConversions.collectionAsScalaIterable(defaultChecked) + def registerHook(self, event, cmd, replName=None): if replName is None: self.z.registerHook(event, cmd) @@ -111,6 +133,18 @@ def unregisterHook(self, event, replName=None): else: self.z.unregisterHook(event, replName) + def registerNoteHook(self, event, cmd, noteId, replName=None): + if replName is None: + self.z.registerNoteHook(event, cmd, noteId) + else: + self.z.registerNoteHook(event, cmd, noteId, replName) + + def unregisterNoteHook(self, event, noteId, replName=None): + if replName is None: + self.z.unregisterNoteHook(event, noteId) + else: + self.z.unregisterNoteHook(event, noteId, replName) + def getHook(self, event, replName=None): if replName is None: return self.z.getHook(event) @@ -223,17 +257,24 @@ def getCompletion(self, text_value): result = json.dumps(list(filter(lambda x : not re.match("^__.*", x), list(completionList)))) self.interpreterObject.setStatementsFinished(result, False) -client = GatewayClient(port=int(sys.argv[1])) +port=int(sys.argv[1]) sparkVersion = SparkVersion(int(sys.argv[2])) +secret = None +if len(sys.argv) == 4: + secret = sys.argv[3] + if sparkVersion.isSpark2(): from pyspark.sql import SparkSession else: from pyspark.sql import SchemaRDD -if sparkVersion.isAutoConvertEnabled(): - gateway = JavaGateway(client, auto_convert = True) +auto_convert = sparkVersion.isAutoConvertEnabled() +if secret: + from py4j.java_gateway import GatewayParameters + gateway = JavaGateway(gateway_parameters=GatewayParameters( + port=port, auth_token=secret, auto_convert=auto_convert)) else: - gateway = JavaGateway(client) + gateway = JavaGateway(GatewayClient(port=port), auto_convert=auto_convert) java_import(gateway.jvm, "org.apache.spark.SparkEnv") java_import(gateway.jvm, "org.apache.spark.SparkConf") @@ -325,7 +366,7 @@ def getCompletion(self, text_value): if (nhooks > 0): to_run_hooks = code.body[-nhooks:] to_run_exec, to_run_single = (code.body[:-(nhooks + 1)], - [code.body[-(nhooks + 1)]]) + [code.body[-(nhooks + 1)]] if len(code.body) > nhooks else []) try: for node in to_run_exec: 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 64% rename from spark/src/main/scala/org/apache/spark/SparkRBackend.scala rename to spark/interpreter/src/main/scala/org/apache/spark/SparkRBackend.scala index 05f1ac0e3e2..2dc3371eb57 100644 --- a/spark/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; } 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 98% 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 index 608807cd032..e177d496565 100644 --- a/spark/src/test/java/org/apache/zeppelin/spark/DepInterpreterTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/DepInterpreterTest.java @@ -64,7 +64,7 @@ public void setUp() throws Exception { dep.setInterpreterGroup(intpGroup); context = new InterpreterContext("note", "id", null, "title", "text", new AuthenticationInfo(), - new HashMap(), new GUI(), + new HashMap(), new GUI(), new GUI(), new AngularObjectRegistry(intpGroup.getId(), null), null, new LinkedList(), 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 new file mode 100644 index 00000000000..d66f89f966b --- /dev/null +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/IPySparkInterpreterTest.java @@ -0,0 +1,237 @@ +/* + * 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.GUI; +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.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; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.net.URL; +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 { + Properties p = new Properties(); + p.setProperty("spark.master", "local[4]"); + p.setProperty("master", "local[4]"); + p.setProperty("spark.submit.deployMode", "client"); + 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(); + sparkInterpreter.getZeppelinContext().setEventClient(mockRemoteEventClient); + + iPySparkInterpreter = new IPySparkInterpreter(p); + intpGroup.get("session_1").add(iPySparkInterpreter); + iPySparkInterpreter.setInterpreterGroup(intpGroup); + iPySparkInterpreter.open(); + sparkInterpreter.getZeppelinContext().setEventClient(mockRemoteEventClient); + } + + + @After + public void tearDown() throws InterpreterException { + if (iPySparkInterpreter != null) { + iPySparkInterpreter.close(); + } + } + + @Test + public void testBasics() throws InterruptedException, IOException, InterpreterException { + // all the ipython test should pass too. + IPythonInterpreterTest.testInterpreter(iPySparkInterpreter); + + // rdd + InterpreterContext context = getInterpreterContext(); + InterpreterResult result = iPySparkInterpreter.interpret("sc.version", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + String sparkVersion = context.out.toInterpreterResultMessage().get(0).getData(); + // spark url is sent + verify(mockRemoteEventClient).onMetaInfosReceived(any(Map.class)); + + context = getInterpreterContext(); + result = iPySparkInterpreter.interpret("sc.range(1,10).sum()", context); + Thread.sleep(100); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + List interpreterResultMessages = context.out.toInterpreterResultMessage(); + 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 (!isSpark2(sparkVersion)) { + result = iPySparkInterpreter.interpret("df = sqlContext.createDataFrame([(1,'a'),(2,'b')])\ndf.show()", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.toInterpreterResultMessage(); + assertEquals( + "+---+---+\n" + + "| _1| _2|\n" + + "+---+---+\n" + + "| 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.toInterpreterResultMessage(); + 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()); + interpreterResultMessages = context.out.toInterpreterResultMessage(); + assertEquals( + "+---+---+\n" + + "| _1| _2|\n" + + "+---+---+\n" + + "| 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.toInterpreterResultMessage(); + assertEquals( + "_1 _2\n" + + "1 a\n" + + "2 b\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.toInterpreterResultMessage(); + assertTrue(interpreterResultMessages.get(0).getData().contains("KeyboardInterrupt")); + } 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("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(1000); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + interpreterResultMessages = context.out.toInterpreterResultMessage(); + assertEquals(1, interpreterResultMessages.size()); + assertTrue(interpreterResultMessages.get(0).getData().contains("(0, 100)")); + } + + private boolean isSpark2(String sparkVersion) { + return sparkVersion.startsWith("'2.") || sparkVersion.startsWith("u'2."); + } + + private InterpreterContext getInterpreterContext() { + InterpreterContext context = new InterpreterContext( + "noteId", + "paragraphId", + "replName", + "paragraphTitle", + "paragraphText", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new GUI(), + null, + 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 new file mode 100644 index 00000000000..38c8e707b91 --- /dev/null +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkInterpreterTest.java @@ -0,0 +1,591 @@ +/* + * 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.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; +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.remote.RemoteEventClient; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +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.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.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + + +public class NewSparkInterpreterTest { + + private SparkInterpreter interpreter; + private DepInterpreter depInterpreter; + + // catch the streaming output in onAppend + private volatile String output = ""; + // catch the interpreter output in onUpdate + private InterpreterResultMessageOutput messageOutput; + + private RemoteEventClient mockRemoteEventClient; + + @Before + public void setUp() { + mockRemoteEventClient = mock(RemoteEventClient.class); + } + + @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"); + properties.setProperty("zeppelin.spark.uiWebUrl", "fake_spark_weburl"); + + mockRemoteEventClient = mock(RemoteEventClient.class); + InterpreterContext context = InterpreterContext.builder() + .setInterpreterOut(new InterpreterOutput(null)) + .setEventClient(mockRemoteEventClient) + .build(); + InterpreterContext.set(context); + + interpreter = new SparkInterpreter(properties); + assertTrue(interpreter.getDelegation() instanceof NewSparkInterpreter); + interpreter.setInterpreterGroup(mock(InterpreterGroup.class)); + interpreter.open(); + + interpreter.getZeppelinContext().setEventClient(mockRemoteEventClient); + + + assertEquals("fake_spark_weburl", interpreter.getSparkUIUrl()); + + 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()); + assertEquals("hello world", output); + + // java stdout + result = interpreter.interpret("System.out.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()); + + // class extend + result = interpreter.interpret("import java.util.ArrayList", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + result = interpreter.interpret("class MyArrayList extends ArrayList{}", 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")); + /*/ + 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)); + + // 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" + + "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" + + ").toDF()", 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()); + + 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); + + // cursor in middle of code + completions = interpreter.completion("sc.ra\n1+1", 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, 5).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(); + } + + //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()); + } + + @Test + public void testDisableReplOutput() 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.spark.printREPLOutput", "false"); + + InterpreterContext.set(getInterpreterContext()); + 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()); + // no output for define new variable + assertEquals("", output); + + result = interpreter.interpret("print(a)", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + // output from print statement will still be displayed + assertEquals("hello world", output); + } + + + // spark.ui.enabled: false + @Test + public void testDisableSparkUI_1() 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("spark.ui.enabled", "false"); + + interpreter = new SparkInterpreter(properties); + assertTrue(interpreter.getDelegation() instanceof NewSparkInterpreter); + interpreter.setInterpreterGroup(mock(InterpreterGroup.class)); + InterpreterContext.set(getInterpreterContext()); + interpreter.open(); + + InterpreterContext context = getInterpreterContext(); + InterpreterResult result = interpreter.interpret("sc.range(1, 10).sum", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + // spark job url is not sent + verify(mockRemoteEventClient, never()).onParaInfosReceived(any(String.class), + any(String.class), any(Map.class)); + } + + // zeppelin.spark.ui.hidden: true + @Test + public void testDisableSparkUI_2() 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.spark.ui.hidden", "true"); + + interpreter = new SparkInterpreter(properties); + assertTrue(interpreter.getDelegation() instanceof NewSparkInterpreter); + interpreter.setInterpreterGroup(mock(InterpreterGroup.class)); + InterpreterContext.set(getInterpreterContext()); + interpreter.open(); + + InterpreterContext context = getInterpreterContext(); + InterpreterResult result = interpreter.interpret("sc.range(1, 10).sum", context); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + + // spark job url is not sent + verify(mockRemoteEventClient, never()).onParaInfosReceived(any(String.class), + any(String.class), any(Map.class)); + } + + @Test + public void testScopedMode() 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"); + + SparkInterpreter interpreter1 = new SparkInterpreter(properties); + SparkInterpreter interpreter2 = new SparkInterpreter(properties); + + InterpreterGroup interpreterGroup = new InterpreterGroup(); + interpreter1.setInterpreterGroup(interpreterGroup); + interpreter2.setInterpreterGroup(interpreterGroup); + + interpreterGroup.addInterpreterToSession(interpreter1, "session_1"); + interpreterGroup.addInterpreterToSession(interpreter2, "session_2"); + + InterpreterContext.set(getInterpreterContext()); + interpreter1.open(); + interpreter2.open(); + + InterpreterContext context = getInterpreterContext(); + + InterpreterResult result1 = interpreter1.interpret("sc.range(1, 10).sum", context); + assertEquals(InterpreterResult.Code.SUCCESS, result1.code()); + + InterpreterResult result2 = interpreter2.interpret("sc.range(1, 10).sum", context); + assertEquals(InterpreterResult.Code.SUCCESS, result2.code()); + + // interpreter2 continue to work after interpreter1 is closed + interpreter1.close(); + + result2 = interpreter2.interpret("sc.range(1, 10).sum", context); + assertEquals(InterpreterResult.Code.SUCCESS, result2.code()); + interpreter2.close(); + } + + @After + public void tearDown() throws InterpreterException { + if (this.interpreter != null) { + this.interpreter.close(); + } + if (this.depInterpreter != null) { + this.depInterpreter.close(); + } + SparkShims.reset(); + } + + private InterpreterContext getInterpreterContext() { + output = ""; + InterpreterContext context = 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; + } + }) + ); + context.setClient(mockRemoteEventClient); + return context; + } +} 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..04813fce36f --- /dev/null +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/NewSparkSqlInterpreterTest.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.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.sql.stacktrace", "true"); + p.setProperty("zeppelin.spark.useNew", "true"); + p.setProperty("zeppelin.spark.useHiveContext", "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); + + 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)); + + InterpreterContext.set(context); + + sparkInterpreter.open(); + sqlInterpreter.open(); + + } + + @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")); + } + + @Test + public void testDDL() throws InterpreterException { + InterpreterResult ret = sqlInterpreter.interpret("create table t1(id int, name string)", context); + assertEquals(InterpreterResult.Code.SUCCESS, ret.code()); + // spark 1.x will still return DataFrame with non-empty columns. + // org.apache.spark.sql.DataFrame = [result: string] + if (!sparkInterpreter.getSparkContext().version().startsWith("1.")) { + assertTrue(ret.message().isEmpty()); + } else { + assertEquals(Type.TABLE, ret.message().get(0).getType()); + assertEquals("result\n", ret.message().get(0).getData()); + } + + // create the same table again + ret = sqlInterpreter.interpret("create table t1(id int, name string)", context); + assertEquals(InterpreterResult.Code.ERROR, ret.code()); + assertEquals(1, ret.message().size()); + assertEquals(Type.TEXT, ret.message().get(0).getType()); + assertTrue(ret.message().get(0).getData().contains("already exists")); + + // invalid DDL + ret = sqlInterpreter.interpret("create temporary function udf1 as 'org.apache.zeppelin.UDF'", context); + assertEquals(InterpreterResult.Code.ERROR, ret.code()); + assertEquals(1, ret.message().size()); + assertEquals(Type.TEXT, ret.message().get(0).getType()); + + // spark 1.x could not detect the root cause correctly + if (!sparkInterpreter.getSparkContext().version().startsWith("1.")) { + assertTrue(ret.message().get(0).getData().contains("ClassNotFoundException") || + ret.message().get(0).getData().contains("Can not load class")); + } + } +} \ No newline at end of file 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 79% 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 3a31e5dd845..068ff50c3d8 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<>(); @@ -78,7 +91,7 @@ public static Properties getSparkTestProperties(TemporaryFolder tmpDir) throws I p.setProperty("zeppelin.spark.maxResult", "1000"); p.setProperty("zeppelin.spark.importImplicit", "true"); p.setProperty("zeppelin.dep.localrepo", tmpDir.newFolder().getAbsolutePath()); - + p.setProperty("zeppelin.spark.property_1", "value_1"); return p; } @@ -109,6 +122,7 @@ public void onMetaInfosReceived(Map infos) { new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), new AngularObjectRegistry(intpGroup.getId(), null), new LocalResourcePool("id"), new LinkedList(), @@ -128,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()); @@ -152,34 +166,35 @@ public void testBasicIntp() { } @Test - public void testNextLineInvocation() { + 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() 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)); - } - + @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); @@ -192,7 +207,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); @@ -206,7 +221,7 @@ public void testZShow() { } @Test - public void testSparkSql() throws IOException { + public void testSparkSql() throws IOException, InterpreterException { 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); assertEquals(Code.SUCCESS, repl.interpret("people.take(3)", context).code()); @@ -228,7 +243,7 @@ public void testSparkSql() throws IOException { } @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()); @@ -236,7 +251,7 @@ public void testReferencingUndefinedVal() { @Test public void emptyConfigurationVariablesOnlyForNonSparkProperties() { - Properties intpProperty = repl.getProperty(); + Properties intpProperty = repl.getProperties(); SparkConf sparkConf = repl.getSparkContext().getConf(); for (Object oKey : intpProperty.keySet()) { String key = (String) oKey; @@ -249,7 +264,7 @@ public void emptyConfigurationVariablesOnlyForNonSparkProperties() { } @Test - public void shareSingleSparkContext() throws InterruptedException, IOException { + public void shareSingleSparkContext() throws InterruptedException, IOException, InterpreterException { // create another SparkInterpreter SparkInterpreter repl2 = new SparkInterpreter(getSparkTestProperties(tmpDir)); repl2.setInterpreterGroup(intpGroup); @@ -265,7 +280,7 @@ public void shareSingleSparkContext() throws InterruptedException, IOException { } @Test - public void testEnableImplicitImport() throws IOException { + public void testEnableImplicitImport() throws IOException, InterpreterException { if (getSparkVersionNumber(repl) >= 13) { // Set option of importing implicits to "true", and initialize new Spark repl Properties p = getSparkTestProperties(tmpDir); @@ -282,7 +297,7 @@ public void testEnableImplicitImport() throws IOException { } @Test - public void testDisableImplicitImport() throws IOException { + public void testDisableImplicitImport() throws IOException, InterpreterException { if (getSparkVersionNumber(repl) >= 13) { // Set option of importing implicits to "false", and initialize new Spark repl // this test should return error status when creating DataFrame from sequence @@ -300,18 +315,35 @@ public void testDisableImplicitImport() throws IOException { } @Test - public void testCompletion() { + public void testCompletion() throws InterpreterException { List completions = repl.completion("sc.", "sc.".length(), null); assertTrue(completions.size() > 0); } @Test - public void testParagraphUrls() { + 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() 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."; + List completions = repl.completion(buf, buf.length(), null); + assertTrue(completions.size() > 0); + } + + @Test + public void testParagraphUrls() throws InterpreterException { String paraId = "test_para_job_url"; InterpreterContext intpCtx = new InterpreterContext("note", paraId, null, "title", "text", new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), new AngularObjectRegistry(intpGroup.getId(), null), new LocalResourcePool("id"), new LinkedList(), 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 82% 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 ebb5e9a9117..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); @@ -68,24 +77,24 @@ public static void setUp() throws Exception { sql.open(); context = new InterpreterContext("note", "id", null, "title", "text", new AuthenticationInfo(), - new HashMap(), new GUI(), + 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 { sql.close(); repl.close(); } boolean isDataFrameSupported() { - return SparkInterpreterTest.getSparkVersionNumber(repl) >= 13; + return OldSparkInterpreterTest.getSparkVersionNumber(repl) >= 13; } @Test - public void test() { + public void test() throws InterpreterException { repl.interpret("case class Test(name:String, age:Int)", context); repl.interpret("val test = sc.parallelize(Seq(Test(\"moon\", 33), Test(\"jobs\", 51), Test(\"gates\", 51), Test(\"park\", 34)))", context); if (isDataFrameSupported()) { @@ -107,7 +116,7 @@ public void test() { } @Test - public void testStruct() { + public void testStruct() throws InterpreterException { repl.interpret("case class Person(name:String, age:Int)", context); repl.interpret("case class People(group:String, person:Person)", context); repl.interpret( @@ -124,7 +133,7 @@ public void testStruct() { } @Test - public void test_null_value_in_row() { + public void test_null_value_in_row() throws InterpreterException { repl.interpret("import org.apache.spark.sql._", context); if (isDataFrameSupported()) { repl.interpret( @@ -144,11 +153,11 @@ public void test_null_value_in_row() { "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); } @@ -162,7 +171,7 @@ public void test_null_value_in_row() { } @Test - public void testMaxResults() { + public void testMaxResults() throws InterpreterException { repl.interpret("case class P(age:Int)", context); repl.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)))", 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 86% 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 7fe8b5e3a8e..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,24 +47,24 @@ 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) { + public InterpreterResult interpret(String st, InterpreterContext context) throws InterpreterException { context.out.clear(); InterpreterResult result = super.interpret(st, context); List resultMessages = null; @@ -82,13 +82,14 @@ public InterpreterResult interpret(String st, 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"); p.setProperty("zeppelin.spark.importImplicit", "true"); p.setProperty("zeppelin.pyspark.python", "python"); p.setProperty("zeppelin.dep.localrepo", tmpDir.newFolder().getAbsolutePath()); + p.setProperty("zeppelin.pyspark.useIPython", "false"); return p; } @@ -110,6 +111,16 @@ 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 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); @@ -122,34 +133,35 @@ public static void setUp() throws Exception { 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)); + 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(); } @Test - public void dependenciesAreInstalled() { + 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()); } @Test - public void showPlot() { + public void showPlot() throws InterpreterException { // Simple plot test InterpreterResult ret; ret = pyspark.interpret("import matplotlib.pyplot as plt", context); @@ -166,7 +178,7 @@ public void showPlot() { @Test // Test for when configuration is set to auto-close figures after show(). - public void testClose() { + public void testClose() throws InterpreterException { InterpreterResult ret; InterpreterResult ret1; InterpreterResult ret2; @@ -175,14 +187,14 @@ public void testClose() { 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); @@ -190,10 +202,10 @@ public void testClose() { 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() { + public void testNoClose() throws InterpreterException { InterpreterResult ret; InterpreterResult ret1; InterpreterResult ret2; @@ -202,7 +214,7 @@ public void testNoClose() { 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 @@ -217,16 +229,16 @@ public void testNoClose() { ret2 = pyspark.interpret("plt.show()", context); assertNotSame(ret1.message().get(0).getData(), ret2.message().get(0).getData()); } - + @Test // Test angular mode - public void testAngular() { + public void testAngular() throws InterpreterException { InterpreterResult ret; ret = pyspark.interpret("import matplotlib.pyplot as plt", context); 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()); @@ -234,5 +246,5 @@ public void testAngular() { 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 74% 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 ce0c86cf00f..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,18 +46,19 @@ 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"); p.setProperty("zeppelin.spark.importImplicit", "true"); 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; } @@ -81,6 +81,17 @@ 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 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,32 +102,41 @@ 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 - public static void tearDown() { + public static void tearDown() throws InterpreterException { pySparkInterpreter.close(); sparkInterpreter.close(); } @Test - public void testBasicIntp() { + public void testBasicIntp() throws InterpreterException { if (getSparkVersionNumber() > 11) { 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 - public void testCompletion() { + public void testCompletion() throws InterpreterException { if (getSparkVersionNumber() > 11) { List completions = pySparkInterpreter.completion("sc.", "sc.".length(), null); assertTrue(completions.size() > 0); @@ -124,7 +144,7 @@ public void testCompletion() { } @Test - public void testRedefinitionZeppelinContext() { + public void testRedefinitionZeppelinContext() throws InterpreterException { if (getSparkVersionNumber() > 11) { String redefinitionCode = "z = 1\n"; String restoreCode = "z = __zeppelin__\n"; @@ -142,7 +162,12 @@ private class infinityPythonJob implements Runnable { @Override public void run() { String code = "import time\nwhile True:\n time.sleep(1)" ; - InterpreterResult ret = pySparkInterpreter.interpret(code, context); + InterpreterResult ret = null; + try { + ret = pySparkInterpreter.interpret(code, context); + } catch (InterpreterException e) { + e.printStackTrace(); + } assertNotNull(ret); Pattern expectedMessage = Pattern.compile("KeyboardInterrupt"); Matcher m = expectedMessage.matcher(ret.message().toString()); @@ -151,7 +176,7 @@ public void run() { } @Test - public void testCancelIntp() throws InterruptedException { + public void testCancelIntp() throws InterruptedException, InterpreterException { if (getSparkVersionNumber() > 11) { assertEquals(InterpreterResult.Code.SUCCESS, pySparkInterpreter.interpret("a = 1\n", context).code()); 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..a3260852b16 --- /dev/null +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkRInterpreterTest.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 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.InterpreterOutput; +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.junit.Assert.fail; +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 InterpreterException, InterruptedException { + 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"); + properties.setProperty("spark.r.backendConnectionTimeout", "10"); + + 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); + + InterpreterContext.set(getInterpreterContext()); + 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()); + 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")); + // spark job url is sent + verify(mockRemoteEventClient, atLeastOnce()).onParaInfosReceived(any(String.class), any(String.class), any(Map.class)); + + // cancel + final InterpreterContext context = getInterpreterContext(); + Thread thread = new Thread() { + @Override + public void run() { + try { + InterpreterResult result = sparkRInterpreter.interpret("ldf <- dapplyCollect(\n" + + " df,\n" + + " function(x) {\n" + + " Sys.sleep(3)\n" + + " x <- cbind(x, \"waiting_secs\" = x$waiting * 60)\n" + + " })\n" + + "head(ldf, 3)", context); + assertTrue(result.message().get(0).getData().contains("cancelled")); + } catch (InterpreterException e) { + fail("Should not throw InterpreterException"); + } + } + }; + thread.setName("Cancel-Thread"); + thread.start(); + Thread.sleep(1000); + sparkRInterpreter.cancel(context); + } 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)); + } + + // plotting + result = sparkRInterpreter.interpret("hist(mtcars$mpg)", getInterpreterContext()); + assertEquals(InterpreterResult.Code.SUCCESS, result.code()); + assertEquals(1, result.message().size()); + assertEquals(InterpreterResult.Type.HTML, result.message().get(0).getType()); + assertTrue(result.message().get(0).getData().contains(" 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(new Properties()) { + @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("zeppelin-user1-note-paragraph"); + + try { + sparkShims = SparkShims.getInstance(SparkVersion.SPARK_2_0_0.toString(), new Properties()); + } catch (Throwable ignore) { + sparkShims = SparkShims.getInstance(SparkVersion.SPARK_1_6_0.toString(), new Properties()); + } + } + + @Test + public void runUnerLocalTest() { + 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=")); + } + + @Test + public void runUnerYarnTest() { + 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")); + + 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/src/test/java/org/apache/zeppelin/spark/SparkVersionTest.java b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkVersionTest.java similarity index 87% 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 index 3dc8f4e9009..f87caed9ffd 100644 --- a/spark/src/test/java/org/apache/zeppelin/spark/SparkVersionTest.java +++ b/spark/interpreter/src/test/java/org/apache/zeppelin/spark/SparkVersionTest.java @@ -64,6 +64,14 @@ public void testSparkVersion() { assertFalse(SparkVersion.SPARK_1_2_0.olderThanEquals(SparkVersion.SPARK_1_1_0)); assertTrue(SparkVersion.SPARK_1_2_0.olderThanEquals(SparkVersion.SPARK_1_3_0)); + // test newerThanEqualsPatchVersion + assertTrue(SparkVersion.fromVersionString("2.3.1") + .newerThanEqualsPatchVersion(SparkVersion.fromVersionString("2.3.0"))); + assertFalse(SparkVersion.fromVersionString("2.3.1") + .newerThanEqualsPatchVersion(SparkVersion.fromVersionString("2.3.2"))); + assertFalse(SparkVersion.fromVersionString("2.3.1") + .newerThanEqualsPatchVersion(SparkVersion.fromVersionString("2.2.0"))); + // conversion assertEquals(10200, SparkVersion.SPARK_1_2_0.toNumber()); assertEquals("1.2.0", SparkVersion.SPARK_1_2_0.toString()); diff --git a/spark/interpreter/src/test/resources/hive-site.xml b/spark/interpreter/src/test/resources/hive-site.xml new file mode 100644 index 00000000000..f26ffcd5e75 --- /dev/null +++ b/spark/interpreter/src/test/resources/hive-site.xml @@ -0,0 +1,7 @@ + + + hive.metastore.warehouse.dir + ${user.home}/hive/warehouse + location of default database for the warehouse + + \ No newline at end of file diff --git a/spark/interpreter/src/test/resources/log4j.properties b/spark/interpreter/src/test/resources/log4j.properties new file mode 100644 index 00000000000..0dc7c89701f --- /dev/null +++ b/spark/interpreter/src/test/resources/log4j.properties @@ -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. +# + +# 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.zeppelin.python=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 d35f9739f0d..ccc85892295 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -16,638 +16,240 @@ ~ limitations under the License. --> - - 4.0.0 + + 4.0.0 + + + interpreter-parent + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + ../interpreter-parent/pom.xml + - - zeppelin org.apache.zeppelin - 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} - - - - 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 - - - - com.google.guava - guava - ${guava.version} - - - - - 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 - - - - - - - - - src/main/resources - - interpreter-setting.json - - - - src/main/sparkr-resources - - - - - - maven-enforcer-plugin - - - enforce - none - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - 1 - false - -Xmx1024m -XX:MaxPermSize=256m - - **/SparkRInterpreterTest.java - ${pyspark.test.exclude} - - - - - - org.apache.maven.plugins - maven-shade-plugin - ${plugin.shade.version} - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - reference.conf - - - - - - 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} - - - - - - - - - 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-2.1 - - 2.1.0 - 2.5.0 - 0.10.4 - 2.11.8 - - - - - spark-2.2 - - true - - + spark-parent + pom + 0.8.2-mapr-1912-r2 + Zeppelin: Spark Parent + Zeppelin Spark Support + + + + 3.2.9 + 3.2.6 + 3.2.10 + + + 2.4.1 + 2.15.2 + 2.2.0 - 2.5.0 - 0.10.4 - - + 0.10.4 + + spark-${spark.version} + + https://archive.apache.org/dist/spark/${spark.archive}/${spark.archive}.tgz + + + https://archive.apache.org/dist/spark/${spark.archive}/${spark.archive}-bin-without-hadoop.tgz + + + + + interpreter + spark-scala-parent + + scala-2.11 + + spark-shims + + spark2-shims + + + - - hadoop-0.23 - - - org.apache.avro - avro + org.slf4j + slf4j-api - - - 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 - - + + org.slf4j + slf4j-log4j12 + - - hadoop-2.4 - - 2.4.0 - 2.5.0 - 0.9.3 - hadoop2 - - + + log4j + log4j + - - hadoop-2.6 - - 2.6.0 - 2.5.0 - 0.9.3 - hadoop2 - - + + org.scalatest + scalatest_${scala.binary.version} + ${scalatest.version} + test + - - hadoop-2.7 - - 2.7.2 - 2.5.0 - 0.9.0 - hadoop2 - - - + + junit + junit + test + + + + + + + maven-enforcer-plugin + + + enforce + none + + + + + + org.apache.maven.plugins + maven-clean-plugin + ${plugin.clean.version} + + + + + 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.4-mapr + + true + + + 2.4.4.0-mapr-630 + 2.5.0 + 0.10.7 + + + + + spark-2.4 + + 2.4.0 + 2.5.0 + 0.10.7 + + + + + spark-2.3 + + 2.3.2 + 2.5.0 + 0.10.7 + + + + + spark-2.2 + + 2.2.1 + 0.10.4 + + + + + spark-2.1 + + 2.1.2 + 0.10.4 + + + + + spark-2.0 + + 2.0.2 + 0.10.3 + + + + + spark-1.6 + + 1.6.3 + 0.9 + + + diff --git a/spark/scala-2.10/pom.xml b/spark/scala-2.10/pom.xml new file mode 100644 index 00000000000..1bf458c6e8a --- /dev/null +++ b/spark/scala-2.10/pom.xml @@ -0,0 +1,42 @@ + + + + + org.apache.zeppelin + spark-scala-parent + 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-r2 + jar + Zeppelin: Spark Interpreter Scala_2.10 + + + 2.2.0 + 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..995ee1539c9 --- /dev/null +++ b/spark/scala-2.10/src/main/scala/org/apache/zeppelin/spark/SparkScala210Interpreter.scala @@ -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.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], + override val printReplOutput: java.lang.Boolean) + extends BaseSparkScalaInterpreter(conf, depFiles, printReplOutput) { + + 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) + sparkILoop = new SparkILoop(null, new JPrintWriter(Console.out, true)) + if (printReplOutput) { + Console.setOut(interpreterOutput) + } + sparkILoop = new SparkILoop() + + 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() + } + + 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..f4a4ab630d2 --- /dev/null +++ b/spark/scala-2.11/pom.xml @@ -0,0 +1,42 @@ + + + + + org.apache.zeppelin + spark-scala-parent + 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-r2 + jar + Zeppelin: Spark Interpreter Scala_2.11 + + + 2.4.0 + 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/src/test/resources/log4j.properties b/spark/scala-2.11/src/main/resources/log4j.properties similarity index 97% rename from spark/src/test/resources/log4j.properties rename to spark/scala-2.11/src/main/resources/log4j.properties index b0d1067bc46..0c90b21ae00 100644 --- a/spark/src/test/resources/log4j.properties +++ b/spark/scala-2.11/src/main/resources/log4j.properties @@ -45,3 +45,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.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..8465145e60e --- /dev/null +++ b/spark/scala-2.11/src/main/scala/org/apache/zeppelin/spark/SparkScala211Interpreter.scala @@ -0,0 +1,179 @@ +/* + * 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.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.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], + override val printReplOutput: java.lang.Boolean) + extends BaseSparkScalaInterpreter(conf, depFiles, printReplOutput) { + + import SparkScala211Interpreter._ + + lazy override val LOGGER: Logger = LoggerFactory.getLogger(getClass) + + private var sparkILoop: SparkILoop = _ + + 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 = if (printReplOutput) { + new JPrintWriter(interpreterOutput, true) + } else { + new JPrintWriter(Console.out, true) + } + sparkILoop = new SparkILoop(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() + loopPostInit(this) + 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() + } + } + + def scalaInterpret(code: String): scala.tools.nsc.interpreter.IR.Result = + sparkILoop.interpret(code) + +} + +private object SparkScala211Interpreter { + + /** + * This is a hack to call `loopPostInit` at `ILoop`. At higher version of Scala such + * as 2.11.12, `loopPostInit` became a nested function which is inaccessible. Here, + * we redefine `loopPostInit` at Scala's 2.11.8 side and ignore `loadInitFiles` being called at + * Scala 2.11.12 since here we do not have to load files. + * + * Both methods `loopPostInit` and `unleashAndSetPhase` are redefined, and `phaseCommand` and + * `asyncMessage` are being called via reflection since both exist in Scala 2.11.8 and 2.11.12. + * + * Please see the codes below: + * https://github.com/scala/scala/blob/v2.11.8/src/repl/scala/tools/nsc/interpreter/ILoop.scala + * https://github.com/scala/scala/blob/v2.11.12/src/repl/scala/tools/nsc/interpreter/ILoop.scala + * + * See also ZEPPELIN-3810. + */ + private def loopPostInit(interpreter: SparkScala211Interpreter): Unit = { + import StdReplTags._ + import scala.reflect.classTag + import scala.reflect.io + + val sparkILoop = interpreter.sparkILoop + val intp = sparkILoop.intp + val power = sparkILoop.power + val in = sparkILoop.in + + def loopPostInit() { + // Bind intp somewhere out of the regular namespace where + // we can get at it in generated code. + intp.quietBind(NamedParam[IMain]("$intp", intp)(tagOfIMain, classTag[IMain])) + // Auto-run code via some setting. + (replProps.replAutorunCode.option + flatMap (f => io.File(f).safeSlurp()) + foreach (intp quietRun _) + ) + // classloader and power mode setup + intp.setContextClassLoader() + if (isReplPower) { + replProps.power setValue true + unleashAndSetPhase() + asyncMessage(power.banner) + } + // SI-7418 Now, and only now, can we enable TAB completion. + in.postInit() + } + + def unleashAndSetPhase() = if (isReplPower) { + power.unleash() + intp beSilentDuring phaseCommand("typer") // Set the phase to "typer" + } + + def phaseCommand(name: String): Results.Result = { + interpreter.callMethod( + sparkILoop, + "scala$tools$nsc$interpreter$ILoop$$phaseCommand", + Array(classOf[String]), + Array(name)).asInstanceOf[Results.Result] + } + + def asyncMessage(msg: String): Unit = { + interpreter.callMethod( + sparkILoop, "asyncMessage", Array(classOf[String]), Array(msg)) + } + + loopPostInit() + } +} diff --git a/spark/spark-dependencies/pom.xml b/spark/spark-dependencies/pom.xml new file mode 100644 index 00000000000..0a45079c8fd --- /dev/null +++ b/spark/spark-dependencies/pom.xml @@ -0,0 +1,359 @@ + + + + + 4.0.0 + + + spark-parent + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + .. + + + org.apache.zeppelin + zeppelin-spark-dependencies + jar + 0.8.2-mapr-1912-r2 + Zeppelin: Spark dependencies + Zeppelin spark support + + + + + 2.7.3 + ${hadoop.version} + 1.7.7 + + 0.7.1 + 2.4.1 + + org.spark-project.akka + 2.3.4-spark + + 2.3 + + + + + + org.apache.spark + spark-core_${scala.binary.version} + ${spark.version} + + + org.apache.hadoop + hadoop-client + + + + + + org.apache.spark + spark-repl_${scala.binary.version} + ${spark.version} + + + + org.apache.spark + spark-sql_${scala.binary.version} + ${spark.version} + + + + org.apache.spark + spark-hive_${scala.binary.version} + ${spark.version} + + + + org.apache.spark + spark-streaming_${scala.binary.version} + ${spark.version} + + + + org.apache.spark + spark-catalyst_${scala.binary.version} + ${spark.version} + + + + + org.apache.hadoop + hadoop-client + ${hadoop.version} + + + + + org.apache.spark + spark-yarn_${scala.binary.version} + ${spark.version} + + + + org.apache.hadoop + hadoop-yarn-api + ${yarn.version} + + + + + + + + maven-enforcer-plugin + + + enforce + none + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + 1 + false + -Xmx1024m -XX:MaxPermSize=256m + + + + + com.googlecode.maven-download-plugin + download-maven-plugin + ${plugin.download.version} + + + + org.apache.maven.plugins + maven-shade-plugin + ${plugin.shade.version} + + + + *:* + + org/datanucleus/** + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + reference.conf + + + + + + package + + shade + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-interpreter-dependencies + package + + copy-dependencies + + + true + + + + + copy-spark-interpreter-dependencies + package + + copy-dependencies + + + ${project.build.directory}/../../../interpreter/spark/dep + false + false + true + org.datanucleus + + + + copy-artifact + package + + copy + + + ${project.build.directory}/../../../interpreter/spark/dep + false + false + true + + + ${project.groupId} + ${project.artifactId} + ${project.version} + ${project.packaging} + + + + + + + + + maven-resources-plugin + + + copy-interpreter-setting + none + + true + + + + + + + + com.googlecode.maven-download-plugin + download-maven-plugin + + + download-pyspark-files + validate + + wget + + + 60000 + 5 + true + ${spark.src.download.url} + ${project.build.directory} + + + + + + + maven-clean-plugin + + + + ${basedir}/../python/build + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + zip-pyspark-files + generate-resources + + run + + + + + + + + + + + + + + + com.googlecode.maven-download-plugin + download-maven-plugin + + + download-sparkr-files + validate + + wget + + + 60000 + 5 + ${spark.bin.download.url} + true + ${project.build.directory} + + + + + + maven-resources-plugin + 2.7 + + + copy-sparkr-files + generate-resources + + copy-resources + + + ${project.build.directory}/../../../interpreter/spark/R/lib + + + + ${project.build.directory}/spark-${spark.version}-bin-without-hadoop/R/lib + + + + + + + + + + + + diff --git a/spark/spark-scala-parent/pom.xml b/spark/spark-scala-parent/pom.xml new file mode 100644 index 00000000000..9b7adec3b49 --- /dev/null +++ b/spark/spark-scala-parent/pom.xml @@ -0,0 +1,243 @@ + + + + + + + org.apache.zeppelin + interpreter-parent + 0.8.2-mapr-1912-r2 + ../../interpreter-parent/pom.xml + + + 4.0.0 + org.apache.zeppelin + spark-scala-parent + 0.8.2-mapr-1912-r2 + pom + Zeppelin: Spark Scala Parent + + + 2.4.0 + 2.11 + 2.11.8 + ${scala.binary.version} + + + + + org.apache.zeppelin + zeppelin-interpreter + ${project.version} + provided + + + + 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.slf4j + slf4j-api + + + + org.slf4j + slf4j-log4j12 + + + + log4j + log4j + + + + + + + + maven-resources-plugin + + + copy-interpreter-setting + none + + true + + + + + + + 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 + + + + + + + + + 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 + + + + + + + + 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..b27e2db2af6 --- /dev/null +++ b/spark/spark-scala-parent/src/main/scala/org/apache/zeppelin/spark/BaseSparkScalaInterpreter.scala @@ -0,0 +1,429 @@ +/* + * 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.net.URLClassLoader +import java.nio.file.Paths +import java.util.concurrent.atomic.AtomicInteger + +import org.apache.hadoop.yarn.client.api.YarnClient +import org.apache.hadoop.yarn.conf.YarnConfiguration +import org.apache.spark.sql.SQLContext +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} +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], + val printReplOutput: java.lang.Boolean) { + + 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')) + + BaseSparkScalaInterpreter.sessionNum.incrementAndGet() + } + + def interpret(code: String, context: InterpreterContext): InterpreterResult = { + + val originalOut = System.out + + def _interpret(code: String): scala.tools.nsc.interpreter.Results.Result = { + Console.withOut(interpreterOutput) { + System.setOut(Console.out) + interpreterOutput.setInterpreterOutput(context.out) + interpreterOutput.ignoreLeadingNewLinesFromScalaReporter() + context.out.clear() + + val status = scalaInterpret(code) match { + case success@scala.tools.nsc.interpreter.IR.Success => + success + case scala.tools.nsc.interpreter.IR.Error => + val errorMsg = new String(interpreterOutput.getInterpreterOutput.toByteArray) + if (errorMsg.contains("value toDF is not a member of org.apache.spark.rdd.RDD") || + errorMsg.contains("value toDS is not a member of org.apache.spark.rdd.RDD")) { + // prepend "import sqlContext.implicits._" due to + // https://issues.scala-lang.org/browse/SI-6649 + context.out.clear() + scalaInterpret("import sqlContext.implicits._\n" + code) + } else { + scala.tools.nsc.interpreter.IR.Error + } + case scala.tools.nsc.interpreter.IR.Incomplete => + // add print("") at the end in case the last line is comment which lead to INCOMPLETE + scalaInterpret(code + "\nprint(\"\")") + } + context.out.flush() + status + } + } + // reset the java stdout + System.setOut(originalOut) + + val lastStatus = _interpret(code) match { + case scala.tools.nsc.interpreter.IR.Success => + InterpreterResult.Code.SUCCESS + case scala.tools.nsc.interpreter.IR.Error => + InterpreterResult.Code.ERROR + case scala.tools.nsc.interpreter.IR.Incomplete => + InterpreterResult.Code.INCOMPLETE + } + + new InterpreterResult(lastStatus) + } + + protected def interpret(code: String): InterpreterResult = + interpret(code, InterpreterContext.get()) + + 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.substring(0, cursor), cursor).candidates + .map(e => new InterpreterCompletion(e, e, null)) + scala.collection.JavaConversions.seqAsJavaList(completions) + } + + protected def getProgress(jobGroup: String, context: InterpreterContext): Int = { + JobProgressUtil.progress(sc, jobGroup) + } + + 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 (BaseSparkScalaInterpreter.sessionNum.decrementAndGet() == 0) { + 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._") + // print empty string otherwise the last statement's output of this method + // (aka. import org.apache.spark.sql.functions._) will mix with the output of user code + interpret("print(\"\")") + } + + 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] + + sparkUrl = if (isOnYarnCluster(sc)) sparkYarnAppUrl(sc) else sparkLocalAppUrl(sc) + + 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._") + // print empty string otherwise the last statement's output of this method + // (aka. import org.apache.spark.sql.functions._) will mix with the output of user code + interpret("print(\"\")") + } + + private def sparkYarnAppUrl(sc: SparkContext): String = { + val yarnClient = createYarnClient(sc) + val yarnApps = yarnClient.getApplications.asScala + yarnApps.find(_.getApplicationId.toString == sc.applicationId) match { + case Some(x) => x.getTrackingUrl + case None => sparkLocalAppUrl(sc) + } + } + + private def createYarnClient(sc: SparkContext): YarnClient = { + val hadoopConf = sc.hadoopConfiguration + val yarnConf = new YarnConfiguration(hadoopConf) + val yarnClient = YarnClient.createYarnClient + yarnClient.init(yarnConf) + yarnClient.start() + return yarnClient + } + + private def sparkLocalAppUrl(sc: SparkContext): String = sc.uiWebUrl.getOrElse("") + + private def isOnYarnCluster(sc: SparkContext): Boolean = + sc.master == "yarn" && sc.deployMode == "cluster" + + 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] = { + var classLoader = Thread.currentThread().getContextClassLoader + var extraJars = Seq.empty[String] + while (classLoader != null) { + if (classLoader.getClass.getCanonicalName == + "org.apache.spark.util.MutableURLClassLoader") { + extraJars = classLoader.asInstanceOf[URLClassLoader].getURLs() + // Check if the file exists. + .filter { u => u.getProtocol == "file" && new File(u.getPath).isFile } + // Some bad spark packages depend on the wrong version of scala-reflect. Blacklist it. + .filterNot { + u => Paths.get(u.toURI).getFileName.toString.contains("org.scala-lang_scala-reflect") + } + .map(url => url.toString).toSeq + classLoader = null + } else { + classLoader = classLoader.getParent + } + } + LOGGER.debug("User jar for spark repl: " + extraJars.mkString(",")) + extraJars + } + + protected def getUserFiles(): Seq[String] = { + depFiles.asScala.filter(!_.endsWith(".jar")) + } +} + +object BaseSparkScalaInterpreter { + val sessionNum = new AtomicInteger(0) +} 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 + } + } +} diff --git a/spark/spark-shims/pom.xml b/spark/spark-shims/pom.xml new file mode 100644 index 00000000000..93f8c5af4fc --- /dev/null +++ b/spark/spark-shims/pom.xml @@ -0,0 +1,74 @@ + + + + + + + spark-parent + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + ../pom.xml + + + 4.0.0 + org.apache.zeppelin + spark-shims + 0.8.2-mapr-1912-r2 + jar + Zeppelin: Spark Shims + + + + org.apache.zeppelin + zeppelin-interpreter + ${project.version} + provided + + + + + org.apache.hadoop + hadoop-common + 2.7.0-mapr-1808 + provided + + + + + + + maven-resources-plugin + + + copy-interpreter-setting + none + + true + + + + + + + + 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..c46da7bbc7f --- /dev/null +++ b/spark/spark-shims/src/main/scala/org/apache/zeppelin/spark/SparkShims.java @@ -0,0 +1,160 @@ +/* + * 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.annotations.VisibleForTesting; +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; +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 { + + // 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; + + protected Properties properties; + + public SparkShims(Properties properties) { + this.properties = properties; + } + + private static SparkShims loadShims(String sparkVersion, Properties properties) + 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(Properties.class); + return (SparkShims) c.newInstance(properties); + } + + public static SparkShims getInstance(String sparkVersion, Properties properties) { + if (sparkShims == null) { + String sparkMajorVersion = getSparkMajorVersion(sparkVersion); + try { + sparkShims = loadShims(sparkMajorVersion, properties); + } 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 master, String sparkWebUrl); + + + protected void buildSparkJobUrl( + String master, String sparkWebUrl, int jobId, Properties jobProperties) { + String jobGroupId = jobProperties.getProperty("spark.jobGroup.id"); + 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); + 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"); + 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 + * + * @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); + } + + @VisibleForTesting + public static void reset() { + sparkShims = null; + } +} diff --git a/spark/spark1-shims/pom.xml b/spark/spark1-shims/pom.xml new file mode 100644 index 00000000000..a645559b097 --- /dev/null +++ b/spark/spark1-shims/pom.xml @@ -0,0 +1,82 @@ + + + + + + + spark-parent + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + ../pom.xml + + + 4.0.0 + org.apache.zeppelin + spark1-shims + 0.8.2-mapr-1912-r2 + 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-resources-plugin + + + copy-interpreter-setting + none + + true + + + + + + + + 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..143ff30f985 --- /dev/null +++ b/spark/spark1-shims/src/main/scala/org/apache/zeppelin/spark/Spark1Shims.java @@ -0,0 +1,48 @@ +/* + * 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.SparkListenerJobStart; +import org.apache.spark.ui.jobs.JobProgressListener; + +import java.util.Properties; + +/** + * Shims for Spark 1.x + */ +public class Spark1Shims extends SparkShims { + + public Spark1Shims(Properties properties) { + super(properties); + } + + public void setupSparkListener(final String master, final String sparkWebUrl) { + final SparkContext sc = SparkContext.getOrCreate(); + sc.addSparkListener(new JobProgressListener(sc.getConf()) { + @Override + public void onJobStart(SparkListenerJobStart jobStart) { + if (sc.getConf().getBoolean("spark.ui.enabled", true) && + !Boolean.parseBoolean(properties.getProperty("zeppelin.spark.ui.hidden", "false"))) { + buildSparkJobUrl(master, 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..d4645b92c2e --- /dev/null +++ b/spark/spark2-shims/pom.xml @@ -0,0 +1,81 @@ + + + + + + spark-parent + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + ../pom.xml + + + 4.0.0 + org.apache.zeppelin + spark2-shims + 0.8.2-mapr-1912-r2 + 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-resources-plugin + + + copy-interpreter-setting + none + + true + + + + + + + + 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..7faf4b1712e --- /dev/null +++ b/spark/spark2-shims/src/main/scala/org/apache/zeppelin/spark/Spark2Shims.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.spark; + +import org.apache.spark.SparkContext; +import org.apache.spark.scheduler.SparkListener; +import org.apache.spark.scheduler.SparkListenerJobStart; + +import java.util.Properties; + + +/** + * Shims for Spark 2.x + */ +public class Spark2Shims extends SparkShims { + + public Spark2Shims(Properties properties) { + super(properties); + } + + public void setupSparkListener(final String master, final String sparkWebUrl) { + final SparkContext sc = SparkContext.getOrCreate(); + sc.addSparkListener(new SparkListener() { + @Override + public void onJobStart(SparkListenerJobStart jobStart) { + if (sc.getConf().getBoolean("spark.ui.enabled", true) && + !Boolean.parseBoolean(properties.getProperty("zeppelin.spark.ui.hidden", "false"))) { + buildSparkJobUrl(master, sparkWebUrl, jobStart.jobId(), jobStart.properties()); + } + } + }); + } +} diff --git a/spark/src/main/resources/interpreter-setting.json b/spark/src/main/resources/interpreter-setting.json deleted file mode 100644 index e96265f00a5..00000000000 --- a/spark/src/main/resources/interpreter-setting.json +++ /dev/null @@ -1,159 +0,0 @@ -[ - { - "group": "spark", - "name": "spark", - "className": "org.apache.zeppelin.spark.SparkInterpreter", - "defaultInterpreter": true, - "properties": { - "spark.executor.memory": { - "envName": null, - "propertyName": "spark.executor.memory", - "defaultValue": "", - "description": "Executor memory per worker instance. ex) 512m, 32g", - "type": "string" - }, - "args": { - "envName": null, - "propertyName": null, - "defaultValue": "", - "description": "spark commandline args", - "type": "textarea" - }, - "zeppelin.spark.useHiveContext": { - "envName": "ZEPPELIN_SPARK_USEHIVECONTEXT", - "propertyName": "zeppelin.spark.useHiveContext", - "defaultValue": true, - "description": "Use HiveContext instead of SQLContext if it is true.", - "type": "checkbox" - }, - "spark.app.name": { - "envName": "SPARK_APP_NAME", - "propertyName": "spark.app.name", - "defaultValue": "Zeppelin", - "description": "The name of spark application.", - "type": "string" - }, - "zeppelin.spark.printREPLOutput": { - "envName": null, - "propertyName": "zeppelin.spark.printREPLOutput", - "defaultValue": true, - "description": "Print REPL output", - "type": "checkbox" - }, - "spark.cores.max": { - "envName": null, - "propertyName": "spark.cores.max", - "defaultValue": "", - "description": "Total number of cores to use. Empty value uses all available core.", - "type": "number" - }, - "zeppelin.spark.maxResult": { - "envName": "ZEPPELIN_SPARK_MAXRESULT", - "propertyName": "zeppelin.spark.maxResult", - "defaultValue": "1000", - "description": "Max number of Spark SQL result to display.", - "type": "number" - }, - "master": { - "envName": "MASTER", - "propertyName": "spark.master", - "defaultValue": "local[*]", - "description": "Spark master uri. ex) spark://masterhost:7077", - "type": "string" - }, - "zeppelin.spark.unSupportedVersionCheck": { - "envName": null, - "propertyName": "zeppelin.spark.enableSupportedVersionCheck", - "defaultValue": true, - "description": "Do not change - developer only setting, not for production use", - "type": "checkbox" - } - }, - "editor": { - "language": "scala", - "editOnDblClick": false - } - }, - { - "group": "spark", - "name": "sql", - "className": "org.apache.zeppelin.spark.SparkSqlInterpreter", - "properties": { - "zeppelin.spark.concurrentSQL": { - "envName": "ZEPPELIN_SPARK_CONCURRENTSQL", - "propertyName": "zeppelin.spark.concurrentSQL", - "defaultValue": false, - "description": "Execute multiple SQL concurrently if set true.", - "type": "checkbox" - }, - "zeppelin.spark.sql.stacktrace": { - "envName": "ZEPPELIN_SPARK_SQL_STACKTRACE", - "propertyName": "zeppelin.spark.sql.stacktrace", - "defaultValue": false, - "description": "Show full exception stacktrace for SQL queries if set to true.", - "type": "checkbox" - }, - "zeppelin.spark.maxResult": { - "envName": "ZEPPELIN_SPARK_MAXRESULT", - "propertyName": "zeppelin.spark.maxResult", - "defaultValue": "1000", - "description": "Max number of Spark SQL result to display.", - "type": "number" - }, - "zeppelin.spark.importImplicit": { - "envName": "ZEPPELIN_SPARK_IMPORTIMPLICIT", - "propertyName": "zeppelin.spark.importImplicit", - "defaultValue": true, - "description": "Import implicits, UDF collection, and sql if set true. true by default.", - "type": "checkbox" - } - }, - "editor": { - "language": "sql", - "editOnDblClick": false - } - }, - { - "group": "spark", - "name": "dep", - "className": "org.apache.zeppelin.spark.DepInterpreter", - "properties": { - "zeppelin.dep.localrepo": { - "envName": "ZEPPELIN_DEP_LOCALREPO", - "propertyName": null, - "defaultValue": "local-repo", - "description": "local repository for dependency loader", - "type": "string" - }, - "zeppelin.dep.additionalRemoteRepository": { - "envName": null, - "propertyName": null, - "defaultValue": "spark-packages,http://dl.bintray.com/spark-packages/maven,false;", - "description": "A list of 'id,remote-repository-URL,is-snapshot;' for each remote repository.", - "type": "textarea" - } - }, - "editor": { - "language": "scala", - "editOnDblClick": false - } - }, - { - "group": "spark", - "name": "pyspark", - "className": "org.apache.zeppelin.spark.PySparkInterpreter", - "properties": { - "zeppelin.pyspark.python": { - "envName": "PYSPARK_PYTHON", - "propertyName": null, - "defaultValue": "python", - "description": "Python command to run pyspark with", - "type": "string" - } - }, - "editor": { - "language": "python", - "editOnDblClick": false - } - } -] 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/downloadLivy.sh b/testing/downloadLivy.sh index 8e5418d1d44..c018f4a4ca9 100755 --- a/testing/downloadLivy.sh +++ b/testing/downloadLivy.sh @@ -49,7 +49,7 @@ download_with_retry() { } LIVY_CACHE=".livy-dist" -LIVY_ARCHIVE="livy-assembly-${LIVY_VERSION}" +LIVY_ARCHIVE="livy-${LIVY_VERSION}-bin" export LIVY_HOME="${ZEPPELIN_HOME}/livy-server-$LIVY_VERSION" echo "LIVY_HOME is ${LIVY_HOME}" @@ -64,7 +64,7 @@ if [[ ! -d "${LIVY_HOME}" ]]; then # download livy from archive if not cached echo "${LIVY_VERSION} being downloaded from archives" STARTTIME=`date +%s` - download_with_retry "https://oss.sonatype.org/content/repositories/releases/com/cloudera/livy/livy-assembly/${LIVY_VERSION}/${LIVY_ARCHIVE}.zip" + download_with_retry "https://dist.apache.org/repos/dist/release/incubator/livy/${LIVY_VERSION}/${LIVY_ARCHIVE}.zip" ENDTIME=`date +%s` DOWNLOADTIME="$((ENDTIME-STARTTIME))" fi diff --git a/testing/install_external_dependencies.sh b/testing/install_external_dependencies.sh index e88f63bc285..6c65ac5b2b9 100755 --- a/testing/install_external_dependencies.sh +++ b/testing/install_external_dependencies.sh @@ -30,6 +30,7 @@ if [[ "${SPARKR}" = "true" ]] ; then R -e "install.packages('evaluate', repos = 'http://cran.us.r-project.org', lib='~/R')" > /dev/null 2>&1 R -e "install.packages('base64enc', repos = 'http://cran.us.r-project.org', lib='~/R')" > /dev/null 2>&1 R -e "install.packages('knitr', repos = 'http://cran.us.r-project.org', lib='~/R')" > /dev/null 2>&1 + R -e "install.packages('ggplot2', repos = 'http://cran.us.r-project.org', lib='~/R')" > /dev/null 2>&1 fi fi @@ -44,5 +45,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 numpy=1.13.3 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 scipy==0.18.0 ggplot==0.11.5 grpcio==1.8.2 bkzep==0.4.0 fi 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: diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml index 4058aefbf11..72e7b4b62c3 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.2-mapr-1912-r2 .. org.apache.zeppelin - zeppelin-display_2.10 + zeppelin-display jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 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-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularElemTest.scala b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularElemTest.scala index 43ad1bd0a23..4ddae9a62d8 100644 --- a/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularElemTest.scala +++ b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularElemTest.scala @@ -35,7 +35,7 @@ trait AbstractAngularElemTest override def beforeEach() { val intpGroup = new InterpreterGroup() val context = new InterpreterContext("note", "paragraph", null, "title", "text", - new AuthenticationInfo(), new util.HashMap[String, Object](), new GUI(), + new AuthenticationInfo(), new util.HashMap[String, Object](), new GUI(), new GUI(), new AngularObjectRegistry(intpGroup.getId(), null), null, new util.LinkedList[InterpreterContextRunner](), diff --git a/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularModelTest.scala b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularModelTest.scala index 0ab52ec2b46..c9b0d8ff762 100644 --- a/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularModelTest.scala +++ b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularModelTest.scala @@ -30,7 +30,7 @@ with BeforeAndAfter with BeforeAndAfterEach with Eventually with Matchers { override def beforeEach() { val intpGroup = new InterpreterGroup() val context = new InterpreterContext("note", "id", null, "title", "text", new AuthenticationInfo(), - new java.util.HashMap[String, Object](), new GUI(), new AngularObjectRegistry( + new java.util.HashMap[String, Object](), new GUI(), new GUI(), new AngularObjectRegistry( intpGroup.getId(), null), null, new java.util.LinkedList[InterpreterContextRunner](), diff --git a/zeppelin-distribution/pom.xml b/zeppelin-distribution/pom.xml index 36c352257e0..2137fcef782 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.2-mapr-1912-r2 .. @@ -75,13 +75,13 @@ - zeppelin-server ${project.groupId} + zeppelin-server ${project.version} - zeppelin-web ${project.groupId} + zeppelin-web ${project.version} war @@ -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 - - - - 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 + + diff --git a/zeppelin-distribution/src/bin_license/LICENSE b/zeppelin-distribution/src/bin_license/LICENSE index 51bc91cd901..b7c13b3ab03 100644 --- a/zeppelin-distribution/src/bin_license/LICENSE +++ b/zeppelin-distribution/src/bin_license/LICENSE @@ -15,7 +15,7 @@ The following components are provided under Apache License. (Apache 2.0) Http Components (org.apache.httpcomponents:httpclient:4.3.6 - https://github.com/apache/httpclient) (Apache 2.0) Http Components (org.apache.httpcomponents:httpasyncclient:4.0.2 - https://github.com/apache/httpclient) (Apache 2.0) Apache Commons Lang (org.apache.commons:commons-lang:2.5 - http://commons.apache.org/proper/commons-lang/) - (Apache 2.0) Apache Commons Lang 3 (org.apache.commons:commons-lang3:3.4 - http://commons.apache.org/proper/commons-lang/) + (Apache 2.0) Apache Commons Lang 3 (org.apache.commons:commons-lang3:3.7 - http://commons.apache.org/proper/commons-lang/) (Apache 2.0) Apache Commons Math 3 (org.apache.commons:commons-math3:3.6.1 - http://commons.apache.org/proper/commons-math/) (Apache 2.0) Apache Commons Net (commons-net:commons-net:2.2 - http://commons.apache.org/proper/commons-net/) (Apache 2.0) Apache Commons Pool2 (commons-exec:commons-pool2:2.3 - https://commons.apache.org/proper/commons-pool/) @@ -217,6 +217,7 @@ The following components are provided under Apache License. (Apache 2.0) frontend-maven-plugin 1.3 (com.github.eirslett:frontend-maven-plugin:1.3 - https://github.com/eirslett/frontend-maven-plugin/blob/frontend-plugins-1.3/LICENSE (Apache 2.0) frontend-plugin-core 1.3 (com.github.eirslett:frontend-plugin-core) - https://github.com/eirslett/frontend-maven-plugin/blob/frontend-plugins-1.3/LICENSE (Apache 2.0) mongo-java-driver 3.4.1 (org.mongodb:mongo-java-driver:3.4.1) - https://github.com/mongodb/mongo-java-driver/blob/master/LICENSE.txt + (Apache 2.0) Neo4j Java Driver (https://github.com/neo4j/neo4j-java-driver) - https://github.com/neo4j/neo4j-java-driver/blob/1.4.3/LICENSE.txt ======================================================================== MIT licenses @@ -233,7 +234,7 @@ The text of each license is also included at licenses/LICENSE-[project]-[version (The MIT License) bootstrap3-dialog v1.34.7 (https://github.com/nakupanda/bootstrap3-dialog/tree/v1.34.7) - https://github.com/nakupanda/bootstrap3-dialog/tree/v1.34.7 (The MIT License) Angular Websocket v1.0.13 (http://angularclass.github.io/angular-websocket/) - https://github.com/AngularClass/angular-websocket/blob/v1.0.13/LICENSE (The MIT License) UI.Ace v0.1.1 (http://angularclass.github.io/angular-websocket/) - https://github.com/angular-ui/ui-ace/blob/master/LICENSE - (The MIT License) jquery.scrollTo v1.4.13 (https://github.com/flesler/jquery.scrollTo) - https://github.com/flesler/jquery.scrollTo/blob/1.4.13/LICENSE + (The MIT License) jquery.scrollTo v2.1.2 (https://github.com/flesler/jquery.scrollTo) - https://github.com/flesler/jquery.scrollTo/blob/2.1.2/LICENSE (The MIT License) angular-dragdrop v1.0.8 (http://codef0rmer.github.io/angular-dragdrop/#/) - https://github.com/codef0rmer/angular-dragdrop/blob/v1.0.8/LICENSE (The MIT License) perfect-scrollbar v0.5.4 (http://noraesae.github.io/perfect-scrollbar/) - https://github.com/noraesae/perfect-scrollbar/tree/0.5.4 (The MIT License) ng-sortable v1.3.6 (https://github.com/a5hik/ng-sortable) - https://github.com/a5hik/ng-sortable/blob/1.3.6/LICENSE @@ -272,6 +273,8 @@ The text of each license is also included at licenses/LICENSE-[project]-[version (The MIT License) ngclipboard v1.1.1 (https://github.com/sachinchoolur/ngclipboard) - https://github.com/sachinchoolur/ngclipboard/blob/1.1.1/LICENSE (The MIT License) headroom.js 0.9.3 (https://github.com/WickyNilliams/headroom.js) - https://github.com/WickyNilliams/headroom.js/blob/master/LICENSE (The MIT License) angular-viewport-watch 0.135 (https://github.com/wix/angular-viewport-watch) - https://github.com/wix/angular-viewport-watch/blob/master/LICENSE + (The MIT License) ansi-up 2.0.2 (https://github.com/drudru/ansi_up) - https://github.com/drudru/ansi_up#license + (The MIT License) bcpkix-jdk15on 1.52 (org.bouncycastle:bcpkix-jdk15on:1.52 https://github.com/bcgit/bc-java) - https://github.com/bcgit/bc-java/blob/master/LICENSE.html ======================================================================== BSD-style licenses @@ -287,7 +290,7 @@ The text of each license is also included at licenses/LICENSE-[project]-[version (BSD Style) JSch v0.1.53 (http://www.jcraft.com) - http://www.jcraft.com/jsch/LICENSE.txt (BSD 3 Clause) highlightjs v9.4.0 (https://highlightjs.org/) - https://github.com/isagalaev/highlight.js/blob/9.4.0/LICENSE (BSD 3 Clause) hamcrest v1.3 (http://hamcrest.org/JavaHamcrest/) - http://opensource.org/licenses/BSD-3-Clause - (BSD Style) JLine v2.12.1 (https://github.com/jline/jline2) - https://github.com/jline/jline2/blob/master/LICENSE.txt + (BSD Style) JLine v2.14.3 (https://github.com/jline/jline2) - https://github.com/jline/jline2/blob/master/LICENSE.txt (BSD New license) Google Auth Library for Java - Credentials (com.google.auth:google-auth-library-credentials:0.4.0 - https://github.com/google/google-auth-library-java/google-auth-library-credentials) (BSD New license) Google Auth Library for Java - OAuth2 HTTP (com.google.auth:google-auth-library-oauth2-http:0.4.0 - https://github.com/google/google-auth-library-java/google-auth-library-oauth2-http) (New BSD license) Protocol Buffer Java API (com.google.protobuf:protobuf-java-util:3.0.0-beta-2 - https://developers.google.com/protocol-buffers/) @@ -308,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.1.1.201511131810-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-distribution/src/bin_license/licenses/LICENSE-jquery.scrollTo-1.4.13 b/zeppelin-distribution/src/bin_license/licenses/LICENSE-jquery.scrollTo-2.1.2 similarity index 89% rename from zeppelin-distribution/src/bin_license/licenses/LICENSE-jquery.scrollTo-1.4.13 rename to zeppelin-distribution/src/bin_license/licenses/LICENSE-jquery.scrollTo-2.1.2 index aeaf77d8a6e..fe5853e27cf 100644 --- a/zeppelin-distribution/src/bin_license/licenses/LICENSE-jquery.scrollTo-1.4.13 +++ b/zeppelin-distribution/src/bin_license/licenses/LICENSE-jquery.scrollTo-2.1.2 @@ -1,6 +1,6 @@ (The MIT License) -Copyright (c) 2007-2014 Ariel Flesler +Copyright (c) 2007-2015 Ariel Flesler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -19,4 +19,4 @@ 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. +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index e9f04731b80..04a2aa14d17 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.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-examples pom - 0.8.0-SNAPSHOT + 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 d4fed2171fa..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.0-SNAPSHOT + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-example-clock jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Example application - Clock diff --git a/zeppelin-examples/zeppelin-example-clock/zeppelin-example-clock.json b/zeppelin-examples/zeppelin-example-clock/zeppelin-example-clock.json index 3db70e93960..64aeefddf3d 100644 --- a/zeppelin-examples/zeppelin-example-clock/zeppelin-example-clock.json +++ b/zeppelin-examples/zeppelin-example-clock/zeppelin-example-clock.json @@ -18,7 +18,7 @@ "type" : "APPLICATION", "name" : "zeppelin.clock", "description" : "Clock (example)", - "artifact" : "zeppelin-examples/zeppelin-example-clock/target/zeppelin-example-clock-0.8.0-SNAPSHOT.jar", + "artifact" : "zeppelin-examples/zeppelin-example-clock/target/zeppelin-example-clock-0.8.1-SNAPSHOT.jar", "className" : "org.apache.zeppelin.example.app.clock.Clock", "resources" : [[":java.util.Date"]], "license" : "Apache-2.0", diff --git a/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml b/zeppelin-examples/zeppelin-example-horizontalbar/pom.xml index 8e08c4a3af2..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.0-SNAPSHOT + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-example-horizontalbar jar - 0.8.0-SNAPSHOT + 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 348abd20354..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.0-SNAPSHOT + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-example-spell-echo jar - 0.8.0-SNAPSHOT + 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 b3575c99c6a..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.0-SNAPSHOT + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-example-spell-flowchart jar - 0.8.0-SNAPSHOT + 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 b615eadc8a7..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.0-SNAPSHOT + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-example-spell-markdown jar - 0.8.0-SNAPSHOT + 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 09e6daaad38..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.0-SNAPSHOT + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-example-spell-translator jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Example Spell - Translator diff --git a/zeppelin-integration/pom.xml b/zeppelin-integration/pom.xml new file mode 100644 index 00000000000..3e8dbeb3da1 --- /dev/null +++ b/zeppelin-integration/pom.xml @@ -0,0 +1,224 @@ + + + + + 4.0.0 + + + zeppelin + org.apache.zeppelin + 0.8.2-mapr-1912-r2 + .. + + + org.apache.zeppelin + zeppelin-integration + jar + 0.8.2-mapr-1912-r2 + Zeppelin: Integration Test + + + + 3.1.0 + + + + UTF-8 + + + 3.8.1 + 3.7 + + + 2.16 + + + + + com.google.code.gson + gson + 2.8.2 + + + com.google.guava + guava + 23.0 + + + org.seleniumhq.selenium + selenium-java + ${selenium.java.version} + + + com.google.code.gson + gson + + + test + + + ${project.groupId} + zeppelin-zengine + ${project.version} + + + com.google.guava + guava + + + com.google.code.gson + gson + + + org.apache.hadoop + hadoop-common + + + test + + + + org.apache.commons + commons-lang3 + ${commons.lang3.version} + + + org.rauschig + jarchivelib + 0.7.1 + + + org.apache.commons + commons-compress + + + + + org.slf4j + slf4j-log4j12 + + + + + junit + junit + test + + + + + + + maven-failsafe-plugin + ${plugin.failsafe.version} + + + + integration-test + verify + + + + + -Xmx2048m + + + + maven-surefire-plugin + ${plugin.surefire.version} + + -Xmx2g -Xms1g -Dfile.encoding=UTF-8 + + ${tests.to.exclude} + + + 1 + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + start-zeppelin + pre-integration-test + + + + + + + + + run + + + + stop-zeppelin + post-integration-test + + + + + + + + + run + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + + + + using-source-tree + + true + + + + ../bin + + + + + using-packaged-distr + + false + + + + ../zeppelin-distribution/target/zeppelin-${project.version}/zeppelin-${project.version}/bin + + + + + + diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java similarity index 93% rename from zeppelin-server/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java rename to zeppelin-integration/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java index 475be50270c..e1992fb4d92 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java +++ b/zeppelin-integration/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; @@ -62,6 +63,10 @@ protected String getParagraphXPath(int paragraphNo) { return "(//div[@ng-controller=\"ParagraphCtrl\"])[" + paragraphNo + "]"; } + protected String getNoteFormsXPath() { + return "(//div[@id='noteForms'])"; + } + protected boolean waitForParagraph(final int paragraphNo, final String state) { By locator = By.xpath(getParagraphXPath(paragraphNo) + "//div[contains(@class, 'control')]//span[2][contains(.,'" + state + "')]"); @@ -98,10 +103,6 @@ public WebElement apply(WebDriver driver) { }); } - protected static boolean endToEndTestEnabled() { - return null != System.getenv("TEST_SELENIUM"); - } - protected void createNewNote() { clickAndWait(By.xpath("//div[contains(@class, \"col-md-4\")]/div/h5/a[contains(.,'Create new" + " note')]")); @@ -109,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) { @@ -129,10 +130,6 @@ protected void clickAndWait(final By locator) { protected void handleException(String message, Exception e) throws Exception { LOG.error(message, e); - LogEntries logEntries = driver.manage().logs().get(LogType.BROWSER); - for (LogEntry entry : logEntries) { - LOG.error(new Date(entry.getTimestamp()) + " " + entry.getLevel() + " " + entry.getMessage()); - } File scrFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); LOG.error("ScreenShot::\ndata:image/png;base64," + new String(Base64.encodeBase64(FileUtils.readFileToByteArray(scrFile)))); throw e; diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/CommandExecutor.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/CommandExecutor.java similarity index 100% rename from zeppelin-server/src/test/java/org/apache/zeppelin/CommandExecutor.java rename to zeppelin-integration/src/test/java/org/apache/zeppelin/CommandExecutor.java diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/ProcessData.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/ProcessData.java similarity index 93% rename from zeppelin-server/src/test/java/org/apache/zeppelin/ProcessData.java rename to zeppelin-integration/src/test/java/org/apache/zeppelin/ProcessData.java index 83f5f4c7443..2a05b1fe73d 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/ProcessData.java +++ b/zeppelin-integration/src/test/java/org/apache/zeppelin/ProcessData.java @@ -18,12 +18,16 @@ package org.apache.zeppelin; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; -import java.util.concurrent.TimeUnit; - public class ProcessData { public enum Types_Of_Data { OUTPUT, @@ -225,13 +229,16 @@ private void buildOutputAndErrorStreamData() throws IOException { (System.currentTimeMillis() > unconditionalExitTime)); this.checked_process.destroy(); try { - if ((System.currentTimeMillis() > unconditionalExitTime)) - LOG.error("!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@Unconditional exit occured@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@!\nsome process hag up for more than " + unconditionalExitDelayMinutes + " minutes."); + if ((System.currentTimeMillis() > unconditionalExitTime)) { + LOG.error( + "!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@Unconditional exit occured@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@!\nsome process hag up for more than " + + unconditionalExitDelayMinutes + " minutes."); + } LOG.error("!##################################!"); StringWriter sw = new StringWriter(); - new Exception("Exited from buildOutputAndErrorStreamData by timeout").printStackTrace(new PrintWriter(sw)); //Get stack trace - String exceptionAsString = sw.toString(); - LOG.error(exceptionAsString); + Exception e = new Exception("Exited from buildOutputAndErrorStreamData by timeout"); + e.printStackTrace(new PrintWriter(sw)); //Get stack trace + LOG.error(String.valueOf(e), e); } catch (Exception ignore) { LOG.info("Exception in ProcessData while buildOutputAndErrorStreamData ", ignore); } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/WebDriverManager.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/WebDriverManager.java similarity index 62% rename from zeppelin-server/src/test/java/org/apache/zeppelin/WebDriverManager.java rename to zeppelin-integration/src/test/java/org/apache/zeppelin/WebDriverManager.java index da34e7299a6..768113f7e25 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/WebDriverManager.java +++ b/zeppelin-integration/src/test/java/org/apache/zeppelin/WebDriverManager.java @@ -17,27 +17,32 @@ package org.apache.zeppelin; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.concurrent.TimeUnit; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.SystemUtils; import org.openqa.selenium.By; import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.firefox.FirefoxBinary; import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxDriver.SystemProperty; +import org.openqa.selenium.firefox.FirefoxOptions; import org.openqa.selenium.firefox.FirefoxProfile; +import org.openqa.selenium.firefox.GeckoDriverService; import org.openqa.selenium.safari.SafariDriver; import org.openqa.selenium.support.ui.ExpectedCondition; import org.openqa.selenium.support.ui.WebDriverWait; +import org.rauschig.jarchivelib.Archiver; +import org.rauschig.jarchivelib.ArchiverFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.fail; - public class WebDriverManager { @@ -45,6 +50,8 @@ public class WebDriverManager { private static String downLoadsDir = ""; + private static String GECKODRIVER_VERSION = "0.19.1"; + public static WebDriver getWebDriver() { WebDriver driver = null; @@ -60,12 +67,9 @@ public static WebDriver getWebDriver() { downLoadsDir = FileUtils.getTempDirectory().toString(); - String tempPath = downLoadsDir + "/firebug/"; - - downloadFireBug(firefoxVersion, tempPath); + String tempPath = downLoadsDir + "/firefox/"; - final String firebugPath = tempPath + "firebug.xpi"; - final String firepathPath = tempPath + "firepath.xpi"; + downloadGeekoDriver(firefoxVersion, tempPath); FirefoxProfile profile = new FirefoxProfile(); profile.setPreference("browser.download.folderList", 2); @@ -78,13 +82,17 @@ public static WebDriver getWebDriver() { profile.setPreference("app.update.enabled", false); profile.setPreference("dom.max_script_run_time", 0); profile.setPreference("dom.max_chrome_script_run_time", 0); - profile.setPreference("browser.helperApps.neverAsk.saveToDisk", "application/x-ustar,application/octet-stream,application/zip,text/csv,text/plain"); + profile.setPreference("browser.helperApps.neverAsk.saveToDisk", + "application/x-ustar,application/octet-stream,application/zip,text/csv,text/plain"); profile.setPreference("network.proxy.type", 0); - profile.addExtension(new File(firebugPath)); - profile.addExtension(new File(firepathPath)); + System.setProperty(GeckoDriverService.GECKO_DRIVER_EXE_PROPERTY, tempPath + "geckodriver"); + System.setProperty(SystemProperty.DRIVER_USE_MARIONETTE, "false"); - driver = new FirefoxDriver(ffox, profile); + FirefoxOptions firefoxOptions = new FirefoxOptions(); + firefoxOptions.setBinary(ffox); + firefoxOptions.setProfile(profile); + driver = new FirefoxDriver(firefoxOptions); } catch (Exception e) { LOG.error("Exception in WebDriverManager while FireFox Driver ", e); } @@ -141,40 +149,52 @@ public Boolean apply(WebDriver d) { fail(); } + driver.manage().window().maximize(); return driver; } - private static void downloadFireBug(int firefoxVersion, String tempPath) { - String firebugUrlString = null; - if (firefoxVersion < 23) - firebugUrlString = "http://getfirebug.com/releases/firebug/1.11/firebug-1.11.4.xpi"; - else if (firefoxVersion >= 23 && firefoxVersion < 30) - firebugUrlString = "http://getfirebug.com/releases/firebug/1.12/firebug-1.12.8.xpi"; - else if (firefoxVersion >= 30 && firefoxVersion < 33) - firebugUrlString = "http://getfirebug.com/releases/firebug/2.0/firebug-2.0.7.xpi"; - else if (firefoxVersion >= 33) - firebugUrlString = "http://getfirebug.com/releases/firebug/2.0/firebug-2.0.17.xpi"; + public static void downloadGeekoDriver(int firefoxVersion, String tempPath) { + String geekoDriverUrlString = + "https://github.com/mozilla/geckodriver/releases/download/v" + GECKODRIVER_VERSION + + "/geckodriver-v" + GECKODRIVER_VERSION + "-"; - - LOG.info("firebug version: " + firefoxVersion + ", will be downloaded to " + tempPath); + LOG.info("Geeko version: " + firefoxVersion + ", will be downloaded to " + tempPath); try { - File firebugFile = new File(tempPath + "firebug.xpi"); - URL firebugUrl = new URL(firebugUrlString); - if (!firebugFile.exists()) { - FileUtils.copyURLToFile(firebugUrl, firebugFile); + if (SystemUtils.IS_OS_WINDOWS) { + if (System.getProperty("sun.arch.data.model").equals("64")) { + geekoDriverUrlString += "win64.zip"; + } else { + geekoDriverUrlString += "win32.zip"; + } + } else if (SystemUtils.IS_OS_LINUX) { + if (System.getProperty("sun.arch.data.model").equals("64")) { + geekoDriverUrlString += "linux64.tar.gz"; + } else { + geekoDriverUrlString += "linux32.tar.gz"; + } + } else if (SystemUtils.IS_OS_MAC_OSX) { + geekoDriverUrlString += "macos.tar.gz"; } - - File firepathFile = new File(tempPath + "firepath.xpi"); - URL firepathUrl = new URL("https://addons.cdn.mozilla.net/user-media/addons/11900/firepath-0.9.7.1-fx.xpi"); - if (!firepathFile.exists()) { - FileUtils.copyURLToFile(firepathUrl, firepathFile); + File geekoDriver = new File(tempPath + "geckodriver"); + File geekoDriverZip = new File(tempPath + "geckodriver.tar"); + File geekoDriverDir = new File(tempPath); + URL geekoDriverUrl = new URL(geekoDriverUrlString); + if (!geekoDriver.exists()) { + FileUtils.copyURLToFile(geekoDriverUrl, geekoDriverZip); + if (SystemUtils.IS_OS_WINDOWS) { + Archiver archiver = ArchiverFactory.createArchiver("zip"); + archiver.extract(geekoDriverZip, geekoDriverDir); + } else { + Archiver archiver = ArchiverFactory.createArchiver("tar", "gz"); + archiver.extract(geekoDriverZip, geekoDriverDir); + } } } catch (IOException e) { - LOG.error("Download of firebug version: " + firefoxVersion + ", falied in path " + tempPath); + LOG.error("Download of Geeko version: " + firefoxVersion + ", falied in path " + tempPath); } - LOG.info("Download of firebug version: " + firefoxVersion + ", successful"); + LOG.info("Download of Geeko version: " + firefoxVersion + ", successful"); } public static int getFirefoxVersion() { @@ -183,8 +203,10 @@ public static int getFirefoxVersion() { if (System.getProperty("os.name").startsWith("Mac OS")) { firefoxVersionCmd = "/Applications/Firefox.app/Contents/MacOS/" + firefoxVersionCmd; } - String versionString = (String) CommandExecutor.executeCommandLocalHost(firefoxVersionCmd, false, ProcessData.Types_Of_Data.OUTPUT); - return Integer.valueOf(versionString.replaceAll("Mozilla Firefox", "").trim().substring(0, 2)); + String versionString = (String) CommandExecutor + .executeCommandLocalHost(firefoxVersionCmd, false, ProcessData.Types_Of_Data.OUTPUT); + return Integer + .valueOf(versionString.replaceAll("Mozilla Firefox", "").trim().substring(0, 2)); } catch (Exception e) { LOG.error("Exception in WebDriverManager while getWebDriver ", e); return -1; diff --git a/zeppelin-integration/src/test/java/org/apache/zeppelin/ZeppelinITUtils.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/ZeppelinITUtils.java new file mode 100644 index 00000000000..402a18d4270 --- /dev/null +++ b/zeppelin-integration/src/test/java/org/apache/zeppelin/ZeppelinITUtils.java @@ -0,0 +1,60 @@ +/* + * 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; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.openqa.selenium.WebDriver; +import java.util.concurrent.TimeUnit; + +public class ZeppelinITUtils { + + public final static Logger LOG = LoggerFactory.getLogger(ZeppelinITUtils.class); + + public static void sleep(long millis, boolean logOutput) { + if (logOutput) { + LOG.info("Starting sleeping for " + (millis / 1000) + " seconds..."); + LOG.info("Caller: " + Thread.currentThread().getStackTrace()[2]); + } + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + LOG.error("Exception in WebDriverManager while getWebDriver ", e); + } + if (logOutput) { + LOG.info("Finished."); + } + } + + public static void restartZeppelin() { + CommandExecutor.executeCommandLocalHost("../bin/zeppelin-daemon.sh restart", + false, ProcessData.Types_Of_Data.OUTPUT); + //wait for server to start. + sleep(5000, false); + } + + public static void turnOffImplicitWaits(WebDriver driver) { + driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS); + } + + public static void turnOnImplicitWaits(WebDriver driver) { + driver.manage().timeouts().implicitlyWait(AbstractZeppelinIT.MAX_IMPLICIT_WAIT, + TimeUnit.SECONDS); + } +} diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java similarity index 79% rename from zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java rename to zeppelin-integration/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java index f87bff2ce5a..ea6ad692044 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java +++ b/zeppelin-integration/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" + + "anyofrolesuser = org.apache.zeppelin.utils.AnyOfRolesUserAuthorizationFilter\n" + "[roles]\n" + "admin = *\n" + "hr = *\n" + "finance = *\n" + "[urls]\n" + "/api/version = anon\n" + + "/api/interpreter/** = authc, anyofrolesuser[admin, finance]\n" + "/** = authc"; static String originalShiro = ""; @@ -76,12 +78,8 @@ public class AuthenticationIT extends AbstractZeppelinIT { @BeforeClass public static void startUp() { - if (!endToEndTestEnabled()) { - return; - } - try { - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName(), "../"); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName(), new File("../").getAbsolutePath()); ZeppelinConfiguration conf = ZeppelinConfiguration.create(); shiroPath = conf.getRelativeDir(String.format("%s/shiro.ini", conf.getConfDir())); File file = new File(shiroPath); @@ -99,9 +97,6 @@ public static void startUp() { @AfterClass public static void tearDown() { - if (!endToEndTestEnabled()) { - return; - } try { if (!StringUtils.isBlank(shiroPath)) { File file = new File(shiroPath); @@ -131,9 +126,6 @@ public void authenticationUser(String userName, String password) { } private void testShowNotebookListOnNavbar() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { pollingWait(By.xpath("//li[@class='dropdown notebook-list-dropdown']"), MAX_BROWSER_TIMEOUT_SEC).click(); @@ -165,9 +157,6 @@ public void logoutUser(String userName) throws URISyntaxException { // @Test public void testSimpleAuthentication() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { AuthenticationIT authenticationIT = new AuthenticationIT(); authenticationIT.authenticationUser("admin", "password1"); @@ -183,10 +172,60 @@ public void testSimpleAuthentication() throws Exception { } @Test - public void testGroupPermission() throws Exception { - if (!endToEndTestEnabled()) { - return; + public void testAnyOfRolesUser() throws Exception { + 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 testAnyOfRolesUser ", e); } + } + + @Test + public void testGroupPermission() throws Exception { try { AuthenticationIT authenticationIT = new AuthenticationIT(); authenticationIT.authenticationUser("finance1", "finance1"); @@ -200,6 +239,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) @@ -254,7 +295,7 @@ public void testGroupPermission() throws Exception { } catch (Exception e) { - handleException("Exception in ParagraphActionsIT while testGroupPermission ", e); + handleException("Exception in AuthenticationIT while testGroupPermission ", e); } } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/InterpreterIT.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/InterpreterIT.java similarity index 94% rename from zeppelin-server/src/test/java/org/apache/zeppelin/integration/InterpreterIT.java rename to zeppelin-integration/src/test/java/org/apache/zeppelin/integration/InterpreterIT.java index 6adc1f73a25..d2e31a0a573 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/InterpreterIT.java +++ b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/InterpreterIT.java @@ -39,25 +39,16 @@ public class InterpreterIT extends AbstractZeppelinIT { @Before public void startUp() { - if (!endToEndTestEnabled()) { - return; - } driver = WebDriverManager.getWebDriver(); } @After public void tearDown() { - if (!endToEndTestEnabled()) { - return; - } driver.quit(); } @Test public void testShowDescriptionOnInterpreterCreate() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { // navigate to interpreter page WebElement settingButton = driver.findElement(By.xpath("//button[@class='nav-btn dropdown-toggle ng-scope']")); @@ -79,4 +70,4 @@ public void testShowDescriptionOnInterpreterCreate() throws Exception { handleException("Exception in InterpreterIT while testShowDescriptionOnInterpreterCreate ", e); } } -} \ No newline at end of file +} diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/InterpreterModeActionsIT.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/InterpreterModeActionsIT.java similarity index 98% rename from zeppelin-server/src/test/java/org/apache/zeppelin/integration/InterpreterModeActionsIT.java rename to zeppelin-integration/src/test/java/org/apache/zeppelin/integration/InterpreterModeActionsIT.java index 9bfeae01846..1f18a658826 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/InterpreterModeActionsIT.java +++ b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/InterpreterModeActionsIT.java @@ -72,17 +72,14 @@ 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"; @BeforeClass public static void startUp() { - if (!endToEndTestEnabled()) { - return; - } try { - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName(), "../"); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName(), new File("../").getAbsolutePath()); ZeppelinConfiguration conf = ZeppelinConfiguration.create(); shiroPath = conf.getRelativeDir(String.format("%s/shiro.ini", conf.getConfDir())); interpreterOptionPath = conf.getRelativeDir(String.format("%s/interpreter.json", conf.getConfDir())); @@ -105,9 +102,6 @@ public static void startUp() { @AfterClass public static void tearDown() { - if (!endToEndTestEnabled()) { - return; - } try { if (!StringUtils.isBlank(shiroPath)) { File shiroFile = new File(shiroPath); @@ -145,19 +139,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) { @@ -174,9 +168,6 @@ private void setPythonParagraph(int num, String text) { @Test public void testGloballyAction() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { //step 1: (admin) login, set 'globally in shared' mode of python interpreter, logout InterpreterModeActionsIT interpreterModeActionsIT = new InterpreterModeActionsIT(); @@ -199,7 +190,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' @@ -327,9 +317,6 @@ public void testGloballyAction() throws Exception { @Test public void testPerUserScopedAction() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { //step 1: (admin) login, set 'Per user in scoped' mode of python interpreter, logout InterpreterModeActionsIT interpreterModeActionsIT = new InterpreterModeActionsIT(); @@ -612,9 +599,6 @@ public void testPerUserScopedAction() throws Exception { @Test public void testPerUserIsolatedAction() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { //step 1: (admin) login, set 'Per user in isolated' mode of python interpreter, logout InterpreterModeActionsIT interpreterModeActionsIT = new InterpreterModeActionsIT(); diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java similarity index 82% rename from zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java rename to zeppelin-integration/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java index 0911bf78b0c..f342e5af6d2 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java +++ b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java @@ -45,26 +45,16 @@ public class ParagraphActionsIT extends AbstractZeppelinIT { @Before public void startUp() { - if (!endToEndTestEnabled()) { - return; - } driver = WebDriverManager.getWebDriver(); } @After public void tearDown() { - if (!endToEndTestEnabled()) { - return; - } - driver.quit(); } @Test public void testCreateNewButton() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); Actions action = new Actions(driver); @@ -133,9 +123,6 @@ public void testCreateNewButton() throws Exception { @Test public void testRemoveButton() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); @@ -170,9 +157,6 @@ public void testRemoveButton() throws Exception { @Test public void testMoveUpAndDown() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); @@ -223,9 +207,6 @@ public void testMoveUpAndDown() throws Exception { @Test public void testDisableParagraphRunButton() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); @@ -257,9 +238,6 @@ public void testDisableParagraphRunButton() throws Exception { @Test public void testRunOnSelectionChange() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { String xpathToRunOnSelectionChangeCheckbox = getParagraphXPath(1) + "//ul/li/form/input[contains(@ng-checked, 'true')]"; String xpathToDropdownMenu = getParagraphXPath(1) + "//select"; @@ -313,9 +291,6 @@ public void testRunOnSelectionChange() throws Exception { @Test public void testClearOutputButton() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); @@ -345,9 +320,6 @@ public void testClearOutputButton() throws Exception { @Test public void testWidth() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); waitForParagraph(1, "READY"); @@ -372,9 +344,6 @@ public void testWidth() throws Exception { @Test public void testFontSize() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); waitForParagraph(1, "READY"); @@ -400,9 +369,6 @@ public void testFontSize() throws Exception { @Test public void testTitleButton() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); @@ -471,9 +437,6 @@ public void testTitleButton() throws Exception { @Test public void testShowAndHideLineNumbers() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); @@ -517,11 +480,8 @@ public void testShowAndHideLineNumbers() throws Exception { } } - @Test + // @Test public void testEditOnDoubleClick() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); Actions action = new Actions(driver); @@ -568,9 +528,6 @@ public void testEditOnDoubleClick() throws Exception { @Test public void testSingleDynamicFormTextInput() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); @@ -604,9 +561,6 @@ public void testSingleDynamicFormTextInput() throws Exception { @Test public void testSingleDynamicFormSelectForm() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); @@ -644,9 +598,6 @@ public void testSingleDynamicFormSelectForm() throws Exception { @Test public void testSingleDynamicFormCheckboxForm() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); @@ -687,9 +638,6 @@ public void testSingleDynamicFormCheckboxForm() throws Exception { @Test public void testMultipleDynamicFormsSameType() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); @@ -725,4 +673,152 @@ public void testMultipleDynamicFormsSameType() throws Exception { handleException("Exception in ParagraphActionsIT while testMultipleDynamicFormsSameType ", e); } } -} \ No newline at end of file + + @Test + public void testNoteDynamicFormTextInput() throws Exception { + try { + createNewNote(); + + setTextOfParagraph(1, "%spark println(\"Hello \"+z.noteTextbox(\"name\", \"world\")) "); + + runParagraph(1); + waitForParagraph(1, "FINISHED"); + collector.checkThat("Output text is equal to value specified initially", driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(), CoreMatchers.equalTo("Hello world")); + driver.findElement(By.xpath(getNoteFormsXPath() + "//input")).clear(); + driver.findElement(By.xpath(getNoteFormsXPath() + "//input")).sendKeys("Zeppelin"); + driver.findElement(By.xpath(getNoteFormsXPath() + "//input")).sendKeys(Keys.RETURN); + + collector.checkThat("After new data in text input form, output should not be changed", + driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(), + CoreMatchers.equalTo("Hello world")); + + runParagraph(1); + waitForParagraph(1, "FINISHED"); + collector.checkThat("Only after running the paragraph, we can see the newly updated output", + driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(), + CoreMatchers.equalTo("Hello Zeppelin")); + + setTextOfParagraph(2, "%spark println(\"Hello \"+z.noteTextbox(\"name\", \"world\")) "); + runParagraph(2); + waitForParagraph(2, "FINISHED"); + collector.checkThat("Running the another paragraph with same form, we can see value from note form", + driver.findElement(By.xpath(getParagraphXPath(2) + "//div[contains(@class, 'text plainTextContent')]")).getText(), + CoreMatchers.equalTo("Hello Zeppelin")); + + deleteTestNotebook(driver); + + } catch (Exception e) { + handleException("Exception in ParagraphActionsIT while testNoteDynamicFormTextInput ", e); + } + } + + @Test + public void testNoteDynamicFormSelect() throws Exception { + try { + createNewNote(); + + setTextOfParagraph(1, "%spark println(\"Howdy \"+z.noteSelect(\"names\", Seq((\"1\",\"Alice\"), " + + "(\"2\",\"Bob\"),(\"3\",\"stranger\"))))"); + + runParagraph(1); + waitForParagraph(1, "FINISHED"); + collector.checkThat("Output text should not display any of the options in select form", + driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(), + CoreMatchers.equalTo("Howdy ")); + + Select dropDownMenu = new Select(driver.findElement(By.xpath("(" + (getNoteFormsXPath() + "//select)[1]")))); + + dropDownMenu.selectByVisibleText("Bob"); + collector.checkThat("After selection in drop down menu, output should not be changed", + driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(), + CoreMatchers.equalTo("Howdy ")); + + runParagraph(1); + waitForParagraph(1, "FINISHED"); + + collector.checkThat("After run paragraph again, we can see the newly updated output", + driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(), + CoreMatchers.equalTo("Howdy 2")); + + setTextOfParagraph(2, "%spark println(\"Howdy \"+z.noteSelect(\"names\", Seq((\"1\",\"Alice\"), " + + "(\"2\",\"Bob\"),(\"3\",\"stranger\"))))"); + + runParagraph(2); + waitForParagraph(2, "FINISHED"); + + collector.checkThat("Running the another paragraph with same form, we can see value from note form", + driver.findElement(By.xpath(getParagraphXPath(2) + "//div[contains(@class, 'text plainTextContent')]")).getText(), + CoreMatchers.equalTo("Howdy 2")); + + deleteTestNotebook(driver); + + } catch (Exception e) { + handleException("Exception in ParagraphActionsIT while testNoteDynamicFormSelect ", e); + } + } + + @Test + public void testDynamicNoteFormCheckbox() throws Exception { + try { + createNewNote(); + + setTextOfParagraph(1, "%spark val options = Seq((\"han\",\"Han\"), (\"leia\",\"Leia\"), " + + "(\"luke\",\"Luke\")); println(\"Greetings \"+z.noteCheckbox(\"skywalkers\",options).mkString(\" and \"))"); + + runParagraph(1); + waitForParagraph(1, "FINISHED"); + collector.checkThat("Output text should display all of the options included in check boxes", + driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(), + CoreMatchers.containsString("Greetings han and leia and luke")); + + WebElement firstCheckbox = driver.findElement(By.xpath("(" + getNoteFormsXPath() + "//input[@type='checkbox'])[1]")); + firstCheckbox.click(); + collector.checkThat("After unchecking one of the boxes, output should not be changed", + driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(), + CoreMatchers.containsString("Greetings han and leia and luke")); + + runParagraph(1); + waitForParagraph(1, "FINISHED"); + + collector.checkThat("After run paragraph again, we can see the newly updated output", + driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(), + CoreMatchers.containsString("Greetings leia and luke")); + + setTextOfParagraph(2, "%spark val options = Seq((\"han\",\"Han\"), (\"leia\",\"Leia\"), " + + "(\"luke\",\"Luke\")); println(\"Greetings \"+z.noteCheckbox(\"skywalkers\",options).mkString(\" and \"))"); + + runParagraph(2); + waitForParagraph(2, "FINISHED"); + + collector.checkThat("Running the another paragraph with same form, we can see value from note form", + driver.findElement(By.xpath(getParagraphXPath(2) + "//div[contains(@class, 'text plainTextContent')]")).getText(), + CoreMatchers.containsString("Greetings leia and luke")); + + deleteTestNotebook(driver); + + } catch (Exception e) { + handleException("Exception in ParagraphActionsIT while testDynamicNoteFormCheckbox ", e); + } + } + + @Test + public void testWithNoteAndParagraphDynamicFormTextInput() throws Exception { + try { + createNewNote(); + + setTextOfParagraph(1, "%spark println(z.noteTextbox(\"name\", \"note\") + \" \" + z.textbox(\"name\", \"paragraph\")) "); + + runParagraph(1); + waitForParagraph(1, "FINISHED"); + + collector.checkThat("After run paragraph, we can see computed output from two forms", + driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(), + CoreMatchers.equalTo("note paragraph")); + + deleteTestNotebook(driver); + + } catch (Exception e) { + handleException("Exception in ParagraphActionsIT while testWithNoteAndParagraphDynamicFormTextInput ", e); + } + } +} diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/PersonalizeActionsIT.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/PersonalizeActionsIT.java similarity index 98% rename from zeppelin-server/src/test/java/org/apache/zeppelin/integration/PersonalizeActionsIT.java rename to zeppelin-integration/src/test/java/org/apache/zeppelin/integration/PersonalizeActionsIT.java index b813ea9ec4d..31335643e6b 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/PersonalizeActionsIT.java +++ b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/PersonalizeActionsIT.java @@ -70,11 +70,8 @@ public class PersonalizeActionsIT extends AbstractZeppelinIT { @BeforeClass public static void startUp() { - if (!endToEndTestEnabled()) { - return; - } try { - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName(), "../"); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName(), new File("../").getAbsolutePath()); ZeppelinConfiguration conf = ZeppelinConfiguration.create(); shiroPath = conf.getRelativeDir(String.format("%s/shiro.ini", conf.getConfDir())); File file = new File(shiroPath); @@ -91,9 +88,6 @@ public static void startUp() { @AfterClass public static void tearDown() { - if (!endToEndTestEnabled()) { - return; - } try { if (!StringUtils.isBlank(shiroPath)) { File file = new File(shiroPath); @@ -118,9 +112,6 @@ private void setParagraphText(String text) { @Test public void testSimpleAction() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { // step 1 : (admin) create a new note, run a paragraph and turn on personalized mode AuthenticationIT authenticationIT = new AuthenticationIT(); @@ -197,9 +188,6 @@ public void testSimpleAction() throws Exception { @Test public void testGraphAction() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { // step 1 : (admin) create a new note, run a paragraph, change active graph to 'Bar chart', turn on personalized mode AuthenticationIT authenticationIT = new AuthenticationIT(); @@ -273,9 +261,6 @@ public void testGraphAction() throws Exception { @Test public void testDynamicFormAction() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { // step 1 : (admin) login, create a new note, run a paragraph with data of spark tutorial, logout. AuthenticationIT authenticationIT = new AuthenticationIT(); diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java similarity index 95% rename from zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java rename to zeppelin-integration/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java index 9b651c1f16f..1804fc4cb24 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java +++ b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java @@ -42,9 +42,6 @@ public class SparkParagraphIT extends AbstractZeppelinIT { @Before public void startUp() { - if (!endToEndTestEnabled()) { - return; - } driver = WebDriverManager.getWebDriver(); createNewNote(); waitForParagraph(1, "READY"); @@ -52,18 +49,12 @@ public void startUp() { @After public void tearDown() { - if (!endToEndTestEnabled()) { - return; - } deleteTestNotebook(driver); driver.quit(); } @Test public void testSpark() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { setTextOfParagraph(1, "sc.version"); runParagraph(1); @@ -116,9 +107,6 @@ case class Bank(age: Integer, job: String, marital: String, education: String, b @Test public void testPySpark() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { setTextOfParagraph(1, "%pyspark\\n" + "for x in range(0, 3):\\n" + @@ -138,7 +126,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 @@ -167,9 +155,6 @@ public void testPySpark() throws Exception { @Test public void testSqlSpark() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { setTextOfParagraph(1,"%sql\\n" + "select * from bank limit 1"); @@ -199,11 +184,8 @@ public void testSqlSpark() throws Exception { } } - @Test +// @Test public void testDep() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { // restart spark interpreter before running %dep clickAndWait(By.xpath("//span[@uib-tooltip='Interpreter binding']")); diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java similarity index 97% rename from zeppelin-server/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java rename to zeppelin-integration/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java index afdae10b66e..4d14641ecff 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java +++ b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java @@ -57,26 +57,16 @@ public class ZeppelinIT extends AbstractZeppelinIT { @Before public void startUp() { - if (!endToEndTestEnabled()) { - return; - } driver = WebDriverManager.getWebDriver(); } @After public void tearDown() { - if (!endToEndTestEnabled()) { - return; - } - driver.quit(); } @Test public void testAngularDisplay() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { createNewNote(); @@ -209,9 +199,6 @@ public void testAngularDisplay() throws Exception { @Test public void testSparkInterpreterDependencyLoading() throws Exception { - if (!endToEndTestEnabled()) { - return; - } try { // navigate to interpreter page WebElement settingButton = driver.findElement(By.xpath("//button[@class='nav-btn dropdown-toggle ng-scope']")); @@ -279,10 +266,6 @@ public void testSparkInterpreterDependencyLoading() throws Exception { @Test public void testAngularRunParagraph() throws Exception { - if (!endToEndTestEnabled()) { - return; - } - try { createNewNote(); diff --git a/zeppelin-integration/src/test/resources/log4j.properties b/zeppelin-integration/src/test/resources/log4j.properties new file mode 100644 index 00000000000..83689930c1a --- /dev/null +++ b/zeppelin-integration/src/test/resources/log4j.properties @@ -0,0 +1,46 @@ +# +# 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.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 diff --git a/zeppelin-interpreter-integration/src/main/java/org/apache/zeppelin/interpreter/integration/DummyClass.java b/zeppelin-interpreter-integration/src/main/java/org/apache/zeppelin/interpreter/integration/DummyClass.java new file mode 100644 index 00000000000..1df4618cd7d --- /dev/null +++ b/zeppelin-interpreter-integration/src/main/java/org/apache/zeppelin/interpreter/integration/DummyClass.java @@ -0,0 +1,21 @@ +/* + * 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.integration; + +public class DummyClass { +} diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index 109099cfc7b..f684f7759f3 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -24,26 +24,27 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-interpreter jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Interpreter Zeppelin Interpreter + 3.7 2.3 1.3 3.0 1.12 3.0.3 1.0 - 2.12.1 + 2.14.3 2.3 @@ -66,6 +67,11 @@ gson-extras + + commons-configuration + commons-configuration + + org.apache.commons commons-exec @@ -79,8 +85,9 @@ - commons-lang - commons-lang + org.apache.commons + commons-lang3 + ${commons.lang3.version} @@ -132,6 +139,12 @@ ${aether.version} + + org.bouncycastle + bcpkix-jdk15on + 1.52 + + org.apache.maven maven-aether-provider @@ -214,11 +227,6 @@ ${jline.version} - - com.google.guava - guava - - junit junit @@ -231,4 +239,28 @@ test + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + false + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/completer/CompletionType.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/completer/CompletionType.java index 20cceda8127..fc5f3809bab 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/completer/CompletionType.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/completer/CompletionType.java @@ -24,5 +24,6 @@ public enum CompletionType { setting, command, keyword, - path + path, + universe } 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 78% 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 9822ecf2c8f..5b4ebc052cb 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. * @@ -46,11 +45,29 @@ public class ZeppelinConfiguration extends XMLConfiguration { "https://s3.amazonaws.com/helium-package/helium.json"; private static ZeppelinConfiguration conf; + private Map properties = new HashMap<>(); + public ZeppelinConfiguration(URL url) throws ConfigurationException { setDelimiterParsingDisabled(true); load(url); + initProperties(); + } + + private void initProperties() { + List nodes = getRootNode().getChildren(); + if (nodes == null || nodes.isEmpty()) { + return; + } + for (ConfigurationNode p : nodes) { + String name = (String) p.getChildren("name").get(0).getValue(); + String value = (String) p.getChildren("value").get(0).getValue(); + if (!StringUtils.isEmpty(name)) { + properties.put(name, value); + } + } } + public ZeppelinConfiguration() { ConfVars[] vars = ConfVars.values(); for (ConfVars v : vars) { @@ -123,71 +140,41 @@ public static synchronized ZeppelinConfiguration create() { private String getStringValue(String name, String d) { - List properties = getRootNode().getChildren(); - if (properties == null || properties.isEmpty()) { - return d; - } - for (ConfigurationNode p : properties) { - if (p.getChildren("name") != null && !p.getChildren("name").isEmpty() - && name.equals(p.getChildren("name").get(0).getValue())) { - return (String) p.getChildren("value").get(0).getValue(); - } + String value = this.properties.get(name); + if (value != null) { + return value; } return d; } private int getIntValue(String name, int d) { - List properties = getRootNode().getChildren(); - if (properties == null || properties.isEmpty()) { - return d; - } - for (ConfigurationNode p : properties) { - if (p.getChildren("name") != null && !p.getChildren("name").isEmpty() - && name.equals(p.getChildren("name").get(0).getValue())) { - return Integer.parseInt((String) p.getChildren("value").get(0).getValue()); - } + String value = this.properties.get(name); + if (value != null) { + return Integer.parseInt(value); } return d; } private long getLongValue(String name, long d) { - List properties = getRootNode().getChildren(); - if (properties == null || properties.isEmpty()) { - return d; - } - for (ConfigurationNode p : properties) { - if (p.getChildren("name") != null && !p.getChildren("name").isEmpty() - && name.equals(p.getChildren("name").get(0).getValue())) { - return Long.parseLong((String) p.getChildren("value").get(0).getValue()); - } + String value = this.properties.get(name); + if (value != null) { + return Long.parseLong(value); } return d; } private float getFloatValue(String name, float d) { - List properties = getRootNode().getChildren(); - if (properties == null || properties.isEmpty()) { - return d; - } - for (ConfigurationNode p : properties) { - if (p.getChildren("name") != null && !p.getChildren("name").isEmpty() - && name.equals(p.getChildren("name").get(0).getValue())) { - return Float.parseFloat((String) p.getChildren("value").get(0).getValue()); - } + String value = this.properties.get(name); + if (value != null) { + return Float.parseFloat(value); } return d; } private boolean getBooleanValue(String name, boolean d) { - List properties = getRootNode().getChildren(); - if (properties == null || properties.isEmpty()) { - return d; - } - for (ConfigurationNode p : properties) { - if (p.getChildren("name") != null && !p.getChildren("name").isEmpty() - && name.equals(p.getChildren("name").get(0).getValue())) { - return Boolean.parseBoolean((String) p.getChildren("value").get(0).getValue()); - } + String value = this.properties.get(name); + if (value != null) { + return Boolean.parseBoolean(value); } return d; } @@ -266,6 +253,10 @@ public boolean getBoolean(String envName, String propertyName, boolean defaultVa return getBooleanValue(propertyName, defaultValue); } + public String getZeppelinHome() { + return getString(ConfVars.ZEPPELIN_HOME); + } + public boolean useSsl() { return getBoolean(ConfVars.ZEPPELIN_SSL); } @@ -356,15 +347,32 @@ public String getNotebookDir() { return getString(ConfVars.ZEPPELIN_NOTEBOOK_DIR); } - public String getUser() { + 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 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); } @@ -384,6 +392,10 @@ public boolean isS3ServerSideEncryption() { return getBoolean(ConfVars.ZEPPELIN_NOTEBOOK_S3_SSE); } + public String getS3SignerOverride() { + return getString(ConfVars.ZEPPELIN_NOTEBOOK_S3_SIGNEROVERRIDE); + } + public String getMongoUri() { return getString(ConfVars.ZEPPELIN_NOTEBOOK_MONGO_URI); } @@ -413,7 +425,7 @@ public String getInterpreterJson() { } public String getInterpreterSettingPath() { - return getRelativeDir(String.format("%s/interpreter.json", getConfDir())); + return getConfigFSDir() + "/interpreter.json"; } public String getHeliumConfPath() { @@ -437,13 +449,17 @@ public String getHeliumYarnInstallerUrl() { } public String getNotebookAuthorizationPath() { - return getRelativeDir(String.format("%s/notebook-authorization.json", getConfDir())); + return getConfigFSDir() + "/notebook-authorization.json"; } public Boolean credentialsPersist() { return getBoolean(ConfVars.ZEPPELIN_CREDENTIALS_PERSIST); } + public String getCredentialsEncryptKey() { + return getString(ConfVars.ZEPPELIN_CREDENTIALS_ENCRYPT_KEY); + } + public String getCredentialsPath() { return getRelativeDir(String.format("%s/credentials.json", getConfDir())); } @@ -477,6 +493,14 @@ public String getRelativeDir(String path) { } } + public String getCallbackPortRange() { + return getString(ConfVars.ZEPPELIN_INTERPRETER_CALLBACK_PORTRANGE); + } + + public String getInterpreterPortRange() { + return getString(ConfVars.ZEPPELIN_INTERPRETER_PORTRANGE); + } + public boolean isWindowsPath(String path){ return path.matches("^[A-Za-z]:\\\\.*"); } @@ -485,12 +509,32 @@ public boolean isAnonymousAllowed() { return getBoolean(ConfVars.ZEPPELIN_ANONYMOUS_ALLOWED); } - public boolean isNotebokPublic() { + public boolean isUsernameForceLowerCase() { + return getBoolean(ConfVars.ZEPPELIN_USERNAME_FORCE_LOWERCASE); + } + + public boolean isNotebookPublic() { return getBoolean(ConfVars.ZEPPELIN_NOTEBOOK_PUBLIC); } public String getConfDir() { - return getString(ConfVars.ZEPPELIN_CONF_DIR); + return getRelativeDir(ConfVars.ZEPPELIN_CONF_DIR); + } + + public String getConfigFSDir() { + String fsConfigDir = getString(ConfVars.ZEPPELIN_CONFIG_FS_DIR); + 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 getConfDir(); + } + if (getString(ConfVars.ZEPPELIN_CONFIG_STORAGE_CLASS) + .equals("org.apache.zeppelin.storage.LocalConfigStorage")) { + // only apply getRelativeDir when it is LocalConfigStorage + return getRelativeDir(fsConfigDir); + } else { + return fsConfigDir; + } } public List getAllowedOrigins() @@ -510,6 +554,10 @@ public String getJettyName() { return getString(ConfVars.ZEPPELIN_SERVER_JETTY_NAME); } + public Integer getJettyRequestHeaderSize() { + return getInt(ConfVars.ZEPPELIN_SERVER_JETTY_REQUEST_HEADER_SIZE); + } + public String getXFrameOptions() { return getString(ConfVars.ZEPPELIN_SERVER_XFRAME_OPTIONS); @@ -523,12 +571,39 @@ public String getStrictTransport() { return getString(ConfVars.ZEPPELIN_SERVER_STRICT_TRANSPORT); } + 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 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<>(); - for (ZeppelinConfiguration.ConfVars v : ZeppelinConfiguration.ConfVars.values()) { + for (ConfVars v : ConfVars.values()) { String key = v.getVarName(); if (!predicate.apply(key)) { @@ -568,7 +643,7 @@ public interface ConfigurationKeyPredicate { */ public static enum ConfVars { ZEPPELIN_HOME("zeppelin.home", "./"), - ZEPPELIN_ADDR("zeppelin.server.addr", "0.0.0.0"), + ZEPPELIN_ADDR("zeppelin.server.addr", "127.0.0.1"), ZEPPELIN_PORT("zeppelin.server.port", 8080), ZEPPELIN_SERVER_CONTEXT_PATH("zeppelin.server.context.path", "/"), ZEPPELIN_SSL("zeppelin.ssl", false), @@ -620,7 +695,9 @@ public static enum ConfVars { + "org.apache.zeppelin.bigquery.BigQueryInterpreter," + "org.apache.zeppelin.beam.BeamInterpreter," + "org.apache.zeppelin.scio.ScioInterpreter," - + "org.apache.zeppelin.groovy.GroovyInterpreter" + + "org.apache.zeppelin.groovy.GroovyInterpreter," + + "org.apache.zeppelin.neo4j.Neo4jCypherInterpreter," + + "org.apache.zeppelin.sap.UniverseInterpreter" ), ZEPPELIN_INTERPRETER_JSON("zeppelin.interpreter.setting", "interpreter-setting.json"), ZEPPELIN_INTERPRETER_DIR("zeppelin.interpreter.dir", "interpreter"), @@ -631,14 +708,20 @@ public static enum ConfVars { ZEPPELIN_INTERPRETER_MAX_POOL_SIZE("zeppelin.interpreter.max.poolsize", 10), ZEPPELIN_INTERPRETER_GROUP_ORDER("zeppelin.interpreter.group.order", "spark,md,angular,sh," + "livy,alluxio,file,psql,flink,python,ignite,lens,cassandra,geode,kylin,elasticsearch," - + "scalding,jdbc,hbase,bigquery,beam,pig,scio,groovy"), + + "scalding,jdbc,hbase,bigquery,beam,pig,scio,groovy,neo4j"), ZEPPELIN_INTERPRETER_OUTPUT_LIMIT("zeppelin.interpreter.output.limit", 1024 * 100), + ZEPPELIN_INTERPRETER_SCHEDULER_POOL_SIZE("zeppelin.scheduler.threadpool.size", 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 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"), @@ -646,6 +729,7 @@ public static enum ConfVars { ZEPPELIN_NOTEBOOK_S3_KMS_KEY_ID("zeppelin.notebook.s3.kmsKeyID", null), ZEPPELIN_NOTEBOOK_S3_KMS_KEY_REGION("zeppelin.notebook.s3.kmsKeyRegion", null), ZEPPELIN_NOTEBOOK_S3_SSE("zeppelin.notebook.s3.sse", false), + ZEPPELIN_NOTEBOOK_S3_SIGNEROVERRIDE("zeppelin.notebook.s3.signerOverride", null), ZEPPELIN_NOTEBOOK_AZURE_CONNECTION_STRING("zeppelin.notebook.azure.connectionString", null), ZEPPELIN_NOTEBOOK_AZURE_SHARE("zeppelin.notebook.azure.share", "zeppelin"), ZEPPELIN_NOTEBOOK_AZURE_USER("zeppelin.notebook.azure.user", "user"), @@ -653,7 +737,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), @@ -663,6 +748,9 @@ public static enum ConfVars { // Decide when new note is created, interpreter settings will be binded automatically or not. ZEPPELIN_NOTEBOOK_AUTO_INTERPRETER_BINDING("zeppelin.notebook.autoInterpreterBinding", true), 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.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", @@ -675,13 +763,38 @@ public static enum ConfVars { // i.e. http://localhost:8080 ZEPPELIN_ALLOWED_ORIGINS("zeppelin.server.allowed.origins", "*"), ZEPPELIN_ANONYMOUS_ALLOWED("zeppelin.anonymous.allowed", true), + ZEPPELIN_USERNAME_FORCE_LOWERCASE("zeppelin.username.force.lowercase", false), ZEPPELIN_CREDENTIALS_PERSIST("zeppelin.credentials.persist", true), + ZEPPELIN_CREDENTIALS_ENCRYPT_KEY("zeppelin.credentials.encryptKey", null), ZEPPELIN_WEBSOCKET_MAX_TEXT_MESSAGE_SIZE("zeppelin.websocket.max.text.message.size", "1024000"), ZEPPELIN_SERVER_DEFAULT_DIR_ALLOWED("zeppelin.server.default.dir.allowed", false), ZEPPELIN_SERVER_XFRAME_OPTIONS("zeppelin.server.xframe.options", "SAMEORIGIN"), ZEPPELIN_SERVER_JETTY_NAME("zeppelin.server.jetty.name", null), + ZEPPELIN_SERVER_JETTY_REQUEST_HEADER_SIZE("zeppelin.server.jetty.request.header.size", 8192), 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_SERVER_KERBEROS_KEYTAB("zeppelin.server.kerberos.keytab", ""), + ZEPPELIN_SERVER_KERBEROS_PRINCIPAL("zeppelin.server.kerberos.principal", ""), + + ZEPPELIN_INTERPRETER_CALLBACK_PORTRANGE("zeppelin.interpreter.callback.portRange", ":"), + ZEPPELIN_INTERPRETER_PORTRANGE("zeppelin.interpreter.portRange", ":"), + + ZEPPELIN_INTERPRETER_LIFECYCLE_MANAGER_CLASS("zeppelin.interpreter.lifecyclemanager.class", + "org.apache.zeppelin.interpreter.lifecycle.NullLifecycleManager"), + ZEPPELIN_INTERPRETER_LIFECYCLE_MANAGER_TIMEOUT_CHECK_INTERVAL( + "zeppelin.interpreter.lifecyclemanager.timeout.checkinterval", 6000L), + ZEPPELIN_INTERPRETER_LIFECYCLE_MANAGER_TIMEOUT_THRESHOLD( + "zeppelin.interpreter.lifecyclemanager.timeout.threshold", 3600000L), + + 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"), + 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-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; } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/Input.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/Input.java index f713f4a9a12..a6860dea5e4 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/Input.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/Input.java @@ -107,7 +107,13 @@ public boolean equals(Object o) { if (displayName != null ? !displayName.equals(input.displayName) : input.displayName != null) { return false; } - if (defaultValue != null ? + if (defaultValue instanceof Object[]) { + if (defaultValue != null ? + !Arrays.equals((Object[]) defaultValue, (Object[]) input.defaultValue) + : input.defaultValue != null) { + return false; + } + } else if (defaultValue != null ? !defaultValue.equals(input.defaultValue) : input.defaultValue != null) { return false; } @@ -149,6 +155,8 @@ public static CheckBox checkbox(String name, Object[] defaultChecked, ParamOptio // checkbox form with " or " as delimiter: will be // expanded to "US or JP" private static final Pattern VAR_PTN = Pattern.compile("([_])?[$][{]([^=}]*([=][^}]*)?)[}]"); + private static final Pattern VAR_NOTE_PTN = + Pattern.compile("([_])?[$]{2}[{]([^=}]*([=][^}]*)?)[}]"); private static String[] getNameAndDisplayName(String str) { Pattern p = Pattern.compile("([^(]*)\\s*[(]([^)]*)[)]"); @@ -275,15 +283,21 @@ private static Input getInputForm(Matcher match) { return input; } - public static LinkedHashMap extractSimpleQueryForm(String script) { + public static LinkedHashMap extractSimpleQueryForm(String script, + boolean noteForm) { LinkedHashMap forms = new LinkedHashMap<>(); if (script == null) { return forms; } String replaced = script; - Matcher match = VAR_PTN.matcher(replaced); + Pattern pattern = noteForm ? VAR_NOTE_PTN : VAR_PTN; + Matcher match = pattern.matcher(replaced); while (match.find()) { + int first = match.start(); + if (!noteForm && first > 0 && replaced.charAt(first - 1) == '$') { + continue; + } Input form = getInputForm(match); forms.put(form.name, form); } @@ -294,11 +308,18 @@ public static LinkedHashMap extractSimpleQueryForm(String script) private static final String DEFAULT_DELIMITER = ","; - public static String getSimpleQuery(Map params, String script) { + public static String getSimpleQuery(Map params, String script, boolean noteForm) { String replaced = script; - Matcher match = VAR_PTN.matcher(replaced); + Pattern pattern = noteForm ? VAR_NOTE_PTN : VAR_PTN; + + Matcher match = pattern.matcher(replaced); while (match.find()) { + int first = match.start(); + + if (!noteForm && first > 0 && replaced.charAt(first - 1) == '$') { + continue; + } Input input = getInputForm(match); Object value; if (params.containsKey(input.name)) { @@ -331,7 +352,7 @@ public static String getSimpleQuery(Map params, String script) { expanded = value.toString(); } replaced = match.replaceFirst(expanded); - match = VAR_PTN.matcher(replaced); + match = pattern.matcher(replaced); } return replaced; diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/ApplicationException.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/ApplicationException.java index 4bf0ac28b94..d3c648886e8 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/ApplicationException.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/ApplicationException.java @@ -31,4 +31,8 @@ public ApplicationException(Exception e) { public ApplicationException() { } + + public ApplicationException(String message, Throwable cause) { + super(message, cause); + } } 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..38dca256337 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 @@ -28,6 +28,8 @@ 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 java.io.IOException; import java.util.Collection; @@ -40,11 +42,13 @@ */ public abstract class BaseZeppelinContext { + private static final Logger LOGGER = LoggerFactory.getLogger(BaseZeppelinContext.class); protected InterpreterContext interpreterContext; protected int maxResult; protected InterpreterHookRegistry hooks; protected GUI gui; + protected GUI noteGui; private static RemoteEventClientWrapper eventClient; @@ -86,44 +90,116 @@ public Object input(String name) { @Deprecated @ZeppelinApi public Object input(String name, Object defaultValue) { - return textbox(name, defaultValue.toString()); + return textbox(name, defaultValue.toString(), false); } @ZeppelinApi public Object textbox(String name) { - return textbox(name, ""); + return textbox(name, "", false); } @ZeppelinApi public Object textbox(String name, String defaultValue) { - return gui.textbox(name, defaultValue); + return textbox(name, defaultValue, false); + } + + @ZeppelinApi + public Collection checkbox(String name, ParamOption[] options) { + return checkbox(name, options, false); } + @ZeppelinApi + public Collection checkbox(String name, List defaultChecked, + ParamOption[] options) { + return checkbox(name, defaultChecked, options, false); + } + + @ZeppelinApi public Object select(String name, Object defaultValue, ParamOption[] paramOptions) { - return gui.select(name, defaultValue, paramOptions); + return select(name, defaultValue, paramOptions, false); } @ZeppelinApi - public Collection checkbox(String name, ParamOption[] options) { + public Object noteTextbox(String name) { + return textbox(name, ""); + } + + @ZeppelinApi + public Object noteTextbox(String name, String defaultValue) { + return textbox(name, defaultValue, true); + } + + @ZeppelinApi + public Collection noteCheckbox(String name, ParamOption[] options) { + return checkbox(name, options, true); + } + + @ZeppelinApi + public Collection noteCheckbox(String name, List defaultChecked, + ParamOption[] options) { + return checkbox(name, defaultChecked, options, true); + } + + @ZeppelinApi + public Object noteSelect(String name, Object defaultValue, ParamOption[] paramOptions) { + return select(name, defaultValue, paramOptions, true); + } + + + private Object select(String name, Object defaultValue, ParamOption[] paramOptions, + boolean noteForm) { + if (noteForm) { + return noteGui.select(name, defaultValue, paramOptions); + } else { + return gui.select(name, defaultValue, paramOptions); + } + } + + private Object textbox(String name, String defaultValue, boolean noteForm) { + if (noteForm) { + return noteGui.textbox(name, defaultValue); + } else { + return gui.textbox(name, defaultValue); + } + } + + private Collection checkbox(String name, ParamOption[] options, + boolean noteForm) { List defaultValues = new LinkedList<>(); for (ParamOption option : options) { defaultValues.add(option.getValue()); } - return checkbox(name, defaultValues, options); + if (noteForm) { + return noteGui.checkbox(name, defaultValues, options); + } else { + return gui.checkbox(name, defaultValues, options); + } } - @ZeppelinApi - public Collection checkbox(String name, - List defaultValues, - ParamOption[] options) { - return gui.checkbox(name, defaultValues, options); + private Collection checkbox(String name, List defaultChecked, + ParamOption[] options, boolean noteForm) { + if (noteForm) { + return noteGui.checkbox(name, defaultChecked, options); + } else { + return gui.checkbox(name, defaultChecked, options); + } } public void setGui(GUI o) { this.gui = o; } - private void restartInterpreter() { + public GUI getGui() { + return gui; + } + + + public GUI getNoteGui() { + return noteGui; + } + + public void setNoteGui(GUI noteGui) { + this.noteGui = noteGui; } public InterpreterContext getInterpreterContext() { @@ -154,21 +230,22 @@ public void show(Object o) { * @param o object * @param maxResult maximum number of rows to display */ - @ZeppelinApi public void show(Object o, int maxResult) { try { 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) { - throw new InterpreterException(e); + throw new RuntimeException(e); } } - private boolean isSupportedObject(Object obj) { + protected boolean isSupportedObject(Object obj) { for (Class supportedClass : getSupportedClasses()) { if (supportedClass.isInstance(obj)) { return true; @@ -225,14 +302,14 @@ public void run(String noteId, String paragraphId, InterpreterContext context) { public void run(String noteId, String paragraphId, InterpreterContext context, boolean checkCurrentParagraph) { if (paragraphId.equals(context.getParagraphId()) && checkCurrentParagraph) { - throw new InterpreterException("Can not run current Paragraph"); + throw new RuntimeException("Can not run current Paragraph"); } List runners = getInterpreterContextRunner(noteId, paragraphId, context); if (runners.size() <= 0) { - throw new InterpreterException("Paragraph " + paragraphId + " not found " + runners.size()); + throw new RuntimeException("Paragraph " + paragraphId + " not found " + runners.size()); } for (InterpreterContextRunner r : runners) { @@ -251,13 +328,14 @@ public void runNote(String noteId, InterpreterContext context) { List runners = getInterpreterContextRunner(noteId, context); if (runners.size() <= 0) { - throw new InterpreterException("Note " + noteId + " not found " + runners.size()); + throw new RuntimeException("Note " + noteId + " not found " + runners.size()); } for (InterpreterContextRunner r : runners) { if (r.getNoteId().equals(runningNoteId) && r.getParagraphId().equals(runningParagraphId)) { continue; } + LOGGER.debug("Run Paragraph: " + r.getParagraphId() + " of Note: " + r.getNoteId()); r.run(); } } @@ -342,12 +420,12 @@ public void run(String noteId, int idx, InterpreterContext context, boolean checkCurrentParagraph) { List runners = getInterpreterContextRunner(noteId, context); if (idx >= runners.size()) { - throw new InterpreterException("Index out of bound"); + throw new RuntimeException("Index out of bound"); } InterpreterContextRunner runner = runners.get(idx); if (runner.getParagraphId().equals(context.getParagraphId()) && checkCurrentParagraph) { - throw new InterpreterException("Can not run current Paragraph: " + runner.getParagraphId()); + throw new RuntimeException("Can not run current Paragraph: " + runner.getParagraphId()); } runner.run(); @@ -373,7 +451,7 @@ public void run(List paragraphIdOrIdx, InterpreterContext context) { Integer idx = (Integer) idOrIdx; run(noteId, idx, context); } else { - throw new InterpreterException("Paragraph " + idOrIdx + " not found"); + throw new RuntimeException("Paragraph " + idOrIdx + " not found"); } } } @@ -661,15 +739,10 @@ private void angularUnbind(String name, String noteId) { * Get the interpreter class name from name entered in paragraph * @param replName if replName is a valid className, return that instead. */ - public String getClassNameFromReplName(String replName) { - for (String name : getInterpreterClassMap().keySet()) { - if (replName.equals(name)) { - return replName; - } - } - - if (replName.contains("spark.")) { - replName = replName.replace("spark.", ""); + private String getClassNameFromReplName(String replName) { + String[] splits = replName.split("."); + if (splits.length > 1) { + replName = splits[splits.length - 1]; } return getInterpreterClassMap().get(replName); } @@ -681,10 +754,9 @@ public String getClassNameFromReplName(String replName) { * @param replName Name of the interpreter */ @Experimental - public void registerHook(String event, String cmd, String replName) { - String noteId = interpreterContext.getNoteId(); + public void registerHook(String event, String cmd, String replName) throws InvalidHookException { String className = getClassNameFromReplName(replName); - hooks.register(noteId, className, event, cmd); + hooks.register(null, className, event, cmd); } /** @@ -693,55 +765,79 @@ public void registerHook(String event, String cmd, String replName) { * @param cmd The code to be executed by the interpreter on given event */ @Experimental - public void registerHook(String event, String cmd) { - String className = interpreterContext.getClassName(); - registerHook(event, cmd, className); + public void registerHook(String event, String cmd) throws InvalidHookException { + String replClassName = interpreterContext.getClassName(); + hooks.register(null, replClassName, event, cmd); + } + + /** + * + * @param event + * @param cmd + * @param noteId + * @throws InvalidHookException + */ + @Experimental + public void registerNoteHook(String event, String cmd, String noteId) + throws InvalidHookException { + String replClassName = interpreterContext.getClassName(); + hooks.register(noteId, replClassName, event, cmd); + } + + @Experimental + public void registerNoteHook(String event, String cmd, String noteId, String replName) + throws InvalidHookException { + String className = getClassNameFromReplName(replName); + hooks.register(noteId, className, event, cmd); } /** - * Get the hook code + * Unbind code from given hook event and given repl + * * @param event The type of event to hook to (pre_exec, post_exec) * @param replName Name of the interpreter */ @Experimental - public String getHook(String event, String replName) { - String noteId = interpreterContext.getNoteId(); + public void unregisterHook(String event, String replName) { String className = getClassNameFromReplName(replName); - return hooks.get(noteId, className, event); + hooks.unregister(null, className, event); } /** - * getHook() wrapper for current repl + * unregisterHook() wrapper for current repl * @param event The type of event to hook to (pre_exec, post_exec) */ @Experimental - public String getHook(String event) { - String className = interpreterContext.getClassName(); - return getHook(event, className); + public void unregisterHook(String event) { + unregisterHook(event, interpreterContext.getReplName()); } /** - * Unbind code from given hook event + * Unbind code from given hook event and given note + * + * @param noteId The id of note * @param event The type of event to hook to (pre_exec, post_exec) - * @param replName Name of the interpreter */ @Experimental - public void unregisterHook(String event, String replName) { - String noteId = interpreterContext.getNoteId(); - String className = getClassNameFromReplName(replName); + public void unregisterNoteHook(String noteId, String event) { + String className = interpreterContext.getClassName(); hooks.unregister(noteId, className, event); } + /** - * unregisterHook() wrapper for current repl + * Unbind code from given hook event, given note and given repl + * @param noteId The id of note * @param event The type of event to hook to (pre_exec, post_exec) + * @param replName Name of the interpreter */ @Experimental - public void unregisterHook(String event) { - String className = interpreterContext.getClassName(); - unregisterHook(event, className); + public void unregisterNoteHook(String noteId, String event, String replName) { + String className = getClassNameFromReplName(replName); + hooks.unregister(noteId, className, event); } + /** * Add object into resource pool * @param name @@ -814,8 +910,7 @@ public static RemoteEventClientWrapper getEventClient() { */ @ZeppelinApi public void setEventClient(RemoteEventClientWrapper eventClient) { - if (BaseZeppelinContext.eventClient == null) { - BaseZeppelinContext.eventClient = eventClient; - } + BaseZeppelinContext.eventClient = eventClient; } + } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/ClassloaderInterpreter.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/ClassloaderInterpreter.java deleted file mode 100644 index a1dafd97212..00000000000 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/ClassloaderInterpreter.java +++ /dev/null @@ -1,282 +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.net.URL; -import java.util.List; -import java.util.Properties; - -import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; -import org.apache.zeppelin.scheduler.Scheduler; - -/** - * Add to the classpath interpreters. - * - */ -public class ClassloaderInterpreter - extends Interpreter - implements WrappedInterpreter { - - private ClassLoader cl; - private Interpreter intp; - - public ClassloaderInterpreter(Interpreter intp, ClassLoader cl) { - super(new Properties()); - this.cl = cl; - this.intp = intp; - } - - @Override - public Interpreter getInnerInterpreter() { - return intp; - } - - public ClassLoader getClassloader() { - return cl; - } - - @Override - public InterpreterResult interpret(String st, InterpreterContext context) { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - return intp.interpret(st, context); - } catch (InterpreterException e) { - throw e; - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - - @Override - public void open() { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - intp.open(); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - @Override - public void close() { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - intp.close(); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - @Override - public void cancel(InterpreterContext context) { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - intp.cancel(context); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - @Override - public FormType getFormType() { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - return intp.getFormType(); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - @Override - public int getProgress(InterpreterContext context) { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - return intp.getProgress(context); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - @Override - public Scheduler getScheduler() { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - return intp.getScheduler(); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - @Override - public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - List completion = intp.completion(buf, cursor, interpreterContext); - return completion; - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - - @Override - public String getClassName() { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - return intp.getClassName(); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - @Override - public void setInterpreterGroup(InterpreterGroup interpreterGroup) { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - intp.setInterpreterGroup(interpreterGroup); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - @Override - public InterpreterGroup getInterpreterGroup() { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - return intp.getInterpreterGroup(); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - @Override - public void setClassloaderUrls(URL [] urls) { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - intp.setClassloaderUrls(urls); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - @Override - public URL [] getClassloaderUrls() { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - return intp.getClassloaderUrls(); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - @Override - public void setProperty(Properties property) { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - intp.setProperty(property); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - @Override - public Properties getProperty() { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - return intp.getProperty(); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } - - @Override - public String getProperty(String key) { - ClassLoader oldcl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(cl); - try { - return intp.getProperty(key); - } catch (Exception e) { - throw new InterpreterException(e); - } finally { - cl = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(oldcl); - } - } -} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Constants.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Constants.java index 87748fffabc..fe2f6742ab2 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Constants.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Constants.java @@ -30,6 +30,8 @@ public class Constants { public static final String ZEPPELIN_INTERPRETER_PORT = "zeppelin.interpreter.port"; public static final String ZEPPELIN_INTERPRETER_HOST = "zeppelin.interpreter.host"; + + public static final String INJECT_CREDENTIALS = "injectCredentials"; public static final String EXISTING_PROCESS = "existing_process"; 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..0af55140ea2 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 @@ -26,12 +26,16 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.reflect.FieldUtils; import org.apache.zeppelin.annotation.Experimental; import org.apache.zeppelin.annotation.ZeppelinApi; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.apache.zeppelin.resource.Resource; +import org.apache.zeppelin.resource.ResourcePool; import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; import org.slf4j.Logger; @@ -55,20 +59,21 @@ public abstract class Interpreter { * open() is called only once */ @ZeppelinApi - public abstract void open(); + public abstract void open() throws InterpreterException; /** * Closes interpreter. You may want to free your resources up here. * close() is called only once */ @ZeppelinApi - public abstract void close(); + public abstract void close() throws InterpreterException; /** * Run precode if exists. */ @ZeppelinApi - public InterpreterResult executePrecode(InterpreterContext interpreterContext) { + public InterpreterResult executePrecode(InterpreterContext interpreterContext) + throws InterpreterException { String simpleName = this.getClass().getSimpleName(); String precode = getProperty(String.format("zeppelin.%s.precode", simpleName)); if (StringUtils.isNotBlank(precode)) { @@ -77,19 +82,50 @@ public InterpreterResult executePrecode(InterpreterContext interpreterContext) { return null; } + protected String interpolate(String cmd, ResourcePool resourcePool) { + Pattern zVariablePattern = Pattern.compile("([^{}]*)([{]+[^{}]*[}]+)(.*)", Pattern.DOTALL); + StringBuilder sb = new StringBuilder(); + Matcher m; + String st = cmd; + while ((m = zVariablePattern.matcher(st)).matches()) { + sb.append(m.group(1)); + String varPat = m.group(2); + if (varPat.matches("[{][^{}]+[}]")) { + // substitute {variable} only if 'variable' has a value ... + Resource resource = resourcePool.get(varPat.substring(1, varPat.length() - 1)); + Object variableValue = resource == null ? null : resource.get(); + if (variableValue != null) + sb.append(variableValue); + else + return cmd; + } else if (varPat.matches("[{]{2}[^{}]+[}]{2}")) { + // escape {{text}} ... + sb.append("{").append(varPat.substring(2, varPat.length() - 2)).append("}"); + } else { + // mismatched {{ }} or more than 2 braces ... + return cmd; + } + st = m.group(3); + } + sb.append(st); + return sb.toString(); + } + /** * Run code and return result, in synchronous way. * * @param st statements to run */ @ZeppelinApi - public abstract InterpreterResult interpret(String st, InterpreterContext context); + public abstract InterpreterResult interpret(String st, + InterpreterContext context) + throws InterpreterException; /** * Optionally implement the canceling routine to abort interpret() method */ @ZeppelinApi - public abstract void cancel(InterpreterContext context); + public abstract void cancel(InterpreterContext context) throws InterpreterException; /** * Dynamic form handling @@ -99,7 +135,7 @@ public InterpreterResult executePrecode(InterpreterContext interpreterContext) { * FormType.NATIVE handles form in API */ @ZeppelinApi - public abstract FormType getFormType(); + public abstract FormType getFormType() throws InterpreterException; /** * get interpret() method running process in percentage. @@ -107,7 +143,7 @@ public InterpreterResult executePrecode(InterpreterContext interpreterContext) { * @return number between 0-100 */ @ZeppelinApi - public abstract int getProgress(InterpreterContext context); + public abstract int getProgress(InterpreterContext context) throws InterpreterException; /** * Get completion list based on cursor position. @@ -120,7 +156,7 @@ public InterpreterResult executePrecode(InterpreterContext interpreterContext) { */ @ZeppelinApi public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { + InterpreterContext interpreterContext) throws InterpreterException { return null; } @@ -144,23 +180,22 @@ public Scheduler getScheduler() { public static Logger logger = LoggerFactory.getLogger(Interpreter.class); private InterpreterGroup interpreterGroup; private URL[] classloaderUrls; - protected Properties property; - private String userName; + protected Properties properties; + protected String userName; @ZeppelinApi - public Interpreter(Properties property) { - logger.debug("Properties: {}", property); - this.property = property; + public Interpreter(Properties properties) { + this.properties = properties; } - public void setProperty(Properties property) { - this.property = property; + public void setProperties(Properties properties) { + this.properties = properties; } @ZeppelinApi - public Properties getProperty() { + public Properties getProperties() { Properties p = new Properties(); - p.putAll(property); + p.putAll(properties); RegisteredInterpreter registeredInterpreter = Interpreter.findRegisteredInterpreterByClassName( getClassName()); @@ -184,11 +219,22 @@ public Properties getProperty() { @ZeppelinApi public String getProperty(String key) { - logger.debug("key: {}, value: {}", key, getProperty().getProperty(key)); + logger.debug("key: {}, value: {}", key, getProperties().getProperty(key)); + + return getProperties().getProperty(key); + } + + @ZeppelinApi + public String getProperty(String key, String defaultValue) { + logger.debug("key: {}, value: {}", key, getProperties().getProperty(key, defaultValue)); - return getProperty().getProperty(key); + return getProperties().getProperty(key, defaultValue); } + @ZeppelinApi + public void setProperty(String key, String value) { + properties.setProperty(key, value); + } public String getClassName() { return this.getClass().getName(); @@ -227,7 +273,7 @@ public void setClassloaderUrls(URL[] classloaderUrls) { * @param cmd The code to be executed by the interpreter on given event */ @Experimental - public void registerHook(String noteId, String event, String cmd) { + public void registerHook(String noteId, String event, String cmd) throws InvalidHookException { InterpreterHookRegistry hooks = interpreterGroup.getInterpreterHookRegistry(); String className = getClassName(); hooks.register(noteId, className, event, cmd); @@ -240,7 +286,7 @@ public void registerHook(String noteId, String event, String cmd) { * @param cmd The code to be executed by the interpreter on given event */ @Experimental - public void registerHook(String event, String cmd) { + public void registerHook(String event, String cmd) throws InvalidHookException { registerHook(null, event, cmd); } 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 4288ea35a9a..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 @@ -17,6 +17,8 @@ package org.apache.zeppelin.interpreter; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,7 +36,7 @@ public class InterpreterContext { private static final ThreadLocal threadIC = new ThreadLocal<>(); - public final InterpreterOutput out; + public InterpreterOutput out; public static InterpreterContext get() { return threadIC.get(); @@ -48,21 +50,71 @@ public static void remove() { threadIC.remove(); } - private final String noteId; - private final String replName; - private final String paragraphTitle; - private final String paragraphId; - private final String paragraphText; + private String noteId; + private String replName; + private String paragraphTitle; + private String paragraphId; + private String paragraphText; private AuthenticationInfo authenticationInfo; - private final Map config; - private GUI gui; + private Map config = new HashMap<>(); + private GUI gui = new GUI(); + private GUI noteGui = new GUI(); private AngularObjectRegistry angularObjectRegistry; private ResourcePool resourcePool; - private List runners; + private List runners = new ArrayList<>(); private String className; private RemoteEventClientWrapper client; private RemoteWorksController remoteWorksController; - private final Map progressMap; + private Map progressMap; + + /** + * Builder class for InterpreterContext + */ + public static class Builder { + private InterpreterContext context = new InterpreterContext(); + + public Builder setNoteId(String noteId) { + context.noteId = noteId; + return this; + } + + public Builder setParagraphId(String paragraphId) { + context.paragraphId = paragraphId; + return this; + } + + public Builder setEventClient(RemoteEventClientWrapper client) { + context.client = client; + return this; + } + + public Builder setInterpreterClassName(String intpClassName) { + context.className = intpClassName; + return this; + } + + public Builder setReplName(String replName) { + context.replName = replName; + return this; + } + + public Builder setInterpreterOut(InterpreterOutput out) { + context.out = out; + return this; + } + + public InterpreterContext build() { + return context; + } + } + + public static Builder builder() { + return new Builder(); + } + + private InterpreterContext() { + + } // visible for testing public InterpreterContext(String noteId, @@ -73,13 +125,14 @@ public InterpreterContext(String noteId, AuthenticationInfo authenticationInfo, Map config, GUI gui, + GUI noteGui, AngularObjectRegistry angularObjectRegistry, ResourcePool resourcePool, List runners, InterpreterOutput out ) { this(noteId, paragraphId, replName, paragraphTitle, paragraphText, authenticationInfo, - config, gui, angularObjectRegistry, resourcePool, runners, out, null, null); + config, gui, noteGui, angularObjectRegistry, resourcePool, runners, out, null, null); } public InterpreterContext(String noteId, @@ -90,6 +143,7 @@ public InterpreterContext(String noteId, AuthenticationInfo authenticationInfo, Map config, GUI gui, + GUI noteGui, AngularObjectRegistry angularObjectRegistry, ResourcePool resourcePool, List runners, @@ -105,6 +159,7 @@ public InterpreterContext(String noteId, this.authenticationInfo = authenticationInfo; this.config = config; this.gui = gui; + this.noteGui = noteGui; this.angularObjectRegistry = angularObjectRegistry; this.resourcePool = resourcePool; this.runners = runners; @@ -121,6 +176,7 @@ public InterpreterContext(String noteId, AuthenticationInfo authenticationInfo, Map config, GUI gui, + GUI noteGui, AngularObjectRegistry angularObjectRegistry, ResourcePool resourcePool, List contextRunners, @@ -129,7 +185,7 @@ public InterpreterContext(String noteId, RemoteInterpreterEventClient eventClient, Map progressMap) { this(noteId, paragraphId, replName, paragraphTitle, paragraphText, authenticationInfo, - config, gui, angularObjectRegistry, resourcePool, contextRunners, output, + config, gui, noteGui, angularObjectRegistry, resourcePool, contextRunners, output, remoteWorksController, progressMap); this.client = new RemoteEventClient(eventClient); } @@ -146,6 +202,10 @@ public String getParagraphId() { return paragraphId; } + public void setParagraphId(String paragraphId) { + this.paragraphId = paragraphId; + } + public String getParagraphText() { return paragraphText; } @@ -166,6 +226,10 @@ public GUI getGui() { return gui; } + public GUI getNoteGui() { + return noteGui; + } + public AngularObjectRegistry getAngularObjectRegistry() { return angularObjectRegistry; } @@ -190,6 +254,10 @@ public RemoteEventClientWrapper getClient() { return client; } + public void setClient(RemoteEventClientWrapper client) { + this.client = client; + } + public RemoteWorksController getRemoteWorksController() { return remoteWorksController; } 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 ebd184ecfbd..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 @@ -17,11 +17,15 @@ package org.apache.zeppelin.interpreter; + /** - * Runtime Exception for interpreters. + * General Exception for interpreters. * */ -public class InterpreterException extends RuntimeException { +public class InterpreterException extends Exception { + + public InterpreterException() { + } public InterpreterException(Throwable e) { super(e); @@ -35,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); + } } 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..9f889013bba 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,107 +17,90 @@ 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 org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; import org.apache.zeppelin.resource.ResourcePool; -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.Collection; +import java.util.List; +import java.util.Map; +import java.security.SecureRandom; +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 interpreter process while its subclass ManagedInterpreterGroup runs + * in zeppelin server 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); - } - - public static Collection getAll() { - return new LinkedList(allInterpreterGroups.values()); - } + protected String id; + // sessionId --> interpreters + protected Map> sessions = new ConcurrentHashMap(); + private AngularObjectRegistry angularObjectRegistry; + private InterpreterHookRegistry hookRegistry; + private ResourcePool resourcePool; + private boolean angularRegistryPushed = false; /** - * Create InterpreterGroup with given id + * Create InterpreterGroup with given id, used in InterpreterProcess * @param id */ public InterpreterGroup(String id) { this.id = id; - allInterpreterGroups.put(id, this); } /** * Create InterpreterGroup with autogenerated id */ 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 SecureRandom().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); + } + + //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); + } - 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; + 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,143 +119,46 @@ public void setInterpreterHookRegistry(InterpreterHookRegistry hookRegistry) { this.hookRegistry = hookRegistry; } - public RemoteInterpreterProcess getRemoteInterpreterProcess() { - return remoteInterpreterProcess; - } - - public void setRemoteInterpreterProcess(RemoteInterpreterProcess remoteInterpreterProcess) { - this.remoteInterpreterProcess = remoteInterpreterProcess; + public int getSessionNum() { + return sessions.size(); } - /** - * 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); - } - close(intpToClose); - - // make sure remote interpreter process terminates - if (remoteInterpreterProcess != null) { - while (remoteInterpreterProcess.referenceCount() > 0) { - remoteInterpreterProcess.dereference(); - } - remoteInterpreterProcess = null; - } - allInterpreterGroups.remove(id); + public void setResourcePool(ResourcePool resourcePool) { + this.resourcePool = resourcePool; } - /** - * Close all interpreter instances in this group for the 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); + public ResourcePool getResourcePool() { + return resourcePool; } - private void close(final Collection intpToClose) { - close(null, null, null, intpToClose); + public boolean isAngularRegistryPushed() { + return angularRegistryPushed; } - 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 void setAngularRegistryPushed(boolean angularRegistryPushed) { + this.angularRegistryPushed = angularRegistryPushed; } - 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); - } - - + public boolean isEmpty() { + return sessions.isEmpty(); } - /** - * 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; + @Override + public boolean equals(Object o) { + if (this == o) { + return true; } - allInterpreterGroups.remove(id); - - List intpToClose = new LinkedList<>(); - for (List intpGroupForSession : this.values()) { - intpToClose.addAll(intpGroupForSession); + if (!(o instanceof InterpreterGroup)) { + return false; } - close(intpToClose); - } - - public void setResourcePool(ResourcePool resourcePool) { - this.resourcePool = resourcePool; - } - public ResourcePool getResourcePool() { - return resourcePool; - } + InterpreterGroup that = (InterpreterGroup) o; - public boolean isAngularRegistryPushed() { - return angularRegistryPushed; + return id != null ? id.equals(that.id) : that.id == null; } - public void setAngularRegistryPushed(boolean angularRegistryPushed) { - this.angularRegistryPushed = angularRegistryPushed; + @Override + public int hashCode() { + return id != null ? id.hashCode() : 0; } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterHookRegistry.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterHookRegistry.java index 9df76f1d036..83917ec62fc 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterHookRegistry.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterHookRegistry.java @@ -18,41 +18,29 @@ package org.apache.zeppelin.interpreter; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** - * The InterpreterinterpreterHookRegistry specifies code to be conditionally executed by an + * The InterpreterHookRegistry specifies code to be conditionally executed by an * interpreter. The constants defined in this class denote currently * supported events. Each instance is bound to a single InterpreterGroup. * Scope is determined on a per-note basis (except when null for global scope). */ public class InterpreterHookRegistry { - public static final String GLOBAL_KEY = "_GLOBAL_"; - private String interpreterId; + static final String GLOBAL_KEY = "_GLOBAL_"; + + // Scope (noteId/global scope) -> (ClassName -> (EventType -> Hook Code)) private Map>> registry = new HashMap<>(); - /** - * hookRegistry constructor. - * - * @param interpreterId The Id of the InterpreterGroup instance to bind to - */ - public InterpreterHookRegistry(final String interpreterId) { - this.interpreterId = interpreterId; - } - - /** - * Get the interpreterGroup id this instance is bound to - */ - public String getInterpreterId() { - return interpreterId; - } - + /** * Adds a note to the registry * * @param noteId The Id of the Note instance to add */ - public void addNote(String noteId) { + private void addNote(String noteId) { synchronized (registry) { if (registry.get(noteId) == null) { registry.put(noteId, new HashMap>()); @@ -66,7 +54,7 @@ public void addNote(String noteId) { * @param noteId The note id * @param className The name of the interpreter repl to map the hooks to */ - public void addRepl(String noteId, String className) { + private void addRepl(String noteId, String className) { synchronized (registry) { addNote(noteId); if (registry.get(noteId).get(className) == null) { @@ -84,19 +72,15 @@ public void addRepl(String noteId, String className) { * @param cmd Code to be executed by the interpreter */ public void register(String noteId, String className, - String event, String cmd) throws IllegalArgumentException { + String event, String cmd) throws InvalidHookException { synchronized (registry) { + if (!HookType.ValidEvents.contains(event)) { + throw new InvalidHookException("event " + event + " is not valid hook event"); + } if (noteId == null) { noteId = GLOBAL_KEY; } addRepl(noteId, className); - if (!event.equals(HookType.POST_EXEC) && !event.equals(HookType.PRE_EXEC) && - !event.equals(HookType.POST_EXEC_DEV) && !event.equals(HookType.PRE_EXEC_DEV)) { - throw new IllegalArgumentException("Must be " + HookType.POST_EXEC + ", " + - HookType.POST_EXEC_DEV + ", " + - HookType.PRE_EXEC + " or " + - HookType.PRE_EXEC_DEV); - } registry.get(noteId).get(className).put(event, cmd); } } @@ -138,18 +122,36 @@ public String get(String noteId, String className, String event) { /** * Container for hook event type constants */ - public static final class HookType { + public enum HookType { + // Execute the hook code PRIOR to main paragraph code execution - public static final String PRE_EXEC = "pre_exec"; + PRE_EXEC("pre_exec"), // Execute the hook code AFTER main paragraph code execution - public static final String POST_EXEC = "post_exec"; + POST_EXEC("post_exec"), // Same as above but reserved for interpreter developers, in order to allow // notebook users to use the above without overwriting registry settings // that are initialized directly in subclasses of Interpreter. - public static final String PRE_EXEC_DEV = "pre_exec_dev"; - public static final String POST_EXEC_DEV = "post_exec_dev"; + PRE_EXEC_DEV("pre_exec_dev"), + POST_EXEC_DEV("post_exec_dev"); + + private String name; + + HookType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static Set ValidEvents = new HashSet(); + static { + for (HookType type : values()) { + ValidEvents.add(type.getName()); + } + } } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterOption.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterOption.java index 37a0d99c4a0..0c01d97ecf7 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterOption.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterOption.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; +import org.apache.zeppelin.conf.ZeppelinConfiguration; /** * @@ -27,8 +28,10 @@ public class InterpreterOption { public static final transient String SHARED = "shared"; public static final transient String SCOPED = "scoped"; public static final transient String ISOLATED = "isolated"; + private static ZeppelinConfiguration conf = ZeppelinConfiguration.create(); - boolean remote; + // always set it as true, keep this field just for backward compatibility + boolean remote = true; String host = null; int port = -1; @@ -65,6 +68,13 @@ public void setUserPermission(boolean setPermission) { } public List getOwners() { + if (null != owners && conf.isUsernameForceLowerCase()) { + List lowerCaseUsers = new ArrayList(); + for (String owner : owners) { + lowerCaseUsers.add(owner.toLowerCase()); + } + return lowerCaseUsers; + } return owners; } @@ -77,14 +87,9 @@ public void setUserImpersonate(boolean userImpersonate) { } public InterpreterOption() { - this(false); - } - - public InterpreterOption(boolean remote) { - this(remote, SHARED, SHARED); } - public InterpreterOption(boolean remote, String perUser, String perNote) { + public InterpreterOption(String perUser, String perNote) { if (perUser == null) { throw new NullPointerException("perUser can not be null."); } @@ -92,7 +97,6 @@ public InterpreterOption(boolean remote, String perUser, String perNote) { throw new NullPointerException("perNote can not be null."); } - this.remote = remote; this.perUser = perUser; this.perNote = perNote; } @@ -112,14 +116,6 @@ public static InterpreterOption fromInterpreterOption(InterpreterOption other) { return option; } - public boolean isRemote() { - return remote; - } - - public void setRemote(boolean remote) { - this.remote = remote; - } - public String getHost() { return host; } 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..8853227e86b 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 @@ -20,13 +20,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; -import java.net.URISyntaxException; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; import java.net.URL; import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.ConcurrentHashMap; /** * InterpreterOutput is OutputStream that supposed to print content on notebook @@ -35,6 +36,7 @@ public class InterpreterOutput extends OutputStream { Logger logger = LoggerFactory.getLogger(InterpreterOutput.class); private final int NEW_LINE_CHAR = '\n'; + private final int LINE_FEED_CHAR = '\r'; private List resultMessageOutputs = new LinkedList<>(); private InterpreterResultMessageOutput currentOut; @@ -46,6 +48,7 @@ public class InterpreterOutput extends OutputStream { private final InterpreterOutputChangeListener changeListener; private int size = 0; + private int lastCRIndex = -1; // change static var to set interpreter output limit // limit will be applied to all InterpreterOutput object. @@ -83,6 +86,7 @@ public void setType(InterpreterResult.Type type) throws IOException { buffer.reset(); size = 0; + lastCRIndex = -1; if (currentOut != null) { currentOut.flush(); @@ -135,6 +139,7 @@ public int size() { public void clear() { size = 0; + lastCRIndex = -1; truncated = false; buffer.reset(); @@ -193,6 +198,14 @@ public void write(int b) throws IOException { } } + if (b == LINE_FEED_CHAR) { + if (lastCRIndex == -1) { + lastCRIndex = size; + } + // reset size to index of last carriage return + size = lastCRIndex; + } + if (startOfTheNewLine) { if (b == '%') { startOfTheNewLine = false; @@ -284,7 +297,12 @@ public void write(File file) throws IOException { } public void write(String string) throws IOException { - write(string.getBytes()); + if (string.startsWith("%") && !startOfTheNewLine) { + // prepend "\n" if it starts with another type of output and startOfTheNewLine is false + write(("\n" + string).getBytes()); + } else { + write(string.getBytes()); + } } /** 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..92cf3a8febc 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 = InterpreterPropertyType.TEXTAREA.getValue(); } public String getName() { 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-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterRunner.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterRunner.java index 020564b5fb3..e60ada7c801 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 @@ -12,6 +12,15 @@ public class InterpreterRunner { @SerializedName("win") private String winPath; + public InterpreterRunner() { + + } + + 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-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroupFactory.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InvalidHookException.java similarity index 79% rename from zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroupFactory.java rename to zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InvalidHookException.java index 3b9be400dd2..9b447263bb2 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterGroupFactory.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InvalidHookException.java @@ -14,13 +14,16 @@ * 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; + +package org.apache.zeppelin.interpreter; /** - * Created InterpreterGroup + * Exception for invalid hook */ -public interface InterpreterGroupFactory { - InterpreterGroup createInterpreterGroup(String interpreterGroupId, InterpreterOption option); +public class InvalidHookException extends Exception { + + public InvalidHookException(String message) { + super(message); + } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/LazyOpenInterpreter.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/LazyOpenInterpreter.java index 96f88eeb5c1..7581e673e6b 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/LazyOpenInterpreter.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/LazyOpenInterpreter.java @@ -44,13 +44,13 @@ public Interpreter getInnerInterpreter() { } @Override - public void setProperty(Properties property) { - intp.setProperty(property); + public void setProperties(Properties properties) { + intp.setProperties(properties); } @Override - public Properties getProperty() { - return intp.getProperty(); + public Properties getProperties() { + return intp.getProperties(); } @Override @@ -59,7 +59,7 @@ public String getProperty(String key) { } @Override - public synchronized void open() { + public synchronized void open() throws InterpreterException { if (opened == true) { return; } @@ -73,12 +73,13 @@ public synchronized void open() { } @Override - public InterpreterResult executePrecode(InterpreterContext interpreterContext) { + public InterpreterResult executePrecode(InterpreterContext interpreterContext) + throws InterpreterException { return intp.executePrecode(interpreterContext); } @Override - public void close() { + public void close() throws InterpreterException { synchronized (intp) { if (opened == true) { intp.close(); @@ -94,7 +95,8 @@ public boolean isOpen() { } @Override - public InterpreterResult interpret(String st, InterpreterContext context) { + public InterpreterResult interpret(String st, InterpreterContext context) + throws InterpreterException { open(); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); try { @@ -105,18 +107,18 @@ public InterpreterResult interpret(String st, InterpreterContext context) { } @Override - public void cancel(InterpreterContext context) { + public void cancel(InterpreterContext context) throws InterpreterException { open(); intp.cancel(context); } @Override - public FormType getFormType() { + public FormType getFormType() throws InterpreterException { return intp.getFormType(); } @Override - public int getProgress(InterpreterContext context) { + public int getProgress(InterpreterContext context) throws InterpreterException { if (opened) { return intp.getProgress(context); } else { @@ -131,7 +133,7 @@ public Scheduler getScheduler() { @Override public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { + InterpreterContext interpreterContext) throws InterpreterException { open(); List completion = intp.completion(buf, cursor, interpreterContext); return completion; @@ -163,12 +165,12 @@ public void setClassloaderUrls(URL [] urls) { } @Override - public void registerHook(String noteId, String event, String cmd) { + public void registerHook(String noteId, String event, String cmd) throws InvalidHookException { intp.registerHook(noteId, event, cmd); } @Override - public void registerHook(String event, String cmd) { + public void registerHook(String event, String cmd) throws InvalidHookException { intp.registerHook(event, cmd); } @@ -191,4 +193,14 @@ public void unregisterHook(String noteId, String event) { public void unregisterHook(String event) { intp.unregisterHook(event); } + + @Override + public void setUserName(String userName) { + this.intp.setUserName(userName); + } + + @Override + public String getUserName() { + return this.intp.getUserName(); + } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/graph/GraphResult.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/graph/GraphResult.java new file mode 100644 index 00000000000..df1b9a3ae6e --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/graph/GraphResult.java @@ -0,0 +1,122 @@ +/* + * 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.graph; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.tabledata.Node; +import org.apache.zeppelin.tabledata.Relationship; + +import com.google.gson.Gson; + +/** + * The intepreter result template for Networks + * + */ +public class GraphResult extends InterpreterResult { + + /** + * The Graph structure parsed from the front-end + * + */ + public static class Graph { + private Collection nodes; + + private Collection edges; + + /** + * The node types in the whole graph, and the related colors + * + */ + private Map labels; + + /** + * The relationship types in the whole graph + * + */ + private Set types; + + /** + * Is a directed graph + */ + private boolean directed; + + public Graph() {} + + public Graph(Collection nodes, Collection edges, + Map labels, Set types, boolean directed) { + super(); + this.setNodes(nodes); + this.setEdges(edges); + this.setLabels(labels); + this.setTypes(types); + this.setDirected(directed); + } + + public Collection getNodes() { + return nodes; + } + + public void setNodes(Collection nodes) { + this.nodes = nodes; + } + + public Collection getEdges() { + return edges; + } + + public void setEdges(Collection edges) { + this.edges = edges; + } + + public Map getLabels() { + return labels; + } + + public void setLabels(Map labels) { + this.labels = labels; + } + + public Set getTypes() { + return types; + } + + public void setTypes(Set types) { + this.types = types; + } + + public boolean isDirected() { + return directed; + } + + public void setDirected(boolean directed) { + this.directed = directed; + } + + } + + private static final Gson gson = new Gson(); + + public GraphResult(Code code, Graph graphObject) { + super(code, Type.NETWORK, gson.toJson(graphObject)); + } + +} 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 new file mode 100644 index 00000000000..26da27032f1 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterClient.java @@ -0,0 +1,38 @@ +/* + * 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.launcher; + +/** + * Interface to InterpreterClient which is created by InterpreterLauncher. This is the component + * that is used to for the communication from zeppelin-server process to zeppelin interpreter + * process. + */ +public interface InterpreterClient { + + String getInterpreterSettingName(); + + void start(String userName); + + 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 new file mode 100644 index 00000000000..28c40f25b90 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLaunchContext.java @@ -0,0 +1,88 @@ +/* + * 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.launcher; + +import org.apache.zeppelin.interpreter.InterpreterOption; +import org.apache.zeppelin.interpreter.InterpreterRunner; + +import java.util.Properties; + +/** + * Context class for Interpreter Launch + */ +public class InterpreterLaunchContext { + + private Properties properties; + private InterpreterOption option; + private InterpreterRunner runner; + private String userName; + private String interpreterGroupId; + private String interpreterSettingId; + private String interpreterSettingGroup; + private String interpreterSettingName; + + public InterpreterLaunchContext(Properties properties, + InterpreterOption option, + InterpreterRunner runner, + String userName, + String interpreterGroupId, + String interpreterSettingId, + String interpreterSettingGroup, + String interpreterSettingName) { + this.properties = properties; + this.option = option; + this.runner = runner; + this.userName = userName; + this.interpreterGroupId = interpreterGroupId; + this.interpreterSettingId = interpreterSettingId; + this.interpreterSettingGroup = interpreterSettingGroup; + this.interpreterSettingName = interpreterSettingName; + } + + public Properties getProperties() { + return properties; + } + + public InterpreterOption getOption() { + return option; + } + + public InterpreterRunner getRunner() { + return runner; + } + + public String getInterpreterGroupId() { + return interpreterGroupId; + } + + public String getInterpreterSettingId() { + return interpreterSettingId; + } + + public String getInterpreterSettingGroup() { + return interpreterSettingGroup; + } + + public String getInterpreterSettingName() { + return interpreterSettingName; + } + + public String getUserName() { + return userName; + } +} 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 new file mode 100644 index 00000000000..1cee20e7a04 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/launcher/InterpreterLauncher.java @@ -0,0 +1,41 @@ +/* + * 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.launcher; + +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.recovery.RecoveryStorage; + +import java.io.IOException; +import java.util.Properties; + +/** + * Component to Launch interpreter process. + */ +public abstract class InterpreterLauncher { + + protected ZeppelinConfiguration zConf; + protected Properties properties; + protected RecoveryStorage recoveryStorage; + + 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-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterContextRunner.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterContextRunner.java index 8d16ec52b14..74b8db6d9d1 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterContextRunner.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterContextRunner.java @@ -33,6 +33,6 @@ public RemoteInterpreterContextRunner(String noteId, String paragraphId) { public void run() { // this class should be used only for gson deserialize abstract class // code should not reach here - throw new InterpreterException("Assert"); + throw new RuntimeException("Assert"); } } 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 deleted file mode 100644 index 1d48a1e6f6e..00000000000 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java +++ /dev/null @@ -1,242 +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 org.apache.commons.pool2.impl.GenericObjectPool; -import org.apache.thrift.TException; -import org.apache.zeppelin.helium.ApplicationEventListener; -import org.apache.zeppelin.interpreter.InterpreterGroup; -import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Abstract class for interpreter process - */ -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; - private int connectTimeout; - - public RemoteInterpreterProcess( - int connectTimeout, - RemoteInterpreterProcessListener listener, - ApplicationEventListener appListener) { - this(new RemoteInterpreterEventPoller(listener, appListener), - connectTimeout); - } - - RemoteInterpreterProcess(RemoteInterpreterEventPoller remoteInterpreterEventPoller, - int connectTimeout) { - this.interpreterContextRunnerPool = new InterpreterContextRunnerPool(); - referenceCount = new AtomicInteger(0); - this.remoteInterpreterEventPoller = remoteInterpreterEventPoller; - this.connectTimeout = connectTimeout; - } - - 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; - } - - 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()) { - return null; - } - return clientPool.borrowObject(); - } - - public void releaseClient(Client client) { - releaseClient(client, false); - } - - public void releaseClient(Client client, boolean broken) { - if (broken) { - releaseBrokenClient(client); - } else { - try { - clientPool.returnObject(client); - } catch (Exception e) { - logger.warn("exception occurred during releasing thrift client", e); - } - } - } - - public void releaseBrokenClient(Client client) { - try { - clientPool.invalidateObject(client); - } catch (Exception e) { - logger.warn("exception occurred during releasing thrift client", e); - } - } - - 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 - * @param name - * @param o - */ - public void updateRemoteAngularObject(String name, String noteId, String paragraphId, Object o) { - Client client = null; - try { - client = getClient(); - } catch (NullPointerException e) { - // remote process not started - logger.info("NullPointerException in RemoteInterpreterProcess while " + - "updateRemoteAngularObject getClient, remote process not started", e); - return; - } catch (Exception e) { - logger.error("Can't update angular object", e); - } - - boolean broken = false; - try { - Gson gson = new Gson(); - client.angularObjectUpdate(name, noteId, paragraphId, gson.toJson(o)); - } catch (TException e) { - broken = true; - logger.error("Can't update angular object", e); - } catch (NullPointerException e) { - logger.error("Remote interpreter process not started", e); - return; - } finally { - if (client != null) { - releaseClient(client, broken); - } - } - } - - public InterpreterContextRunnerPool getInterpreterContextRunnerPool() { - return interpreterContextRunnerPool; - } -} 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..3eb8daa7be1 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 @@ -17,29 +17,56 @@ package org.apache.zeppelin.interpreter.remote; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.URL; -import java.nio.ByteBuffer; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.apache.commons.lang3.StringUtils; import org.apache.thrift.TException; import org.apache.thrift.server.TThreadPoolServer; import org.apache.thrift.transport.TServerSocket; import org.apache.thrift.transport.TTransportException; import org.apache.zeppelin.dep.DependencyResolver; -import org.apache.zeppelin.display.*; -import org.apache.zeppelin.helium.*; -import org.apache.zeppelin.interpreter.*; -import org.apache.zeppelin.interpreter.InterpreterHookRegistry.HookType; +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.helium.Application; +import org.apache.zeppelin.helium.ApplicationContext; +import org.apache.zeppelin.helium.ApplicationException; +import org.apache.zeppelin.helium.ApplicationLoader; +import org.apache.zeppelin.helium.HeliumAppAngularObjectRegistry; +import org.apache.zeppelin.helium.HeliumPackage; +import org.apache.zeppelin.interpreter.Constants; +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.InterpreterHookListener; +import org.apache.zeppelin.interpreter.InterpreterHookRegistry; +import org.apache.zeppelin.interpreter.InterpreterHookRegistry.HookType; +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.thrift.*; -import org.apache.zeppelin.resource.*; +import org.apache.zeppelin.interpreter.InterpreterResultMessage; +import org.apache.zeppelin.interpreter.InterpreterResultMessageOutput; +import org.apache.zeppelin.interpreter.LazyOpenInterpreter; +import org.apache.zeppelin.interpreter.RemoteWorksController; +import org.apache.zeppelin.interpreter.RemoteZeppelinServerResource; +import org.apache.zeppelin.interpreter.thrift.CallbackInfo; +import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.apache.zeppelin.interpreter.thrift.RemoteApplicationResult; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterContext; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterEvent; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterResult; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterResultMessage; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; +import org.apache.zeppelin.interpreter.thrift.ZeppelinServerResourceParagraphRunner; +import org.apache.zeppelin.resource.DistributedResourcePool; +import org.apache.zeppelin.resource.Resource; +import org.apache.zeppelin.resource.ResourcePool; +import org.apache.zeppelin.resource.ResourceSet; +import org.apache.zeppelin.resource.WellKnownResourceName; import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.scheduler.Job.Status; import org.apache.zeppelin.scheduler.JobListener; @@ -49,17 +76,32 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; /** * 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; @@ -70,6 +112,9 @@ public class RemoteInterpreterServer Gson gson = new Gson(); RemoteInterpreterService.Processor processor; + private String callbackHost; + private int callbackPort; + private String host; private int port; private TThreadPoolServer server; @@ -87,28 +132,92 @@ public class RemoteInterpreterServer // Hold information for manual progress update private ConcurrentMap progressMap = new ConcurrentHashMap<>(); - public RemoteInterpreterServer(int port) throws TTransportException { - this.port = port; + private boolean isTest; + + public RemoteInterpreterServer(String callbackHost, int callbackPort, String portRange) + throws IOException, TTransportException { + this(callbackHost, callbackPort, portRange, false); + } + + public RemoteInterpreterServer(String callbackHost, int callbackPort, String portRange, + boolean isTest) throws TTransportException, IOException { + if (null != callbackHost) { + this.callbackHost = callbackHost; + this.callbackPort = callbackPort; + } else { + // DevInterpreter + this.port = callbackPort; + } + this.isTest = isTest; processor = new RemoteInterpreterService.Processor<>(this); - TServerSocket serverTransport = new TServerSocket(port); + TServerSocket serverTransport; + if (null == callbackHost) { + // Dev Interpreter + serverTransport = new TServerSocket(callbackPort); + } else { + serverTransport = RemoteInterpreterUtils.createTServerSocket(portRange); + this.port = serverTransport.getServerSocket().getLocalPort(); + this.host = RemoteInterpreterUtils.findAvailableHostAddress(); + logger.info("Launching ThriftServer at " + this.host + ":" + this.port); + } server = new TThreadPoolServer( new TThreadPoolServer.Args(serverTransport).processor(processor)); + logger.info("Starting remote interpreter server on port {}", port); remoteWorksResponsePool = Collections.synchronizedMap(new HashMap()); remoteWorksController = new ZeppelinRemoteWorksController(this, remoteWorksResponsePool); } @Override public void run() { + if (null != callbackHost && !isTest) { + new Thread(new Runnable() { + boolean interrupted = false; + @Override + public void run() { + while (!interrupted && !server.isServing()) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + interrupted = true; + } + } + + if (!interrupted) { + CallbackInfo callbackInfo = new CallbackInfo(host, port); + try { + RemoteInterpreterUtils + .registerInterpreter(callbackHost, callbackPort, callbackInfo); + } catch (TException e) { + logger.error("Error while registering interpreter: {}", callbackInfo, e); + try { + shutdown(); + } catch (TException e1) { + logger.warn("Exception occurs while shutting down", e1); + } + } + } + } + }).start(); + } logger.info("Starting remote interpreter server on port {}", port); server.serve(); } @Override public void shutdown() throws TException { + logger.info("Shutting down..."); eventClient.waitForEventQueueBecomesEmpty(DEFAULT_SHUTDOWN_TIMEOUT); if (interpreterGroup != null) { - interpreterGroup.close(); + for (List session : interpreterGroup.values()) { + for (Interpreter interpreter : session) { + try { + interpreter.close(); + } catch (InterpreterException e) { + logger.warn("Fail to close interpreter", e); + } + } + } } server.stop(); @@ -146,25 +255,34 @@ public boolean isRunning() { public static void main(String[] args) - throws TTransportException, InterruptedException { - + 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 = ":"; if (args.length > 0) { - port = Integer.parseInt(args[0]); + callbackHost = args[0]; + port = Integer.parseInt(args[1]); + if (args.length > 2) { + portRange = args[2]; + } } - RemoteInterpreterServer remoteInterpreterServer = new RemoteInterpreterServer(port); + RemoteInterpreterServer remoteInterpreterServer = + new RemoteInterpreterServer(callbackHost, port, portRange); remoteInterpreterServer.start(); remoteInterpreterServer.join(); System.exit(0); } @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); angularObjectRegistry = new AngularObjectRegistry(interpreterGroup.getId(), this); - hookRegistry = new InterpreterHookRegistry(interpreterGroup.getId()); + hookRegistry = new InterpreterHookRegistry(); resourcePool = new DistributedResourcePool(interpreterGroup.getId(), eventClient); interpreterGroup.setInterpreterHookRegistry(hookRegistry); interpreterGroup.setAngularObjectRegistry(angularObjectRegistry); @@ -190,20 +308,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) { @@ -228,22 +337,20 @@ private void setSystemProperty(Properties properties) { for (Object key : properties.keySet()) { if (!RemoteInterpreterUtils.isEnvString((String) key)) { String value = properties.getProperty((String) key); - if (value == null || value.isEmpty()) { - System.clearProperty((String) key); - } else { + if (!StringUtils.isBlank(value)) { System.setProperty((String) key, properties.getProperty((String) key)); } } } } - 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 +366,24 @@ protected Interpreter getInterpreter(String sessionKey, String className) throws } @Override - public void open(String noteId, String className) throws TException { - Interpreter intp = getInterpreter(noteId, className); - intp.open(); + 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); + try { + intp.open(); + } catch (InterpreterException e) { + throw new TException("Fail to open interpreter", e); + } } @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,14 +398,18 @@ 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(); while (it.hasNext()) { Interpreter inp = it.next(); if (inp.getClassName().equals(className)) { - inp.close(); + try { + inp.close(); + } catch (InterpreterException e) { + logger.warn("Fail to close interpreter", e); + } it.remove(); break; } @@ -322,7 +438,6 @@ public RemoteInterpreterResult interpret(String noteId, String className, String intp, st, context); - scheduler.submit(job); while (!job.isTerminated()) { @@ -337,20 +452,15 @@ public RemoteInterpreterResult interpret(String noteId, String className, String progressMap.remove(interpreterContext.getParagraphId()); - InterpreterResult result; - if (job.getStatus() == Status.ERROR) { - result = new InterpreterResult(Code.ERROR, Job.getStack(job.getException())); - } else { - result = (InterpreterResult) job.getReturn(); - - // in case of job abort in PENDING status, result can be null - if (result == null) { - result = new InterpreterResult(Code.KEEP_PREVIOUS_RESULT); - } + InterpreterResult result = (InterpreterResult) job.getReturn(); + // in case of job abort in PENDING status, result can be null + if (result == null) { + result = new InterpreterResult(Code.KEEP_PREVIOUS_RESULT); } return convert(result, context.getConfig(), - context.getGui()); + context.getGui(), + context.getNoteGui()); } @Override @@ -406,7 +516,12 @@ public void afterStatusChange(Job job, Status before, Status after) { } } - class InterpretJob extends Job { + + + /** + * TODO(jl): Need to extract this class from RemoteInterpreterServer to test it + */ + public static class InterpretJob extends Job { private Interpreter interpreter; private String script; @@ -450,8 +565,8 @@ private void processInterpreterHooks(final String noteId) { InterpreterHookListener hookListener = new InterpreterHookListener() { @Override public void onPreExecute(String script) { - String cmdDev = interpreter.getHook(noteId, HookType.PRE_EXEC_DEV); - String cmdUser = interpreter.getHook(noteId, HookType.PRE_EXEC); + String cmdDev = interpreter.getHook(noteId, HookType.PRE_EXEC_DEV.getName()); + String cmdUser = interpreter.getHook(noteId, HookType.PRE_EXEC.getName()); // User defined hook should be executed before dev hook List cmds = Arrays.asList(cmdDev, cmdUser); @@ -466,8 +581,8 @@ public void onPreExecute(String script) { @Override public void onPostExecute(String script) { - String cmdDev = interpreter.getHook(noteId, HookType.POST_EXEC_DEV); - String cmdUser = interpreter.getHook(noteId, HookType.POST_EXEC); + String cmdDev = interpreter.getHook(noteId, HookType.POST_EXEC_DEV.getName()); + String cmdUser = interpreter.getHook(noteId, HookType.POST_EXEC.getName()); // User defined hook should be executed after dev hook List cmds = Arrays.asList(cmdUser, cmdDev); @@ -485,7 +600,9 @@ public void onPostExecute(String script) { } @Override - protected Object jobRun() throws Throwable { + // TODO(jl): need to redesign this class + public Object jobRun() throws Throwable { + ClassLoader currentThreadContextClassloader = Thread.currentThread().getContextClassLoader(); try { InterpreterContext.set(context); @@ -502,9 +619,16 @@ protected Object jobRun() throws Throwable { if (result == null || result.code() == Code.SUCCESS) { // Add hooks to script from registry. - // Global scope first, followed by notebook scope - processInterpreterHooks(null); + // note scope first, followed by global scope. + // Here's the code after hooking: + // global_pre_hook + // note_pre_hook + // script + // note_post_hook + // global_post_hook processInterpreterHooks(context.getNoteId()); + processInterpreterHooks(null); + logger.debug("Script after hooks: " + script); result = interpreter.interpret(script, context); } @@ -513,6 +637,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; @@ -527,6 +658,7 @@ protected Object jobRun() throws Throwable { } return new InterpreterResult(result.code(), resultMessages); } finally { + Thread.currentThread().setContextClassLoader(currentThreadContextClassloader); InterpreterContext.remove(); } } @@ -554,37 +686,56 @@ public void cancel(String noteId, String className, RemoteInterpreterContext int if (job != null) { job.setStatus(Status.ABORT); } else { - intp.cancel(convert(interpreterContext, null)); + try { + intp.cancel(convert(interpreterContext, null)); + } catch (InterpreterException e) { + throw new TException("Fail to cancel", e); + } } } @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); - return intp.getProgress(convert(interpreterContext, null)); + Interpreter intp = getInterpreter(sessionId, className); + if (intp == null) { + throw new TException("No interpreter {} existed for session {}".format( + className, sessionId)); + } + try { + return intp.getProgress(convert(interpreterContext, null)); + } catch (InterpreterException e) { + throw new TException("Fail to getProgress", e); + } } } @Override - public String getFormType(String noteId, String className) throws TException { - Interpreter intp = getInterpreter(noteId, className); - return intp.getFormType().toString(); + public String getFormType(String sessionId, String className) throws TException { + Interpreter intp = getInterpreter(sessionId, className); + try { + return intp.getFormType().toString(); + } catch (InterpreterException e) { + throw new TException(e); + } } @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); - List completion = intp.completion(buf, cursor, convert(remoteInterpreterContext, null)); - return completion; + Interpreter intp = getInterpreter(sessionId, className); + try { + return intp.completion(buf, cursor, convert(remoteInterpreterContext, null)); + } catch (InterpreterException e) { + throw new TException("Fail to get completion", e); + } } private InterpreterContext convert(RemoteInterpreterContext ric) { @@ -611,6 +762,7 @@ private InterpreterContext convert(RemoteInterpreterContext ric, InterpreterOutp (Map) gson.fromJson(ric.getConfig(), new TypeToken>() {}.getType()), GUI.fromJson(ric.getGui()), + GUI.fromJson(ric.getNoteGui()), interpreterGroup.getAngularObjectRegistry(), interpreterGroup.getResourcePool(), contextRunners, output, remoteWorksController, eventClient, progressMap); @@ -742,7 +894,7 @@ public List getRemoteContextRunner( } private RemoteInterpreterResult convert(InterpreterResult result, - Map config, GUI gui) { + Map config, GUI gui, GUI noteGui) { List msg = new LinkedList<>(); for (InterpreterResultMessage m : result.message()) { @@ -755,20 +907,21 @@ private RemoteInterpreterResult convert(InterpreterResult result, result.code().name(), msg, gson.toJson(config), - gui.toJson()); + gui.toJson(), + noteGui.toJson()); } @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) { @@ -785,7 +938,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/interpreter/remote/RemoteInterpreterUtils.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtils.java index 4ee6690f7c4..223588f1561 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtils.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtils.java @@ -17,20 +17,40 @@ package org.apache.zeppelin.interpreter.remote; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.net.ConnectException; +import java.net.Inet4Address; +import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; import java.net.ServerSocket; import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Collections; + +import org.apache.commons.lang.StringUtils; +import org.apache.thrift.TException; +import org.apache.thrift.protocol.TBinaryProtocol; +import org.apache.thrift.protocol.TProtocol; +import org.apache.thrift.transport.TServerSocket; +import org.apache.thrift.transport.TSocket; +import org.apache.thrift.transport.TTransport; +import org.apache.thrift.transport.TTransportException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.zeppelin.interpreter.thrift.CallbackInfo; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterCallbackService; /** * */ public class RemoteInterpreterUtils { static Logger LOGGER = LoggerFactory.getLogger(RemoteInterpreterUtils.class); + + public static int findRandomAvailablePortOnAllLocalInterfaces() throws IOException { int port; try (ServerSocket socket = new ServerSocket(0);) { @@ -40,6 +60,65 @@ public static int findRandomAvailablePortOnAllLocalInterfaces() throws IOExcepti return port; } + /** + * start:end + * + * @param portRange + * @return + * @throws IOException + */ + public static TServerSocket createTServerSocket(String portRange) + throws IOException { + + TServerSocket tSocket = null; + // ':' is the default value which means no constraints on the portRange + if (StringUtils.isBlank(portRange) || portRange.equals(":")) { + try { + tSocket = new TServerSocket(0); + return tSocket; + } catch (TTransportException e) { + throw new IOException("Fail to create TServerSocket", e); + } + } + // valid user registered port https://en.wikipedia.org/wiki/Registered_port + int start = 1024; + int end = 65535; + String[] ports = portRange.split(":", -1); + if (!ports[0].isEmpty()) { + start = Integer.parseInt(ports[0]); + } + if (!ports[1].isEmpty()) { + end = Integer.parseInt(ports[1]); + } + for (int i = start; i <= end; ++i) { + try { + tSocket = new TServerSocket(i); + return tSocket; + } catch (Exception e) { + // ignore this + } + } + throw new IOException("No available port in the portRange: " + portRange); + } + + public static String findAvailableHostAddress() throws UnknownHostException, SocketException { + InetAddress address = InetAddress.getLocalHost(); + if (address.isLoopbackAddress()) { + for (NetworkInterface networkInterface : Collections + .list(NetworkInterface.getNetworkInterfaces())) { + if (!networkInterface.isLoopback()) { + for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) { + InetAddress a = interfaceAddress.getAddress(); + if (a instanceof Inet4Address) { + return a.getHostAddress(); + } + } + } + } + } + return address.getHostAddress(); + } + public static boolean checkIfRemoteEndpointAccessible(String host, int port) { try { Socket discover = new Socket(); @@ -80,4 +159,17 @@ public static boolean isEnvString(String key) { return key.matches("^[A-Z_0-9]*"); } + + public static void registerInterpreter(String callbackHost, int callbackPort, + final CallbackInfo callbackInfo) throws TException { + LOGGER.info("callbackHost: {}, callbackPort: {}, callbackInfo: {}", callbackHost, callbackPort, + callbackInfo); + try (TTransport transport = new TSocket(callbackHost, callbackPort)) { + transport.open(); + TProtocol protocol = new TBinaryProtocol(transport); + RemoteInterpreterCallbackService.Client client = new RemoteInterpreterCallbackService.Client( + protocol); + client.callback(callbackInfo); + } + } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/CallbackInfo.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/CallbackInfo.java new file mode 100644 index 00000000000..c36a7ac5be3 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/CallbackInfo.java @@ -0,0 +1,518 @@ +/** + * 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. + */ +/** + * Autogenerated by Thrift Compiler (0.9.2) + * + * DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING + * @generated + */ +package org.apache.zeppelin.interpreter.thrift; + +import org.apache.thrift.scheme.IScheme; +import org.apache.thrift.scheme.SchemeFactory; +import org.apache.thrift.scheme.StandardScheme; + +import org.apache.thrift.scheme.TupleScheme; +import org.apache.thrift.protocol.TTupleProtocol; +import org.apache.thrift.protocol.TProtocolException; +import org.apache.thrift.EncodingUtils; +import org.apache.thrift.TException; +import org.apache.thrift.async.AsyncMethodCallback; +import org.apache.thrift.server.AbstractNonblockingServer.*; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.EnumMap; +import java.util.Set; +import java.util.HashSet; +import java.util.EnumSet; +import java.util.Collections; +import java.util.BitSet; +import java.nio.ByteBuffer; +import java.util.Arrays; +import javax.annotation.Generated; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked"}) +@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-10-22") +public class CallbackInfo implements org.apache.thrift.TBase, java.io.Serializable, Cloneable, Comparable { + private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("CallbackInfo"); + + private static final org.apache.thrift.protocol.TField HOST_FIELD_DESC = new org.apache.thrift.protocol.TField("host", org.apache.thrift.protocol.TType.STRING, (short)1); + private static final org.apache.thrift.protocol.TField PORT_FIELD_DESC = new org.apache.thrift.protocol.TField("port", org.apache.thrift.protocol.TType.I32, (short)2); + + private static final Map, SchemeFactory> schemes = new HashMap, SchemeFactory>(); + static { + schemes.put(StandardScheme.class, new CallbackInfoStandardSchemeFactory()); + schemes.put(TupleScheme.class, new CallbackInfoTupleSchemeFactory()); + } + + public String host; // required + public int port; // required + + /** The set of fields this struct contains, along with convenience methods for finding and manipulating them. */ + public enum _Fields implements org.apache.thrift.TFieldIdEnum { + HOST((short)1, "host"), + PORT((short)2, "port"); + + private static final Map byName = new HashMap(); + + static { + for (_Fields field : EnumSet.allOf(_Fields.class)) { + byName.put(field.getFieldName(), field); + } + } + + /** + * Find the _Fields constant that matches fieldId, or null if its not found. + */ + public static _Fields findByThriftId(int fieldId) { + switch(fieldId) { + case 1: // HOST + return HOST; + case 2: // PORT + return PORT; + default: + return null; + } + } + + /** + * Find the _Fields constant that matches fieldId, throwing an exception + * if it is not found. + */ + public static _Fields findByThriftIdOrThrow(int fieldId) { + _Fields fields = findByThriftId(fieldId); + if (fields == null) throw new IllegalArgumentException("Field " + fieldId + " doesn't exist!"); + return fields; + } + + /** + * Find the _Fields constant that matches name, or null if its not found. + */ + public static _Fields findByName(String name) { + return byName.get(name); + } + + private final short _thriftId; + private final String _fieldName; + + _Fields(short thriftId, String fieldName) { + _thriftId = thriftId; + _fieldName = fieldName; + } + + public short getThriftFieldId() { + return _thriftId; + } + + public String getFieldName() { + return _fieldName; + } + } + + // isset id assignments + private static final int __PORT_ISSET_ID = 0; + private byte __isset_bitfield = 0; + public static final Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> metaDataMap; + static { + Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> tmpMap = new EnumMap<_Fields, org.apache.thrift.meta_data.FieldMetaData>(_Fields.class); + tmpMap.put(_Fields.HOST, new org.apache.thrift.meta_data.FieldMetaData("host", org.apache.thrift.TFieldRequirementType.DEFAULT, + new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING))); + tmpMap.put(_Fields.PORT, new org.apache.thrift.meta_data.FieldMetaData("port", org.apache.thrift.TFieldRequirementType.DEFAULT, + new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.I32))); + metaDataMap = Collections.unmodifiableMap(tmpMap); + org.apache.thrift.meta_data.FieldMetaData.addStructMetaDataMap(CallbackInfo.class, metaDataMap); + } + + public CallbackInfo() { + } + + public CallbackInfo( + String host, + int port) + { + this(); + this.host = host; + this.port = port; + setPortIsSet(true); + } + + /** + * Performs a deep copy on other. + */ + public CallbackInfo(CallbackInfo other) { + __isset_bitfield = other.__isset_bitfield; + if (other.isSetHost()) { + this.host = other.host; + } + this.port = other.port; + } + + public CallbackInfo deepCopy() { + return new CallbackInfo(this); + } + + @Override + public void clear() { + this.host = null; + setPortIsSet(false); + this.port = 0; + } + + public String getHost() { + return this.host; + } + + public CallbackInfo setHost(String host) { + this.host = host; + return this; + } + + public void unsetHost() { + this.host = null; + } + + /** Returns true if field host is set (has been assigned a value) and false otherwise */ + public boolean isSetHost() { + return this.host != null; + } + + public void setHostIsSet(boolean value) { + if (!value) { + this.host = null; + } + } + + public int getPort() { + return this.port; + } + + public CallbackInfo setPort(int port) { + this.port = port; + setPortIsSet(true); + return this; + } + + public void unsetPort() { + __isset_bitfield = EncodingUtils.clearBit(__isset_bitfield, __PORT_ISSET_ID); + } + + /** Returns true if field port is set (has been assigned a value) and false otherwise */ + public boolean isSetPort() { + return EncodingUtils.testBit(__isset_bitfield, __PORT_ISSET_ID); + } + + public void setPortIsSet(boolean value) { + __isset_bitfield = EncodingUtils.setBit(__isset_bitfield, __PORT_ISSET_ID, value); + } + + public void setFieldValue(_Fields field, Object value) { + switch (field) { + case HOST: + if (value == null) { + unsetHost(); + } else { + setHost((String)value); + } + break; + + case PORT: + if (value == null) { + unsetPort(); + } else { + setPort((Integer)value); + } + break; + + } + } + + public Object getFieldValue(_Fields field) { + switch (field) { + case HOST: + return getHost(); + + case PORT: + return Integer.valueOf(getPort()); + + } + throw new IllegalStateException(); + } + + /** Returns true if field corresponding to fieldID is set (has been assigned a value) and false otherwise */ + public boolean isSet(_Fields field) { + if (field == null) { + throw new IllegalArgumentException(); + } + + switch (field) { + case HOST: + return isSetHost(); + case PORT: + return isSetPort(); + } + throw new IllegalStateException(); + } + + @Override + public boolean equals(Object that) { + if (that == null) + return false; + if (that instanceof CallbackInfo) + return this.equals((CallbackInfo)that); + return false; + } + + public boolean equals(CallbackInfo that) { + if (that == null) + return false; + + boolean this_present_host = true && this.isSetHost(); + boolean that_present_host = true && that.isSetHost(); + if (this_present_host || that_present_host) { + if (!(this_present_host && that_present_host)) + return false; + if (!this.host.equals(that.host)) + return false; + } + + boolean this_present_port = true; + boolean that_present_port = true; + if (this_present_port || that_present_port) { + if (!(this_present_port && that_present_port)) + return false; + if (this.port != that.port) + return false; + } + + return true; + } + + @Override + public int hashCode() { + List list = new ArrayList(); + + boolean present_host = true && (isSetHost()); + list.add(present_host); + if (present_host) + list.add(host); + + boolean present_port = true; + list.add(present_port); + if (present_port) + list.add(port); + + return list.hashCode(); + } + + @Override + public int compareTo(CallbackInfo other) { + if (!getClass().equals(other.getClass())) { + return getClass().getName().compareTo(other.getClass().getName()); + } + + int lastComparison = 0; + + lastComparison = Boolean.valueOf(isSetHost()).compareTo(other.isSetHost()); + if (lastComparison != 0) { + return lastComparison; + } + if (isSetHost()) { + lastComparison = org.apache.thrift.TBaseHelper.compareTo(this.host, other.host); + if (lastComparison != 0) { + return lastComparison; + } + } + lastComparison = Boolean.valueOf(isSetPort()).compareTo(other.isSetPort()); + if (lastComparison != 0) { + return lastComparison; + } + if (isSetPort()) { + lastComparison = org.apache.thrift.TBaseHelper.compareTo(this.port, other.port); + if (lastComparison != 0) { + return lastComparison; + } + } + return 0; + } + + public _Fields fieldForId(int fieldId) { + return _Fields.findByThriftId(fieldId); + } + + public void read(org.apache.thrift.protocol.TProtocol iprot) throws org.apache.thrift.TException { + schemes.get(iprot.getScheme()).getScheme().read(iprot, this); + } + + public void write(org.apache.thrift.protocol.TProtocol oprot) throws org.apache.thrift.TException { + schemes.get(oprot.getScheme()).getScheme().write(oprot, this); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("CallbackInfo("); + boolean first = true; + + sb.append("host:"); + if (this.host == null) { + sb.append("null"); + } else { + sb.append(this.host); + } + first = false; + if (!first) sb.append(", "); + sb.append("port:"); + sb.append(this.port); + first = false; + sb.append(")"); + return sb.toString(); + } + + public void validate() throws org.apache.thrift.TException { + // check for required fields + // check for sub-struct validity + } + + private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException { + try { + write(new org.apache.thrift.protocol.TCompactProtocol(new org.apache.thrift.transport.TIOStreamTransport(out))); + } catch (org.apache.thrift.TException te) { + throw new java.io.IOException(te); + } + } + + private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, ClassNotFoundException { + try { + // it doesn't seem like you should have to do this, but java serialization is wacky, and doesn't call the default constructor. + __isset_bitfield = 0; + read(new org.apache.thrift.protocol.TCompactProtocol(new org.apache.thrift.transport.TIOStreamTransport(in))); + } catch (org.apache.thrift.TException te) { + throw new java.io.IOException(te); + } + } + + private static class CallbackInfoStandardSchemeFactory implements SchemeFactory { + public CallbackInfoStandardScheme getScheme() { + return new CallbackInfoStandardScheme(); + } + } + + private static class CallbackInfoStandardScheme extends StandardScheme { + + public void read(org.apache.thrift.protocol.TProtocol iprot, CallbackInfo struct) throws org.apache.thrift.TException { + org.apache.thrift.protocol.TField schemeField; + iprot.readStructBegin(); + while (true) + { + schemeField = iprot.readFieldBegin(); + if (schemeField.type == org.apache.thrift.protocol.TType.STOP) { + break; + } + switch (schemeField.id) { + case 1: // HOST + if (schemeField.type == org.apache.thrift.protocol.TType.STRING) { + struct.host = iprot.readString(); + struct.setHostIsSet(true); + } else { + org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); + } + break; + case 2: // PORT + if (schemeField.type == org.apache.thrift.protocol.TType.I32) { + struct.port = iprot.readI32(); + struct.setPortIsSet(true); + } else { + org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); + } + break; + default: + org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); + } + iprot.readFieldEnd(); + } + iprot.readStructEnd(); + + // check for required fields of primitive type, which can't be checked in the validate method + struct.validate(); + } + + public void write(org.apache.thrift.protocol.TProtocol oprot, CallbackInfo struct) throws org.apache.thrift.TException { + struct.validate(); + + oprot.writeStructBegin(STRUCT_DESC); + if (struct.host != null) { + oprot.writeFieldBegin(HOST_FIELD_DESC); + oprot.writeString(struct.host); + oprot.writeFieldEnd(); + } + oprot.writeFieldBegin(PORT_FIELD_DESC); + oprot.writeI32(struct.port); + oprot.writeFieldEnd(); + oprot.writeFieldStop(); + oprot.writeStructEnd(); + } + + } + + private static class CallbackInfoTupleSchemeFactory implements SchemeFactory { + public CallbackInfoTupleScheme getScheme() { + return new CallbackInfoTupleScheme(); + } + } + + private static class CallbackInfoTupleScheme extends TupleScheme { + + @Override + public void write(org.apache.thrift.protocol.TProtocol prot, CallbackInfo struct) throws org.apache.thrift.TException { + TTupleProtocol oprot = (TTupleProtocol) prot; + BitSet optionals = new BitSet(); + if (struct.isSetHost()) { + optionals.set(0); + } + if (struct.isSetPort()) { + optionals.set(1); + } + oprot.writeBitSet(optionals, 2); + if (struct.isSetHost()) { + oprot.writeString(struct.host); + } + if (struct.isSetPort()) { + oprot.writeI32(struct.port); + } + } + + @Override + public void read(org.apache.thrift.protocol.TProtocol prot, CallbackInfo struct) throws org.apache.thrift.TException { + TTupleProtocol iprot = (TTupleProtocol) prot; + BitSet incoming = iprot.readBitSet(2); + if (incoming.get(0)) { + struct.host = iprot.readString(); + struct.setHostIsSet(true); + } + if (incoming.get(1)) { + struct.port = iprot.readI32(); + struct.setPortIsSet(true); + } + } + } + +} + diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/InterpreterCompletion.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/InterpreterCompletion.java index 43713e9c312..2ec653ed430 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/InterpreterCompletion.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/InterpreterCompletion.java @@ -51,7 +51,7 @@ import org.slf4j.LoggerFactory; @SuppressWarnings({"cast", "rawtypes", "serial", "unchecked"}) -@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-3-27") +@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-10-22") public class InterpreterCompletion implements org.apache.thrift.TBase, java.io.Serializable, Cloneable, Comparable { private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("InterpreterCompletion"); diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteApplicationResult.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteApplicationResult.java index cf8e50a004c..0398bf9622e 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteApplicationResult.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteApplicationResult.java @@ -51,7 +51,7 @@ import org.slf4j.LoggerFactory; @SuppressWarnings({"cast", "rawtypes", "serial", "unchecked"}) -@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-3-27") +@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-10-22") public class RemoteApplicationResult implements org.apache.thrift.TBase, java.io.Serializable, Cloneable, Comparable { private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("RemoteApplicationResult"); diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterCallbackService.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterCallbackService.java new file mode 100644 index 00000000000..baa5a2d82c5 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterCallbackService.java @@ -0,0 +1,879 @@ +/** + * 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. + */ +/** + * Autogenerated by Thrift Compiler (0.9.2) + * + * DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING + * @generated + */ +package org.apache.zeppelin.interpreter.thrift; + +import org.apache.thrift.scheme.IScheme; +import org.apache.thrift.scheme.SchemeFactory; +import org.apache.thrift.scheme.StandardScheme; + +import org.apache.thrift.scheme.TupleScheme; +import org.apache.thrift.protocol.TTupleProtocol; +import org.apache.thrift.protocol.TProtocolException; +import org.apache.thrift.EncodingUtils; +import org.apache.thrift.TException; +import org.apache.thrift.async.AsyncMethodCallback; +import org.apache.thrift.server.AbstractNonblockingServer.*; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.EnumMap; +import java.util.Set; +import java.util.HashSet; +import java.util.EnumSet; +import java.util.Collections; +import java.util.BitSet; +import java.nio.ByteBuffer; +import java.util.Arrays; +import javax.annotation.Generated; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked"}) +@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-10-22") +public class RemoteInterpreterCallbackService { + + public interface Iface { + + public void callback(CallbackInfo callbackInfo) throws org.apache.thrift.TException; + + } + + public interface AsyncIface { + + public void callback(CallbackInfo callbackInfo, org.apache.thrift.async.AsyncMethodCallback resultHandler) throws org.apache.thrift.TException; + + } + + public static class Client extends org.apache.thrift.TServiceClient implements Iface { + public static class Factory implements org.apache.thrift.TServiceClientFactory { + public Factory() {} + public Client getClient(org.apache.thrift.protocol.TProtocol prot) { + return new Client(prot); + } + public Client getClient(org.apache.thrift.protocol.TProtocol iprot, org.apache.thrift.protocol.TProtocol oprot) { + return new Client(iprot, oprot); + } + } + + public Client(org.apache.thrift.protocol.TProtocol prot) + { + super(prot, prot); + } + + public Client(org.apache.thrift.protocol.TProtocol iprot, org.apache.thrift.protocol.TProtocol oprot) { + super(iprot, oprot); + } + + public void callback(CallbackInfo callbackInfo) throws org.apache.thrift.TException + { + send_callback(callbackInfo); + recv_callback(); + } + + public void send_callback(CallbackInfo callbackInfo) throws org.apache.thrift.TException + { + callback_args args = new callback_args(); + args.setCallbackInfo(callbackInfo); + sendBase("callback", args); + } + + public void recv_callback() throws org.apache.thrift.TException + { + callback_result result = new callback_result(); + receiveBase(result, "callback"); + return; + } + + } + public static class AsyncClient extends org.apache.thrift.async.TAsyncClient implements AsyncIface { + public static class Factory implements org.apache.thrift.async.TAsyncClientFactory { + private org.apache.thrift.async.TAsyncClientManager clientManager; + private org.apache.thrift.protocol.TProtocolFactory protocolFactory; + public Factory(org.apache.thrift.async.TAsyncClientManager clientManager, org.apache.thrift.protocol.TProtocolFactory protocolFactory) { + this.clientManager = clientManager; + this.protocolFactory = protocolFactory; + } + public AsyncClient getAsyncClient(org.apache.thrift.transport.TNonblockingTransport transport) { + return new AsyncClient(protocolFactory, clientManager, transport); + } + } + + public AsyncClient(org.apache.thrift.protocol.TProtocolFactory protocolFactory, org.apache.thrift.async.TAsyncClientManager clientManager, org.apache.thrift.transport.TNonblockingTransport transport) { + super(protocolFactory, clientManager, transport); + } + + public void callback(CallbackInfo callbackInfo, org.apache.thrift.async.AsyncMethodCallback resultHandler) throws org.apache.thrift.TException { + checkReady(); + callback_call method_call = new callback_call(callbackInfo, resultHandler, this, ___protocolFactory, ___transport); + this.___currentMethod = method_call; + ___manager.call(method_call); + } + + public static class callback_call extends org.apache.thrift.async.TAsyncMethodCall { + private CallbackInfo callbackInfo; + public callback_call(CallbackInfo callbackInfo, org.apache.thrift.async.AsyncMethodCallback resultHandler, org.apache.thrift.async.TAsyncClient client, org.apache.thrift.protocol.TProtocolFactory protocolFactory, org.apache.thrift.transport.TNonblockingTransport transport) throws org.apache.thrift.TException { + super(client, protocolFactory, transport, resultHandler, false); + this.callbackInfo = callbackInfo; + } + + public void write_args(org.apache.thrift.protocol.TProtocol prot) throws org.apache.thrift.TException { + prot.writeMessageBegin(new org.apache.thrift.protocol.TMessage("callback", org.apache.thrift.protocol.TMessageType.CALL, 0)); + callback_args args = new callback_args(); + args.setCallbackInfo(callbackInfo); + args.write(prot); + prot.writeMessageEnd(); + } + + public void getResult() throws org.apache.thrift.TException { + if (getState() != org.apache.thrift.async.TAsyncMethodCall.State.RESPONSE_READ) { + throw new IllegalStateException("Method call not finished!"); + } + org.apache.thrift.transport.TMemoryInputTransport memoryTransport = new org.apache.thrift.transport.TMemoryInputTransport(getFrameBuffer().array()); + org.apache.thrift.protocol.TProtocol prot = client.getProtocolFactory().getProtocol(memoryTransport); + (new Client(prot)).recv_callback(); + } + } + + } + + public static class Processor extends org.apache.thrift.TBaseProcessor implements org.apache.thrift.TProcessor { + private static final Logger LOGGER = LoggerFactory.getLogger(Processor.class.getName()); + public Processor(I iface) { + super(iface, getProcessMap(new HashMap>())); + } + + protected Processor(I iface, Map> processMap) { + super(iface, getProcessMap(processMap)); + } + + private static Map> getProcessMap(Map> processMap) { + processMap.put("callback", new callback()); + return processMap; + } + + public static class callback extends org.apache.thrift.ProcessFunction { + public callback() { + super("callback"); + } + + public callback_args getEmptyArgsInstance() { + return new callback_args(); + } + + protected boolean isOneway() { + return false; + } + + public callback_result getResult(I iface, callback_args args) throws org.apache.thrift.TException { + callback_result result = new callback_result(); + iface.callback(args.callbackInfo); + return result; + } + } + + } + + public static class AsyncProcessor extends org.apache.thrift.TBaseAsyncProcessor { + private static final Logger LOGGER = LoggerFactory.getLogger(AsyncProcessor.class.getName()); + public AsyncProcessor(I iface) { + super(iface, getProcessMap(new HashMap>())); + } + + protected AsyncProcessor(I iface, Map> processMap) { + super(iface, getProcessMap(processMap)); + } + + private static Map> getProcessMap(Map> processMap) { + processMap.put("callback", new callback()); + return processMap; + } + + public static class callback extends org.apache.thrift.AsyncProcessFunction { + public callback() { + super("callback"); + } + + public callback_args getEmptyArgsInstance() { + return new callback_args(); + } + + public AsyncMethodCallback getResultHandler(final AsyncFrameBuffer fb, final int seqid) { + final org.apache.thrift.AsyncProcessFunction fcall = this; + return new AsyncMethodCallback() { + public void onComplete(Void o) { + callback_result result = new callback_result(); + try { + fcall.sendResponse(fb,result, org.apache.thrift.protocol.TMessageType.REPLY,seqid); + return; + } catch (Exception e) { + LOGGER.error("Exception writing to internal frame buffer", e); + } + fb.close(); + } + public void onError(Exception e) { + byte msgType = org.apache.thrift.protocol.TMessageType.REPLY; + org.apache.thrift.TBase msg; + callback_result result = new callback_result(); + { + msgType = org.apache.thrift.protocol.TMessageType.EXCEPTION; + msg = (org.apache.thrift.TBase)new org.apache.thrift.TApplicationException(org.apache.thrift.TApplicationException.INTERNAL_ERROR, e.getMessage()); + } + try { + fcall.sendResponse(fb,msg,msgType,seqid); + return; + } catch (Exception ex) { + LOGGER.error("Exception writing to internal frame buffer", ex); + } + fb.close(); + } + }; + } + + protected boolean isOneway() { + return false; + } + + public void start(I iface, callback_args args, org.apache.thrift.async.AsyncMethodCallback resultHandler) throws TException { + iface.callback(args.callbackInfo,resultHandler); + } + } + + } + + public static class callback_args implements org.apache.thrift.TBase, java.io.Serializable, Cloneable, Comparable { + private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("callback_args"); + + private static final org.apache.thrift.protocol.TField CALLBACK_INFO_FIELD_DESC = new org.apache.thrift.protocol.TField("callbackInfo", org.apache.thrift.protocol.TType.STRUCT, (short)1); + + private static final Map, SchemeFactory> schemes = new HashMap, SchemeFactory>(); + static { + schemes.put(StandardScheme.class, new callback_argsStandardSchemeFactory()); + schemes.put(TupleScheme.class, new callback_argsTupleSchemeFactory()); + } + + public CallbackInfo callbackInfo; // required + + /** The set of fields this struct contains, along with convenience methods for finding and manipulating them. */ + public enum _Fields implements org.apache.thrift.TFieldIdEnum { + CALLBACK_INFO((short)1, "callbackInfo"); + + private static final Map byName = new HashMap(); + + static { + for (_Fields field : EnumSet.allOf(_Fields.class)) { + byName.put(field.getFieldName(), field); + } + } + + /** + * Find the _Fields constant that matches fieldId, or null if its not found. + */ + public static _Fields findByThriftId(int fieldId) { + switch(fieldId) { + case 1: // CALLBACK_INFO + return CALLBACK_INFO; + default: + return null; + } + } + + /** + * Find the _Fields constant that matches fieldId, throwing an exception + * if it is not found. + */ + public static _Fields findByThriftIdOrThrow(int fieldId) { + _Fields fields = findByThriftId(fieldId); + if (fields == null) throw new IllegalArgumentException("Field " + fieldId + " doesn't exist!"); + return fields; + } + + /** + * Find the _Fields constant that matches name, or null if its not found. + */ + public static _Fields findByName(String name) { + return byName.get(name); + } + + private final short _thriftId; + private final String _fieldName; + + _Fields(short thriftId, String fieldName) { + _thriftId = thriftId; + _fieldName = fieldName; + } + + public short getThriftFieldId() { + return _thriftId; + } + + public String getFieldName() { + return _fieldName; + } + } + + // isset id assignments + public static final Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> metaDataMap; + static { + Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> tmpMap = new EnumMap<_Fields, org.apache.thrift.meta_data.FieldMetaData>(_Fields.class); + tmpMap.put(_Fields.CALLBACK_INFO, new org.apache.thrift.meta_data.FieldMetaData("callbackInfo", org.apache.thrift.TFieldRequirementType.DEFAULT, + new org.apache.thrift.meta_data.StructMetaData(org.apache.thrift.protocol.TType.STRUCT, CallbackInfo.class))); + metaDataMap = Collections.unmodifiableMap(tmpMap); + org.apache.thrift.meta_data.FieldMetaData.addStructMetaDataMap(callback_args.class, metaDataMap); + } + + public callback_args() { + } + + public callback_args( + CallbackInfo callbackInfo) + { + this(); + this.callbackInfo = callbackInfo; + } + + /** + * Performs a deep copy on other. + */ + public callback_args(callback_args other) { + if (other.isSetCallbackInfo()) { + this.callbackInfo = new CallbackInfo(other.callbackInfo); + } + } + + public callback_args deepCopy() { + return new callback_args(this); + } + + @Override + public void clear() { + this.callbackInfo = null; + } + + public CallbackInfo getCallbackInfo() { + return this.callbackInfo; + } + + public callback_args setCallbackInfo(CallbackInfo callbackInfo) { + this.callbackInfo = callbackInfo; + return this; + } + + public void unsetCallbackInfo() { + this.callbackInfo = null; + } + + /** Returns true if field callbackInfo is set (has been assigned a value) and false otherwise */ + public boolean isSetCallbackInfo() { + return this.callbackInfo != null; + } + + public void setCallbackInfoIsSet(boolean value) { + if (!value) { + this.callbackInfo = null; + } + } + + public void setFieldValue(_Fields field, Object value) { + switch (field) { + case CALLBACK_INFO: + if (value == null) { + unsetCallbackInfo(); + } else { + setCallbackInfo((CallbackInfo)value); + } + break; + + } + } + + public Object getFieldValue(_Fields field) { + switch (field) { + case CALLBACK_INFO: + return getCallbackInfo(); + + } + throw new IllegalStateException(); + } + + /** Returns true if field corresponding to fieldID is set (has been assigned a value) and false otherwise */ + public boolean isSet(_Fields field) { + if (field == null) { + throw new IllegalArgumentException(); + } + + switch (field) { + case CALLBACK_INFO: + return isSetCallbackInfo(); + } + throw new IllegalStateException(); + } + + @Override + public boolean equals(Object that) { + if (that == null) + return false; + if (that instanceof callback_args) + return this.equals((callback_args)that); + return false; + } + + public boolean equals(callback_args that) { + if (that == null) + return false; + + boolean this_present_callbackInfo = true && this.isSetCallbackInfo(); + boolean that_present_callbackInfo = true && that.isSetCallbackInfo(); + if (this_present_callbackInfo || that_present_callbackInfo) { + if (!(this_present_callbackInfo && that_present_callbackInfo)) + return false; + if (!this.callbackInfo.equals(that.callbackInfo)) + return false; + } + + return true; + } + + @Override + public int hashCode() { + List list = new ArrayList(); + + boolean present_callbackInfo = true && (isSetCallbackInfo()); + list.add(present_callbackInfo); + if (present_callbackInfo) + list.add(callbackInfo); + + return list.hashCode(); + } + + @Override + public int compareTo(callback_args other) { + if (!getClass().equals(other.getClass())) { + return getClass().getName().compareTo(other.getClass().getName()); + } + + int lastComparison = 0; + + lastComparison = Boolean.valueOf(isSetCallbackInfo()).compareTo(other.isSetCallbackInfo()); + if (lastComparison != 0) { + return lastComparison; + } + if (isSetCallbackInfo()) { + lastComparison = org.apache.thrift.TBaseHelper.compareTo(this.callbackInfo, other.callbackInfo); + if (lastComparison != 0) { + return lastComparison; + } + } + return 0; + } + + public _Fields fieldForId(int fieldId) { + return _Fields.findByThriftId(fieldId); + } + + public void read(org.apache.thrift.protocol.TProtocol iprot) throws org.apache.thrift.TException { + schemes.get(iprot.getScheme()).getScheme().read(iprot, this); + } + + public void write(org.apache.thrift.protocol.TProtocol oprot) throws org.apache.thrift.TException { + schemes.get(oprot.getScheme()).getScheme().write(oprot, this); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("callback_args("); + boolean first = true; + + sb.append("callbackInfo:"); + if (this.callbackInfo == null) { + sb.append("null"); + } else { + sb.append(this.callbackInfo); + } + first = false; + sb.append(")"); + return sb.toString(); + } + + public void validate() throws org.apache.thrift.TException { + // check for required fields + // check for sub-struct validity + if (callbackInfo != null) { + callbackInfo.validate(); + } + } + + private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException { + try { + write(new org.apache.thrift.protocol.TCompactProtocol(new org.apache.thrift.transport.TIOStreamTransport(out))); + } catch (org.apache.thrift.TException te) { + throw new java.io.IOException(te); + } + } + + private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, ClassNotFoundException { + try { + read(new org.apache.thrift.protocol.TCompactProtocol(new org.apache.thrift.transport.TIOStreamTransport(in))); + } catch (org.apache.thrift.TException te) { + throw new java.io.IOException(te); + } + } + + private static class callback_argsStandardSchemeFactory implements SchemeFactory { + public callback_argsStandardScheme getScheme() { + return new callback_argsStandardScheme(); + } + } + + private static class callback_argsStandardScheme extends StandardScheme { + + public void read(org.apache.thrift.protocol.TProtocol iprot, callback_args struct) throws org.apache.thrift.TException { + org.apache.thrift.protocol.TField schemeField; + iprot.readStructBegin(); + while (true) + { + schemeField = iprot.readFieldBegin(); + if (schemeField.type == org.apache.thrift.protocol.TType.STOP) { + break; + } + switch (schemeField.id) { + case 1: // CALLBACK_INFO + if (schemeField.type == org.apache.thrift.protocol.TType.STRUCT) { + struct.callbackInfo = new CallbackInfo(); + struct.callbackInfo.read(iprot); + struct.setCallbackInfoIsSet(true); + } else { + org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); + } + break; + default: + org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); + } + iprot.readFieldEnd(); + } + iprot.readStructEnd(); + + // check for required fields of primitive type, which can't be checked in the validate method + struct.validate(); + } + + public void write(org.apache.thrift.protocol.TProtocol oprot, callback_args struct) throws org.apache.thrift.TException { + struct.validate(); + + oprot.writeStructBegin(STRUCT_DESC); + if (struct.callbackInfo != null) { + oprot.writeFieldBegin(CALLBACK_INFO_FIELD_DESC); + struct.callbackInfo.write(oprot); + oprot.writeFieldEnd(); + } + oprot.writeFieldStop(); + oprot.writeStructEnd(); + } + + } + + private static class callback_argsTupleSchemeFactory implements SchemeFactory { + public callback_argsTupleScheme getScheme() { + return new callback_argsTupleScheme(); + } + } + + private static class callback_argsTupleScheme extends TupleScheme { + + @Override + public void write(org.apache.thrift.protocol.TProtocol prot, callback_args struct) throws org.apache.thrift.TException { + TTupleProtocol oprot = (TTupleProtocol) prot; + BitSet optionals = new BitSet(); + if (struct.isSetCallbackInfo()) { + optionals.set(0); + } + oprot.writeBitSet(optionals, 1); + if (struct.isSetCallbackInfo()) { + struct.callbackInfo.write(oprot); + } + } + + @Override + public void read(org.apache.thrift.protocol.TProtocol prot, callback_args struct) throws org.apache.thrift.TException { + TTupleProtocol iprot = (TTupleProtocol) prot; + BitSet incoming = iprot.readBitSet(1); + if (incoming.get(0)) { + struct.callbackInfo = new CallbackInfo(); + struct.callbackInfo.read(iprot); + struct.setCallbackInfoIsSet(true); + } + } + } + + } + + public static class callback_result implements org.apache.thrift.TBase, java.io.Serializable, Cloneable, Comparable { + private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("callback_result"); + + + private static final Map, SchemeFactory> schemes = new HashMap, SchemeFactory>(); + static { + schemes.put(StandardScheme.class, new callback_resultStandardSchemeFactory()); + schemes.put(TupleScheme.class, new callback_resultTupleSchemeFactory()); + } + + + /** The set of fields this struct contains, along with convenience methods for finding and manipulating them. */ + public enum _Fields implements org.apache.thrift.TFieldIdEnum { +; + + private static final Map byName = new HashMap(); + + static { + for (_Fields field : EnumSet.allOf(_Fields.class)) { + byName.put(field.getFieldName(), field); + } + } + + /** + * Find the _Fields constant that matches fieldId, or null if its not found. + */ + public static _Fields findByThriftId(int fieldId) { + switch(fieldId) { + default: + return null; + } + } + + /** + * Find the _Fields constant that matches fieldId, throwing an exception + * if it is not found. + */ + public static _Fields findByThriftIdOrThrow(int fieldId) { + _Fields fields = findByThriftId(fieldId); + if (fields == null) throw new IllegalArgumentException("Field " + fieldId + " doesn't exist!"); + return fields; + } + + /** + * Find the _Fields constant that matches name, or null if its not found. + */ + public static _Fields findByName(String name) { + return byName.get(name); + } + + private final short _thriftId; + private final String _fieldName; + + _Fields(short thriftId, String fieldName) { + _thriftId = thriftId; + _fieldName = fieldName; + } + + public short getThriftFieldId() { + return _thriftId; + } + + public String getFieldName() { + return _fieldName; + } + } + public static final Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> metaDataMap; + static { + Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> tmpMap = new EnumMap<_Fields, org.apache.thrift.meta_data.FieldMetaData>(_Fields.class); + metaDataMap = Collections.unmodifiableMap(tmpMap); + org.apache.thrift.meta_data.FieldMetaData.addStructMetaDataMap(callback_result.class, metaDataMap); + } + + public callback_result() { + } + + /** + * Performs a deep copy on other. + */ + public callback_result(callback_result other) { + } + + public callback_result deepCopy() { + return new callback_result(this); + } + + @Override + public void clear() { + } + + public void setFieldValue(_Fields field, Object value) { + switch (field) { + } + } + + public Object getFieldValue(_Fields field) { + switch (field) { + } + throw new IllegalStateException(); + } + + /** Returns true if field corresponding to fieldID is set (has been assigned a value) and false otherwise */ + public boolean isSet(_Fields field) { + if (field == null) { + throw new IllegalArgumentException(); + } + + switch (field) { + } + throw new IllegalStateException(); + } + + @Override + public boolean equals(Object that) { + if (that == null) + return false; + if (that instanceof callback_result) + return this.equals((callback_result)that); + return false; + } + + public boolean equals(callback_result that) { + if (that == null) + return false; + + return true; + } + + @Override + public int hashCode() { + List list = new ArrayList(); + + return list.hashCode(); + } + + @Override + public int compareTo(callback_result other) { + if (!getClass().equals(other.getClass())) { + return getClass().getName().compareTo(other.getClass().getName()); + } + + int lastComparison = 0; + + return 0; + } + + public _Fields fieldForId(int fieldId) { + return _Fields.findByThriftId(fieldId); + } + + public void read(org.apache.thrift.protocol.TProtocol iprot) throws org.apache.thrift.TException { + schemes.get(iprot.getScheme()).getScheme().read(iprot, this); + } + + public void write(org.apache.thrift.protocol.TProtocol oprot) throws org.apache.thrift.TException { + schemes.get(oprot.getScheme()).getScheme().write(oprot, this); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("callback_result("); + boolean first = true; + + sb.append(")"); + return sb.toString(); + } + + public void validate() throws org.apache.thrift.TException { + // check for required fields + // check for sub-struct validity + } + + private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException { + try { + write(new org.apache.thrift.protocol.TCompactProtocol(new org.apache.thrift.transport.TIOStreamTransport(out))); + } catch (org.apache.thrift.TException te) { + throw new java.io.IOException(te); + } + } + + private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, ClassNotFoundException { + try { + read(new org.apache.thrift.protocol.TCompactProtocol(new org.apache.thrift.transport.TIOStreamTransport(in))); + } catch (org.apache.thrift.TException te) { + throw new java.io.IOException(te); + } + } + + private static class callback_resultStandardSchemeFactory implements SchemeFactory { + public callback_resultStandardScheme getScheme() { + return new callback_resultStandardScheme(); + } + } + + private static class callback_resultStandardScheme extends StandardScheme { + + public void read(org.apache.thrift.protocol.TProtocol iprot, callback_result struct) throws org.apache.thrift.TException { + org.apache.thrift.protocol.TField schemeField; + iprot.readStructBegin(); + while (true) + { + schemeField = iprot.readFieldBegin(); + if (schemeField.type == org.apache.thrift.protocol.TType.STOP) { + break; + } + switch (schemeField.id) { + default: + org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); + } + iprot.readFieldEnd(); + } + iprot.readStructEnd(); + + // check for required fields of primitive type, which can't be checked in the validate method + struct.validate(); + } + + public void write(org.apache.thrift.protocol.TProtocol oprot, callback_result struct) throws org.apache.thrift.TException { + struct.validate(); + + oprot.writeStructBegin(STRUCT_DESC); + oprot.writeFieldStop(); + oprot.writeStructEnd(); + } + + } + + private static class callback_resultTupleSchemeFactory implements SchemeFactory { + public callback_resultTupleScheme getScheme() { + return new callback_resultTupleScheme(); + } + } + + private static class callback_resultTupleScheme extends TupleScheme { + + @Override + public void write(org.apache.thrift.protocol.TProtocol prot, callback_result struct) throws org.apache.thrift.TException { + TTupleProtocol oprot = (TTupleProtocol) prot; + } + + @Override + public void read(org.apache.thrift.protocol.TProtocol prot, callback_result struct) throws org.apache.thrift.TException { + TTupleProtocol iprot = (TTupleProtocol) prot; + } + } + + } + +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterContext.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterContext.java index d6619fcc35f..cea7e654608 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterContext.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterContext.java @@ -51,7 +51,7 @@ import org.slf4j.LoggerFactory; @SuppressWarnings({"cast", "rawtypes", "serial", "unchecked"}) -@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-3-27") +@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-10-22") public class RemoteInterpreterContext implements org.apache.thrift.TBase, java.io.Serializable, Cloneable, Comparable { private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("RemoteInterpreterContext"); @@ -63,7 +63,8 @@ public class RemoteInterpreterContext implements org.apache.thrift.TBase, SchemeFactory> schemes = new HashMap, SchemeFactory>(); static { @@ -79,6 +80,7 @@ public class RemoteInterpreterContext implements org.apache.thrift.TBase byName = new HashMap(); @@ -122,7 +125,9 @@ public static _Fields findByThriftId(int fieldId) { return CONFIG; case 8: // GUI return GUI; - case 9: // RUNNERS + case 9: // NOTE_GUI + return NOTE_GUI; + case 10: // RUNNERS return RUNNERS; default: return null; @@ -183,6 +188,8 @@ public String getFieldName() { new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING))); tmpMap.put(_Fields.GUI, new org.apache.thrift.meta_data.FieldMetaData("gui", org.apache.thrift.TFieldRequirementType.DEFAULT, new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING))); + tmpMap.put(_Fields.NOTE_GUI, new org.apache.thrift.meta_data.FieldMetaData("noteGui", org.apache.thrift.TFieldRequirementType.DEFAULT, + new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING))); tmpMap.put(_Fields.RUNNERS, new org.apache.thrift.meta_data.FieldMetaData("runners", org.apache.thrift.TFieldRequirementType.DEFAULT, new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING))); metaDataMap = Collections.unmodifiableMap(tmpMap); @@ -201,6 +208,7 @@ public RemoteInterpreterContext( String authenticationInfo, String config, String gui, + String noteGui, String runners) { this(); @@ -212,6 +220,7 @@ public RemoteInterpreterContext( this.authenticationInfo = authenticationInfo; this.config = config; this.gui = gui; + this.noteGui = noteGui; this.runners = runners; } @@ -243,6 +252,9 @@ public RemoteInterpreterContext(RemoteInterpreterContext other) { if (other.isSetGui()) { this.gui = other.gui; } + if (other.isSetNoteGui()) { + this.noteGui = other.noteGui; + } if (other.isSetRunners()) { this.runners = other.runners; } @@ -262,6 +274,7 @@ public void clear() { this.authenticationInfo = null; this.config = null; this.gui = null; + this.noteGui = null; this.runners = null; } @@ -457,6 +470,30 @@ public void setGuiIsSet(boolean value) { } } + public String getNoteGui() { + return this.noteGui; + } + + public RemoteInterpreterContext setNoteGui(String noteGui) { + this.noteGui = noteGui; + return this; + } + + public void unsetNoteGui() { + this.noteGui = null; + } + + /** Returns true if field noteGui is set (has been assigned a value) and false otherwise */ + public boolean isSetNoteGui() { + return this.noteGui != null; + } + + public void setNoteGuiIsSet(boolean value) { + if (!value) { + this.noteGui = null; + } + } + public String getRunners() { return this.runners; } @@ -547,6 +584,14 @@ public void setFieldValue(_Fields field, Object value) { } break; + case NOTE_GUI: + if (value == null) { + unsetNoteGui(); + } else { + setNoteGui((String)value); + } + break; + case RUNNERS: if (value == null) { unsetRunners(); @@ -584,6 +629,9 @@ public Object getFieldValue(_Fields field) { case GUI: return getGui(); + case NOTE_GUI: + return getNoteGui(); + case RUNNERS: return getRunners(); @@ -614,6 +662,8 @@ public boolean isSet(_Fields field) { return isSetConfig(); case GUI: return isSetGui(); + case NOTE_GUI: + return isSetNoteGui(); case RUNNERS: return isSetRunners(); } @@ -705,6 +755,15 @@ public boolean equals(RemoteInterpreterContext that) { return false; } + boolean this_present_noteGui = true && this.isSetNoteGui(); + boolean that_present_noteGui = true && that.isSetNoteGui(); + if (this_present_noteGui || that_present_noteGui) { + if (!(this_present_noteGui && that_present_noteGui)) + return false; + if (!this.noteGui.equals(that.noteGui)) + return false; + } + boolean this_present_runners = true && this.isSetRunners(); boolean that_present_runners = true && that.isSetRunners(); if (this_present_runners || that_present_runners) { @@ -761,6 +820,11 @@ public int hashCode() { if (present_gui) list.add(gui); + boolean present_noteGui = true && (isSetNoteGui()); + list.add(present_noteGui); + if (present_noteGui) + list.add(noteGui); + boolean present_runners = true && (isSetRunners()); list.add(present_runners); if (present_runners) @@ -857,6 +921,16 @@ public int compareTo(RemoteInterpreterContext other) { return lastComparison; } } + lastComparison = Boolean.valueOf(isSetNoteGui()).compareTo(other.isSetNoteGui()); + if (lastComparison != 0) { + return lastComparison; + } + if (isSetNoteGui()) { + lastComparison = org.apache.thrift.TBaseHelper.compareTo(this.noteGui, other.noteGui); + if (lastComparison != 0) { + return lastComparison; + } + } lastComparison = Boolean.valueOf(isSetRunners()).compareTo(other.isSetRunners()); if (lastComparison != 0) { return lastComparison; @@ -951,6 +1025,14 @@ public String toString() { } first = false; if (!first) sb.append(", "); + sb.append("noteGui:"); + if (this.noteGui == null) { + sb.append("null"); + } else { + sb.append(this.noteGui); + } + first = false; + if (!first) sb.append(", "); sb.append("runners:"); if (this.runners == null) { sb.append("null"); @@ -1065,7 +1147,15 @@ public void read(org.apache.thrift.protocol.TProtocol iprot, RemoteInterpreterCo org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); } break; - case 9: // RUNNERS + case 9: // NOTE_GUI + if (schemeField.type == org.apache.thrift.protocol.TType.STRING) { + struct.noteGui = iprot.readString(); + struct.setNoteGuiIsSet(true); + } else { + org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); + } + break; + case 10: // RUNNERS if (schemeField.type == org.apache.thrift.protocol.TType.STRING) { struct.runners = iprot.readString(); struct.setRunnersIsSet(true); @@ -1128,6 +1218,11 @@ public void write(org.apache.thrift.protocol.TProtocol oprot, RemoteInterpreterC oprot.writeString(struct.gui); oprot.writeFieldEnd(); } + if (struct.noteGui != null) { + oprot.writeFieldBegin(NOTE_GUI_FIELD_DESC); + oprot.writeString(struct.noteGui); + oprot.writeFieldEnd(); + } if (struct.runners != null) { oprot.writeFieldBegin(RUNNERS_FIELD_DESC); oprot.writeString(struct.runners); @@ -1175,10 +1270,13 @@ public void write(org.apache.thrift.protocol.TProtocol prot, RemoteInterpreterCo if (struct.isSetGui()) { optionals.set(7); } - if (struct.isSetRunners()) { + if (struct.isSetNoteGui()) { optionals.set(8); } - oprot.writeBitSet(optionals, 9); + if (struct.isSetRunners()) { + optionals.set(9); + } + oprot.writeBitSet(optionals, 10); if (struct.isSetNoteId()) { oprot.writeString(struct.noteId); } @@ -1203,6 +1301,9 @@ public void write(org.apache.thrift.protocol.TProtocol prot, RemoteInterpreterCo if (struct.isSetGui()) { oprot.writeString(struct.gui); } + if (struct.isSetNoteGui()) { + oprot.writeString(struct.noteGui); + } if (struct.isSetRunners()) { oprot.writeString(struct.runners); } @@ -1211,7 +1312,7 @@ public void write(org.apache.thrift.protocol.TProtocol prot, RemoteInterpreterCo @Override public void read(org.apache.thrift.protocol.TProtocol prot, RemoteInterpreterContext struct) throws org.apache.thrift.TException { TTupleProtocol iprot = (TTupleProtocol) prot; - BitSet incoming = iprot.readBitSet(9); + BitSet incoming = iprot.readBitSet(10); if (incoming.get(0)) { struct.noteId = iprot.readString(); struct.setNoteIdIsSet(true); @@ -1245,6 +1346,10 @@ public void read(org.apache.thrift.protocol.TProtocol prot, RemoteInterpreterCon struct.setGuiIsSet(true); } if (incoming.get(8)) { + struct.noteGui = iprot.readString(); + struct.setNoteGuiIsSet(true); + } + if (incoming.get(9)) { struct.runners = iprot.readString(); struct.setRunnersIsSet(true); } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterEvent.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterEvent.java index e2527753741..c75a42f4cc6 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterEvent.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterEvent.java @@ -51,7 +51,7 @@ import org.slf4j.LoggerFactory; @SuppressWarnings({"cast", "rawtypes", "serial", "unchecked"}) -@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-3-27") +@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-10-22") public class RemoteInterpreterEvent implements org.apache.thrift.TBase, java.io.Serializable, Cloneable, Comparable { private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("RemoteInterpreterEvent"); diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterResult.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterResult.java index b18bad53a96..efe05aa13a2 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterResult.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterResult.java @@ -51,7 +51,7 @@ import org.slf4j.LoggerFactory; @SuppressWarnings({"cast", "rawtypes", "serial", "unchecked"}) -@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-3-27") +@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-10-22") public class RemoteInterpreterResult implements org.apache.thrift.TBase, java.io.Serializable, Cloneable, Comparable { private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("RemoteInterpreterResult"); @@ -59,6 +59,7 @@ public class RemoteInterpreterResult implements org.apache.thrift.TBase, SchemeFactory> schemes = new HashMap, SchemeFactory>(); static { @@ -70,13 +71,15 @@ public class RemoteInterpreterResult implements org.apache.thrift.TBase msg; // required public String config; // required public String gui; // required + public String noteGui; // required /** The set of fields this struct contains, along with convenience methods for finding and manipulating them. */ public enum _Fields implements org.apache.thrift.TFieldIdEnum { CODE((short)1, "code"), MSG((short)2, "msg"), CONFIG((short)3, "config"), - GUI((short)4, "gui"); + GUI((short)4, "gui"), + NOTE_GUI((short)5, "noteGui"); private static final Map byName = new HashMap(); @@ -99,6 +102,8 @@ public static _Fields findByThriftId(int fieldId) { return CONFIG; case 4: // GUI return GUI; + case 5: // NOTE_GUI + return NOTE_GUI; default: return null; } @@ -151,6 +156,8 @@ public String getFieldName() { new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING))); tmpMap.put(_Fields.GUI, new org.apache.thrift.meta_data.FieldMetaData("gui", org.apache.thrift.TFieldRequirementType.DEFAULT, new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING))); + tmpMap.put(_Fields.NOTE_GUI, new org.apache.thrift.meta_data.FieldMetaData("noteGui", org.apache.thrift.TFieldRequirementType.DEFAULT, + new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING))); metaDataMap = Collections.unmodifiableMap(tmpMap); org.apache.thrift.meta_data.FieldMetaData.addStructMetaDataMap(RemoteInterpreterResult.class, metaDataMap); } @@ -162,13 +169,15 @@ public RemoteInterpreterResult( String code, List msg, String config, - String gui) + String gui, + String noteGui) { this(); this.code = code; this.msg = msg; this.config = config; this.gui = gui; + this.noteGui = noteGui; } /** @@ -191,6 +200,9 @@ public RemoteInterpreterResult(RemoteInterpreterResult other) { if (other.isSetGui()) { this.gui = other.gui; } + if (other.isSetNoteGui()) { + this.noteGui = other.noteGui; + } } public RemoteInterpreterResult deepCopy() { @@ -203,6 +215,7 @@ public void clear() { this.msg = null; this.config = null; this.gui = null; + this.noteGui = null; } public String getCode() { @@ -316,6 +329,30 @@ public void setGuiIsSet(boolean value) { } } + public String getNoteGui() { + return this.noteGui; + } + + public RemoteInterpreterResult setNoteGui(String noteGui) { + this.noteGui = noteGui; + return this; + } + + public void unsetNoteGui() { + this.noteGui = null; + } + + /** Returns true if field noteGui is set (has been assigned a value) and false otherwise */ + public boolean isSetNoteGui() { + return this.noteGui != null; + } + + public void setNoteGuiIsSet(boolean value) { + if (!value) { + this.noteGui = null; + } + } + public void setFieldValue(_Fields field, Object value) { switch (field) { case CODE: @@ -350,6 +387,14 @@ public void setFieldValue(_Fields field, Object value) { } break; + case NOTE_GUI: + if (value == null) { + unsetNoteGui(); + } else { + setNoteGui((String)value); + } + break; + } } @@ -367,6 +412,9 @@ public Object getFieldValue(_Fields field) { case GUI: return getGui(); + case NOTE_GUI: + return getNoteGui(); + } throw new IllegalStateException(); } @@ -386,6 +434,8 @@ public boolean isSet(_Fields field) { return isSetConfig(); case GUI: return isSetGui(); + case NOTE_GUI: + return isSetNoteGui(); } throw new IllegalStateException(); } @@ -439,6 +489,15 @@ public boolean equals(RemoteInterpreterResult that) { return false; } + boolean this_present_noteGui = true && this.isSetNoteGui(); + boolean that_present_noteGui = true && that.isSetNoteGui(); + if (this_present_noteGui || that_present_noteGui) { + if (!(this_present_noteGui && that_present_noteGui)) + return false; + if (!this.noteGui.equals(that.noteGui)) + return false; + } + return true; } @@ -466,6 +525,11 @@ public int hashCode() { if (present_gui) list.add(gui); + boolean present_noteGui = true && (isSetNoteGui()); + list.add(present_noteGui); + if (present_noteGui) + list.add(noteGui); + return list.hashCode(); } @@ -517,6 +581,16 @@ public int compareTo(RemoteInterpreterResult other) { return lastComparison; } } + lastComparison = Boolean.valueOf(isSetNoteGui()).compareTo(other.isSetNoteGui()); + if (lastComparison != 0) { + return lastComparison; + } + if (isSetNoteGui()) { + lastComparison = org.apache.thrift.TBaseHelper.compareTo(this.noteGui, other.noteGui); + if (lastComparison != 0) { + return lastComparison; + } + } return 0; } @@ -568,6 +642,14 @@ public String toString() { sb.append(this.gui); } first = false; + if (!first) sb.append(", "); + sb.append("noteGui:"); + if (this.noteGui == null) { + sb.append("null"); + } else { + sb.append(this.noteGui); + } + first = false; sb.append(")"); return sb.toString(); } @@ -654,6 +736,14 @@ public void read(org.apache.thrift.protocol.TProtocol iprot, RemoteInterpreterRe org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); } break; + case 5: // NOTE_GUI + if (schemeField.type == org.apache.thrift.protocol.TType.STRING) { + struct.noteGui = iprot.readString(); + struct.setNoteGuiIsSet(true); + } else { + org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); + } + break; default: org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); } @@ -696,6 +786,11 @@ public void write(org.apache.thrift.protocol.TProtocol oprot, RemoteInterpreterR oprot.writeString(struct.gui); oprot.writeFieldEnd(); } + if (struct.noteGui != null) { + oprot.writeFieldBegin(NOTE_GUI_FIELD_DESC); + oprot.writeString(struct.noteGui); + oprot.writeFieldEnd(); + } oprot.writeFieldStop(); oprot.writeStructEnd(); } @@ -726,7 +821,10 @@ public void write(org.apache.thrift.protocol.TProtocol prot, RemoteInterpreterRe if (struct.isSetGui()) { optionals.set(3); } - oprot.writeBitSet(optionals, 4); + if (struct.isSetNoteGui()) { + optionals.set(4); + } + oprot.writeBitSet(optionals, 5); if (struct.isSetCode()) { oprot.writeString(struct.code); } @@ -745,12 +843,15 @@ public void write(org.apache.thrift.protocol.TProtocol prot, RemoteInterpreterRe if (struct.isSetGui()) { oprot.writeString(struct.gui); } + if (struct.isSetNoteGui()) { + oprot.writeString(struct.noteGui); + } } @Override public void read(org.apache.thrift.protocol.TProtocol prot, RemoteInterpreterResult struct) throws org.apache.thrift.TException { TTupleProtocol iprot = (TTupleProtocol) prot; - BitSet incoming = iprot.readBitSet(4); + BitSet incoming = iprot.readBitSet(5); if (incoming.get(0)) { struct.code = iprot.readString(); struct.setCodeIsSet(true); @@ -777,6 +878,10 @@ public void read(org.apache.thrift.protocol.TProtocol prot, RemoteInterpreterRes struct.gui = iprot.readString(); struct.setGuiIsSet(true); } + if (incoming.get(4)) { + struct.noteGui = iprot.readString(); + struct.setNoteGuiIsSet(true); + } } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterResultMessage.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterResultMessage.java index a2aff298423..37b3a8725fb 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterResultMessage.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterResultMessage.java @@ -51,7 +51,7 @@ import org.slf4j.LoggerFactory; @SuppressWarnings({"cast", "rawtypes", "serial", "unchecked"}) -@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-3-27") +@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-10-22") public class RemoteInterpreterResultMessage implements org.apache.thrift.TBase, java.io.Serializable, Cloneable, Comparable { private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("RemoteInterpreterResultMessage"); diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterService.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterService.java index def96fa9323..ba13f64c41d 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterService.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/RemoteInterpreterService.java @@ -51,7 +51,7 @@ import org.slf4j.LoggerFactory; @SuppressWarnings({"cast", "rawtypes", "serial", "unchecked"}) -@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-3-27") +@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-10-22") public class RemoteInterpreterService { public interface Iface { diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/ZeppelinServerResourceParagraphRunner.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/ZeppelinServerResourceParagraphRunner.java index 78cb0905601..17b6bd8cfa7 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/ZeppelinServerResourceParagraphRunner.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/ZeppelinServerResourceParagraphRunner.java @@ -51,7 +51,7 @@ import org.slf4j.LoggerFactory; @SuppressWarnings({"cast", "rawtypes", "serial", "unchecked"}) -@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-3-27") +@Generated(value = "Autogenerated by Thrift Compiler (0.9.2)", date = "2017-10-22") public class ZeppelinServerResourceParagraphRunner implements org.apache.thrift.TBase, java.io.Serializable, Cloneable, Comparable { private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("ZeppelinServerResourceParagraphRunner"); 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-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/FIFOScheduler.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/FIFOScheduler.java index 7ca4a0e894a..fd467b6e6b6 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/FIFOScheduler.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/FIFOScheduler.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.concurrent.ExecutorService; +import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.scheduler.Job.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -137,15 +138,26 @@ public void run() { listener.jobStarted(scheduler, runningJob); } runningJob.run(); + Object jobResult = runningJob.getReturn(); if (runningJob.isAborted()) { runningJob.setStatus(Status.ABORT); + LOGGER.debug("Job Aborted, " + runningJob.getId() + ", " + + runningJob.getErrorMessage()); + } else if (runningJob.getException() != null) { + LOGGER.debug("Job Error, " + runningJob.getId() + ", " + + runningJob.getReturn()); + runningJob.setStatus(Status.ERROR); + } else if (jobResult != null && jobResult instanceof InterpreterResult + && ((InterpreterResult) jobResult).code() == InterpreterResult.Code.ERROR) { + LOGGER.debug("Job Error, " + runningJob.getId() + ", " + + runningJob.getReturn()); + runningJob.setStatus(Status.ERROR); } else { - if (runningJob.getException() != null) { - runningJob.setStatus(Status.ERROR); - } else { - runningJob.setStatus(Status.FINISHED); - } + LOGGER.debug("Job Finished, " + runningJob.getId() + ", Result: " + + runningJob.getReturn()); + runningJob.setStatus(Status.FINISHED); } + if (listener != null) { listener.jobFinished(scheduler, runningJob); } 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..2a9dd6377a3 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 @@ -22,6 +22,8 @@ import java.util.Map; import org.apache.commons.lang.exception.ExceptionUtils; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResult.Code; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,6 +43,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 +51,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; @@ -62,6 +65,10 @@ public boolean isRunning() { public boolean isPending() { return this == PENDING; } + + public boolean isCompleted() { + return this == FINISHED || this == ERROR || this == ABORT; + } } private String jobName; @@ -70,14 +77,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; @@ -198,7 +205,7 @@ private synchronized void completeWithSuccess(Object result) { } private synchronized void completeWithError(Throwable error) { - setResult(error.getMessage()); + setResult(new InterpreterResult(Code.ERROR, getStack(error))); setException(error); dateFinished = new Date(); } 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..cc86f6734b3 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 @@ -17,24 +17,22 @@ package org.apache.zeppelin.scheduler; -import java.util.Collection; import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; +import org.apache.zeppelin.conf.ZeppelinConfiguration; 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<>(); + protected ExecutorService executor; + protected Map schedulers = new LinkedHashMap<>(); private static SchedulerFactory singleton; private static Long singletonLock = new Long(0); @@ -54,17 +52,21 @@ public static SchedulerFactory singleton() { return singleton; } - public SchedulerFactory() throws Exception { - executor = ExecutorFactory.singleton().createOrGet("schedulerFactory", 100); + SchedulerFactory() throws Exception { + ZeppelinConfiguration zConf = ZeppelinConfiguration.create(); + int threadPoolSize = + zConf.getInt(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_SCHEDULER_POOL_SIZE); + logger.info("Scheduler Thread Pool Size: " + threadPoolSize); + executor = ExecutorFactory.singleton().createOrGet("SchedulerFactory", threadPoolSize); } 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 +77,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); @@ -84,60 +86,38 @@ public Scheduler createOrGetParallelScheduler(String name, int maxConcurrency) { } } - public Scheduler createOrGetRemoteScheduler( - String name, - String noteId, - RemoteInterpreterProcess interpreterProcess, - int maxConcurrency) { - + public Scheduler createOrGetScheduler(Scheduler scheduler) { synchronized (schedulers) { - if (schedulers.containsKey(name) == false) { - Scheduler s = new RemoteScheduler( - name, - executor, - noteId, - interpreterProcess, - this, - maxConcurrency); - schedulers.put(name, s); - executor.execute(s); + if (!schedulers.containsKey(scheduler.getName())) { + schedulers.put(scheduler.getName(), scheduler); + executor.execute(scheduler); } - return schedulers.get(name); + return schedulers.get(scheduler.getName()); } } - 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; + public ExecutorService getExecutor() { + return executor; } @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/GraphEntity.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/GraphEntity.java new file mode 100644 index 00000000000..320b1447269 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/GraphEntity.java @@ -0,0 +1,74 @@ +/* + * 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.tabledata; + +import java.util.Map; + +/** + * The base network entity + * + */ +public abstract class GraphEntity { + + private long id; + + /** + * The data of the entity + * + */ + private Map data; + + /** + * The primary type of the entity + */ + private String label; + + public GraphEntity() {} + + public GraphEntity(long id, Map data, String label) { + super(); + this.setId(id); + this.setData(data); + this.setLabel(label); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Map getData() { + return data; + } + + public void setData(Map data) { + this.data = data; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + +} diff --git a/zeppelin-server/src/test/scala/org/apache/zeppelin/WelcomePageSuite.scala b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/Node.java similarity index 60% rename from zeppelin-server/src/test/scala/org/apache/zeppelin/WelcomePageSuite.scala rename to zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/Node.java index 1798214aecb..2efabc40894 100644 --- a/zeppelin-server/src/test/scala/org/apache/zeppelin/WelcomePageSuite.scala +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/Node.java @@ -15,23 +15,35 @@ * limitations under the License. */ -package org.apache.zeppelin - -import org.openqa.selenium.WebDriver -import org.scalatest.concurrent.Eventually._ -import org.scalatest.time._ -import org.scalatest.selenium.WebBrowser -import org.scalatest.{DoNotDiscover, FunSuite} -import AbstractFunctionalSuite.SERVER_ADDRESS - -@DoNotDiscover -class WelcomePageSuite(implicit driver: WebDriver) extends FunSuite with WebBrowser { - - test("Welcome sign is correct") { - eventually (timeout(Span(180, Seconds))) { - go to SERVER_ADDRESS - assert(find("welcome").isDefined) - } +package org.apache.zeppelin.tabledata; + +import java.util.Map; +import java.util.Set; + +/** + * The Zeppelin Node Entity + * + */ +public class Node extends GraphEntity { + + /** + * The labels (types) attached to a node + */ + private Set labels; + + public Node() {} + + + public Node(long id, Map data, Set labels) { + super(id, data, labels.iterator().next()); } + public Set getLabels() { + return labels; + } + + public void setLabels(Set labels) { + this.labels = labels; + } + } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/Relationship.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/Relationship.java new file mode 100644 index 00000000000..aa8ddb7854a --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/Relationship.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.tabledata; + +import java.util.Map; + +/** + * The Zeppelin Relationship entity + * + */ +public class Relationship extends GraphEntity { + + /** + * Source node ID + */ + private long source; + + /** + * End node ID + */ + private long target; + + public Relationship() {} + + public Relationship(long id, Map data, long source, + long target, String label) { + super(id, data, label); + this.setSource(source); + this.setTarget(target); + } + + public long getSource() { + return source; + } + + public void setSource(long startNodeId) { + this.source = startNodeId; + } + + public long getTarget() { + return target; + } + + public void setTarget(long endNodeId) { + this.target = endNodeId; + } + +} 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/user/AuthenticationInfo.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/user/AuthenticationInfo.java index d00d1605e2b..455fd8b1287 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/user/AuthenticationInfo.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/user/AuthenticationInfo.java @@ -20,14 +20,15 @@ import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import com.google.gson.Gson; + import org.apache.commons.lang.StringUtils; import org.apache.zeppelin.common.JsonSerializable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.Gson; + /*** * */ @@ -56,10 +57,7 @@ public AuthenticationInfo(String user) { public AuthenticationInfo(String user, String roles, String ticket) { this.user = user; this.ticket = ticket; - if (StringUtils.isNotBlank(roles) && roles.length() > 2) { - String[] r = roles.substring(1, roles.length() - 1).split(","); - this.roles = Arrays.asList(r); - } + this.roles = gson.fromJson(roles, ArrayList.class); } public String getUser() { @@ -78,6 +76,10 @@ public void setRoles(List roles) { this.roles = roles; } + public void setRoles(String roles) { + this.roles = gson.fromJson(roles, ArrayList.class); + } + public List getUsersAndRoles() { List usersAndRoles = new ArrayList<>(); if (roles != null) { @@ -120,6 +122,7 @@ public boolean isAnonymous() { || StringUtils.isEmpty(this.getUser()); } + @Override public String toJson() { return gson.toJson(this); } 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..052aaefd219 --- /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.security.SecureRandom; + +/** + * 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 SecureRandom().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-zengine/src/main/resources/project.properties b/zeppelin-interpreter/src/main/resources/project.properties similarity index 100% rename from zeppelin-zengine/src/main/resources/project.properties rename to zeppelin-interpreter/src/main/resources/project.properties diff --git a/zeppelin-interpreter/src/main/thrift/RemoteInterpreterService.thrift b/zeppelin-interpreter/src/main/thrift/RemoteInterpreterService.thrift index f2eb13f23d8..559648a24b6 100644 --- a/zeppelin-interpreter/src/main/thrift/RemoteInterpreterService.thrift +++ b/zeppelin-interpreter/src/main/thrift/RemoteInterpreterService.thrift @@ -27,7 +27,8 @@ struct RemoteInterpreterContext { 6: string authenticationInfo, 7: string config, // json serialized config 8: string gui, // json serialized gui - 9: string runners // json serialized runner + 9: string noteGui, // json serialized note gui + 10: string runners // json serialized runner } struct RemoteInterpreterResultMessage { @@ -39,6 +40,7 @@ struct RemoteInterpreterResult { 2: list msg, 3: string config, // json serialized config 4: string gui // json serialized gui + 5: string noteGui // json serialized note gui } enum RemoteInterpreterEventType { @@ -88,6 +90,11 @@ struct InterpreterCompletion { 3: string meta } +struct CallbackInfo { + 1: string host, + 2: i32 port +} + service RemoteInterpreterService { void createInterpreter(1: string intpGroupId, 2: string sessionKey, 3: string className, 4: map properties, 5: string userName); @@ -131,3 +138,7 @@ service RemoteInterpreterService { void onReceivedZeppelinResource(1: string object); } + +service RemoteInterpreterCallbackService { + void callback(1: CallbackInfo callbackInfo); +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/dep/DependencyResolverTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/dep/DependencyResolverTest.java index 876e8e7c88c..4afd080fd1d 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/dep/DependencyResolverTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/dep/DependencyResolverTest.java @@ -85,9 +85,10 @@ public void testLoad() throws Exception { FileUtils.cleanDirectory(testCopyPath); // load from added repository - resolver.addRepo("sonatype", "https://oss.sonatype.org/content/repositories/agimatec-releases/", false); - resolver.load("com.agimatec:agimatec-validation:0.9.3", testCopyPath); - assertEquals(testCopyPath.list().length, 8); + resolver.addRepo("sonatype", + "https://oss.sonatype.org/content/repositories/ksoap2-android-releases/", false); + resolver.load("com.google.code.ksoap2-android:ksoap2-jsoup:3.6.3", testCopyPath); + assertEquals(testCopyPath.list().length, 10); // load invalid artifact resolver.delRepo("sonatype"); @@ -103,4 +104,4 @@ public void should_throw_exception_if_dependency_not_found() throws Exception { resolver.load("one.two:1.0", testCopyPath); } -} \ No newline at end of file +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/InputTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/InputTest.java index d15fab4a576..d3d5a009c04 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/InputTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/InputTest.java @@ -47,7 +47,7 @@ public void tearDown() throws Exception { public void testFormExtraction() { // textbox form String script = "${input_form=}"; - Map forms = Input.extractSimpleQueryForm(script); + Map forms = Input.extractSimpleQueryForm(script, false); assertEquals(1, forms.size()); Input form = forms.get("input_form"); assertEquals("input_form", form.name); @@ -57,14 +57,14 @@ public void testFormExtraction() { // textbox form with display name & default value script = "${input_form(Input Form)=xxx}"; - forms = Input.extractSimpleQueryForm(script); + forms = Input.extractSimpleQueryForm(script, false); form = forms.get("input_form"); assertEquals("xxx", form.defaultValue); assertTrue(form instanceof TextBox); // selection form script = "${select_form(Selection Form)=op1,op1|op2(Option 2)|op3}"; - form = Input.extractSimpleQueryForm(script).get("select_form"); + form = Input.extractSimpleQueryForm(script, false).get("select_form"); assertEquals("select_form", form.name); assertEquals("op1", form.defaultValue); assertTrue(form instanceof Select); @@ -74,7 +74,7 @@ public void testFormExtraction() { // checkbox form script = "${checkbox:checkbox_form=op1,op1|op2|op3}"; - form = Input.extractSimpleQueryForm(script).get("checkbox_form"); + form = Input.extractSimpleQueryForm(script, false).get("checkbox_form"); assertEquals("checkbox_form", form.name); assertTrue(form instanceof CheckBox); @@ -85,7 +85,7 @@ public void testFormExtraction() { // checkbox form with multiple default checks script = "${checkbox:checkbox_form(Checkbox Form)=op1|op3,op1(Option 1)|op2|op3}"; - form = Input.extractSimpleQueryForm(script).get("checkbox_form"); + form = Input.extractSimpleQueryForm(script, false).get("checkbox_form"); assertEquals("checkbox_form", form.name); assertEquals("Checkbox Form", form.displayName); assertTrue(form instanceof CheckBox); @@ -96,7 +96,7 @@ public void testFormExtraction() { // checkbox form with no default check script = "${checkbox:checkbox_form(Checkbox Form)=,op1(Option 1)|op2(Option 2)|op3(Option 3)}"; - form = Input.extractSimpleQueryForm(script).get("checkbox_form"); + form = Input.extractSimpleQueryForm(script, false).get("checkbox_form"); assertEquals("checkbox_form", form.name); assertEquals("Checkbox Form", form.displayName); assertTrue(form instanceof CheckBox); @@ -116,14 +116,14 @@ public void testFormSubstitution() { params.put("input_form", "some_input"); params.put("select_form", "s_op2"); params.put("checkbox_form", new String[]{"c_op1", "c_op3"}); - String replaced = Input.getSimpleQuery(params, script); + String replaced = Input.getSimpleQuery(params, script, false); assertEquals("INPUT=some_inputSELECTED=s_op2\nCHECKED=c_op1,c_op3", replaced); // test form substitution with new forms script = "INPUT=${input_form=}SELECTED=${select_form(Selection Form)=,s_op1|s_op2|s_op3}\n" + "CHECKED=${checkbox:checkbox_form=c_op1|c_op2,c_op1|c_op2|c_op3}\n" + "NEW_CHECKED=${checkbox( and ):new_check=nc_a|nc_c,nc_a|nc_b|nc_c}"; - replaced = Input.getSimpleQuery(params, script); + replaced = Input.getSimpleQuery(params, script, false); assertEquals("INPUT=some_inputSELECTED=s_op2\nCHECKED=c_op1,c_op3\n" + "NEW_CHECKED=nc_a and nc_c", replaced); @@ -131,7 +131,7 @@ public void testFormSubstitution() { script = "INPUT=${input_form=}SELECTED=${select_form(Selection Form)=,s_op1|s_op2|s_op3}\n" + "CHECKED=${checkbox:checkbox_form=c_op1|c_op2,c_op1|c_op2|c_op3_new}\n" + "NEW_CHECKED=${checkbox( and ):new_check=nc_a|nc_c,nc_a|nc_b|nc_c}"; - replaced = Input.getSimpleQuery(params, script); + replaced = Input.getSimpleQuery(params, script, false); assertEquals("INPUT=some_inputSELECTED=s_op2\nCHECKED=c_op1\n" + "NEW_CHECKED=nc_a and nc_c", replaced); } diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/BaseZeppelinContextTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/BaseZeppelinContextTest.java new file mode 100644 index 00000000000..db9cfd8be36 --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/BaseZeppelinContextTest.java @@ -0,0 +1,133 @@ +/* + * 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.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + + +public class BaseZeppelinContextTest { + + @Test + public void testHooks() throws InvalidHookException { + InterpreterHookRegistry hookRegistry = new InterpreterHookRegistry(); + TestZeppelinContext z = new TestZeppelinContext(hookRegistry, 10); + InterpreterContext context = InterpreterContext.builder() + .setNoteId("note_1") + .setParagraphId("paragraph_1") + .setInterpreterClassName("Test1Interpreter") + .setReplName("test1") + .build(); + z.setInterpreterContext(context); + + // register global hook for current interpreter + z.registerHook(InterpreterHookRegistry.HookType.PRE_EXEC.getName(), "pre_cmd"); + z.registerHook(InterpreterHookRegistry.HookType.POST_EXEC.getName(), "post_cmd"); + assertEquals("pre_cmd", hookRegistry.get(null, "Test1Interpreter", + InterpreterHookRegistry.HookType.PRE_EXEC.getName())); + assertEquals("post_cmd", hookRegistry.get(null, "Test1Interpreter", + InterpreterHookRegistry.HookType.POST_EXEC.getName())); + + z.unregisterHook(InterpreterHookRegistry.HookType.PRE_EXEC.getName()); + z.unregisterHook(InterpreterHookRegistry.HookType.POST_EXEC.getName()); + assertEquals(null, hookRegistry.get(null, "Test1Interpreter", + InterpreterHookRegistry.HookType.PRE_EXEC.getName())); + assertEquals(null, hookRegistry.get(null, "Test1Interpreter", + InterpreterHookRegistry.HookType.POST_EXEC.getName())); + + // register global hook for interpreter test2 + z.registerHook(InterpreterHookRegistry.HookType.PRE_EXEC.getName(), "pre_cmd2", "test2"); + z.registerHook(InterpreterHookRegistry.HookType.POST_EXEC.getName(), "post_cmd2", "test2"); + assertEquals("pre_cmd2", hookRegistry.get(null, "Test2Interpreter", + InterpreterHookRegistry.HookType.PRE_EXEC.getName())); + assertEquals("post_cmd2", hookRegistry.get(null, "Test2Interpreter", + InterpreterHookRegistry.HookType.POST_EXEC.getName())); + + z.unregisterHook(InterpreterHookRegistry.HookType.PRE_EXEC.getName(), "test2"); + z.unregisterHook(InterpreterHookRegistry.HookType.POST_EXEC.getName(), "test2"); + assertEquals(null, hookRegistry.get(null, "Test2Interpreter", + InterpreterHookRegistry.HookType.PRE_EXEC.getName())); + assertEquals(null, hookRegistry.get(null, "Test2Interpreter", + InterpreterHookRegistry.HookType.POST_EXEC.getName())); + + // register hook for note_1 and current interpreter + z.registerNoteHook(InterpreterHookRegistry.HookType.PRE_EXEC.getName(), "pre_cmd", "note_1"); + z.registerNoteHook(InterpreterHookRegistry.HookType.POST_EXEC.getName(), "post_cmd", "note_1"); + assertEquals("pre_cmd", hookRegistry.get("note_1", "Test1Interpreter", + InterpreterHookRegistry.HookType.PRE_EXEC.getName())); + assertEquals("post_cmd", hookRegistry.get("note_1", "Test1Interpreter", + InterpreterHookRegistry.HookType.POST_EXEC.getName())); + + z.unregisterNoteHook("note_1", InterpreterHookRegistry.HookType.PRE_EXEC.getName(), "test1"); + z.unregisterNoteHook("note_1", InterpreterHookRegistry.HookType.POST_EXEC.getName(), "test1"); + assertEquals(null, hookRegistry.get("note_1", "Test1Interpreter", + InterpreterHookRegistry.HookType.PRE_EXEC.getName())); + assertEquals(null, hookRegistry.get("note_1", "Test1Interpreter", + InterpreterHookRegistry.HookType.POST_EXEC.getName())); + + // register hook for note_1 and interpreter test2 + z.registerNoteHook(InterpreterHookRegistry.HookType.PRE_EXEC.getName(), + "pre_cmd2", "note_1", "test2"); + z.registerNoteHook(InterpreterHookRegistry.HookType.POST_EXEC.getName(), + "post_cmd2", "note_1", "test2"); + assertEquals("pre_cmd2", hookRegistry.get("note_1", "Test2Interpreter", + InterpreterHookRegistry.HookType.PRE_EXEC.getName())); + assertEquals("post_cmd2", hookRegistry.get("note_1", "Test2Interpreter", + InterpreterHookRegistry.HookType.POST_EXEC.getName())); + + z.unregisterNoteHook("note_1", InterpreterHookRegistry.HookType.PRE_EXEC.getName(), "test2"); + z.unregisterNoteHook("note_1", InterpreterHookRegistry.HookType.POST_EXEC.getName(), "test2"); + assertEquals(null, hookRegistry.get("note_1", "Test2Interpreter", + InterpreterHookRegistry.HookType.PRE_EXEC.getName())); + assertEquals(null, hookRegistry.get("note_1", "Test2Interpreter", + InterpreterHookRegistry.HookType.POST_EXEC.getName())); + } + + + public static class TestZeppelinContext extends BaseZeppelinContext { + + public TestZeppelinContext(InterpreterHookRegistry hooks, int maxResult) { + super(hooks, maxResult); + } + + @Override + public Map getInterpreterClassMap() { + Map map = new HashMap<>(); + map.put("test1", "Test1Interpreter"); + map.put("test2", "Test2Interpreter"); + return map; + } + + @Override + public List getSupportedClasses() { + return null; + } + + @Override + protected String showData(Object obj) { + 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-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterContextTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterContextTest.java index ecdf1089efe..70e2cbabbd0 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterContextTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterContextTest.java @@ -27,7 +27,7 @@ public class InterpreterContextTest { public void testThreadLocal() { assertNull(InterpreterContext.get()); - InterpreterContext.set(new InterpreterContext(null, null, null, null, null, null, null, null, null, null, null, null)); + InterpreterContext.set(new InterpreterContext(null, null, null, null, null, null, null, null, null, null, null, null, null)); assertNotNull(InterpreterContext.get()); InterpreterContext.remove(); diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterHookRegistryTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterHookRegistryTest.java index 7614e9eb204..8eb6865cfcb 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterHookRegistryTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterHookRegistryTest.java @@ -17,59 +17,57 @@ package org.apache.zeppelin.interpreter; +import org.junit.Test; + +import static org.apache.zeppelin.interpreter.InterpreterHookRegistry.HookType.POST_EXEC; +import static org.apache.zeppelin.interpreter.InterpreterHookRegistry.HookType.POST_EXEC_DEV; +import static org.apache.zeppelin.interpreter.InterpreterHookRegistry.HookType.PRE_EXEC; +import static org.apache.zeppelin.interpreter.InterpreterHookRegistry.HookType.PRE_EXEC_DEV; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - public class InterpreterHookRegistryTest { @Test - public void testBasic() { - final String PRE_EXEC = InterpreterHookRegistry.HookType.PRE_EXEC; - final String POST_EXEC = InterpreterHookRegistry.HookType.POST_EXEC; - final String PRE_EXEC_DEV = InterpreterHookRegistry.HookType.PRE_EXEC_DEV; - final String POST_EXEC_DEV = InterpreterHookRegistry.HookType.POST_EXEC_DEV; + public void testBasic() throws InvalidHookException { final String GLOBAL_KEY = InterpreterHookRegistry.GLOBAL_KEY; final String noteId = "note"; final String className = "class"; final String preExecHook = "pre"; final String postExecHook = "post"; - InterpreterHookRegistry registry = new InterpreterHookRegistry("intpId"); - + + InterpreterHookRegistry registry = new InterpreterHookRegistry(); + // Test register() - registry.register(noteId, className, PRE_EXEC, preExecHook); - registry.register(noteId, className, POST_EXEC, postExecHook); - registry.register(noteId, className, PRE_EXEC_DEV, preExecHook); - registry.register(noteId, className, POST_EXEC_DEV, postExecHook); + registry.register(noteId, className, PRE_EXEC.getName(), preExecHook); + registry.register(noteId, className, POST_EXEC.getName(), postExecHook); + registry.register(noteId, className, PRE_EXEC_DEV.getName(), preExecHook); + registry.register(noteId, className, POST_EXEC_DEV.getName(), postExecHook); + + assertEquals(registry.get(noteId, className, PRE_EXEC.getName()), preExecHook); + assertEquals(registry.get(noteId, className, POST_EXEC.getName()), postExecHook); + assertEquals(registry.get(noteId, className, PRE_EXEC_DEV.getName()), preExecHook); + assertEquals(registry.get(noteId, className, POST_EXEC_DEV.getName()), postExecHook); - // Test get() - assertEquals(registry.get(noteId, className, PRE_EXEC), preExecHook); - assertEquals(registry.get(noteId, className, POST_EXEC), postExecHook); - assertEquals(registry.get(noteId, className, PRE_EXEC_DEV), preExecHook); - assertEquals(registry.get(noteId, className, POST_EXEC_DEV), postExecHook); - // Test Unregister - registry.unregister(noteId, className, PRE_EXEC); - registry.unregister(noteId, className, POST_EXEC); - registry.unregister(noteId, className, PRE_EXEC_DEV); - registry.unregister(noteId, className, POST_EXEC_DEV); - assertNull(registry.get(noteId, className, PRE_EXEC)); - assertNull(registry.get(noteId, className, POST_EXEC)); - assertNull(registry.get(noteId, className, PRE_EXEC_DEV)); - assertNull(registry.get(noteId, className, POST_EXEC_DEV)); - + registry.unregister(noteId, className, PRE_EXEC.getName()); + registry.unregister(noteId, className, POST_EXEC.getName()); + registry.unregister(noteId, className, PRE_EXEC_DEV.getName()); + registry.unregister(noteId, className, POST_EXEC_DEV.getName()); + assertNull(registry.get(noteId, className, PRE_EXEC.getName())); + assertNull(registry.get(noteId, className, POST_EXEC.getName())); + assertNull(registry.get(noteId, className, PRE_EXEC_DEV.getName())); + assertNull(registry.get(noteId, className, POST_EXEC_DEV.getName())); + // Test Global Scope - registry.register(null, className, PRE_EXEC, preExecHook); - assertEquals(registry.get(GLOBAL_KEY, className, PRE_EXEC), preExecHook); + registry.register(null, className, PRE_EXEC.getName(), preExecHook); + assertEquals(registry.get(GLOBAL_KEY, className, PRE_EXEC.getName()), preExecHook); } - - @Test(expected = IllegalArgumentException.class) - public void testValidEventCode() { - InterpreterHookRegistry registry = new InterpreterHookRegistry("intpId"); - + + @Test(expected = InvalidHookException.class) + public void testValidEventCode() throws InvalidHookException { + InterpreterHookRegistry registry = new InterpreterHookRegistry(); + // Test that only valid event codes ("pre_exec", "post_exec") are accepted registry.register("foo", "bar", "baz", "whatever"); } 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/InterpreterTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/InterpreterTest.java index 305268c89df..4156691c6d8 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,6 +24,7 @@ import static org.junit.Assert.assertEquals; +//TODO(zjffdu) add more test for Interpreter which is a very important class public class InterpreterTest { @Test @@ -32,8 +33,8 @@ public void testDefaultProperty() { p.put("p1", "v1"); Interpreter intp = new DummyInterpreter(p); - assertEquals(1, intp.getProperty().size()); - assertEquals("v1", intp.getProperty().get("p1")); + assertEquals(1, intp.getProperties().size()); + assertEquals("v1", intp.getProperties().get("p1")); assertEquals("v1", intp.getProperty("p1")); } @@ -44,10 +45,10 @@ public void testOverriddenProperty() { Interpreter intp = new DummyInterpreter(p); Properties overriddenProperty = new Properties(); overriddenProperty.put("p1", "v2"); - intp.setProperty(overriddenProperty); + intp.setProperties(overriddenProperty); - assertEquals(1, intp.getProperty().size()); - assertEquals("v2", intp.getProperty().get("p1")); + assertEquals(1, intp.getProperties().size()); + assertEquals("v2", intp.getProperties().get("p1")); assertEquals("v2", intp.getProperty("p1")); } @@ -69,6 +70,7 @@ public void testPropertyWithReplacedContextFields() { null, null, null, + null, null)); Properties p = new Properties(); p.put("p1", "replName #{noteId}, #{paragraphTitle}, #{paragraphId}, #{paragraphText}, #{replName}, #{noteId}, #{user}," + @@ -85,4 +87,41 @@ public void testPropertyWithReplacedContextFields() { ); } + public static 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/LazyOpenInterpreterTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/LazyOpenInterpreterTest.java index 26e835f56d4..165625ed925 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/LazyOpenInterpreterTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/LazyOpenInterpreterTest.java @@ -28,7 +28,7 @@ public class LazyOpenInterpreterTest { Interpreter interpreter = mock(Interpreter.class); @Test - public void isOpenTest() { + public void isOpenTest() throws InterpreterException { InterpreterResult interpreterResult = new InterpreterResult(InterpreterResult.Code.SUCCESS, ""); when(interpreter.interpret(any(String.class), any(InterpreterContext.class))).thenReturn(interpreterResult); @@ -36,7 +36,7 @@ public void isOpenTest() { assertFalse("Interpreter is not open", lazyOpenInterpreter.isOpen()); InterpreterContext interpreterContext = - new InterpreterContext("note", "id", null, "title", "text", null, null, null, null, null, null, null); + new InterpreterContext("note", "id", null, "title", "text", null, null, null, null, null, null, null, null); lazyOpenInterpreter.interpret("intp 1", interpreterContext); assertTrue("Interpeter is open", lazyOpenInterpreter.isOpen()); } diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/ZeppCtxtVariableTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/ZeppCtxtVariableTest.java new file mode 100644 index 00000000000..cf8daa3add7 --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/ZeppCtxtVariableTest.java @@ -0,0 +1,212 @@ +/* + * 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.resource.LocalResourcePool; +import org.apache.zeppelin.resource.ResourcePool; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.Before; +import org.junit.After; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.assertTrue; + +public class ZeppCtxtVariableTest { + + public static class TestInterpreter extends Interpreter { + + TestInterpreter(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; + } + } + + private Interpreter interpreter; + private ResourcePool resourcePool; + + @Before + public void setUp() throws Exception { + + resourcePool = new LocalResourcePool("ZeppelinContextVariableInterpolationTest"); + + InterpreterContext.set(new InterpreterContext("InterpolationTestNoteId", + "InterpolationTestParagraphTitle", + null, + "InterpolationTestParagraphTitle", + "InterpolationTestParagraphText", + new AuthenticationInfo("InterpolationTestUser", null, "testTicket"), + null, + null, + null, + null, + resourcePool, + null, + null)); + + interpreter = new TestInterpreter(new Properties()); + + resourcePool.put("PI", "3.1415"); + + } + + @After + public void tearDown() throws Exception { + InterpreterContext.remove(); + } + + @Test + public void stringWithoutPatterns() { + String result = interpreter.interpolate("The value of PI is not exactly 3.14", resourcePool); + assertTrue("String without patterns", "The value of PI is not exactly 3.14".equals(result)); + } + + @Test + public void substitutionInTheMiddle() { + String result = interpreter.interpolate("The value of {{PI}} is {PI} now", resourcePool); + assertTrue("Substitution in the middle", "The value of {PI} is 3.1415 now".equals(result)); + } + + @Test + public void substitutionAtTheEnds() { + String result = interpreter.interpolate("{{PI}} is now {PI}", resourcePool); + assertTrue("Substitution at the ends", "{PI} is now 3.1415".equals(result)); + } + + @Test + public void multiLineSubstitutionSuccessful1() { + String result = interpreter.interpolate("{{PI}}\n{PI}\n{{PI}}\n{PI}", resourcePool); + assertTrue("multiLineSubstitutionSuccessful1", "{PI}\n3.1415\n{PI}\n3.1415".equals(result)); + } + + + @Test + public void multiLineSubstitutionSuccessful2() { + String result = interpreter.interpolate("prefix {PI} {{PI\n}} suffix", resourcePool); + assertTrue("multiLineSubstitutionSuccessful2", "prefix 3.1415 {PI\n} suffix".equals(result)); + } + + + @Test + public void multiLineSubstitutionSuccessful3() { + String result = interpreter.interpolate("prefix {{\nPI}} {PI} suffix", resourcePool); + assertTrue("multiLineSubstitutionSuccessful3", "prefix {\nPI} 3.1415 suffix".equals(result)); + } + + + @Test + public void multiLineSubstitutionFailure2() { + String result = interpreter.interpolate("prefix {PI\n} suffix", resourcePool); + assertTrue("multiLineSubstitutionFailure2", "prefix {PI\n} suffix".equals(result)); + } + + + @Test + public void multiLineSubstitutionFailure3() { + String result = interpreter.interpolate("prefix {\nPI} suffix", resourcePool); + assertTrue("multiLineSubstitutionFailure3", "prefix {\nPI} suffix".equals(result)); + } + + @Test + public void noUndefinedVariableError() { + String result = interpreter.interpolate("This {pi} will pass silently", resourcePool); + assertTrue("No partial substitution", "This {pi} will pass silently".equals(result)); + } + + @Test + public void noPartialSubstitution() { + String result = interpreter.interpolate("A {PI} and a {PIE} are different", resourcePool); + assertTrue("No partial substitution", "A {PI} and a {PIE} are different".equals(result)); + } + + @Test + public void substitutionAndEscapeMixed() { + String result = interpreter.interpolate("A {PI} is not a {{PIE}}", resourcePool); + assertTrue("Substitution and escape mixed", "A 3.1415 is not a {PIE}".equals(result)); + } + + @Test + public void unbalancedBracesOne() { + String result = interpreter.interpolate("A {PI} and a {{PIE} remain unchanged", resourcePool); + assertTrue("Unbalanced braces - one", "A {PI} and a {{PIE} remain unchanged".equals(result)); + } + + @Test + public void unbalancedBracesTwo() { + String result = interpreter.interpolate("A {PI} and a {PIE}} remain unchanged", resourcePool); + assertTrue("Unbalanced braces - one", "A {PI} and a {PIE}} remain unchanged".equals(result)); + } + + @Test + public void tooManyBraces() { + String result = interpreter.interpolate("This {{{PI}}} remain unchanged", resourcePool); + assertTrue("Too many braces", "This {{{PI}}} remain unchanged".equals(result)); + } + + @Test + public void randomBracesOne() { + String result = interpreter.interpolate("A {{ starts an escaped sequence", resourcePool); + assertTrue("Random braces - one", "A {{ starts an escaped sequence".equals(result)); + } + + @Test + public void randomBracesTwo() { + String result = interpreter.interpolate("A }} ends an escaped sequence", resourcePool); + assertTrue("Random braces - two", "A }} ends an escaped sequence".equals(result)); + } + + @Test + public void randomBracesThree() { + String result = interpreter.interpolate("Paired { begin an escaped sequence", resourcePool); + assertTrue("Random braces - three", "Paired { begin an escaped sequence".equals(result)); + } + + @Test + public void randomBracesFour() { + String result = interpreter.interpolate("Paired } end an escaped sequence", resourcePool); + assertTrue("Random braces - four", "Paired } end an escaped sequence".equals(result)); + } + +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServerTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServerTest.java index a4b3a2573ba..1cb2cb6447a 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServerTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServerTest.java @@ -42,8 +42,8 @@ public void tearDown() throws Exception { @Test public void testStartStop() throws InterruptedException, IOException, TException { - RemoteInterpreterServer server = new RemoteInterpreterServer( - RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces()); + RemoteInterpreterServer server = new RemoteInterpreterServer("localhost", + RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces(), ":", true); assertEquals(false, server.isRunning()); server.start(); @@ -90,8 +90,8 @@ public void run() { @Test public void testStartStopWithQueuedEvents() throws InterruptedException, IOException, TException { - RemoteInterpreterServer server = new RemoteInterpreterServer( - RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces()); + RemoteInterpreterServer server = new RemoteInterpreterServer("localhost", + RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces(), ":", true); assertEquals(false, server.isRunning()); server.start(); diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java new file mode 100644 index 00000000000..8eeb85a2162 --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterUtilsTest.java @@ -0,0 +1,44 @@ +/* + * 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.junit.Test; + +import java.io.IOException; + +import static org.junit.Assert.assertTrue; + +public class RemoteInterpreterUtilsTest { + + @Test + public void testCreateTServerSocket() throws IOException { + assertTrue(RemoteInterpreterUtils.createTServerSocket(":").getServerSocket().getLocalPort() > 0); + + String portRange = ":30000"; + assertTrue(RemoteInterpreterUtils.createTServerSocket(portRange).getServerSocket().getLocalPort() <= 30000); + + portRange = "30000:"; + assertTrue(RemoteInterpreterUtils.createTServerSocket(portRange).getServerSocket().getLocalPort() >= 30000); + + portRange = "30000:40000"; + int port = RemoteInterpreterUtils.createTServerSocket(portRange).getServerSocket().getLocalPort(); + assertTrue(port >= 30000 && port <= 40000); + } + + +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/JobTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/JobTest.java new file mode 100644 index 00000000000..ea80a14a096 --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/scheduler/JobTest.java @@ -0,0 +1,85 @@ +/* + * 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.scheduler; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; + +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.InterpreterResult.Code; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterServer.InterpretJob; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class JobTest { + + @Mock private JobListener mockJobListener; + @Mock private Interpreter mockInterpreter; + @Mock private InterpreterContext mockInterpreterContext; + private InterpretJob spyInterpretJob; + + @Before + public void setUp() throws Exception { + InterpretJob interpretJob = + new InterpretJob( + "jobid", + "jobName", + mockJobListener, + 10000, + mockInterpreter, + "script", + mockInterpreterContext); + spyInterpretJob = spy(interpretJob); + } + + @Test + public void testNormalCase() throws Throwable { + + InterpreterResult successInterpreterResult = + new InterpreterResult(Code.SUCCESS, "success result"); + doReturn(successInterpreterResult).when(spyInterpretJob).jobRun(); + + spyInterpretJob.run(); + + assertEquals(successInterpreterResult, spyInterpretJob.getReturn()); + } + + @Test + public void testErrorCase() throws Throwable { + String failedMessage = "failed message"; + InterpreterException interpreterException = new InterpreterException(failedMessage); + doThrow(interpreterException).when(spyInterpretJob).jobRun(); + + spyInterpretJob.run(); + + Object failedResult = spyInterpretJob.getReturn(); + assertTrue(failedResult instanceof InterpreterResult); + assertTrue( + ((InterpreterResult) failedResult).message().get(0).getData().contains(failedMessage)); + } +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/user/AuthenticationInfoTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/user/AuthenticationInfoTest.java new file mode 100644 index 00000000000..b757033e96a --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/user/AuthenticationInfoTest.java @@ -0,0 +1,42 @@ +/* + * 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.user; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; + +import org.junit.Test; + +public class AuthenticationInfoTest { + + @Test + public void testRoles() { + final String roles = "[\"role1\", \"role2\", \"role with space\"]"; + + final AuthenticationInfo authenticationInfo = new AuthenticationInfo("foo", + roles, "bar"); + + assertEquals( + new ArrayList<>(Arrays.asList("role1", "role2", "role with space")), + authenticationInfo.getRoles()); + + } + +} 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-jupyter/pom.xml b/zeppelin-jupyter/pom.xml index ae00db38d75..5df73f3662b 100644 --- a/zeppelin-jupyter/pom.xml +++ b/zeppelin-jupyter/pom.xml @@ -24,18 +24,18 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 .. zeppelin-jupyter jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Jupyter Support Jupyter support for Apache Zeppelin - 1.6.0 + ${project.version} @@ -52,6 +52,7 @@ com.google.guava guava + 15.0 @@ -60,9 +61,9 @@ - org.pegdown - pegdown - ${pegdown.version} + org.apache.zeppelin + zeppelin-markdown + ${project.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")); } } diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 07eaab0c2f9..2fd6517c942 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -22,31 +22,31 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-server jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Server 4.3.6 - 2.22.2 - 2.6.0 + 2.27 2.2.1 1.13 - 2.0.1 + 2.1 1.8 4.1.0 2.48.2 1.4.01 + 1.6.6 2.16 @@ -93,6 +93,18 @@ com.fasterxml.jackson.core jackson-databind + + com.sun.jersey + jersey-core + + + com.sun.jersey + jersey-json + + + com.sun.jersey + jersey-server + @@ -131,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 @@ -153,6 +179,17 @@ org.apache.shiro shiro-core + + + commons-beanutils + commons-beanutils + + + + + commons-beanutils + commons-beanutils + 1.9.2 @@ -182,7 +219,7 @@ com.fasterxml.jackson.core jackson-annotations - 2.5.4 + 2.8.0 @@ -195,99 +232,16 @@ websocket-server ${jetty.version} - 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 - - + com.nimbusds + nimbus-jose-jwt + 4.41.2 @@ -332,6 +286,12 @@ scalatest_${scala.binary.version} ${scalatest.version} test + + + org.scala-lang.modules + scala-xml_${scala.binary.version} + + @@ -364,6 +324,10 @@ org.apache.commons commons-lang3 + + com.google.guava + guava + @@ -374,8 +338,52 @@ test -
    + + org.powermock + powermock-module-junit4 + ${powermock.version} + test + + + org.javassist + javassist + + + + + org.powermock + powermock-api-mockito + ${powermock.version} + test + + + org.hamcrest + hamcrest-core + + + org.objenesis + objenesis + + + + + org.apache.zeppelin + zeppelin-zengine + ${project.version} + tests + test + + + + org.apache.hadoop + hadoop-common + 2.7.0-mapr-1808 + provided + + + + @@ -394,6 +402,21 @@ + + maven-surefire-plugin + ${plugin.surefire.version} + + -Xmx2g -Xms1g -Dfile.encoding=UTF-8 + + ${tests.to.exclude} + + + 1 + + + + + org.scala-tools maven-scala-plugin @@ -463,6 +486,12 @@ + + + org.apache.maven.plugins + maven-dependency-plugin + + diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ActiveDirectoryGroupRealm.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ActiveDirectoryGroupRealm.java index 4f3626cbc5e..bad501db5b8 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ActiveDirectoryGroupRealm.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ActiveDirectoryGroupRealm.java @@ -16,19 +16,32 @@ */ package org.apache.zeppelin.realm; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapContext; import org.apache.commons.lang.StringUtils; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.security.alias.CredentialProvider; -import org.apache.hadoop.security.alias.CredentialProviderFactory; +import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; -import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; -import org.apache.shiro.realm.Realm; import org.apache.shiro.realm.ldap.AbstractLdapRealm; import org.apache.shiro.realm.ldap.DefaultLdapContextFactory; import org.apache.shiro.realm.ldap.LdapContextFactory; @@ -37,18 +50,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.SearchControls; -import javax.naming.directory.SearchResult; -import javax.naming.ldap.LdapContext; -import java.util.*; - /** - * A {@link Realm} that authenticates with an active directory LDAP + * A {@link org.apache.shiro.realm.Realm} that authenticates with an active directory LDAP * server to determine the roles for a particular user. This implementation * queries for the user's groups and then maps the group names to roles using the * {@link #groupRolesMap}. @@ -61,7 +65,7 @@ public class ActiveDirectoryGroupRealm extends AbstractLdapRealm { private static final String ROLE_NAMES_DELIMETER = ","; - String KEYSTORE_PASS = "activeDirectoryRealm.systemPassword"; + final String keystorePass = "activeDirectoryRealm.systemPassword"; private String hadoopSecurityCredentialPath; public void setHadoopSecurityCredentialPath(String hadoopSecurityCredentialPath) { @@ -77,14 +81,14 @@ public void setHadoopSecurityCredentialPath(String hadoopSecurityCredentialPath) * group names (e.g. CN=Group,OU=Company,DC=MyDomain,DC=local) * as returned by the active directory LDAP server to role names. */ - private Map groupRolesMap; + private Map groupRolesMap = new LinkedHashMap<>(); /*-------------------------------------------- | C O N S T R U C T O R S | ============================================*/ public void setGroupRolesMap(Map groupRolesMap) { - this.groupRolesMap = groupRolesMap; + this.groupRolesMap.putAll(groupRolesMap); } /*-------------------------------------------- @@ -147,20 +151,7 @@ private String getSystemPassword() { if (StringUtils.isEmpty(this.hadoopSecurityCredentialPath)) { password = this.systemPassword; } else { - try { - Configuration configuration = new Configuration(); - configuration.set(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH, - this.hadoopSecurityCredentialPath); - CredentialProvider provider = - CredentialProviderFactory.getProviders(configuration).get(0); - CredentialProvider.CredentialEntry credEntry = provider.getCredentialEntry( - KEYSTORE_PASS); - if (credEntry != null) { - password = new String(credEntry.getCredential()); - } - } catch (Exception e) { - - } + password = LdapRealm.getSystemPassword(hadoopSecurityCredentialPath, keystorePass); } return password; } @@ -263,12 +254,13 @@ protected AuthorizationInfo buildAuthorizationInfo(Set roleNames) { return new SimpleAuthorizationInfo(roleNames); } - public List searchForUserName(String containString, LdapContext ldapContext) throws - NamingException { + public List searchForUserName(String containString, LdapContext ldapContext, + int numUsersToFetch) throws NamingException { List userNameList = new ArrayList<>(); SearchControls searchCtls = new SearchControls(); searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchCtls.setCountLimit(numUsersToFetch); String searchFilter = "(&(objectClass=*)(userPrincipalName=*" + containString + "*))"; Object[] searchArguments = new Object[]{containString}; diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java index dc10749c5b1..1cc60d64677 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java @@ -19,26 +19,6 @@ package org.apache.zeppelin.realm; -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.authc.AuthenticationInfo; -import org.apache.shiro.authc.AuthenticationToken; -import org.apache.shiro.authc.SimpleAuthenticationInfo; -import org.apache.shiro.authc.credential.HashedCredentialsMatcher; -import org.apache.shiro.authz.AuthorizationInfo; -import org.apache.shiro.authz.SimpleAuthorizationInfo; -import org.apache.shiro.crypto.hash.DefaultHashService; -import org.apache.shiro.crypto.hash.Hash; -import org.apache.shiro.crypto.hash.HashRequest; -import org.apache.shiro.crypto.hash.HashService; -import org.apache.shiro.realm.ldap.JndiLdapRealm; -import org.apache.shiro.realm.ldap.LdapContextFactory; -import org.apache.shiro.realm.ldap.LdapUtils; -import org.apache.shiro.subject.MutablePrincipalCollection; -import org.apache.shiro.subject.PrincipalCollection; -import org.apache.shiro.util.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -65,65 +45,87 @@ import javax.naming.ldap.LdapContext; import javax.naming.ldap.LdapName; import javax.naming.ldap.PagedResultsControl; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.alias.CredentialProvider; +import org.apache.hadoop.security.alias.CredentialProviderFactory; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.ShiroException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authc.credential.HashedCredentialsMatcher; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.crypto.hash.DefaultHashService; +import org.apache.shiro.crypto.hash.Hash; +import org.apache.shiro.crypto.hash.HashRequest; +import org.apache.shiro.crypto.hash.HashService; +import org.apache.shiro.realm.ldap.JndiLdapContextFactory; +import org.apache.shiro.realm.ldap.JndiLdapRealm; +import org.apache.shiro.realm.ldap.LdapContextFactory; +import org.apache.shiro.realm.ldap.LdapUtils; +import org.apache.shiro.session.Session; +import org.apache.shiro.subject.MutablePrincipalCollection; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * Implementation of {@link org.apache.shiro.realm.ldap.JndiLdapRealm} that also - * returns each user's groups. This implementation is heavily based on - * org.apache.isis.security.shiro.IsisLdapRealm. - * + * Implementation of {@link org.apache.shiro.realm.ldap.JndiLdapRealm} that also returns each user's + * groups. This implementation is heavily based on org.apache.isis.security.shiro.IsisLdapRealm. + * *

    This implementation saves looked up ldap groups in Shiro Session to make them * easy to be looked up outside of this object - * - *

    Sample config for shiro.ini: - * - *

    [main] - * ldapRealm = org.apache.zeppelin.realm.LdapRealm - * ldapRealm.contextFactory.url = ldap://localhost:33389 - * ldapRealm.contextFactory.authenticationMechanism = simple - * ldapRealm.contextFactory.systemUsername = uid=guest,ou=people,dc=hadoop,dc= - * apache,dc=org - * ldapRealm.contextFactory.systemPassword = S{ALIAS=ldcSystemPassword} - * ldapRealm.userDnTemplate = uid={0},ou=people,dc=hadoop,dc=apache,dc=org - * # Ability to set ldap paging Size if needed default is 100 - * ldapRealm.pagingSize = 200 - * ldapRealm.authorizationEnabled = true - * ldapRealm.searchBase = dc=hadoop,dc=apache,dc=org - * ldapRealm.userSearchBase = dc=hadoop,dc=apache,dc=org - * ldapRealm.groupSearchBase = ou=groups,dc=hadoop,dc=apache,dc=org - * ldapRealm.userObjectClass = person - * ldapRealm.groupObjectClass = groupofnames - * # Allow userSearchAttribute to be customized - * ldapRealm.userSearchAttributeName = sAMAccountName - * ldapRealm.memberAttribute = member - * # force usernames returned from ldap to lowercase useful for AD - * ldapRealm.userLowerCase = true - * # ability set searchScopes subtree (default), one, base - * ldapRealm.userSearchScope = subtree; - * ldapRealm.groupSearchScope = subtree; - * ldapRealm.userSearchFilter = (&(objectclass=person)(sAMAccountName={0})) - * ldapRealm.groupSearchFilter = (&(objectclass=groupofnames)(member={0})) - * ldapRealm.memberAttributeValueTemplate=cn={0},ou=people,dc=hadoop,dc=apache, - * dc=org - * # enable support for nested groups using the LDAP_MATCHING_RULE_IN_CHAIN operator - * ldapRealm.groupSearchEnableMatchingRuleInChain = true * - *

    # optional mapping from physical groups to logical application roles - * ldapRealm.rolesByGroup = \ LDN_USERS: user_role,\ NYK_USERS: user_role,\ - * HKG_USERS: user_role,\ GLOBAL_ADMIN: admin_role,\ DEMOS: self-install_role + *

    Sample config for shiro.ini: * - *

    # optional list of roles that are allowed to authenticate - * ldapRealm.allowedRolesForAuthentication = admin_role,user_role - * - *

    ldapRealm.permissionsByRole=\ user_role = *:ToDoItemsJdo:*:*,\ - * *:ToDoItem:*:*; \ self-install_role = *:ToDoItemsFixturesService:install:* ; - * \ admin_role = * - * - *

    [urls] - * **=authcBasic - * - *

    securityManager.realms = $ldapRealm - * + *

    + * [main] + * ldapRealm = org.apache.zeppelin.realm.LdapRealm + * ldapRealm.contextFactory.url = ldap://localhost:33389 + * ldapRealm.contextFactory.authenticationMechanism = simple + * ldapRealm.contextFactory.systemUsername = uid=guest,ou=people,dc=hadoop,dc= apache,dc=org + * ldapRealm.contextFactory.systemPassword = S{ALIAS=ldcSystemPassword} + * ldapRealm.hadoopSecurityCredentialPath = jceks://file/user/zeppelin/zeppelin.jceks + * ldapRealm.userDnTemplate = uid={0},ou=people,dc=hadoop,dc=apache,dc=org + * # Ability to set ldap paging Size if needed default is 100 + * ldapRealm.pagingSize = 200 + * ldapRealm.authorizationEnabled = true + * ldapRealm.searchBase = dc=hadoop,dc=apache,dc=org + * ldapRealm.userSearchBase = dc=hadoop,dc=apache,dc=org + * ldapRealm.groupSearchBase = ou=groups,dc=hadoop,dc=apache,dc=org + * ldapRealm.userObjectClass = person + * ldapRealm.groupObjectClass = groupofnames + * # Allow userSearchAttribute to be customized + * ldapRealm.userSearchAttributeName = sAMAccountName + * ldapRealm.memberAttribute = member + * # force usernames returned from ldap to lowercase useful for AD + * ldapRealm.userLowerCase = true + * # ability set searchScopes subtree (default), one, base + * ldapRealm.userSearchScope = subtree; + * ldapRealm.groupSearchScope = subtree; + * ldapRealm.userSearchFilter = (&(objectclass=person)(sAMAccountName={0})) + * ldapRealm.groupSearchFilter = (&(objectclass=groupofnames)(member={0})) + * ldapRealm.memberAttributeValueTemplate=cn={0},ou=people,dc=hadoop,dc=apache,dc=org + * # enable support for nested groups using the LDAP_MATCHING_RULE_IN_CHAIN operator + * ldapRealm.groupSearchEnableMatchingRuleInChain = true + *

    + * # optional mapping from physical groups to logical application roles + * ldapRealm.rolesByGroup = \ LDN_USERS: user_role,\ NYK_USERS: user_role,\ HKG_USERS: user_role, + * \GLOBAL_ADMIN: admin_role,\ DEMOS: self-install_role + *

    + * # optional list of roles that are allowed to authenticate + * ldapRealm.allowedRolesForAuthentication = admin_role,user_role + *

    + * ldapRealm.permissionsByRole=\ user_role = *:ToDoItemsJdo:*:*,\*:ToDoItem:*:*; + * \ self-install_role = *:ToDoItemsFixturesService:install:* ; \ admin_role = * + *

    + * [urls] + * **=authcBasic + *

    + * securityManager.realms = $ldapRealm */ public class LdapRealm extends JndiLdapRealm { @@ -134,11 +136,11 @@ public class LdapRealm extends JndiLdapRealm { private static final String SUBJECT_USER_GROUPS = "subject.userGroups"; private static final String MEMBER_URL = "memberUrl"; private static final String POSIX_GROUP = "posixGroup"; - + // LDAP Operator '1.2.840.113556.1.4.1941' // walks the chain of ancestry in objects all the way to the root until it finds a match // see https://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx - private static final String MATCHING_RULE_IN_CHAIN_FORMAT = + private static final String MATCHING_RULE_IN_CHAIN_FORMAT = "(&(objectClass=%s)(%s:1.2.840.113556.1.4.1941:=%s))"; private static Pattern TEMPLATE_PATTERN = Pattern.compile("\\{(\\d+?)\\}"); @@ -178,7 +180,7 @@ public class LdapRealm extends JndiLdapRealm { private String groupIdAttribute = "cn"; - private String memberAttributeValuePrefix = "uid={0}"; + private String memberAttributeValuePrefix = "uid="; private String memberAttributeValueSuffix = ""; private final Map rolesByGroup = new LinkedHashMap(); @@ -186,6 +188,9 @@ public class LdapRealm extends JndiLdapRealm { private final Map> permissionsByRole = new LinkedHashMap>(); + private String hadoopSecurityCredentialPath; + final String keystorePass = "ldapRealm.systemPassword"; + private boolean authorizationEnabled; private String userSearchAttributeName; @@ -193,6 +198,12 @@ public class LdapRealm extends JndiLdapRealm { private HashService hashService = new DefaultHashService(); + + + public void setHadoopSecurityCredentialPath(String hadoopSecurityCredentialPath) { + this.hadoopSecurityCredentialPath = hadoopSecurityCredentialPath; + } + public LdapRealm() { HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(HASHING_ALGORITHM); setCredentialsMatcher(credentialsMatcher); @@ -209,6 +220,37 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) } } + protected void onInit() { + super.onInit(); + if (!org.apache.commons.lang.StringUtils.isEmpty(this.hadoopSecurityCredentialPath) + && getContextFactory() != null) { + ((JndiLdapContextFactory) getContextFactory()).setSystemPassword( + getSystemPassword(this.hadoopSecurityCredentialPath, keystorePass)); + } + } + + static String getSystemPassword(String hadoopSecurityCredentialPath, + String keystorePass) { + String password = ""; + try { + Configuration configuration = new Configuration(); + configuration.set(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH, + hadoopSecurityCredentialPath); + CredentialProvider provider = CredentialProviderFactory.getProviders(configuration).get(0); + CredentialProvider.CredentialEntry credEntry = provider.getCredentialEntry(keystorePass); + if (credEntry != null) { + password = new String(credEntry.getCredential()); + } + } catch (IOException e) { + throw new ShiroException("Error from getting credential entry from keystore", e); + } + if (org.apache.commons.lang.StringUtils.isEmpty(password)) { + throw new ShiroException("Error getting SystemPassword from the provided keystore:" + + keystorePass + ", in path:" + hadoopSecurityCredentialPath); + } + return password; + } + /** * This overrides the implementation of queryForAuthenticationInfo inside JndiLdapRealm. * In addition to calling the super method for authentication it also tries to validate @@ -222,8 +264,7 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) */ @Override protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token, - LdapContextFactory ldapContextFactory) - throws NamingException { + LdapContextFactory ldapContextFactory) throws NamingException { AuthenticationInfo info = super.queryForAuthenticationInfo(token, ldapContextFactory); // Credentials were verified. Verify that the principal has all allowedRulesForAuthentication if (!hasAllowedAuthenticationRules(info.getPrincipals(), ldapContextFactory)) { @@ -246,7 +287,7 @@ protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken toke * if any LDAP errors occur during the search. */ @Override - protected AuthorizationInfo queryForAuthorizationInfo(final PrincipalCollection principals, + public AuthorizationInfo queryForAuthorizationInfo(final PrincipalCollection principals, final LdapContextFactory ldapContextFactory) throws NamingException { if (!isAuthorizationEnabled()) { return null; @@ -267,7 +308,7 @@ private boolean hasAllowedAuthenticationRules(PrincipalCollection principals, boolean allowed = allowedRolesForAuthentication.isEmpty(); if (!allowed) { Set roles = getRoles(principals, ldapContextFactory); - for (String allowedRole: allowedRolesForAuthentication) { + for (String allowedRole : allowedRolesForAuthentication) { if (roles.contains(allowedRole)) { log.debug("Allowed role for user [" + allowedRole + "] found."); allowed = true; @@ -286,7 +327,8 @@ private Set getRoles(PrincipalCollection principals, LdapContext systemLdapCtx = null; try { systemLdapCtx = ldapContextFactory.getSystemLdapContext(); - return rolesFor(principals, username, systemLdapCtx, ldapContextFactory); + return rolesFor(principals, username, systemLdapCtx, + ldapContextFactory, SecurityUtils.getSubject().getSession()); } catch (AuthenticationException ae) { ae.printStackTrace(); return Collections.emptySet(); @@ -295,9 +337,9 @@ private Set getRoles(PrincipalCollection principals, } } - private Set rolesFor(PrincipalCollection principals, + protected Set rolesFor(PrincipalCollection principals, String userNameIn, final LdapContext ldapCtx, - final LdapContextFactory ldapContextFactory) throws NamingException { + final LdapContextFactory ldapContextFactory, Session session) throws NamingException { final Set roleNames = new HashSet<>(); final Set groupNames = new HashSet<>(); final String userName; @@ -308,14 +350,7 @@ private Set rolesFor(PrincipalCollection principals, userName = userNameIn; } - String userDn; - if (userSearchAttributeName == null || userSearchAttributeName.isEmpty()) { - // memberAttributeValuePrefix and memberAttributeValueSuffix - // were computed from memberAttributeValueTemplate - userDn = memberAttributeValuePrefix + userName + memberAttributeValueSuffix; - } else { - userDn = getUserDn(userName); - } + String userDn = getUserDnForSearch(userName); // Activate paged results int pageSize = getPagingSize(); @@ -343,7 +378,7 @@ private Set rolesFor(PrincipalCollection principals, String.format( MATCHING_RULE_IN_CHAIN_FORMAT, groupObjectClass, memberAttribute, userDn), searchControls); - while (searchResultEnum != null && searchResultEnum.hasMore()) { + while (searchResultEnum != null && searchResultEnum.hasMore()) { // searchResults contains all the groups in search scope numResults++; final SearchResult group = searchResultEnum.next(); @@ -357,15 +392,14 @@ private Set rolesFor(PrincipalCollection principals, } else { roleNames.add(groupName); } - } + } } else { // Default group search filter String searchFilter = String.format("(objectclass=%1$s)", groupObjectClass); // If group search filter is defined in Shiro config, then use it if (groupSearchFilter != null) { - Matcher matchedPrincipal = matchPrincipal(userDn); - searchFilter = expandTemplate(groupSearchFilter, matchedPrincipal); + searchFilter = expandTemplate(groupSearchFilter, userName); //searchFilter = String.format("%1$s", groupSearchFilter); } if (log.isDebugEnabled()) { @@ -376,7 +410,7 @@ private Set rolesFor(PrincipalCollection principals, getGroupSearchBase(), searchFilter, searchControls); - while (searchResultEnum != null && searchResultEnum.hasMore()) { + while (searchResultEnum != null && searchResultEnum.hasMore()) { // searchResults contains all the groups in search scope numResults++; final SearchResult group = searchResultEnum.next(); @@ -391,32 +425,41 @@ private Set rolesFor(PrincipalCollection principals, } } // Re-activate paged results - ldapCtx.setRequestControls(new Control[]{new PagedResultsControl(pageSize, - cookie, Control.CRITICAL)}); + ldapCtx.setRequestControls(new Control[]{new PagedResultsControl(pageSize, + cookie, Control.CRITICAL)}); } while (cookie != null); } catch (SizeLimitExceededException e) { - log.info("Only retrieved first " + numResults + - " groups due to SizeLimitExceededException."); + log.info("Only retrieved first " + numResults + + " groups due to SizeLimitExceededException."); } catch (IOException e) { log.error("Unabled to setup paged results"); } // save role names and group names in session so that they can be // easily looked up outside of this object - SecurityUtils.getSubject().getSession().setAttribute(SUBJECT_USER_ROLES, roleNames); - SecurityUtils.getSubject().getSession().setAttribute(SUBJECT_USER_GROUPS, groupNames); + session.setAttribute(SUBJECT_USER_ROLES, roleNames); + session.setAttribute(SUBJECT_USER_GROUPS, groupNames); if (!groupNames.isEmpty() && (principals instanceof MutablePrincipalCollection)) { ((MutablePrincipalCollection) principals).addAll(groupNames, getName()); } if (log.isDebugEnabled()) { - log.debug("User RoleNames: " + userName + "::" + roleNames); + log.debug("User RoleNames: " + userName + "::" + roleNames); } return roleNames; } - private void addRoleIfMember(final String userDn, final SearchResult group, - final Set roleNames, final Set groupNames, - final LdapContextFactory ldapContextFactory) throws NamingException { + protected String getUserDnForSearch(String userName) { + if (userSearchAttributeName == null || userSearchAttributeName.isEmpty()) { + // memberAttributeValuePrefix and memberAttributeValueSuffix + // were computed from memberAttributeValueTemplate + return memberDn(userName); + } else { + return getUserDn(userName); + } + } + private void addRoleIfMember(final String userDn, final SearchResult group, + final Set roleNames, final Set groupNames, + final LdapContextFactory ldapContextFactory) throws NamingException { NamingEnumeration attributeEnum = null; NamingEnumeration ne = null; try { @@ -435,7 +478,7 @@ private void addRoleIfMember(final String userDn, final SearchResult group, String attrValue = ne.next().toString(); if (memberAttribute.equalsIgnoreCase(MEMBER_URL)) { boolean dynamicGroupMember = isUserMemberOfDynamicGroup(userLdapDn, attrValue, - ldapContextFactory); + ldapContextFactory); if (dynamicGroupMember) { groupNames.add(groupName); String roleName = roleNameFor(groupName); @@ -446,8 +489,9 @@ private void addRoleIfMember(final String userDn, final SearchResult group, } } } else { + // posix groups' members don' include the entire dn if (groupObjectClass.equalsIgnoreCase(POSIX_GROUP)) { - attrValue = memberAttributeValuePrefix + attrValue + memberAttributeValueSuffix; + attrValue = memberDn(attrValue); } if (userLdapDn.equals(new LdapName(attrValue))) { groupNames.add(groupName); @@ -474,11 +518,15 @@ private void addRoleIfMember(final String userDn, final SearchResult group, } } } - + + private String memberDn(String attrValue) { + return memberAttributeValuePrefix + attrValue + memberAttributeValueSuffix; + } + public Map getListRoles() { Map groupToRoles = getRolesByGroup(); Map roles = new HashMap<>(); - for (Map.Entry entry : groupToRoles.entrySet()){ + for (Map.Entry entry : groupToRoles.entrySet()) { roles.put(entry.getValue(), entry.getKey()); } return roles; @@ -522,7 +570,7 @@ public void setUserSearchBase(String userSearchBase) { public int getPagingSize() { return pagingSize; } - + public void setPagingSize(int pagingSize) { this.pagingSize = pagingSize; } @@ -558,7 +606,7 @@ public String getGroupIdAttribute() { public void setGroupIdAttribute(String groupIdAttribute) { this.groupIdAttribute = groupIdAttribute; } - + /** * Set Member Attribute Template for LDAP. * @@ -575,7 +623,7 @@ public void setMemberAttributeValueTemplate(String template) { int index = template.indexOf(MEMBER_SUBSTITUTION_TOKEN); if (index < 0) { String msg = "Member attribute value template must contain the '" + MEMBER_SUBSTITUTION_TOKEN - + "' replacement token to understand how to " + "parse the group members."; + + "' replacement token to understand how to " + "parse the group members."; throw new IllegalArgumentException(msg); } String prefix = template.substring(0, index); @@ -591,7 +639,7 @@ public void setAllowedRolesForAuthentication(List allowedRolesForAuthenc public void setRolesByGroup(Map rolesByGroup) { this.rolesByGroup.putAll(rolesByGroup); } - + public Map getRolesByGroup() { return rolesByGroup; } @@ -599,7 +647,7 @@ public Map getRolesByGroup() { public void setPermissionsByRole(String permissionsByRoleStr) { permissionsByRole.putAll(parsePermissionByRoleString(permissionsByRoleStr)); } - + public Map> getPermissionsByRole() { return permissionsByRole; } @@ -615,7 +663,7 @@ public void setAuthorizationEnabled(boolean authorizationEnabled) { public String getUserSearchAttributeName() { return userSearchAttributeName; } - + /** * Set User Search Attribute Name for LDAP. * @@ -661,8 +709,7 @@ private Map> parsePermissionByRoleString(String permissions } boolean isUserMemberOfDynamicGroup(LdapName userLdapDn, String memberUrl, - final LdapContextFactory ldapContextFactory) throws NamingException { - + final LdapContextFactory ldapContextFactory) throws NamingException { // ldap://host:port/dn?attributes?scope?filter?extensions if (memberUrl == null) { @@ -699,7 +746,7 @@ boolean isUserMemberOfDynamicGroup(LdapName userLdapDn, String memberUrl, NamingEnumeration searchResultEnum = null; try { searchResultEnum = systemLdapCtx.search(userLdapDn, searchFilter, - searchScope.equalsIgnoreCase("sub") ? SUBTREE_SCOPE : ONELEVEL_SCOPE); + searchScope.equalsIgnoreCase("sub") ? SUBTREE_SCOPE : ONELEVEL_SCOPE); if (searchResultEnum.hasMore()) { return true; } @@ -718,7 +765,7 @@ boolean isUserMemberOfDynamicGroup(LdapName userLdapDn, String memberUrl, public String getPrincipalRegex() { return principalRegex; } - + /** * Set Regex for Principal LDAP. * @@ -752,7 +799,7 @@ public String getUserSearchFilter() { public void setUserSearchFilter(final String filter) { this.userSearchFilter = (filter == null ? null : filter.trim()); } - + public String getGroupSearchFilter() { return groupSearchFilter; } @@ -764,11 +811,11 @@ public void setGroupSearchFilter(final String filter) { public boolean getUserLowerCase() { return userLowerCase; } - + public void setUserLowerCase(boolean userLowerCase) { this.userLowerCase = userLowerCase; } - + public String getUserSearchScope() { return userSearchScope; } @@ -784,7 +831,7 @@ public String getGroupSearchScope() { public void setGroupSearchScope(final String scope) { this.groupSearchScope = (scope == null ? null : scope.trim().toLowerCase()); } - + public boolean isGroupSearchEnableMatchingRuleInChain() { return groupSearchEnableMatchingRuleInChain; } @@ -803,8 +850,8 @@ private SearchControls getUserSearchControls() { } return searchControls; } - - private SearchControls getGroupSearchControls() { + + protected SearchControls getGroupSearchControls() { SearchControls searchControls = SUBTREE_SCOPE; if ("onelevel".equalsIgnoreCase(groupSearchScope)) { searchControls = ONELEVEL_SCOPE; @@ -819,13 +866,13 @@ public void setUserDnTemplate(final String template) throws IllegalArgumentExcep userDnTemplate = template; } - private Matcher matchPrincipal(final String principal) { + private String matchPrincipal(final String principal) { Matcher matchedPrincipal = principalPattern.matcher(principal); if (!matchedPrincipal.matches()) { - throw new IllegalArgumentException("Principal " - + principal + " does not match " + principalRegex); + throw new IllegalArgumentException("Principal " + + principal + " does not match " + principalRegex); } - return matchedPrincipal; + return matchedPrincipal.group(); } /** @@ -853,16 +900,16 @@ private Matcher matchPrincipal(final String principal) { * @see LdapContextFactory#getLdapContext(Object, Object) */ @Override - protected String getUserDn(final String principal) throws IllegalArgumentException, - IllegalStateException { + protected String getUserDn(final String principal) throws IllegalArgumentException, + IllegalStateException { String userDn; - Matcher matchedPrincipal = matchPrincipal(principal); + String matchedPrincipal = matchPrincipal(principal); String userSearchBase = getUserSearchBase(); String userSearchAttributeName = getUserSearchAttributeName(); // If not searching use the userDnTemplate and return. if ((userSearchBase == null || userSearchBase.isEmpty()) || (userSearchAttributeName == null - && userSearchFilter == null && !"object".equalsIgnoreCase(userSearchScope))) { + && userSearchFilter == null && !"object".equalsIgnoreCase(userSearchScope))) { userDn = expandTemplate(userDnTemplate, matchedPrincipal); if (log.isDebugEnabled()) { log.debug("LDAP UserDN and Principal: " + userDn + "," + principal); @@ -878,8 +925,8 @@ protected String getUserDn(final String principal) throws IllegalArgumentExcepti searchFilter = String.format("(objectclass=%1$s)", getUserObjectClass()); } else { searchFilter = String.format("(&(objectclass=%1$s)(%2$s=%3$s))", getUserObjectClass(), - userSearchAttributeName, expandTemplate(getUserSearchAttributeTemplate(), - matchedPrincipal)); + userSearchAttributeName, expandTemplate(getUserSearchAttributeTemplate(), + matchedPrincipal)); } } else { searchFilter = expandTemplate(userSearchFilter, matchedPrincipal); @@ -892,8 +939,8 @@ userSearchAttributeName, expandTemplate(getUserSearchAttributeTemplate(), try { systemLdapCtx = getContextFactory().getSystemLdapContext(); if (log.isDebugEnabled()) { - log.debug("SearchBase,SearchFilter,UserSearchScope: " + searchBase - + "," + searchFilter + "," + userSearchScope); + log.debug("SearchBase,SearchFilter,UserSearchScope: " + searchBase + + "," + searchFilter + "," + userSearchScope); } searchResultEnum = systemLdapCtx.search(searchBase, searchFilter, searchControls); // SearchResults contains all the entries in search scope @@ -926,28 +973,19 @@ userSearchAttributeName, expandTemplate(getUserSearchAttributeTemplate(), } @Override - protected AuthenticationInfo createAuthenticationInfo(AuthenticationToken token, - Object ldapPrincipal, - Object ldapCredentials, LdapContext ldapContext) throws NamingException { + protected AuthenticationInfo createAuthenticationInfo(AuthenticationToken token, + Object ldapPrincipal, Object ldapCredentials, LdapContext ldapContext) + throws NamingException { HashRequest.Builder builder = new HashRequest.Builder(); Hash credentialsHash = hashService - .computeHash(builder.setSource(token.getCredentials()) - .setAlgorithmName(HASHING_ALGORITHM).build()); - return new SimpleAuthenticationInfo(token.getPrincipal(), - credentialsHash.toHex(), credentialsHash.getSalt(), - getName()); - } - - private static final String expandTemplate(final String template, final Matcher input) { - String output = template; - Matcher matcher = TEMPLATE_PATTERN.matcher(output); - while (matcher.find()) { - String lookupStr = matcher.group(1); - int lookupIndex = Integer.parseInt(lookupStr); - String lookupValue = input.group(lookupIndex); - output = matcher.replaceFirst(lookupValue == null ? "" : lookupValue); - matcher = TEMPLATE_PATTERN.matcher(output); - } - return output; + .computeHash(builder.setSource(token.getCredentials()) + .setAlgorithmName(HASHING_ALGORITHM).build()); + return new SimpleAuthenticationInfo(token.getPrincipal(), + credentialsHash.toHex(), credentialsHash.getSalt(), + getName()); + } + + protected static final String expandTemplate(final String template, final String input) { + return template.replace(MEMBER_SUBSTITUTION_TOKEN, input); } } 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..0ba403b22d2 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java @@ -0,0 +1,344 @@ +/* + * 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.Date; +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 Boolean logoutAPI; + + 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; + } + + public 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); + boolean sigValid = validateSignature(signed); + if (!sigValid) { + LOGGER.warn("Signature of JWT token could not be verified. Please check the public key"); + return false; + } + boolean expValid = validateExpiration(signed); + if (!expValid) { + 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); + 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; + } + + /** + * Validate that the expiration time of the JWT token has not been violated. + * If it has then throw an AuthenticationException. Override this method in + * subclasses in order to customize the expiration validation behavior. + * + * @param jwtToken + * the token that contains the expiration date to validate + * @return valid true if the token has not expired; false otherwise + */ + protected boolean validateExpiration(SignedJWT jwtToken) { + boolean valid = false; + try { + Date expires = jwtToken.getJWTClaimsSet().getExpirationTime(); + if (expires == null || new Date().before(expires)) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("SSO token expiration date has been " + "successfully validated"); + } + valid = true; + } else { + LOGGER.warn("SSO expiration date validation failed."); + } + } catch (ParseException pe) { + LOGGER.warn("SSO expiration date validation failed.", pe); + } + 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 Boolean getLogoutAPI() { + return logoutAPI; + } + + public void setLogoutAPI(Boolean logoutAPI) { + this.logoutAPI = logoutAPI; + } + + 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/GetUserList.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java index 458d5bd8f7f..5876de2501c 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java @@ -90,7 +90,7 @@ public List getRolesList(IniRealm r) { /** * function to extract users from LDAP */ - public List getUserList(JndiLdapRealm r, String searchText) { + public List getUserList(JndiLdapRealm r, String searchText, int numUsersToFetch) { List userList = new ArrayList<>(); String userDnTemplate = r.getUserDnTemplate(); String userDn[] = userDnTemplate.split(",", 2); @@ -100,6 +100,7 @@ public List getUserList(JndiLdapRealm r, String searchText) { try { LdapContext ctx = CF.getSystemLdapContext(); SearchControls constraints = new SearchControls(); + constraints.setCountLimit(numUsersToFetch); constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); String[] attrIDs = {userDnPrefix}; constraints.setReturningAttributes(attrIDs); @@ -122,7 +123,7 @@ public List getUserList(JndiLdapRealm r, String searchText) { /** * function to extract users from Zeppelin LdapRealm */ - public List getUserList(LdapRealm r, String searchText) { + public List getUserList(LdapRealm r, String searchText, int numUsersToFetch) { List userList = new ArrayList<>(); if (LOG.isDebugEnabled()) { LOG.debug("SearchText: " + searchText); @@ -135,11 +136,12 @@ public List getUserList(LdapRealm r, String searchText) { LdapContext ctx = CF.getSystemLdapContext(); SearchControls constraints = new SearchControls(); constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); + constraints.setCountLimit(numUsersToFetch); String[] attrIDs = {userAttribute}; constraints.setReturningAttributes(attrIDs); NamingEnumeration result = ctx.search(userSearchRealm, "(&(objectclass=" + userObjectClass + ")(" - + userAttribute + "=" + searchText + "))", constraints); + + userAttribute + "=*" + searchText + "*))", constraints); while (result.hasMore()) { Attributes attrs = ((SearchResult) result.next()).getAttributes(); if (attrs.get(userAttribute) != null) { @@ -186,11 +188,12 @@ public List getRolesList(LdapRealm r) { } - public List getUserList(ActiveDirectoryGroupRealm r, String searchText) { + public List getUserList(ActiveDirectoryGroupRealm r, String searchText, + int numUsersToFetch) { List userList = new ArrayList<>(); try { LdapContext ctx = r.getLdapContextFactory().getSystemLdapContext(); - userList = r.searchForUserName(searchText, ctx); + userList = r.searchForUserName(searchText, ctx, numUsersToFetch); } catch (Exception e) { LOG.error("Error retrieving User list from ActiveDirectory Realm", e); } @@ -202,6 +205,7 @@ public List getUserList(ActiveDirectoryGroupRealm r, String searchText) */ public List getUserList(JdbcRealm obj) { List userlist = new ArrayList<>(); + Connection con = null; PreparedStatement ps = null; ResultSet rs = null; DataSource dataSource = null; @@ -231,7 +235,7 @@ public List getUserList(JdbcRealm obj) { return userlist; } - userquery = "select ? from ?"; + userquery = String.format("SELECT %s FROM %s", username, tablename); } catch (IllegalAccessException e) { LOG.error("Error while accessing dataSource for JDBC Realm", e); @@ -239,10 +243,8 @@ public List getUserList(JdbcRealm obj) { } try { - Connection con = dataSource.getConnection(); + con = dataSource.getConnection(); ps = con.prepareStatement(userquery); - ps.setString(1, username); - ps.setString(2, tablename); rs = ps.executeQuery(); while (rs.next()) { userlist.add(rs.getString(1).trim()); @@ -252,6 +254,7 @@ public List getUserList(JdbcRealm obj) { } finally { JdbcUtils.closeResultSet(rs); JdbcUtils.closeStatement(ps); + JdbcUtils.closeConnection(con); } return userlist; } 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..e2a10e62b31 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 @@ -123,7 +123,7 @@ public Response newSettings(String message) { request.getOption(), request.getProperties()); logger.info("new setting created with {}", interpreterSetting.getId()); return new JsonResponse<>(Status.OK, "", interpreterSetting).build(); - } catch (InterpreterException | IOException e) { + } catch (IOException e) { logger.error("Exception in InterpreterRestApi while creating ", e); return new JsonResponse<>(Status.NOT_FOUND, e.getMessage(), ExceptionUtils.getStackTrace(e)) .build(); @@ -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/rest/LoginRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java index bd96684d9e1..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 @@ -16,25 +16,42 @@ */ package org.apache.zeppelin.rest; -import org.apache.shiro.authc.*; +import com.google.gson.Gson; + +import java.text.ParseException; +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,7 +59,9 @@ @Path("/login") @Produces("application/json") public class LoginRestApi { + private static final Logger LOG = LoggerFactory.getLogger(LoginRestApi.class); + private static final Gson gson = new Gson(); /** * Required by Swagger. @@ -52,6 +71,109 @@ 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(); + 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) { + 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 proceedToLogin(Subject currentUser, AuthenticationToken token) { + JsonResponse response = null; + try { + logoutCurrentUser(); + 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", gson.toJson(roles)); + 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 +185,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 +193,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 = proceedToLogin(currentUser, token); } if (response == null) { @@ -125,13 +212,34 @@ public Response postLogin(@FormParam("userName") String userName, @ZeppelinApi public Response logout() { JsonResponse response; + logoutCurrentUser(); + if (isKnoxSSOEnabled()) { + 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, "", ""); + + } + 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(); + } + + private void logoutCurrentUser() { Subject currentUser = org.apache.shiro.SecurityUtils.getSubject(); TicketContainer.instance.removeTicket(SecurityUtils.getPrincipal()); currentUser.getSession().stop(); currentUser.logout(); - response = new JsonResponse(Response.Status.UNAUTHORIZED, "", ""); - LOG.warn(response.toString()); - return response.build(); } - } 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..e7e162a321c 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 @@ -41,10 +41,7 @@ import org.apache.zeppelin.rest.exception.BadRequestException; import org.apache.zeppelin.rest.exception.NotFoundException; import org.apache.zeppelin.rest.exception.ForbiddenException; -import org.apache.zeppelin.rest.message.CronRequest; -import org.apache.zeppelin.rest.message.NewNoteRequest; -import org.apache.zeppelin.rest.message.NewParagraphRequest; -import org.apache.zeppelin.rest.message.RunParagraphWithParametersRequest; +import org.apache.zeppelin.rest.message.*; import org.apache.zeppelin.search.SearchService; import org.apache.zeppelin.server.JsonResponse; import org.apache.zeppelin.socket.NotebookServer; @@ -59,6 +56,12 @@ import com.google.common.collect.Sets; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; /** * Rest api endpoint for the notebook. @@ -98,6 +101,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(); } @@ -141,7 +145,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. */ @@ -153,7 +157,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. */ @@ -165,19 +169,38 @@ 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) { 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 */ @@ -195,19 +218,35 @@ 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); - - 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 +259,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); @@ -352,6 +393,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(); @@ -465,6 +507,37 @@ public Response getParagraph(@PathParam("noteId") String noteId, return new JsonResponse<>(Status.OK, "", p).build(); } + /** + * Update paragraph + * + * @param message json containing the "text" and optionally the "title" of the paragraph, e.g. + * {"text" : "updated text", "title" : "Updated title" } + * + */ + @PUT + @Path("{noteId}/paragraph/{paragraphId}") + @ZeppelinApi + public Response updateParagraph(@PathParam("noteId") String noteId, + @PathParam("paragraphId") String paragraphId, + String message) throws IOException { + String user = SecurityUtils.getPrincipal(); + LOG.info("{} will update paragraph {} {}", user, noteId, paragraphId); + + Note note = notebook.getNote(noteId); + checkIfNoteIsNotNull(note); + checkIfUserCanWrite(noteId, "Insufficient privileges you cannot update this paragraph"); + Paragraph p = note.getParagraph(paragraphId); + checkIfParagraphIsNotNull(p); + + UpdateParagraphRequest updatedParagraph = gson.fromJson(message, UpdateParagraphRequest.class); + p.setText(updatedParagraph.getText()); + if (updatedParagraph.getTitle() != null) { p.setTitle(updatedParagraph.getTitle()); } + AuthenticationInfo subject = new AuthenticationInfo(user); + note.persist(subject); + notebookServer.broadcastParagraph(note, p); + return new JsonResponse<>(Status.OK, "").build(); + } + @PUT @Path("{noteId}/paragraph/{paragraphId}/config") @ZeppelinApi @@ -483,7 +556,6 @@ public Response updateParagraphConfig(@PathParam("noteId") String noteId, configureParagraph(p, newConfig, user); AuthenticationInfo subject = new AuthenticationInfo(user); note.persist(subject); - return new JsonResponse<>(Status.OK, "", p).build(); } @@ -588,11 +660,12 @@ public Response runNoteJobs(@PathParam("noteId") String noteId) LOG.info("run note jobs {} ", noteId); Note note = notebook.getNote(noteId); AuthenticationInfo subject = new AuthenticationInfo(SecurityUtils.getPrincipal()); + subject.setRoles(new LinkedList<>(SecurityUtils.getRoles())); 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); + note.runAll(subject, true); } catch (Exception ex) { LOG.error("Exception from run", ex); return new JsonResponse<>(Status.PRECONDITION_FAILED, @@ -616,7 +689,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()) { @@ -657,7 +730,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."); @@ -690,7 +763,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); @@ -698,6 +771,7 @@ public Response runParagraph(@PathParam("noteId") String noteId, handleParagraphParams(message, note, paragraph); AuthenticationInfo subject = new AuthenticationInfo(SecurityUtils.getPrincipal()); + subject.setRoles(new LinkedList<>(SecurityUtils.getRoles())); paragraph.setAuthenticationInfo(subject); note.persist(subject); @@ -728,7 +802,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); @@ -739,6 +813,10 @@ public Response runParagraphSynchronously(@PathParam("noteId") String noteId, note.initializeJobListenerForParagraph(paragraph); } + AuthenticationInfo subject = new AuthenticationInfo(SecurityUtils.getPrincipal()); + subject.setRoles(new LinkedList<>(SecurityUtils.getRoles())); + paragraph.setAuthenticationInfo(subject); + paragraph.run(); final InterpreterResult result = paragraph.getResult(); @@ -766,7 +844,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 +869,8 @@ 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"); + checkIfNoteSupportsCron(note); if (!CronExpression.isValidExpression(request.getCronString())) { return new JsonResponse<>(Status.BAD_REQUEST, "wrong cron expressions.").build(); @@ -799,6 +878,7 @@ public Response registerCronJob(@PathParam("noteId") String noteId, String messa Map config = note.getConfig(); config.put("cron", request.getCronString()); + config.put("releaseresource", request.getReleaseResource()); note.setConfig(config); notebook.refreshCron(note.getId()); @@ -818,14 +898,16 @@ 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); + config.remove("cron"); + config.remove("releaseresource"); note.setConfig(config); notebook.refreshCron(note.getId()); @@ -849,8 +931,12 @@ 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); + Map response = new HashMap<>(); + response.put("cron", note.getConfig().get("cron")); + response.put("releaseResource", note.getConfig().get("releaseresource")); - return new JsonResponse<>(Status.OK, note.getConfig().get("cron")).build(); + return new JsonResponse<>(Status.OK, response).build(); } /** @@ -922,7 +1008,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--; } @@ -953,22 +1040,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/rest/SecurityRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java index 742af9e5241..484bccb3704 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java @@ -17,7 +17,7 @@ package org.apache.zeppelin.rest; - +import com.google.gson.Gson; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.realm.Realm; import org.apache.shiro.realm.jdbc.JdbcRealm; @@ -47,6 +47,7 @@ @Produces("application/json") public class SecurityRestApi { private static final Logger LOG = LoggerFactory.getLogger(SecurityRestApi.class); + private static final Gson gson = new Gson(); /** * Required by Swagger. @@ -80,7 +81,7 @@ public Response ticket() { Map data = new HashMap<>(); data.put("principal", principal); - data.put("roles", roles.toString()); + data.put("roles", gson.toJson(roles)); data.put("ticket", ticket); response = new JsonResponse(Response.Status.OK, "", data); @@ -98,6 +99,7 @@ public Response ticket() { @Path("userlist/{searchText}") public Response getUserList(@PathParam("searchText") final String searchText) { + final int numUsersToFetch = 5; List usersList = new ArrayList<>(); List rolesList = new ArrayList<>(); try { @@ -114,13 +116,15 @@ public Response getUserList(@PathParam("searchText") final String searchText) { usersList.addAll(getUserListObj.getUserList((IniRealm) realm)); rolesList.addAll(getUserListObj.getRolesList((IniRealm) realm)); } else if (name.equals("org.apache.zeppelin.realm.LdapGroupRealm")) { - usersList.addAll(getUserListObj.getUserList((JndiLdapRealm) realm, searchText)); + usersList.addAll(getUserListObj.getUserList((JndiLdapRealm) realm, searchText, + numUsersToFetch)); } else if (name.equals("org.apache.zeppelin.realm.LdapRealm")) { - usersList.addAll(getUserListObj.getUserList((LdapRealm) realm, searchText)); + usersList.addAll(getUserListObj.getUserList((LdapRealm) realm, searchText, + numUsersToFetch)); rolesList.addAll(getUserListObj.getRolesList((LdapRealm) realm)); } else if (name.equals("org.apache.zeppelin.realm.ActiveDirectoryGroupRealm")) { usersList.addAll(getUserListObj.getUserList((ActiveDirectoryGroupRealm) realm, - searchText)); + searchText, numUsersToFetch)); } else if (name.equals("org.apache.shiro.realm.jdbc.JdbcRealm")) { usersList.addAll(getUserListObj.getUserList((JdbcRealm) realm)); } @@ -150,7 +154,7 @@ public int compare(String o1, String o2) { autoSuggestUserList.add(user); maxLength++; } - if (maxLength == 5) { + if (maxLength == numUsersToFetch) { break; } } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/CronRequest.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/CronRequest.java index 0cd1b63aba1..02b58129610 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/CronRequest.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/CronRequest.java @@ -30,7 +30,8 @@ public class CronRequest implements JsonSerializable { private static final Gson gson = new Gson(); - String cron; + private String cron; + private Boolean releaseResource; public CronRequest (){ @@ -40,6 +41,13 @@ public String getCronString() { return cron; } + public Boolean getReleaseResource() { + if (releaseResource == null) { + return Boolean.FALSE; + } + return releaseResource; + } + public String toJson() { return gson.toJson(this); } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/UpdateParagraphRequest.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/UpdateParagraphRequest.java new file mode 100644 index 00000000000..3b4a6f68cc7 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/UpdateParagraphRequest.java @@ -0,0 +1,40 @@ +/* + * 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.message; + +/** + * UpdateParagraphRequest + */ +public class UpdateParagraphRequest { + String title; + String text; + + public UpdateParagraphRequest() { + + } + + public String getTitle() { + return title; + } + + public String getText() { + return text; + } + + +} diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/server/CorsFilter.java b/zeppelin-server/src/main/java/org/apache/zeppelin/server/CorsFilter.java index 3a74bf4084c..b5cca5b3abd 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/server/CorsFilter.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/server/CorsFilter.java @@ -73,19 +73,19 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } private void addCorsHeaders(HttpServletResponse response, String origin) { - response.addHeader("Access-Control-Allow-Origin", origin); - response.addHeader("Access-Control-Allow-Credentials", "true"); - response.addHeader("Access-Control-Allow-Headers", "authorization,Content-Type"); - response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, HEAD, DELETE"); + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Allow-Headers", "authorization,Content-Type"); + response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, HEAD, DELETE"); DateFormat fullDateFormatEN = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, new Locale("EN", "en")); - response.addHeader("Date", fullDateFormatEN.format(new Date())); + response.setHeader("Date", fullDateFormatEN.format(new Date())); ZeppelinConfiguration zeppelinConfiguration = ZeppelinConfiguration.create(); - response.addHeader("X-FRAME-OPTIONS", zeppelinConfiguration.getXFrameOptions()); + response.setHeader("X-FRAME-OPTIONS", zeppelinConfiguration.getXFrameOptions()); if (zeppelinConfiguration.useSsl()) { - response.addHeader("Strict-Transport-Security", zeppelinConfiguration.getStrictTransport()); + response.setHeader("Strict-Transport-Security", zeppelinConfiguration.getStrictTransport()); } - response.addHeader("X-XSS-Protection", zeppelinConfiguration.getXxssProtection()); + response.setHeader("X-XSS-Protection", zeppelinConfiguration.getXxssProtection()); } @Override 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..a27e08048b2 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,24 +19,34 @@ import java.io.File; import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.util.Collection; import java.util.EnumSet; import java.util.HashSet; import java.util.Set; +import javax.management.InstanceAlreadyExistsException; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.NotCompliantMBeanException; +import javax.management.ObjectName; import javax.servlet.DispatcherType; 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; +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; -import org.apache.zeppelin.dep.DependencyResolver; import org.apache.zeppelin.helium.Helium; import org.apache.zeppelin.helium.HeliumApplicationFactory; import org.apache.zeppelin.helium.HeliumBundleFactory; import org.apache.zeppelin.interpreter.InterpreterFactory; -import org.apache.zeppelin.interpreter.InterpreterOption; import org.apache.zeppelin.interpreter.InterpreterOutput; import org.apache.zeppelin.interpreter.InterpreterSettingManager; import org.apache.zeppelin.notebook.Notebook; @@ -55,15 +65,11 @@ import org.apache.zeppelin.search.LuceneSearch; import org.apache.zeppelin.search.SearchService; import org.apache.zeppelin.socket.NotebookServer; +import org.apache.zeppelin.storage.ConfigStorage; import org.apache.zeppelin.user.Credentials; import org.apache.zeppelin.utils.SecurityUtils; import org.eclipse.jetty.http.HttpVersion; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.SecureRequestCustomizer; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.*; import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.servlet.DefaultServlet; @@ -89,17 +95,36 @@ public class ZeppelinServer extends Application { private final InterpreterSettingManager interpreterSettingManager; private SchedulerFactory schedulerFactory; private InterpreterFactory replFactory; + private ConfigStorage configStorage; private SearchService noteSearchService; private NotebookRepoSync notebookRepo; private NotebookAuthorization notebookAuthorization; private Credentials credentials; - private DependencyResolver depResolver; public ZeppelinServer() throws Exception { ZeppelinConfiguration conf = ZeppelinConfiguration.create(); + 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"); + } + } + } catch (UnavailableSecurityManagerException e) { + LOG.error("Failed to initialise shiro configuraion", e); + } + } - this.depResolver = new DependencyResolver( - conf.getString(ConfVars.ZEPPELIN_INTERPRETER_LOCALREPO)); InterpreterOutput.limit = conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_OUTPUT_LIMIT); @@ -129,13 +154,30 @@ 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.getInstance(); + this.credentials = new Credentials( + conf.credentialsPersist(), + conf.getCredentialsPath(), + conf.getCredentialsEncryptKey()); + notebook = new Notebook(conf, + notebookRepo, schedulerFactory, replFactory, interpreterSettingManager, notebookWsServer, + noteSearchService, notebookAuthorization, credentials); + this.configStorage = ConfigStorage.getInstance(conf); + 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 { @@ -143,21 +185,7 @@ public ZeppelinServer() throws Exception { } catch (Exception e) { 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. @@ -165,11 +193,30 @@ public ZeppelinServer() throws Exception { notebook.addNotebookEventListener(heliumApplicationFactory); notebook.addNotebookEventListener(notebookWsServer.getNotebookInformationListener()); + + // Register MBean + if ("true".equals(System.getenv("ZEPPELIN_JMX_ENABLE"))) { + MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); + try { + mBeanServer.registerMBean( + notebookWsServer, + new ObjectName("org.apache.zeppelin:type=" + NotebookServer.class.getSimpleName())); + mBeanServer.registerMBean( + interpreterSettingManager, + new ObjectName( + "org.apache.zeppelin:type=" + InterpreterSettingManager.class.getSimpleName())); + } catch (InstanceAlreadyExistsException + | MBeanRegistrationException + | MalformedObjectNameException + | NotCompliantMBeanException e) { + LOG.error("Failed to register MBeans", e); + } + } } public static void main(String[] args) throws InterruptedException { - ZeppelinConfiguration conf = ZeppelinConfiguration.create(); + final ZeppelinConfiguration conf = ZeppelinConfiguration.create(); conf.setProperty("args", args); jettyWebServer = setupJettyServer(conf); @@ -206,7 +253,9 @@ public static void main(String[] args) throws InterruptedException { LOG.info("Shutting down Zeppelin Server ... "); try { jettyWebServer.stop(); - notebook.getInterpreterSettingManager().shutdown(); + if (!conf.isRecoveryEnabled()) { + ZeppelinServer.notebook.getInterpreterSettingManager().close(); + } notebook.close(); Thread.sleep(3000); } catch (Exception e) { @@ -229,7 +278,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) { @@ -243,7 +294,6 @@ private static Server setupJettyServer(ZeppelinConfiguration conf) { httpConfig.setSecureScheme("https"); httpConfig.setSecurePort(conf.getServerSslPort()); httpConfig.setOutputBufferSize(32768); - httpConfig.setRequestHeaderSize(8192); httpConfig.setResponseHeaderSize(8192); httpConfig.setSendServerVersion(true); @@ -262,6 +312,7 @@ private static Server setupJettyServer(ZeppelinConfiguration conf) { connector = new ServerConnector(server); } + configureRequestHeaderSize(conf, connector); // Set some timeout options to make debugging easier. int timeout = 1000 * 30; connector.setIdleTimeout(timeout); @@ -278,6 +329,14 @@ private static Server setupJettyServer(ZeppelinConfiguration conf) { return server; } + private static void configureRequestHeaderSize(ZeppelinConfiguration conf, + ServerConnector connector) { + HttpConnectionFactory cf = (HttpConnectionFactory) + connector.getConnectionFactory(HttpVersion.HTTP_1_1.toString()); + int requestHeaderSize = conf.getJettyRequestHeaderSize(); + cf.getHttpConfiguration().setRequestHeaderSize(requestHeaderSize); + } + private static void setupNotebookServer(WebAppContext webapp, ZeppelinConfiguration conf) { notebookWsServer = new NotebookServer(); 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..bdc3b889a17 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 @@ -29,29 +29,18 @@ import javax.servlet.http.HttpServletRequest; -import com.google.common.base.Strings; -import com.google.common.collect.Sets; 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.AngularObject; -import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.display.AngularObjectRegistryListener; -import org.apache.zeppelin.display.Input; +import org.apache.zeppelin.display.*; 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; -import org.apache.zeppelin.notebook.JobListenerFactory; import org.apache.zeppelin.notebook.Folder; +import org.apache.zeppelin.notebook.JobListenerFactory; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.Notebook; import org.apache.zeppelin.notebook.NotebookAuthorization; @@ -59,8 +48,7 @@ import org.apache.zeppelin.notebook.NotebookImportDeserializer; import org.apache.zeppelin.notebook.Paragraph; import org.apache.zeppelin.notebook.ParagraphJobListener; -import org.apache.zeppelin.notebook.ParagraphRuntimeInfo; -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; @@ -74,6 +62,7 @@ import org.apache.zeppelin.util.WatcherSecurityKey; import org.apache.zeppelin.utils.InterpreterBindingUtils; import org.apache.zeppelin.utils.SecurityUtils; +import org.eclipse.jetty.websocket.api.WebSocketException; import org.eclipse.jetty.websocket.servlet.WebSocketServlet; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.joda.time.DateTime; @@ -83,11 +72,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.Strings; import com.google.common.collect.Queues; +import com.google.common.collect.Sets; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; /** @@ -95,7 +84,7 @@ */ public class NotebookServer extends WebSocketServlet implements NotebookSocketListener, JobListenerFactory, AngularObjectRegistryListener, - RemoteInterpreterProcessListener, ApplicationEventListener { + RemoteInterpreterProcessListener, ApplicationEventListener, NotebookServerMBean { /** * Job manager service type @@ -340,6 +329,9 @@ public void onMessage(NotebookSocket conn, String msg) { case NOTE_REVISION: getNoteByRevision(conn, notebook, messagereceived); break; + case NOTE_REVISION_FOR_COMPARE: + getNoteByRevisionForCompare(conn, notebook, messagereceived); + break; case LIST_NOTE_JOBS: unicastNoteJobInfo(conn, messagereceived); break; @@ -361,11 +353,17 @@ public void onMessage(NotebookSocket conn, String msg) { case WATCHER: switchConnectionToWatcher(conn, messagereceived); break; + case SAVE_NOTE_FORMS: + saveNoteForms(conn, userAndRoles, notebook, messagereceived); + break; + case REMOVE_NOTE_FORMS: + removeNoteForms(conn, userAndRoles, notebook, messagereceived); + break; default: break; } } catch (Exception e) { - LOG.error("Can't handle message", e); + LOG.error("Can't handle message: " + msg, e); } } @@ -463,7 +461,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); @@ -472,6 +471,19 @@ private void broadcastToNoteBindedInterpreter(String interpreterGroupId, Message } } + public void broadcast(Message m) { + synchronized (connectedSockets) { + for (NotebookSocket ns : connectedSockets) { + try { + ns.send(serializeMessage(m)); + } catch (IOException | WebSocketException e) { + LOG.error("Send error: " + m, e); + } + } + } + } + + private void broadcast(String noteId, Message m) { List socketsToBroadcast = Collections.emptyList(); synchronized (noteSocketMap) { @@ -486,7 +498,7 @@ private void broadcast(String noteId, Message m) { for (NotebookSocket conn : socketsToBroadcast) { try { conn.send(serializeMessage(m)); - } catch (IOException e) { + } catch (IOException | WebSocketException e) { LOG.error("socket error", e); } } @@ -510,7 +522,7 @@ private void broadcastExcept(String noteId, Message m, NotebookSocket exclude) { } try { conn.send(serializeMessage(m)); - } catch (IOException e) { + } catch (IOException | WebSocketException e) { LOG.error("socket error", e); } } @@ -530,7 +542,7 @@ private void multicastToUser(String user, Message m) { private void unicast(Message m, NotebookSocket conn) { try { conn.send(serializeMessage(m)); - } catch (IOException e) { + } catch (IOException | WebSocketException e) { LOG.error("socket error", e); } broadcastToWatchers(StringUtils.EMPTY, StringUtils.EMPTY, m); @@ -653,6 +665,8 @@ public void unicastParagraph(Note note, Paragraph p, String user) { } public void broadcastParagraph(Note note, Paragraph p) { + broadcastNoteForms(note); + if (note.isPersonalizedMode()) { broadcastParagraphs(p.getUserParagraphMap(), p); } else { @@ -751,6 +765,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 */ @@ -804,8 +837,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; @@ -849,7 +882,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"); @@ -867,6 +900,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); @@ -930,6 +968,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); @@ -1003,7 +1042,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); } @@ -1020,12 +1059,13 @@ private void createNote(NotebookSocket conn, HashSet userAndRoles, Noteb noteName = "Note " + note.getId(); } note.setName(noteName); + note.setCronSupported(notebook.getConf()); } 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. " @@ -1061,7 +1101,7 @@ private void removeFolder(NotebookSocket conn, HashSet userAndRoles, return; } - List notes = notebook.getNotesUnderFolder(folderId); + List notes = notebook.getNotesUnderFolder(folderId, userAndRoles); for (Note note : notes) { String noteId = note.getId(); @@ -1088,6 +1128,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"); @@ -1112,6 +1159,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"); } @@ -1127,6 +1182,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"); @@ -1146,6 +1208,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); @@ -1186,6 +1257,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")) { @@ -1209,7 +1283,6 @@ private void updateParagraph(NotebookSocket conn, HashSet userAndRoles, p.setText((String) fromMessage.get("paragraph")); } - note.persist(subject); if (note.isPersonalizedMode()) { @@ -1282,9 +1355,10 @@ private void removeParagraph(NotebookSocket conn, HashSet userAndRoles, return; } - /** We dont want to remove the last paragraph */ final Note note = notebook.getNote(noteId); - if (!note.isLastParagraph(paragraphId)) { + + /** Don't allow removing paragraph when there is only one paragraph in the Notebook */ + if (note.getParagraphCount() > 1) { AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal); Paragraph para = note.removeParagraph(subject.getUser(), paragraphId); note.persist(subject); @@ -1332,7 +1406,13 @@ private void completion(NotebookSocket conn, HashSet userAndRoles, Noteb } final Note note = notebook.getNote(getOpenNoteId(conn)); - List candidates = note.completion(paragraphId, buffer, cursor); + List candidates; + try { + candidates = note.completion(paragraphId, buffer, cursor); + } catch (RuntimeException e) { + LOG.info("Fail to get completion", e); + candidates = new ArrayList<>(); + } resp.put("completions", candidates); conn.send(serializeMessage(resp)); } @@ -1363,7 +1443,8 @@ private void angularObjectUpdated(NotebookSocket conn, HashSet userAndRo if (setting.getInterpreterGroup(user, note.getId()) == null) { continue; } - if (interpreterGroupId.equals(setting.getInterpreterGroup(user, note.getId()).getId())) { + if (interpreterGroupId.equals(setting.getInterpreterGroup(user, note.getId()) + .getId())) { AngularObjectRegistry angularObjectRegistry = setting.getInterpreterGroup(user, note.getId()).getAngularObjectRegistry(); @@ -1405,7 +1486,8 @@ private void angularObjectUpdated(NotebookSocket conn, HashSet userAndRo if (setting.getInterpreterGroup(user, n.getId()) == null) { continue; } - if (interpreterGroupId.equals(setting.getInterpreterGroup(user, n.getId()).getId())) { + if (interpreterGroupId.equals(setting.getInterpreterGroup(user, n.getId()) + .getId())) { AngularObjectRegistry angularObjectRegistry = setting.getInterpreterGroup(user, n.getId()).getAngularObjectRegistry(); this.broadcastExcept(n.getId(), @@ -1497,7 +1579,7 @@ private InterpreterGroup findInterpreterGroupForParagraph(Note note, String para if (paragraph == null) { throw new IllegalArgumentException("Unknown paragraph with id : " + paragraphId); } - return paragraph.getCurrentRepl().getInterpreterGroup(); + return paragraph.getBindedInterpreter().getInterpreterGroup(); } private void pushAngularObjectToRemoteRegistry(String noteId, String paragraphId, String varName, @@ -1618,7 +1700,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; } @@ -1636,7 +1718,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; } @@ -1660,7 +1742,10 @@ private void runAllParagraphs(NotebookSocket conn, HashSet userAndRoles, Paragraph p = setParagraphUsingMessage(note, fromMessage, paragraphId, text, title, params, config); - persistAndExecuteSingleParagraph(conn, note, p); + if (p.isEnabled() && !persistAndExecuteSingleParagraph(conn, note, p, true)) { + // stop execution when one paragraph fails. + break; + } } } @@ -1729,7 +1814,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; } @@ -1752,14 +1837,14 @@ private void runParagraph(NotebookSocket conn, HashSet userAndRoles, Not Paragraph p = setParagraphUsingMessage(note, fromMessage, paragraphId, text, title, params, config); - persistAndExecuteSingleParagraph(conn, note, p); + persistAndExecuteSingleParagraph(conn, note, p, false); } private void addNewParagraphIfLastParagraphIsExecuted(Note note, Paragraph p) { // if it's the last paragraph and not empty, let's add a new one boolean isTheLastParagraph = note.isLastParagraph(p.getId()); if (!(Strings.isNullOrEmpty(p.getText()) || - p.getText().trim().equals(p.getMagic())) && + Strings.isNullOrEmpty(p.getScriptText())) && isTheLastParagraph) { Paragraph newPara = note.addNewParagraph(p.getAuthenticationInfo()); broadcastNewParagraph(note, newPara); @@ -1774,7 +1859,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. " @@ -1784,15 +1869,16 @@ private boolean persistNoteWithAuthInfo(NotebookSocket conn, } } - private void persistAndExecuteSingleParagraph(NotebookSocket conn, - Note note, Paragraph p) throws IOException { + private boolean persistAndExecuteSingleParagraph(NotebookSocket conn, + Note note, Paragraph p, + boolean blocking) throws IOException { addNewParagraphIfLastParagraphIsExecuted(note, p); if (!persistNoteWithAuthInfo(conn, note, p)) { - return; + return false; } try { - note.run(p.getId()); + return note.run(p.getId(), blocking); } catch (Exception ex) { LOG.error("Exception from run", ex); if (p != null) { @@ -1800,6 +1886,7 @@ private void persistAndExecuteSingleParagraph(NotebookSocket conn, p.setStatus(Status.ERROR); broadcast(note.getId(), new Message(OP.PARAGRAPH).put("paragraph", p)); } + return false; } } @@ -1840,7 +1927,7 @@ public boolean apply(String key) { .getVarName()); } }); - + configurations.put("isRevisionSupported", String.valueOf(notebook.isRevisionSupported())); conn.send(serializeMessage( new Message(OP.CONFIGURATIONS_INFO).put("configurations", configurations))); } @@ -1920,6 +2007,25 @@ private void getNoteByRevision(NotebookSocket conn, Notebook notebook, Message f .put("note", revisionNote))); } + private void getNoteByRevisionForCompare(NotebookSocket conn, Notebook notebook, + Message fromMessage) throws IOException { + String noteId = (String) fromMessage.get("noteId"); + String revisionId = (String) fromMessage.get("revisionId"); + + String position = (String) fromMessage.get("position"); + AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal); + Note revisionNote; + if (revisionId.equals("Head")) { + revisionNote = notebook.getNote(noteId); + } else { + revisionNote = notebook.getNoteByRevision(noteId, revisionId, subject); + } + + conn.send(serializeMessage( + new Message(OP.NOTE_REVISION_FOR_COMPARE).put("noteId", noteId) + .put("revisionId", revisionId).put("position", position).put("note", revisionNote))); + } + /** * This callback is for the paragraph that runs on ZeppelinServer * @@ -2057,14 +2163,14 @@ 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)); } AuthenticationInfo subject = new AuthenticationInfo(SecurityUtils.getPrincipal()); paragraph.setAuthenticationInfo(subject); - noteIns.run(paragraphId); + noteIns.run(paragraphId, false); } catch (Exception e) { throw e; @@ -2279,6 +2385,9 @@ private void sendAllAngularObjects(Note note, String user, NotebookSocket conn) } for (InterpreterSetting intpSetting : settings) { + if (intpSetting.getInterpreterGroup(user, note.getId()) == null) { + continue; + } AngularObjectRegistry registry = intpSetting.getInterpreterGroup(user, note.getId()).getAngularObjectRegistry(); List objects = registry.getAllWithGlobal(note.getId()); @@ -2332,7 +2441,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(), @@ -2352,10 +2461,17 @@ private void getEditorSetting(NotebookSocket conn, Message fromMessage) throws I Message resp = new Message(OP.EDITOR_SETTING); resp.put("paragraphId", paragraphId); Interpreter interpreter = - notebook().getInterpreterFactory().getInterpreter(user, noteId, replName); - resp.put("editor", notebook().getInterpreterSettingManager(). - getEditorSetting(interpreter, user, noteId, replName)); - conn.send(serializeMessage(resp)); + null; + try { + interpreter = notebook().getInterpreterFactory().getInterpreter(user, noteId, replName); + LOG.debug("getEditorSetting for interpreter: {} for paragraph {}", replName, paragraphId); + resp.put("editor", notebook().getInterpreterSettingManager(). + getEditorSetting(interpreter, user, noteId, replName)); + conn.send(serializeMessage(resp)); + } catch (InterpreterNotFoundException e) { + LOG.warn("Fail to get interpreter: " + replName); + return; + } } private void getInterpreterSettings(NotebookSocket conn, AuthenticationInfo subject) @@ -2414,7 +2530,7 @@ private void broadcastToAllConnectionsExcept(NotebookSocket exclude, String seri try { conn.send(serialized); - } catch (IOException e) { + } catch (IOException | WebSocketException e) { LOG.error("Cannot broadcast message to watcher", e); } } @@ -2426,9 +2542,12 @@ private void broadcastToWatchers(String noteId, String subject, Message message) for (NotebookSocket watcher : watcherSockets) { try { watcher.send( - WatcherMessage.builder(noteId).subject(subject).message(serializeMessage(message)) - .build().toJson()); - } catch (IOException e) { + WatcherMessage.builder(noteId) + .subject(subject) + .message(serializeMessage(message)) + .build() + .toJson()); + } catch (IOException | WebSocketException e) { LOG.error("Cannot broadcast message to watcher", e); } } @@ -2482,4 +2601,69 @@ public void clearParagraphRuntimeInfo(InterpreterSetting setting) { } setting.clearNoteIdAndParaMap(); } + + public void broadcastNoteForms(Note note) { + GUI formsSettings = new GUI(); + formsSettings.setForms(note.getNoteForms()); + formsSettings.setParams(note.getNoteParams()); + + broadcast(note.getId(), new Message(OP.SAVE_NOTE_FORMS).put("formsData", formsSettings)); + } + + private void saveNoteForms(NotebookSocket conn, HashSet userAndRoles, Notebook notebook, + Message fromMessage) throws IOException { + String noteId = (String) fromMessage.get("noteId"); + Map noteParams = (Map) fromMessage.get("noteParams"); + + if (!hasParagraphWriterPermission(conn, notebook, noteId, + userAndRoles, fromMessage.principal, "update")) { + return; + } + + Note note = notebook.getNote(noteId); + if (note != null) { + note.setNoteParams(noteParams); + + AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal); + note.persist(subject); + broadcastNoteForms(note); + } + } + + private void removeNoteForms(NotebookSocket conn, HashSet userAndRoles, Notebook notebook, + Message fromMessage) throws IOException { + String noteId = (String) fromMessage.get("noteId"); + String formName = (String) fromMessage.get("formName"); + + if (!hasParagraphWriterPermission(conn, notebook, noteId, + userAndRoles, fromMessage.principal, "update")) { + return; + } + + Note note = notebook.getNote(noteId); + if (note != null) { + note.getNoteForms().remove(formName); + note.getNoteParams().remove(formName); + + AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal); + note.persist(subject); + broadcastNoteForms(note); + } + } + + @Override + public Set getConnectedUsers() { + Set connectionList = Sets.newHashSet(); + for (NotebookSocket notebookSocket : connectedSockets) { + connectionList.add(notebookSocket.getUser()); + } + return connectionList; + } + + @Override + public void sendMessage(String message) { + Message m = new Message(OP.NOTICE); + m.data.put("notice", message); + broadcast(m); + } } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServerMBean.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServerMBean.java new file mode 100644 index 00000000000..29a3bf908ed --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServerMBean.java @@ -0,0 +1,29 @@ +/* + * 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.socket; + +import java.util.Set; + +/** + * MBean for NotebookServer + */ +public interface NotebookServerMBean { + Set getConnectedUsers(); + + void sendMessage(String message); +} 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..ed63d8991ff 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,7 +44,7 @@ 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; } } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java b/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java index b2029ecf6fd..b7ce42b5e1d 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java @@ -20,23 +20,28 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; +import java.security.Principal; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Map; +import javax.naming.NamingException; + +import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.realm.text.IniRealm; +import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.realm.ActiveDirectoryGroupRealm; import org.apache.zeppelin.realm.LdapRealm; -import org.mortbay.log.Log; +import org.apache.zeppelin.server.ZeppelinServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,7 +56,7 @@ public class SecurityUtils { private static final HashSet EMPTY_HASHSET = Sets.newHashSet(); private static boolean isEnabled = false; private static final Logger log = LoggerFactory.getLogger(SecurityUtils.class); - + public static void setIsEnabled(boolean value) { isEnabled = value; } @@ -88,13 +93,29 @@ public static String getPrincipal() { String principal; if (subject.isAuthenticated()) { - principal = subject.getPrincipal().toString(); + principal = extractPrincipal(subject); + if (ZeppelinServer.notebook.getConf().isUsernameForceLowerCase()) { + log.debug("Converting principal name " + principal + + " to lower case:" + principal.toLowerCase()); + principal = principal.toLowerCase(); + } } else { principal = ANONYMOUS; } return principal; } + private static String extractPrincipal(Subject subject) { + String principal; + Object principalObject = subject.getPrincipal(); + if (principalObject instanceof Principal) { + principal = ((Principal) principalObject).getName(); + } else { + principal = String.valueOf(principalObject); + } + return principal; + } + public static Collection getRealmsList() { if (!isEnabled) { return Collections.emptyList(); @@ -129,7 +150,17 @@ public static HashSet getRoles() { allRoles = ((IniRealm) realm).getIni().get("roles"); break; } else if (name.equals("org.apache.zeppelin.realm.LdapRealm")) { - allRoles = ((LdapRealm) realm).getListRoles(); + try { + AuthorizationInfo auth = ((LdapRealm) realm).queryForAuthorizationInfo( + new SimplePrincipalCollection(subject.getPrincipal(), realm.getName()), + ((LdapRealm) realm).getContextFactory() + ); + if (auth != null) { + roles = new HashSet<>(auth.getRoles()); + } + } catch (NamingException e) { + log.error("Can't fetch roles", e); + } break; } else if (name.equals("org.apache.zeppelin.realm.ActiveDirectoryGroupRealm")) { allRoles = ((ActiveDirectoryGroupRealm) realm).getListRoles(); diff --git a/zeppelin-server/src/test/java/com/webautomation/ScreenCaptureHtmlUnitDriver.java b/zeppelin-server/src/test/java/com/webautomation/ScreenCaptureHtmlUnitDriver.java deleted file mode 100644 index adad8075e21..00000000000 --- a/zeppelin-server/src/test/java/com/webautomation/ScreenCaptureHtmlUnitDriver.java +++ /dev/null @@ -1,224 +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 com.webautomation; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.net.URL; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.OutputType; -import org.openqa.selenium.TakesScreenshot; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.htmlunit.HtmlUnitDriver; -import org.openqa.selenium.internal.Base64Encoder; -import org.openqa.selenium.remote.CapabilityType; -import org.openqa.selenium.remote.DesiredCapabilities; - -import com.gargoylesoftware.htmlunit.BrowserVersion; -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.WebRequest; -import com.gargoylesoftware.htmlunit.WebWindow; -import com.gargoylesoftware.htmlunit.html.HtmlElement; -import com.gargoylesoftware.htmlunit.html.HtmlPage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * from https://code.google.com/p/selenium/issues/detail?id=1361 - */ -public class ScreenCaptureHtmlUnitDriver extends HtmlUnitDriver implements TakesScreenshot { - - private static Map imagesCache = Collections.synchronizedMap(new HashMap()); - - private static Map cssjsCache = Collections.synchronizedMap(new HashMap()); - - // http://stackoverflow.com/questions/4652777/java-regex-to-get-the-urls-from-css - private final static Pattern cssUrlPattern = Pattern.compile("background(-image)?[\\s]*:[^url]*url[\\s]*\\([\\s]*([^\\)]*)[\\s]*\\)[\\s]*");// ? - - static Logger LOGGER = LoggerFactory.getLogger(ScreenCaptureHtmlUnitDriver.class); - - public ScreenCaptureHtmlUnitDriver() { - super(); - } - - public ScreenCaptureHtmlUnitDriver(boolean enableJavascript) { - super(enableJavascript); - } - - public ScreenCaptureHtmlUnitDriver(Capabilities capabilities) { - super(capabilities); - } - - public ScreenCaptureHtmlUnitDriver(BrowserVersion version) { - super(version); - DesiredCapabilities var = ((DesiredCapabilities) getCapabilities()); - var.setCapability(CapabilityType.TAKES_SCREENSHOT, true); - } - - @Override - @SuppressWarnings("unchecked") - public X getScreenshotAs(OutputType target) throws WebDriverException { - byte[] archive = new byte[0]; - try { - archive = downloadCssAndImages(getWebClient(), (HtmlPage) getCurrentWindow().getEnclosedPage()); - } catch (Exception e) { - LOGGER.error("Exception in ScreenCaptureHtmlUnitDriver while getScreenshotAs ", e); - } - if(target.equals(OutputType.BASE64)){ - return target.convertFromBase64Png(new Base64Encoder().encode(archive)); - } - if(target.equals(OutputType.FILE)){ - File f = new File("screen.tmp"); - try { - FileOutputStream scr = new FileOutputStream(f); - scr.write(archive); - scr.close(); - } catch (IOException e) { - throw new WebDriverException(e); - } - return (X) f; - } - return (X) archive; - } - - // http://stackoverflow.com/questions/2244272/how-can-i-tell-htmlunits-webclient-to-download-images-and-css - protected byte[] downloadCssAndImages(WebClient webClient, HtmlPage page) throws Exception { - WebWindow currentWindow = webClient.getCurrentWindow(); - Map urlMapping = new HashMap<>(); - Map files = new HashMap<>(); - WebWindow window = null; - try { - window = webClient.getWebWindowByName(page.getUrl().toString()+"_screenshot"); - webClient.getPage(window, new WebRequest(page.getUrl())); - } catch (Exception e) { - LOGGER.error("Exception in ScreenCaptureHtmlUnitDriver while downloadCssAndImages ", e); - window = webClient.openWindow(page.getUrl(), page.getUrl().toString()+"_screenshot"); - } - - String xPathExpression = "//*[name() = 'img' or name() = 'link' and (@type = 'text/css' or @type = 'image/x-icon') or @type = 'text/javascript']"; - List resultList = page.getByXPath(xPathExpression); - - Iterator i = resultList.iterator(); - while (i.hasNext()) { - try { - HtmlElement el = (HtmlElement) i.next(); - String resourceSourcePath = el.getAttribute("src").equals("") ? el.getAttribute("href") : el - .getAttribute("src"); - if (resourceSourcePath == null || resourceSourcePath.equals("")) - continue; - URL resourceRemoteLink = page.getFullyQualifiedUrl(resourceSourcePath); - String resourceLocalPath = mapLocalUrl(page, resourceRemoteLink, resourceSourcePath, urlMapping); - urlMapping.put(resourceSourcePath, resourceLocalPath); - if (!resourceRemoteLink.toString().endsWith(".css")) { - byte[] image = downloadImage(webClient, window, resourceRemoteLink); - files.put(resourceLocalPath, image); - } else { - String css = downloadCss(webClient, window, resourceRemoteLink); - for (String cssImagePath : getLinksFromCss(css)) { - URL cssImagelink = page.getFullyQualifiedUrl(cssImagePath.replace("\"", "").replace("\'", "") - .replace(" ", "")); - String cssImageLocalPath = mapLocalUrl(page, cssImagelink, cssImagePath, urlMapping); - files.put(cssImageLocalPath, downloadImage(webClient, window, cssImagelink)); - } - files.put(resourceLocalPath, replaceRemoteUrlsWithLocal(css, urlMapping) - .replace("resources/", "./").getBytes()); - } - } catch (Exception e) { - LOGGER.error("Exception in ScreenCaptureHtmlUnitDriver while resultList.iterator ", e); - } - } - String pagesrc = replaceRemoteUrlsWithLocal(page.getWebResponse().getContentAsString(), urlMapping); - files.put("page.html", pagesrc.getBytes()); - webClient.setCurrentWindow(currentWindow); - return createZip(files); - } - - String downloadCss(WebClient webClient, WebWindow window, URL resourceUrl) throws Exception { - if (cssjsCache.get(resourceUrl.toString()) == null) { - cssjsCache.put(resourceUrl.toString(), webClient.getPage(window, new WebRequest(resourceUrl)) - .getWebResponse().getContentAsString()); - - } - return cssjsCache.get(resourceUrl.toString()); - } - - byte[] downloadImage(WebClient webClient, WebWindow window, URL resourceUrl) throws Exception { - if (imagesCache.get(resourceUrl.toString()) == null) { - imagesCache.put( - resourceUrl.toString(), - IOUtils.toByteArray(webClient.getPage(window, new WebRequest(resourceUrl)).getWebResponse() - .getContentAsStream())); - } - return imagesCache.get(resourceUrl.toString()); - } - - public static byte[] createZip(Map files) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ZipOutputStream zipfile = new ZipOutputStream(bos); - Iterator i = files.keySet().iterator(); - String fileName = null; - ZipEntry zipentry = null; - while (i.hasNext()) { - fileName = i.next(); - zipentry = new ZipEntry(fileName); - zipfile.putNextEntry(zipentry); - zipfile.write(files.get(fileName)); - } - zipfile.close(); - return bos.toByteArray(); - } - - List getLinksFromCss(String css) { - List result = new LinkedList<>(); - Matcher m = cssUrlPattern.matcher(css); - while (m.find()) { // find next match - result.add( m.group(2)); - } - return result; - } - - String replaceRemoteUrlsWithLocal(String source, Map replacement) { - for (String object : replacement.keySet()) { - // background:url(http://org.com/images/image.gif) - source = source.replace(object, replacement.get(object)); - } - return source; - } - - String mapLocalUrl(HtmlPage page, URL link, String path, Map replacementToAdd) throws Exception { - String resultingFileName = "resources/" + FilenameUtils.getName(link.getFile()); - replacementToAdd.put(path, resultingFileName); - return resultingFileName; - } - -} \ No newline at end of file diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/ZeppelinITUtils.java b/zeppelin-server/src/test/java/org/apache/zeppelin/ZeppelinITUtils.java index 402a18d4270..74ae4954d9d 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/ZeppelinITUtils.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/ZeppelinITUtils.java @@ -41,20 +41,4 @@ public static void sleep(long millis, boolean logOutput) { LOG.info("Finished."); } } - - public static void restartZeppelin() { - CommandExecutor.executeCommandLocalHost("../bin/zeppelin-daemon.sh restart", - false, ProcessData.Types_Of_Data.OUTPUT); - //wait for server to start. - sleep(5000, false); - } - - public static void turnOffImplicitWaits(WebDriver driver) { - driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS); - } - - public static void turnOnImplicitWaits(WebDriver driver) { - driver.manage().timeouts().implicitlyWait(AbstractZeppelinIT.MAX_IMPLICIT_WAIT, - TimeUnit.SECONDS); - } } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/configuration/RequestHeaderSizeTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/configuration/RequestHeaderSizeTest.java new file mode 100644 index 00000000000..55e0d178ddd --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/configuration/RequestHeaderSizeTest.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.configuration; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.lang.RandomStringUtils; +import org.eclipse.jetty.http.HttpStatus; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.rest.AbstractTestRestApi; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class RequestHeaderSizeTest extends AbstractTestRestApi { + private static final int REQUEST_HEADER_MAX_SIZE = 20000; + + @Before + public void startZeppelin() throws Exception { + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_SERVER_JETTY_REQUEST_HEADER_SIZE.getVarName(), String.valueOf(REQUEST_HEADER_MAX_SIZE)); + startUp(RequestHeaderSizeTest.class.getSimpleName()); + } + + @After + public void stopZeppelin() throws Exception { + shutDown(); + } + + + @Test + public void increased_request_header_size_do_not_cause_431_when_request_size_is_over_8K() throws Exception { + HttpClient httpClient = new HttpClient(); + + GetMethod getMethod = new GetMethod(getUrlToTest() + "/version"); + String headerValue = RandomStringUtils.randomAlphanumeric(REQUEST_HEADER_MAX_SIZE - 2000); + getMethod.setRequestHeader("not_too_large_header", headerValue); + int httpCode = httpClient.executeMethod(getMethod); + assertThat(httpCode, is(HttpStatus.OK_200)); + + + getMethod = new GetMethod(getUrlToTest() + "/version"); + headerValue = RandomStringUtils.randomAlphanumeric(REQUEST_HEADER_MAX_SIZE + 2000); + getMethod.setRequestHeader("too_large_header", headerValue); + httpCode = httpClient.executeMethod(getMethod); + assertThat(httpCode, is(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431)); + } + + +} diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/realm/LdapRealmTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/realm/LdapRealmTest.java new file mode 100644 index 00000000000..9070c5f4e33 --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/realm/LdapRealmTest.java @@ -0,0 +1,176 @@ +/* + * 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; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapContext; + +import org.apache.shiro.realm.ldap.LdapContextFactory; +import org.apache.shiro.session.Session; +import org.apache.shiro.subject.SimplePrincipalCollection; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + + +public class LdapRealmTest { + + @Test + public void testGetUserDn() { + LdapRealm realm = new LdapRealm(); + + // without a user search filter + realm.setUserSearchFilter(null); + assertEquals( + "foo ", + realm.getUserDn("foo ") + ); + + // with a user search filter + realm.setUserSearchFilter("memberUid={0}"); + assertEquals( + "foo", + realm.getUserDn("foo") + ); + } + + @Test + public void testExpandTemplate() { + assertEquals( + "uid=foo,cn=users,dc=ods,dc=foo", + LdapRealm.expandTemplate("uid={0},cn=users,dc=ods,dc=foo", "foo") + ); + } + + @Test + public void getUserDnForSearch() { + LdapRealm realm = new LdapRealm(); + + realm.setUserSearchAttributeName("uid"); + assertEquals( + "foo", + realm.getUserDnForSearch("foo") + ); + + // using a template + realm.setUserSearchAttributeName(null); + realm.setMemberAttributeValueTemplate("cn={0},ou=people,dc=hadoop,dc=apache"); + assertEquals( + "cn=foo,ou=people,dc=hadoop,dc=apache", + realm.getUserDnForSearch("foo") + ); + } + + @Test + public void testRolesFor() throws NamingException { + LdapRealm realm = new LdapRealm(); + realm.setGroupSearchBase("cn=groups,dc=apache"); + realm.setGroupObjectClass("posixGroup"); + realm.setMemberAttributeValueTemplate("cn={0},ou=people,dc=apache"); + HashMap rolesByGroups = new HashMap<>(); + rolesByGroups.put("group-three", "zeppelin-role"); + realm.setRolesByGroup(rolesByGroups); + + LdapContextFactory ldapContextFactory = mock(LdapContextFactory.class); + LdapContext ldapCtx = mock(LdapContext.class); + Session session = mock(Session.class); + + + // expected search results + BasicAttributes group1 = new BasicAttributes(); + group1.put(realm.getGroupIdAttribute(), "group-one"); + group1.put(realm.getMemberAttribute(), "principal"); + + // user doesn't belong to this group + BasicAttributes group2 = new BasicAttributes(); + group2.put(realm.getGroupIdAttribute(), "group-two"); + group2.put(realm.getMemberAttribute(), "someoneelse"); + + // mapped to a different Zeppelin role + BasicAttributes group3 = new BasicAttributes(); + group3.put(realm.getGroupIdAttribute(), "group-three"); + group3.put(realm.getMemberAttribute(), "principal"); + + NamingEnumeration results = enumerationOf(group1, group2, group3); + when(ldapCtx.search(any(String.class), any(String.class), any(SearchControls.class))).thenReturn(results); + + + Set roles = realm.rolesFor( + new SimplePrincipalCollection("principal", "ldapRealm"), + "principal", + ldapCtx, + ldapContextFactory, + session + ); + + verify(ldapCtx).search( + "cn=groups,dc=apache", + "(objectclass=posixGroup)", + realm.getGroupSearchControls() + ); + + assertEquals( + new HashSet(Arrays.asList("group-one", "zeppelin-role")), + roles + ); + } + + private NamingEnumeration enumerationOf(BasicAttributes... attrs) { + final Iterator iterator = Arrays.asList(attrs).iterator(); + return new NamingEnumeration() { + @Override + public SearchResult next() throws NamingException { + return nextElement(); + } + + @Override + public boolean hasMore() throws NamingException { + return iterator.hasNext(); + } + + @Override + public void close() throws NamingException { + } + + @Override + public boolean hasMoreElements() { + return iterator.hasNext(); + } + + @Override + public SearchResult nextElement() { + final BasicAttributes attrs = iterator.next(); + return new SearchResult(null, null, attrs); + } + }; + } +} 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 ae0911cc424..dfb5ac28ea2 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); @@ -65,30 +63,73 @@ public abstract class AbstractTestRestApi { static final String restApiUrl = "/api"; static final String url = getUrlToTest(); protected static final boolean wasRunning = checkIfServerIsRunning(); - static boolean pySpark = false; - static boolean sparkR = false; static boolean isRunningWithAuth = false; private static File shiroIni = null; private static String zeppelinShiro = "[users]\n" + - "admin = password1, admin\n" + - "user1 = password2, role1, role2\n" + - "user2 = password3, role3\n" + - "user3 = password4, role2\n" + - "[main]\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" + - "role1 = *\n" + - "role2 = *\n" + - "role3 = *\n" + - "admin = *\n" + - "[urls]\n" + - "/api/version = anon\n" + - "/** = authc"; + "admin = password1, admin\n" + + "user1 = password2, role1, role2\n" + + "user2 = password3, role3\n" + + "user3 = password4, role2\n" + + "[main]\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" + + "role1 = *\n" + + "role2 = *\n" + + "role3 = *\n" + + "admin = *\n" + + "[urls]\n" + + "/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; private String getUrl(String path) { String url; @@ -116,7 +157,7 @@ protected static String getUrlToTest() { @Override public void run() { try { - ZeppelinServer.main(new String[] {""}); + ZeppelinServer.main(new String[]{""}); } catch (Exception e) { LOG.error("Exception in WebDriverManager while getWebDriver ", e); throw new RuntimeException(e); @@ -124,10 +165,17 @@ public void run() { } }; - private static void start(boolean withAuth) throws Exception { + private static void start(boolean withAuth, String testClassName, boolean withKnox) throws Exception { if (!wasRunning) { - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName(), "../"); - System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_WAR.getVarName(), "../zeppelin-web/dist"); + // copy the resources files to a temp folder + zeppelinHome = new File(".."); + LOG.info("ZEPPELIN_HOME: " + zeppelinHome.getAbsolutePath()); + confDir = new File(zeppelinHome, "conf_" + testClassName); + confDir.mkdirs(); + + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName(), zeppelinHome.getAbsolutePath()); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_WAR.getVarName(), new File("../zeppelin-web/dist").getAbsolutePath()); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONF_DIR.getVarName(), confDir.getAbsolutePath()); // some test profile does not build zeppelin-web. // to prevent zeppelin starting up fail, create zeppelin-web/dist directory @@ -140,13 +188,24 @@ private static void start(boolean withAuth) throws Exception { isRunningWithAuth = true; // Set Anonymous session to false. System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_ANONYMOUS_ALLOWED.getVarName(), "false"); - + // Create a shiro env test. - shiroIni = new File("../conf/shiro.ini"); + shiroIni = new File(confDir, "shiro.ini"); 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 @@ -185,69 +244,20 @@ private static void start(boolean withAuth) throws Exception { throw new RuntimeException("Can not start Zeppelin server"); } LOG.info("Test Zeppelin stared."); + } + } - // assume first one is spark - InterpreterSetting sparkIntpSetting = null; - for(InterpreterSetting intpSetting : - ZeppelinServer.notebook.getInterpreterSettingManager().get()) { - if (intpSetting.getName().equals("spark")) { - sparkIntpSetting = intpSetting; - } - } - - Map sparkProperties = - (Map) sparkIntpSetting.getProperties(); - // ci environment runs spark cluster for testing - // so configure zeppelin use spark cluster - if ("true".equals(System.getenv("CI"))) { - // set spark master and other properties - sparkProperties.put("master", - new InterpreterProperty("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())); - - sparkIntpSetting.setProperties(sparkProperties); - pySpark = true; - sparkR = true; - ZeppelinServer.notebook.getInterpreterSettingManager().restart(sparkIntpSetting.getId()); - } else { - String sparkHome = getSparkHome(); - if (sparkHome != null) { - if (System.getenv("SPARK_MASTER") != null) { - sparkProperties.put("master", - new InterpreterProperty("master", System.getenv("SPARK_MASTER"), InterpreterPropertyType.TEXTAREA.getValue())); - } else { - sparkProperties.put("master", - new InterpreterProperty("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())); - pySpark = true; - sparkR = true; - } - - ZeppelinServer.notebook.getInterpreterSettingManager().restart(sparkIntpSetting.getId()); - } - } + protected static void startUpWithKnoxEnable(String testClassName) throws Exception { + start(true, testClassName, true); } - protected static void startUpWithAuthenticationEnable() throws Exception { - start(true); + protected static void startUpWithAuthenticationEnable(String testClassName) throws Exception { + start(true, testClassName, false); } - protected static void startUp() throws Exception { - start(false); + protected static void startUp(String testClassName) throws Exception { + start(false, testClassName, false); } private static String getHostname() { @@ -259,55 +269,18 @@ private static String getHostname() { } } - private static String getSparkHome() { - String sparkHome = System.getenv("SPARK_HOME"); - if (sparkHome != null) { - return sparkHome; - } - sparkHome = getSparkHomeRecursively(new File(System.getProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName()))); - System.out.println("SPARK HOME detected " + sparkHome); - return sparkHome; - } - - boolean isPyspark() { - return pySpark; - } - - boolean isSparkR() { - return sparkR; - } - - private static String getSparkHomeRecursively(File dir) { - if (dir == null) return null; - File files [] = dir.listFiles(); - if (files == null) return null; - - File homeDetected = null; - for (File f : files) { - if (isActiveSparkHome(f)) { - homeDetected = f; - break; - } - } - - if (homeDetected != null) { - return homeDetected.getAbsolutePath(); - } else { - return getSparkHomeRecursively(dir.getParentFile()); - } - } - - private static boolean isActiveSparkHome(File dir) { - return dir.getName().matches("spark-[0-9\\.]+[A-Za-z-]*-bin-hadoop[0-9\\.]+"); + protected static void shutDown() throws Exception { + shutDown(true); } - protected static void shutDown() throws Exception { - if (!wasRunning) { + protected static void shutDown(final boolean deleteConfDir) throws Exception { + if (!wasRunning && ZeppelinServer.notebook != null) { // 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(); + if (!ZeppelinServer.notebook.getConf().isRecoveryEnabled()) { + for (InterpreterSetting setting : settingList) { + ZeppelinServer.notebook.getInterpreterSettingManager().restart(setting.getId()); + } } if (shiroIni != null) { FileUtils.deleteQuietly(shiroIni); @@ -337,6 +310,13 @@ protected static void shutDown() throws Exception { System .clearProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_ANONYMOUS_ALLOWED.getVarName()); } + + if (deleteConfDir && !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); + } } } @@ -362,6 +342,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); @@ -369,6 +353,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/ConfigurationsRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ConfigurationsRestApiTest.java index 1c1ebacd249..f85d5190a09 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ConfigurationsRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ConfigurationsRestApiTest.java @@ -36,7 +36,7 @@ public class ConfigurationsRestApiTest extends AbstractTestRestApi { @BeforeClass public static void init() throws Exception { - AbstractTestRestApi.startUp(); + AbstractTestRestApi.startUp(ConfigurationsRestApi.class.getSimpleName()); } @AfterClass diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/CredentialsRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/CredentialsRestApiTest.java index 29c2914991d..17373244d9f 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/CredentialsRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/CredentialsRestApiTest.java @@ -43,7 +43,7 @@ public class CredentialsRestApiTest extends AbstractTestRestApi { @BeforeClass public static void init() throws Exception { - AbstractTestRestApi.startUp(); + AbstractTestRestApi.startUp(CredentialsRestApiTest.class.getSimpleName()); } @AfterClass diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/HeliumRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/HeliumRestApiTest.java index f63f207dfa8..7d29dcb26fc 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/HeliumRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/HeliumRestApiTest.java @@ -39,7 +39,7 @@ public class HeliumRestApiTest extends AbstractTestRestApi { @BeforeClass public static void init() throws Exception { - AbstractTestRestApi.startUp(); + AbstractTestRestApi.startUp(HeliumRestApi.class.getSimpleName()); } @AfterClass 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..7de4dc6c5a8 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 @@ -59,7 +59,7 @@ public class InterpreterRestApiTest extends AbstractTestRestApi { @BeforeClass public static void init() throws Exception { - AbstractTestRestApi.startUp(); + AbstractTestRestApi.startUp(InterpreterRestApiTest.class.getSimpleName()); } @AfterClass @@ -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/rest/KnoxRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/KnoxRestApiTest.java new file mode 100644 index 00000000000..4abe3a54986 --- /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-server/src/test/java/org/apache/zeppelin/rest/NotebookRepoRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRepoRestApiTest.java index 307339d7697..b852d6ef135 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRepoRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRepoRestApiTest.java @@ -50,7 +50,7 @@ public class NotebookRepoRestApiTest extends AbstractTestRestApi { @BeforeClass public static void init() throws Exception { - AbstractTestRestApi.startUp(); + AbstractTestRestApi.startUp(NotebookRepoRestApiTest.class.getSimpleName()); } @AfterClass diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRestApiTest.java index ac8a27fd06a..d5c29c9934c 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRestApiTest.java @@ -19,6 +19,7 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; + import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.PutMethod; @@ -55,7 +56,7 @@ public class NotebookRestApiTest extends AbstractTestRestApi { @BeforeClass public static void init() throws Exception { - AbstractTestRestApi.startUp(); + startUp(NotebookRestApiTest.class.getSimpleName()); } @AfterClass @@ -120,6 +121,68 @@ public void testRunParagraphJob() throws IOException { ZeppelinServer.notebook.removeNote(note1.getId(), anonymous); } + @Test + public void testRunAllParagraph_AllSuccess() throws IOException { + Note note1 = ZeppelinServer.notebook.createNote(anonymous); + // 2 paragraphs + // P1: + // %python + // import time + // time.sleep(1) + // user='abc' + // P2: + // %python + // from __future__ import print_function + // print(user) + // + Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); + Paragraph p2 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); + p1.setText("%python import time\ntime.sleep(1)\nuser='abc'"); + p2.setText("%python from __future__ import print_function\nprint(user)"); + + 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()); + assertEquals(Job.Status.FINISHED, p2.getStatus()); + assertEquals("abc\n", p2.getResult().message().get(0).getData()); + } + + @Test + public void testRunAllParagraph_FirstFailed() throws IOException { + Note note1 = ZeppelinServer.notebook.createNote(anonymous); + // 2 paragraphs + // P1: + // %python + // import time + // time.sleep(1) + // from __future__ import print_function + // print(user) + // P2: + // %python + // user='abc' + // + Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); + Paragraph p2 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); + p1.setText("%python import time\ntime.sleep(1)\nfrom __future__ import print_function\nprint(user2)"); + p2.setText("%python user2='abc'\nprint(user2)"); + + 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.ERROR, p1.getStatus()); + // p2 will be skipped because p1 is failed. + assertEquals(Job.Status.READY, p2.getStatus()); + } + @Test public void testCloneNote() throws IOException { Note note1 = ZeppelinServer.notebook.createNote(anonymous); @@ -137,7 +200,7 @@ public void testCloneNote() throws IOException { }.getType()); Map resp2Body = (Map) resp2.get("body"); - assertEquals((String)resp2Body.get("name"), "Note " + clonedNoteId); + assertEquals(resp2Body.get("name"), "Note " + clonedNoteId); get.releaseConnection(); //cleanup @@ -207,4 +270,53 @@ public void testClearAllParagraphOutput() throws IOException { //cleanup ZeppelinServer.notebook.removeNote(note.getId(), anonymous); } + + @Test + public void testRunWithServerRestart() throws Exception { + Note note1 = ZeppelinServer.notebook.createNote(anonymous); + // 2 paragraphs + // P1: + // %python + // import time + // time.sleep(1) + // from __future__ import print_function + // print(user) + // P2: + // %python + // user='abc' + // + Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); + Paragraph p2 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); + p1.setText("%python import time\ntime.sleep(1)\nuser='abc'"); + p2.setText("%python from __future__ import print_function\nprint(user)"); + + PostMethod post1 = httpPost("/notebook/job/" + note1.getId(), ""); + assertThat(post1, isAllowed()); + post1.releaseConnection(); + PutMethod put = httpPut("/notebook/" + note1.getId() + "/clear", ""); + LOG.info("test clear paragraph output response\n" + put.getResponseBodyAsString()); + assertThat(put, isAllowed()); + put.releaseConnection(); + + // restart server (while keeping interpreter configuration) + AbstractTestRestApi.shutDown(false); + startUp(NotebookRestApiTest.class.getSimpleName()); + + note1 = ZeppelinServer.notebook.getNote(note1.getId()); + p1 = note1.getParagraph(p1.getId()); + p2 = note1.getParagraph(p2.getId()); + + PostMethod post2 = httpPost("/notebook/job/" + note1.getId(), ""); + assertThat(post2, isAllowed()); + Map resp = gson.fromJson(post2.getResponseBodyAsString(), + new TypeToken>() {}.getType()); + assertEquals(resp.get("status"), "OK"); + post2.releaseConnection(); + + assertEquals(Job.Status.FINISHED, p1.getStatus()); + assertEquals(Job.Status.FINISHED, p2.getStatus()); + assertNotNull(p2.getResult()); + assertEquals("abc\n", p2.getResult().message().get(0).getData()); + + } } 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..808cfd8cf34 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 @@ -46,7 +46,7 @@ public class NotebookSecurityRestApiTest extends AbstractTestRestApi { @BeforeClass public static void init() throws Exception { - AbstractTestRestApi.startUpWithAuthenticationEnable(); + AbstractTestRestApi.startUpWithAuthenticationEnable(NotebookSecurityRestApiTest.class.getSimpleName()); } @AfterClass @@ -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-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java index bc38f740328..f4eac4b7d67 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java @@ -40,7 +40,7 @@ public class SecurityRestApiTest extends AbstractTestRestApi { @BeforeClass public static void init() throws Exception { - AbstractTestRestApi.startUpWithAuthenticationEnable(); + AbstractTestRestApi.startUpWithAuthenticationEnable(SecurityRestApiTest.class.getSimpleName()); } @AfterClass @@ -85,5 +85,17 @@ public void testGetUserList() throws IOException { notUser.releaseConnection(); } + + @Test + public void testRolesEscaped() throws IOException { + GetMethod get = httpGet("/security/ticket", "admin", "password1"); + Map resp = gson.fromJson(get.getResponseBodyAsString(), + new TypeToken>(){}.getType()); + String roles = (String) ((Map) resp.get("body")).get("roles"); + collector.checkThat("Paramater roles", roles, + CoreMatchers.equalTo("[\"admin\"]")); + get.releaseConnection(); + } + } 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 5093cb838ff..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 @@ -27,7 +27,9 @@ import org.apache.commons.httpclient.methods.DeleteMethod; import org.apache.commons.httpclient.methods.GetMethod; 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; @@ -55,7 +57,7 @@ public class ZeppelinRestApiTest extends AbstractTestRestApi { @BeforeClass public static void init() throws Exception { - AbstractTestRestApi.startUp(); + AbstractTestRestApi.startUp(ZeppelinRestApiTest.class.getSimpleName()); } @AfterClass @@ -374,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(); @@ -393,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()); @@ -402,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); } @@ -440,12 +442,6 @@ public void testGetNoteJob() throws IOException, InterruptedException { String noteId = note.getId(); note.runAll(); - - // wait until paragraph gets started - while (!paragraph.getStatus().isRunning()) { - Thread.sleep(100); - } - // assume that status of the paragraph is running GetMethod get = httpGet("/notebook/job/" + noteId); assertThat("test get note job: ", get, isAllowed()); @@ -493,15 +489,6 @@ public void testRunParagraphWithParams() throws IOException, InterruptedExceptio String noteId = note.getId(); note.runAll(); - // wait until job is finished or timeout. - int timeout = 1; - while (!paragraph.isTerminated()) { - Thread.sleep(1000); - if (timeout++ > 120) { - LOG.info("testRunParagraphWithParams timeout job."); - break; - } - } // Call Run paragraph REST API PostMethod postParagraph = httpPost("/notebook/job/" + noteId + "/" + paragraph.getId(), @@ -528,41 +515,32 @@ 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); - note.runAll(); - // wait until job is finished or timeout. - int timeout = 1; - while (!paragraph.isTerminated()) { - Thread.sleep(1000); - if (timeout++ > 10) { - LOG.info("testNoteJobs timeout job."); - break; - } - } - + note.runAll(AuthenticationInfo.ANONYMOUS, false); + String jsonRequest = "{\"cron\":\"* * * * * ?\" }"; // right cron expression but not exist note. 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()); @@ -570,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); @@ -651,6 +665,43 @@ public void testInsertParagraph() throws IOException { ZeppelinServer.notebook.removeNote(note.getId(), anonymous); } + @Test + public void testUpdateParagraph() throws IOException { + Note note = ZeppelinServer.notebook.createNote(anonymous); + + String jsonRequest = "{\"title\": \"title1\", \"text\": \"text1\"}"; + PostMethod post = httpPost("/notebook/" + note.getId() + "/paragraph", jsonRequest); + Map resp = gson.fromJson(post.getResponseBodyAsString(), new TypeToken>() {}.getType()); + post.releaseConnection(); + + String newParagraphId = (String) resp.get("body"); + Paragraph newParagraph = ZeppelinServer.notebook.getNote(note.getId()).getParagraph(newParagraphId); + + assertEquals("title1", newParagraph.getTitle()); + assertEquals("text1", newParagraph.getText()); + + String updateRequest = "{\"text\": \"updated text\"}"; + PutMethod put = httpPut("/notebook/" + note.getId() + "/paragraph/" + newParagraphId, updateRequest); + assertThat("Test update method:", put, isAllowed()); + put.releaseConnection(); + + Paragraph updatedParagraph = ZeppelinServer.notebook.getNote(note.getId()).getParagraph(newParagraphId); + + assertEquals("title1", updatedParagraph.getTitle()); + assertEquals("updated text", updatedParagraph.getText()); + + String updateBothRequest = "{\"title\": \"updated title\", \"text\" : \"updated text 2\" }"; + PutMethod updatePut = httpPut("/notebook/" + note.getId() + "/paragraph/" + newParagraphId, updateBothRequest); + updatePut.releaseConnection(); + + Paragraph updatedBothParagraph = ZeppelinServer.notebook.getNote(note.getId()).getParagraph(newParagraphId); + + assertEquals("updated title", updatedBothParagraph.getTitle()); + assertEquals("updated text 2", updatedBothParagraph.getText()); + + ZeppelinServer.notebook.removeNote(note.getId(), anonymous); + } + @Test public void testGetParagraph() throws IOException { Note note = ZeppelinServer.notebook.createNote(anonymous); diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinServerTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinServerTest.java new file mode 100644 index 00000000000..76a0758e492 --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinServerTest.java @@ -0,0 +1,23 @@ +/* + * 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; + +public class ZeppelinServerTest extends AbstractTestRestApi { + + +} 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..15f2c4764e5 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 @@ -16,554 +16,558 @@ */ package org.apache.zeppelin.rest; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; - -import java.io.File; -import java.io.IOException; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterProperty; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterSetting; +import org.apache.zeppelin.interpreter.SparkDownloadUtils; import org.apache.zeppelin.notebook.Note; +import org.apache.zeppelin.notebook.Notebook; import org.apache.zeppelin.notebook.Paragraph; import org.apache.zeppelin.scheduler.Job.Status; import org.apache.zeppelin.server.ZeppelinServer; import org.apache.zeppelin.user.AuthenticationInfo; import org.junit.AfterClass; -import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.StringReader; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** * Test against spark cluster. - * Spark cluster is started by CI server using testing/startSparkCluster.sh */ -public class ZeppelinSparkClusterTest extends AbstractTestRestApi { - AuthenticationInfo anonymous; - - @BeforeClass - public static void init() throws Exception { - AbstractTestRestApi.startUp(); - } - - @AfterClass - public static void destroy() throws Exception { - AbstractTestRestApi.shutDown(); - } - - @Before - public void setUp() { - anonymous = new AuthenticationInfo("anonymous"); - } - - private void waitForFinish(Paragraph p) { - while (p.getStatus() != Status.FINISHED - && p.getStatus() != Status.ERROR - && p.getStatus() != Status.ABORT) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - LOG.error("Exception in WebDriverManager while getWebDriver ", e); - } - } - } - - @Test - public void scalaOutputTest() throws IOException { - // create new note - Note note = ZeppelinServer.notebook.createNote(anonymous); - Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - Map config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - p.setText("%spark import java.util.Date\n" + - "import java.net.URL\n" + - "println(\"hello\")\n" - ); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.FINISHED, p.getStatus()); - assertEquals("import java.util.Date\n" + - "import java.net.URL\n" + - "hello\n", p.getResult().message().get(0).getData()); - ZeppelinServer.notebook.removeNote(note.getId(), anonymous); - } - - - - @Test - public void basicRDDTransformationAndActionTest() throws IOException { - // create new note - Note note = ZeppelinServer.notebook.createNote(anonymous); - - // run markdown paragraph, again - Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - Map config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - p.setText("%spark print(sc.parallelize(1 to 10).reduce(_ + _))"); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.FINISHED, p.getStatus()); - assertEquals("55", p.getResult().message().get(0).getData()); - ZeppelinServer.notebook.removeNote(note.getId(), anonymous); - } - - @Test - public void sparkSQLTest() throws IOException { - // create new note - Note note = ZeppelinServer.notebook.createNote(anonymous); - int sparkVersion = getSparkVersionNumber(note); - // DataFrame API is available from spark 1.3 - if (sparkVersion >= 13) { - // test basic dataframe api - Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - Map config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - p.setText("%spark val df=sqlContext.createDataFrame(Seq((\"hello\",20)))\n" + - "df.collect()"); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.FINISHED, p.getStatus()); - assertTrue(p.getResult().message().get(0).getData().contains( - "Array[org.apache.spark.sql.Row] = Array([hello,20])")); - - // test display DataFrame - p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - p.setText("%spark val df=sqlContext.createDataFrame(Seq((\"hello\",20)))\n" + - "z.show(df)"); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.FINISHED, p.getStatus()); - assertEquals(InterpreterResult.Type.TABLE, p.getResult().message().get(1).getType()); - assertEquals("_1\t_2\nhello\t20\n", p.getResult().message().get(1).getData()); - - // test display DataSet - if (sparkVersion >= 20) { - p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - p.setText("%spark val ds=spark.createDataset(Seq((\"hello\",20)))\n" + - "z.show(ds)"); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.FINISHED, p.getStatus()); - 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); - } +public abstract class ZeppelinSparkClusterTest extends AbstractTestRestApi { + private static Logger LOGGER = LoggerFactory.getLogger(ZeppelinSparkClusterTest.class); + + private String sparkVersion; + private AuthenticationInfo anonymous = new AuthenticationInfo("anonymous"); + + public ZeppelinSparkClusterTest(String sparkVersion) throws Exception { + this.sparkVersion = sparkVersion; + LOGGER.info("Testing SparkVersion: " + sparkVersion); + String sparkHome = SparkDownloadUtils.downloadSpark(sparkVersion); + setupSparkInterpreter(sparkHome); + verifySparkVersionNumber(); + } + + public void setupSparkInterpreter(String sparkHome) throws InterpreterException { + InterpreterSetting sparkIntpSetting = ZeppelinServer.notebook.getInterpreterSettingManager() + .getInterpreterSettingByName("spark"); + + Map sparkProperties = + (Map) sparkIntpSetting.getProperties(); + LOG.info("SPARK HOME detected " + sparkHome); + if (System.getenv("SPARK_MASTER") != null) { + sparkProperties.put("master", + new InterpreterProperty("master", System.getenv("SPARK_MASTER"))); + } else { + sparkProperties.put("master", new InterpreterProperty("master", "local[2]")); } - - @Test - public void sparkRTest() throws IOException { - // create new note - Note note = ZeppelinServer.notebook.createNote(anonymous); - int sparkVersion = getSparkVersionNumber(note); - - if (isSparkR() && sparkVersion >= 14) { // sparkr supported from 1.4.0 - // restart spark interpreter - List settings = - ZeppelinServer.notebook.getBindedInterpreterSettings(note.getId()); - - for (InterpreterSetting setting : settings) { - if (setting.getName().equals("spark")) { - ZeppelinServer.notebook.getInterpreterSettingManager().restart(setting.getId()); - break; - } - } - - String sqlContextName = "sqlContext"; - if (sparkVersion >= 20) { - sqlContextName = "spark"; - } - Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - Map config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - p.setText("%r localDF <- data.frame(name=c(\"a\", \"b\", \"c\"), age=c(19, 23, 18))\n" + - "df <- createDataFrame(" + sqlContextName + ", localDF)\n" + - "count(df)" - ); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - System.err.println("sparkRTest=" + p.getResult().message().get(0).getData()); - assertEquals(Status.FINISHED, p.getStatus()); - assertEquals("[1] 3", p.getResult().message().get(0).getData().trim()); + sparkProperties.put("SPARK_HOME", new InterpreterProperty("SPARK_HOME", sparkHome)); + sparkProperties.put("spark.master", new InterpreterProperty("spark.master", "local[2]")); + sparkProperties.put("spark.cores.max", + new InterpreterProperty("spark.cores.max", "2")); + sparkProperties.put("zeppelin.spark.useHiveContext", + new InterpreterProperty("zeppelin.spark.useHiveContext", "false")); + sparkProperties.put("zeppelin.pyspark.useIPython", new InterpreterProperty("zeppelin.pyspark.useIPython", "false")); + sparkProperties.put("zeppelin.spark.useNew", new InterpreterProperty("zeppelin.spark.useNew", "true")); + sparkProperties.put("zeppelin.spark.test", new InterpreterProperty("zeppelin.spark.test", "true")); + + ZeppelinServer.notebook.getInterpreterSettingManager().restart(sparkIntpSetting.getId()); + } + + @BeforeClass + public static void setUp() throws Exception { + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HELIUM_REGISTRY.getVarName(), "helium"); + AbstractTestRestApi.startUp(ZeppelinSparkClusterTest.class.getSimpleName()); + } + + @AfterClass + public static void destroy() throws Exception { + AbstractTestRestApi.shutDown(); + } + + private void waitForFinish(Paragraph p) { + while (p.getStatus() != Status.FINISHED + && p.getStatus() != Status.ERROR + && p.getStatus() != Status.ABORT) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + LOG.error("Exception in WebDriverManager while getWebDriver ", e); } - ZeppelinServer.notebook.removeNote(note.getId(), anonymous); } - - @Test - public void pySparkTest() throws IOException { - // create new note - Note note = ZeppelinServer.notebook.createNote(anonymous); - note.setName("note"); - int sparkVersion = getSparkVersionNumber(note); - - if (isPyspark() && sparkVersion >= 12) { // pyspark supported from 1.2.1 - // run markdown paragraph, again - Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - Map config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - p.setText("%pyspark print(sc.parallelize(range(1, 11)).reduce(lambda a, b: a + b))"); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.FINISHED, p.getStatus()); - assertEquals("55\n", p.getResult().message().get(0).getData()); - if (sparkVersion >= 13) { - // run sqlContext test - p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - p.setText("%pyspark from pyspark.sql import Row\n" + - "df=sqlContext.createDataFrame([Row(id=1, age=20)])\n" + - "df.collect()"); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.FINISHED, p.getStatus()); - assertEquals("[Row(age=20, id=1)]\n", p.getResult().message().get(0).getData()); - - // test display Dataframe - p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - p.setText("%pyspark from pyspark.sql import Row\n" + - "df=sqlContext.createDataFrame([Row(id=1, age=20)])\n" + - "z.show(df)"); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.FINISHED, p.getStatus()); - assertEquals(InterpreterResult.Type.TABLE, p.getResult().message().get(0).getType()); - // TODO (zjffdu), one more \n is appended, need to investigate why. - assertEquals("age\tid\n20\t1\n", p.getResult().message().get(0).getData()); - - // test udf - p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - p.setText("%pyspark sqlContext.udf.register(\"f1\", lambda x: len(x))\n" + - "sqlContext.sql(\"select f1(\\\"abc\\\") as len\").collect()"); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.FINISHED, p.getStatus()); - assertEquals("[Row(len=u'3')]\n", p.getResult().message().get(0).getData()); - - // test exception - p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - /** - %pyspark - a=1 - - print(a2) - */ - p.setText("%pyspark a=1\n\nprint(a2)"); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.ERROR, p.getStatus()); - assertTrue(p.getResult().message().get(0).getData() - .contains("Fail to execute line 3: print(a2)")); - assertTrue(p.getResult().message().get(0).getData() - .contains("name 'a2' is not defined")); - } - if (sparkVersion >= 20) { - // run SparkSession test - p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - p.setText("%pyspark from pyspark.sql import Row\n" + - "df=sqlContext.createDataFrame([Row(id=1, age=20)])\n" + - "df.collect()"); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.FINISHED, p.getStatus()); - assertEquals("[Row(age=20, id=1)]\n", p.getResult().message().get(0).getData()); - - // test udf - p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - // use SQLContext to register UDF but use this UDF through SparkSession - p.setText("%pyspark sqlContext.udf.register(\"f1\", lambda x: len(x))\n" + - "spark.sql(\"select f1(\\\"abc\\\") as len\").collect()"); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.FINISHED, p.getStatus()); - assertEquals("[Row(len=u'3')]\n", p.getResult().message().get(0).getData()); - } - } - ZeppelinServer.notebook.removeNote(note.getId(), anonymous); + } + + @Test + public void scalaOutputTest() throws IOException { + // create new note + Note note = ZeppelinServer.notebook.createNote(anonymous); + Paragraph p = note.addNewParagraph(anonymous); + p.setText("%spark import java.util.Date\n" + + "import java.net.URL\n" + + "println(\"hello\")\n" + ); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + assertEquals("hello\n" + + "import java.util.Date\n" + + "import java.net.URL\n", + p.getResult().message().get(0).getData()); + + p.setText("%spark invalid_code"); + note.run(p.getId(), true); + assertEquals(Status.ERROR, p.getStatus()); + assertTrue(p.getResult().message().get(0).getData().contains("error: ")); + } + + @Test + public void basicRDDTransformationAndActionTest() throws IOException { + Note note = ZeppelinServer.notebook.createNote(anonymous); + Paragraph p = note.addNewParagraph(anonymous); + p.setText("%spark print(sc.parallelize(1 to 10).reduce(_ + _))"); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + assertEquals("55", p.getResult().message().get(0).getData()); + } + + @Test + public void sparkReadJSONTest() throws IOException { + Note note = ZeppelinServer.notebook.createNote(anonymous); + Paragraph p = note.addNewParagraph(anonymous); + File tmpJsonFile = File.createTempFile("test", ".json"); + FileWriter jsonFileWriter = new FileWriter(tmpJsonFile); + IOUtils.copy(new StringReader("{\"metadata\": { \"key\": 84896, \"value\": 54 }}\n"), + jsonFileWriter); + jsonFileWriter.close(); + if (isSpark2()) { + p.setText("%spark spark.read.json(\"file://" + tmpJsonFile.getAbsolutePath() + "\")"); + } else { + p.setText("%spark sqlContext.read.json(\"file://" + tmpJsonFile.getAbsolutePath() + "\")"); } - - @Test - public void pySparkAutoConvertOptionTest() throws IOException { - // create new note - Note note = ZeppelinServer.notebook.createNote(anonymous); - note.setName("note"); - - int sparkVersionNumber = getSparkVersionNumber(note); - - if (isPyspark() && sparkVersionNumber >= 14) { // auto_convert enabled from spark 1.4 - // run markdown paragraph, again - Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - Map config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - - String sqlContextName = "sqlContext"; - if (sparkVersionNumber >= 20) { - sqlContextName = "spark"; - } - - p.setText("%pyspark\nfrom pyspark.sql.functions import *\n" - + "print(" + sqlContextName + ".range(0, 10).withColumn('uniform', rand(seed=10) * 3.14).count())"); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.FINISHED, p.getStatus()); - assertEquals("10\n", p.getResult().message().get(0).getData()); - } - ZeppelinServer.notebook.removeNote(note.getId(), anonymous); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + if (isSpark2()) { + assertTrue(p.getResult().message().get(0).getData().contains( + "org.apache.spark.sql.DataFrame = [metadata: struct]")); + } else { + assertTrue(p.getResult().message().get(0).getData().contains( + "org.apache.spark.sql.DataFrame = [metadata: struct]")); } + } - @Test - public void zRunTest() throws IOException { - // create new note - Note note = ZeppelinServer.notebook.createNote(anonymous); - Paragraph p0 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - Map config0 = p0.getConfig(); - config0.put("enabled", true); - p0.setConfig(config0); - p0.setText("%spark z.run(1)"); - p0.setAuthenticationInfo(anonymous); - Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - Map config1 = p1.getConfig(); - config1.put("enabled", true); - p1.setConfig(config1); - p1.setText("%spark val a=10"); - p1.setAuthenticationInfo(anonymous); - Paragraph p2 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - Map config2 = p2.getConfig(); - config2.put("enabled", true); - p2.setConfig(config2); - p2.setText("%spark print(a)"); - p2.setAuthenticationInfo(anonymous); - - note.run(p0.getId()); - waitForFinish(p0); - assertEquals(Status.FINISHED, p0.getStatus()); - - // z.run is not blocking call. So p1 may not be finished when p0 is done. - waitForFinish(p1); - note.run(p2.getId()); - waitForFinish(p2); - assertEquals(Status.FINISHED, p2.getStatus()); - assertEquals("10", p2.getResult().message().get(0).getData()); - - Paragraph p3 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - Map config3 = p3.getConfig(); - config3.put("enabled", true); - p3.setConfig(config3); - p3.setText("%spark println(new java.util.Date())"); - p3.setAuthenticationInfo(anonymous); - - p0.setText(String.format("%%spark z.runNote(\"%s\")", note.getId())); - note.run(p0.getId()); - waitForFinish(p0); - waitForFinish(p1); - waitForFinish(p2); - waitForFinish(p3); - - assertEquals(Status.FINISHED, p3.getStatus()); - String p3result = p3.getResult().message().get(0).getData(); - assertNotEquals(null, p3result); - assertNotEquals("", p3result); - - p0.setText(String.format("%%spark z.run(\"%s\", \"%s\")", note.getId(), p3.getId())); - p3.setText("%%spark println(\"END\")"); - - note.run(p0.getId()); - waitForFinish(p0); - waitForFinish(p3); - - assertNotEquals(p3result, p3.getResult().message()); - - ZeppelinServer.notebook.removeNote(note.getId(), anonymous); + @Test + public void sparkReadCSVTest() throws IOException { + if (!isSpark2()) { + // csv if not supported in spark 1.x natively + return; } - - @Test - public void pySparkDepLoaderTest() throws IOException { - // create new note - Note note = ZeppelinServer.notebook.createNote(anonymous); - int sparkVersionNumber = getSparkVersionNumber(note); - - if (isPyspark() && sparkVersionNumber >= 14) { - // restart spark interpreter - List settings = - ZeppelinServer.notebook.getBindedInterpreterSettings(note.getId()); - - for (InterpreterSetting setting : settings) { - if (setting.getName().equals("spark")) { - ZeppelinServer.notebook.getInterpreterSettingManager().restart(setting.getId()); - break; - } - } - - // load dep - Paragraph p0 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - Map config = p0.getConfig(); - config.put("enabled", true); - p0.setConfig(config); - p0.setText("%dep z.load(\"com.databricks:spark-csv_2.11:1.2.0\")"); - p0.setAuthenticationInfo(anonymous); - note.run(p0.getId()); - waitForFinish(p0); - assertEquals(Status.FINISHED, p0.getStatus()); - - // write test csv file - File tmpFile = File.createTempFile("test", "csv"); - FileUtils.write(tmpFile, "a,b\n1,2"); - - // load data using libraries from dep loader - Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - p1.setConfig(config); - - String sqlContextName = "sqlContext"; - if (sparkVersionNumber >= 20) { - sqlContextName = "spark"; - } - p1.setText("%pyspark\n" + - "from pyspark.sql import SQLContext\n" + - "print(" + sqlContextName + ".read.format('com.databricks.spark.csv')" + - ".load('"+ tmpFile.getAbsolutePath() +"').count())"); - p1.setAuthenticationInfo(anonymous); - note.run(p1.getId()); - - waitForFinish(p1); - assertEquals(Status.FINISHED, p1.getStatus()); - assertEquals("2\n", p1.getResult().message().get(0).getData()); - } - ZeppelinServer.notebook.removeNote(note.getId(), anonymous); + Note note = ZeppelinServer.notebook.createNote(anonymous); + Paragraph p = note.addNewParagraph(anonymous); + File tmpCSVFile = File.createTempFile("test", ".csv"); + FileWriter csvFileWriter = new FileWriter(tmpCSVFile); + IOUtils.copy(new StringReader("84896,54"), csvFileWriter); + csvFileWriter.close(); + p.setText("%spark spark.read.csv(\"file://" + tmpCSVFile.getAbsolutePath() + "\")"); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + assertTrue(p.getResult().message().get(0).getData().contains( + "org.apache.spark.sql.DataFrame = [_c0: string, _c1: string]\n")); + } + + @Test + public void sparkSQLTest() throws IOException { + Note note = ZeppelinServer.notebook.createNote(anonymous); + // test basic dataframe api + Paragraph p = note.addNewParagraph(anonymous); + p.setText("%spark val df=sqlContext.createDataFrame(Seq((\"hello\",20)))\n" + + "df.collect()"); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + assertTrue(p.getResult().message().get(0).getData().contains( + "Array[org.apache.spark.sql.Row] = Array([hello,20])")); + + // test display DataFrame + p = note.addNewParagraph(anonymous); + p.setText("%spark val df=sqlContext.createDataFrame(Seq((\"hello\",20)))\n" + + "z.show(df)"); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + assertEquals(InterpreterResult.Type.TABLE, p.getResult().message().get(0).getType()); + assertEquals("_1\t_2\nhello\t20\n", p.getResult().message().get(0).getData()); + + // test display DataSet + if (isSpark2()) { + p = note.addNewParagraph(anonymous); + p.setText("%spark val ds=spark.createDataset(Seq((\"hello\",20)))\n" + + "z.show(ds)"); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + assertEquals(InterpreterResult.Type.TABLE, p.getResult().message().get(0).getType()); + assertEquals("_1\t_2\nhello\t20\n", p.getResult().message().get(0).getData()); } + } - /** - * Get spark version number as a numerical value. - * eg. 1.1.x => 11, 1.2.x => 12, 1.3.x => 13 ... - */ - private int getSparkVersionNumber(Note note) { - Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - note.setName("note"); - Map config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - p.setText("%spark print(sc.version)"); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - assertEquals(Status.FINISHED, p.getStatus()); - String sparkVersion = p.getResult().message().get(0).getData(); - System.out.println("Spark version detected " + sparkVersion); - String[] split = sparkVersion.split("\\."); - int version = Integer.parseInt(split[0]) * 10 + Integer.parseInt(split[1]); - return version; - } + @Test + public void sparkRTest() throws IOException { + Note note = ZeppelinServer.notebook.createNote(anonymous); - @Test - public void testSparkZeppelinContextDynamicForms() throws IOException { - Note note = ZeppelinServer.notebook.createNote(anonymous); - Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - note.setName("note"); - Map config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - String code = "%spark.spark println(z.textbox(\"my_input\", \"default_name\"))\n" + - "println(z.select(\"my_select\", \"1\"," + - "Seq((\"1\", \"select_1\"), (\"2\", \"select_2\"))))\n" + - "val items=z.checkbox(\"my_checkbox\", Seq(\"2\"), " + - "Seq((\"1\", \"check_1\"), (\"2\", \"check_2\")))\n" + - "println(items(0))"; - p.setText(code); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - - assertEquals(Status.FINISHED, p.getStatus()); - Iterator formIter = p.settings.getForms().keySet().iterator(); - assert(formIter.next().equals("my_input")); - assert(formIter.next().equals("my_select")); - assert(formIter.next().equals("my_checkbox")); - - // check dynamic forms values - String[] result = p.getResult().message().get(0).getData().split("\n"); - assertEquals(4, result.length); - assertEquals("default_name", result[0]); - assertEquals("1", result[1]); - assertEquals("items: Seq[Object] = Buffer(2)", result[2]); - assertEquals("2", result[3]); + String sqlContextName = "sqlContext"; + if (isSpark2()) { + sqlContextName = "spark"; } - - @Test - public void testPySparkZeppelinContextDynamicForms() throws IOException { - Note note = ZeppelinServer.notebook.createNote(anonymous); - Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - note.setName("note"); - Map config = p.getConfig(); - config.put("enabled", true); - p.setConfig(config); - String code = "%spark.pyspark print(z.input('my_input', 'default_name'))\n" + - "print(z.select('my_select', " + - "[('1', 'select_1'), ('2', 'select_2')], defaultValue='1'))\n" + - "items=z.checkbox('my_checkbox', " + - "[('1', 'check_1'), ('2', 'check_2')], defaultChecked=['2'])\n" + - "print(items[0])"; - p.setText(code); - p.setAuthenticationInfo(anonymous); - note.run(p.getId()); - waitForFinish(p); - - assertEquals(Status.FINISHED, p.getStatus()); - Iterator formIter = p.settings.getForms().keySet().iterator(); - assert(formIter.next().equals("my_input")); - assert(formIter.next().equals("my_select")); - assert(formIter.next().equals("my_checkbox")); - - // check dynamic forms values - String[] result = p.getResult().message().get(0).getData().split("\n"); - assertEquals(3, result.length); - assertEquals("default_name", result[0]); - assertEquals("1", result[1]); - assertEquals("2", result[2]); + Paragraph p = note.addNewParagraph(anonymous); + p.setText("%spark.r localDF <- data.frame(name=c(\"a\", \"b\", \"c\"), age=c(19, 23, 18))\n" + + "df <- createDataFrame(" + sqlContextName + ", localDF)\n" + + "count(df)" + ); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + assertEquals("[1] 3", p.getResult().message().get(0).getData().trim()); + } + + @Test + public void pySparkTest() throws IOException { + // create new note + Note note = ZeppelinServer.notebook.createNote(anonymous); + + // run markdown paragraph, again + Paragraph p = note.addNewParagraph(anonymous); + p.setText("%spark.pyspark sc.parallelize(range(1, 11)).reduce(lambda a, b: a + b)"); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + assertEquals("55\n", p.getResult().message().get(0).getData()); + if (!isSpark2()) { + // run sqlContext test + p = note.addNewParagraph(anonymous); + p.setText("%pyspark from pyspark.sql import Row\n" + + "df=sqlContext.createDataFrame([Row(id=1, age=20)])\n" + + "df.collect()"); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + assertEquals("[Row(age=20, id=1)]\n", p.getResult().message().get(0).getData()); + + // test display Dataframe + p = note.addNewParagraph(anonymous); + p.setText("%pyspark from pyspark.sql import Row\n" + + "df=sqlContext.createDataFrame([Row(id=1, age=20)])\n" + + "z.show(df)"); + note.run(p.getId(), true); + waitForFinish(p); + assertEquals(Status.FINISHED, p.getStatus()); + assertEquals(InterpreterResult.Type.TABLE, p.getResult().message().get(0).getType()); + // TODO (zjffdu), one more \n is appended, need to investigate why. + assertEquals("age\tid\n20\t1\n", p.getResult().message().get(0).getData()); + + // test udf + p = note.addNewParagraph(anonymous); + p.setText("%pyspark sqlContext.udf.register(\"f1\", lambda x: len(x))\n" + + "sqlContext.sql(\"select f1(\\\"abc\\\") as len\").collect()"); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + 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(anonymous); + /** + %pyspark + a=1 + + print(a2) + */ + p.setText("%pyspark a=1\n\nprint(a2)"); + note.run(p.getId(), true); + assertEquals(Status.ERROR, p.getStatus()); + assertTrue(p.getResult().message().get(0).getData() + .contains("Fail to execute line 3: print(a2)")); + assertTrue(p.getResult().message().get(0).getData() + .contains("name 'a2' is not defined")); + } else { + // run SparkSession test + p = note.addNewParagraph(anonymous); + p.setText("%pyspark from pyspark.sql import Row\n" + + "df=sqlContext.createDataFrame([Row(id=1, age=20)])\n" + + "df.collect()"); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + assertEquals("[Row(age=20, id=1)]\n", p.getResult().message().get(0).getData()); + + // test udf + p = note.addNewParagraph(anonymous); + // use SQLContext to register UDF but use this UDF through SparkSession + p.setText("%pyspark sqlContext.udf.register(\"f1\", lambda x: len(x))\n" + + "spark.sql(\"select f1(\\\"abc\\\") as len\").collect()"); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + 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 + public void zRunTest() throws IOException { + // create new note + Note note = ZeppelinServer.notebook.createNote(anonymous); + Paragraph p0 = note.addNewParagraph(anonymous); + // z.run(paragraphIndex) + p0.setText("%spark z.run(1)"); + Paragraph p1 = note.addNewParagraph(anonymous); + p1.setText("%spark val a=10"); + Paragraph p2 = note.addNewParagraph(anonymous); + p2.setText("%spark print(a)"); + + note.run(p0.getId(), true); + assertEquals(Status.FINISHED, p0.getStatus()); + + // z.run is not blocking call. So p1 may not be finished when p0 is done. + waitForFinish(p1); + assertEquals(Status.FINISHED, p1.getStatus()); + note.run(p2.getId(), true); + assertEquals(Status.FINISHED, p2.getStatus()); + assertEquals("10", p2.getResult().message().get(0).getData()); + + Paragraph p3 = note.addNewParagraph(anonymous); + p3.setText("%spark println(new java.util.Date())"); + + // run current Node, z.runNote(noteId) + p0.setText(String.format("%%spark z.runNote(\"%s\")", note.getId())); + note.run(p0.getId()); + waitForFinish(p0); + waitForFinish(p1); + waitForFinish(p2); + waitForFinish(p3); + + assertEquals(Status.FINISHED, p3.getStatus()); + String p3result = p3.getResult().message().get(0).getData(); + assertTrue(p3result.length() > 0); + + // z.run(noteId, paragraphId) + p0.setText(String.format("%%spark z.run(\"%s\", \"%s\")", note.getId(), p3.getId())); + p3.setText("%spark println(\"END\")"); + + note.run(p0.getId(), true); + waitForFinish(p3); + assertEquals(Status.FINISHED, p3.getStatus()); + assertEquals("END\n", p3.getResult().message().get(0).getData()); + + // run paragraph in note2 via paragraph in note1 + Note note2 = ZeppelinServer.notebook.createNote(anonymous); + Paragraph p20 = note2.addNewParagraph(anonymous); + p20.setText("%spark val a = 1"); + Paragraph p21 = note2.addNewParagraph(anonymous); + p21.setText("%spark print(a)"); + + // run p20 of note2 via paragraph in note1 + p0.setText(String.format("%%spark z.run(\"%s\", \"%s\")", note2.getId(), p20.getId())); + note.run(p0.getId(), true); + waitForFinish(p20); + assertEquals(Status.FINISHED, p20.getStatus()); + assertEquals(Status.READY, p21.getStatus()); + + p0.setText(String.format("%%spark z.runNote(\"%s\")", note2.getId())); + note.run(p0.getId(), true); + waitForFinish(p20); + waitForFinish(p21); + assertEquals(Status.FINISHED, p20.getStatus()); + assertEquals(Status.FINISHED, p21.getStatus()); + assertEquals("1", p21.getResult().message().get(0).getData()); + } + + @Test + public void testZeppelinContextResource() throws IOException { + Note note = ZeppelinServer.notebook.createNote(anonymous); + + Paragraph p1 = note.addNewParagraph(anonymous); + p1.setText("%spark z.put(\"var_1\", \"hello world\")"); + + Paragraph p2 = note.addNewParagraph(anonymous); + p2.setText("%spark println(z.get(\"var_1\"))"); + + Paragraph p3 = note.addNewParagraph(anonymous); + p3.setText("%spark.pyspark print(z.get(\"var_1\"))"); + + note.run(p1.getId(), true); + note.run(p2.getId(), true); + note.run(p3.getId(), true); + + assertEquals(Status.FINISHED, p1.getStatus()); + assertEquals(Status.FINISHED, p2.getStatus()); + assertEquals("hello world\n", p2.getResult().message().get(0).getData()); + assertEquals(Status.FINISHED, p3.getStatus()); + assertEquals("hello world\n", p3.getResult().message().get(0).getData()); + } + + @Test + public void testZeppelinContextHook() throws IOException { + Note note = ZeppelinServer.notebook.createNote(anonymous); + + // register global hook & note1 hook + Paragraph p1 = note.addNewParagraph(anonymous); + p1.setText("%python from __future__ import print_function\n" + + "z.registerHook('pre_exec', 'print(1)')\n" + + "z.registerHook('post_exec', 'print(2)')\n" + + "z.registerNoteHook('pre_exec', 'print(3)', '" + note.getId() + "')\n" + + "z.registerNoteHook('post_exec', 'print(4)', '" + note.getId() + "')\n"); + + Paragraph p2 = note.addNewParagraph(anonymous); + p2.setText("%python print(5)"); + + note.run(p1.getId(), true); + note.run(p2.getId(), true); + + assertEquals(Status.FINISHED, p1.getStatus()); + assertEquals(Status.FINISHED, p2.getStatus()); + assertEquals("1\n3\n5\n4\n2\n", p2.getResult().message().get(0).getData()); + + Note note2 = ZeppelinServer.notebook.createNote(anonymous); + Paragraph p3 = note2.addNewParagraph(anonymous); + p3.setText("%python print(6)"); + note2.run(p3.getId(), true); + assertEquals("1\n6\n2\n", p3.getResult().message().get(0).getData()); + } + + @Test + public void pySparkDepLoaderTest() throws IOException { + Note note = ZeppelinServer.notebook.createNote(anonymous); + + // restart spark interpreter to make dep loader work + ZeppelinServer.notebook.getInterpreterSettingManager().close(); + + // load dep + Paragraph p0 = note.addNewParagraph(anonymous); + p0.setText("%dep z.load(\"com.databricks:spark-csv_2.11:1.2.0\")"); + note.run(p0.getId(), true); + assertEquals(Status.FINISHED, p0.getStatus()); + + // write test csv file + File tmpFile = File.createTempFile("test", "csv"); + FileUtils.write(tmpFile, "a,b\n1,2"); + + // load data using libraries from dep loader + Paragraph p1 = note.addNewParagraph(anonymous); + + String sqlContextName = "sqlContext"; + if (isSpark2()) { + sqlContextName = "spark"; } + p1.setText("%pyspark\n" + + "from pyspark.sql import SQLContext\n" + + "print(" + sqlContextName + ".read.format('com.databricks.spark.csv')" + + ".load('file://" + tmpFile.getAbsolutePath() + "').count())"); + note.run(p1.getId(), true); + + assertEquals(Status.FINISHED, p1.getStatus()); + assertEquals("2\n", p1.getResult().message().get(0).getData()); + } + + private void verifySparkVersionNumber() throws IOException { + Note note = ZeppelinServer.notebook.createNote(anonymous); + Paragraph p = note.addNewParagraph(anonymous); + + p.setText("%spark print(sc.version)"); + note.run(p.getId()); + waitForFinish(p); + assertEquals(Status.FINISHED, p.getStatus()); + assertEquals(sparkVersion, p.getResult().message().get(0).getData()); + } + + private int toIntSparkVersion(String sparkVersion) { + String[] split = sparkVersion.split("\\."); + int version = Integer.parseInt(split[0]) * 10 + Integer.parseInt(split[1]); + return version; + } + + private boolean isSpark2() { + return toIntSparkVersion(sparkVersion) >= 20; + } + + @Test + public void testSparkZeppelinContextDynamicForms() throws IOException { + Note note = ZeppelinServer.notebook.createNote(anonymous); + Paragraph p = note.addNewParagraph(anonymous); + String code = "%spark.spark println(z.textbox(\"my_input\", \"default_name\"))\n" + + "println(z.select(\"my_select\", \"1\"," + + "Seq((\"1\", \"select_1\"), (\"2\", \"select_2\"))))\n" + + "val items=z.checkbox(\"my_checkbox\", Seq(\"2\"), " + + "Seq((\"1\", \"check_1\"), (\"2\", \"check_2\")))\n" + + "println(items(0))"; + p.setText(code); + note.run(p.getId()); + waitForFinish(p); + + assertEquals(Status.FINISHED, p.getStatus()); + Iterator formIter = p.settings.getForms().keySet().iterator(); + assert (formIter.next().equals("my_input")); + assert (formIter.next().equals("my_select")); + assert (formIter.next().equals("my_checkbox")); + + // check dynamic forms values + String[] result = p.getResult().message().get(0).getData().split("\n"); + assertEquals(4, result.length); + assertEquals("default_name", result[0]); + assertEquals("1", result[1]); + assertEquals("2", result[2]); + assertEquals("items: Seq[Object] = Buffer(2)", result[3]); + } + + @Test + public void testPySparkZeppelinContextDynamicForms() throws IOException { + Note note = ZeppelinServer.notebook.createNote(anonymous); + Paragraph p = note.addNewParagraph(anonymous); + String code = "%spark.pyspark print(z.input('my_input', 'default_name'))\n" + + "print(z.select('my_select', " + + "[('1', 'select_1'), ('2', 'select_2')], defaultValue='1'))\n" + + "items=z.checkbox('my_checkbox', " + + "[('1', 'check_1'), ('2', 'check_2')], defaultChecked=['2'])\n" + + "print(items[0])"; + p.setText(code); + note.run(p.getId()); + waitForFinish(p); + + assertEquals(Status.FINISHED, p.getStatus()); + Iterator formIter = p.settings.getForms().keySet().iterator(); + assert (formIter.next().equals("my_input")); + assert (formIter.next().equals("my_select")); + assert (formIter.next().equals("my_checkbox")); + + // check dynamic forms values + String[] result = p.getResult().message().get(0).getData().split("\n"); + assertEquals(3, result.length); + assertEquals("default_name", result[0]); + assertEquals("1", result[1]); + assertEquals("2", result[2]); + } + + @Test + public void testConfInterpreter() throws IOException { + ZeppelinServer.notebook.getInterpreterSettingManager().close(); + Note note = ZeppelinServer.notebook.createNote(anonymous); + Paragraph p = note.addNewParagraph(anonymous); + p.setText("%spark.conf spark.jars.packages\tcom.databricks:spark-csv_2.11:1.2.0"); + note.run(p.getId(), true); + assertEquals(Status.FINISHED, p.getStatus()); + + Paragraph p1 = note.addNewParagraph(anonymous); + p1.setText("%spark\nimport com.databricks.spark.csv._"); + note.run(p1.getId(), true); + assertEquals(Status.FINISHED, p1.getStatus()); + } } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest16.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest16.java new file mode 100644 index 00000000000..abf5cb280d3 --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest16.java @@ -0,0 +1,40 @@ +/* + * 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 org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +@RunWith(value = Parameterized.class) +public class ZeppelinSparkClusterTest16 extends ZeppelinSparkClusterTest { + + public ZeppelinSparkClusterTest16(String sparkVersion) throws Exception { + super(sparkVersion); + } + + + @Parameterized.Parameters + public static List data() { + return Arrays.asList(new Object[][]{ + {"1.6.3"} + }); + } +} diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest20.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest20.java new file mode 100644 index 00000000000..27352bd94aa --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest20.java @@ -0,0 +1,40 @@ +/* + * 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 org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +@RunWith(value = Parameterized.class) +public class ZeppelinSparkClusterTest20 extends ZeppelinSparkClusterTest { + + public ZeppelinSparkClusterTest20(String sparkVersion) throws Exception { + super(sparkVersion); + } + + + @Parameterized.Parameters + public static List data() { + return Arrays.asList(new Object[][]{ + {"2.0.2"} + }); + } +} diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest21.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest21.java new file mode 100644 index 00000000000..db54b646f14 --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest21.java @@ -0,0 +1,40 @@ +/* + * 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 org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +@RunWith(value = Parameterized.class) +public class ZeppelinSparkClusterTest21 extends ZeppelinSparkClusterTest { + + public ZeppelinSparkClusterTest21(String sparkVersion) throws Exception { + super(sparkVersion); + } + + + @Parameterized.Parameters + public static List data() { + return Arrays.asList(new Object[][]{ + {"2.1.3"} + }); + } +} diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest22.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest22.java new file mode 100644 index 00000000000..eca09c6975d --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest22.java @@ -0,0 +1,40 @@ +/* + * 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 org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +@RunWith(value = Parameterized.class) +public class ZeppelinSparkClusterTest22 extends ZeppelinSparkClusterTest { + + public ZeppelinSparkClusterTest22(String sparkVersion) throws Exception { + super(sparkVersion); + } + + + @Parameterized.Parameters + public static List data() { + return Arrays.asList(new Object[][]{ + {"2.2.2"} + }); + } +} diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest23.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest23.java new file mode 100644 index 00000000000..d5575fed711 --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest23.java @@ -0,0 +1,40 @@ +/* + * 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 org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +@RunWith(value = Parameterized.class) +public class ZeppelinSparkClusterTest23 extends ZeppelinSparkClusterTest { + + public ZeppelinSparkClusterTest23(String sparkVersion) throws Exception { + super(sparkVersion); + } + + + @Parameterized.Parameters + public static List data() { + return Arrays.asList(new Object[][]{ + {"2.3.2"} + }); + } +} diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest24.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest24.java new file mode 100644 index 00000000000..9450d4ace74 --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest24.java @@ -0,0 +1,39 @@ +/* + * 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 org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +@RunWith(value = Parameterized.class) +public class ZeppelinSparkClusterTest24 extends ZeppelinSparkClusterTest { + + public ZeppelinSparkClusterTest24(String sparkVersion) throws Exception { + super(sparkVersion); + } + + @Parameterized.Parameters + public static List data() { + return Arrays.asList(new Object[][]{ + {"2.4.0"} + }); + } +} diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/security/DirAccessTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/security/DirAccessTest.java index 2f2486ba348..b8d5b20c184 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/security/DirAccessTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/security/DirAccessTest.java @@ -30,7 +30,7 @@ public class DirAccessTest extends AbstractTestRestApi { public void testDirAccessForbidden() throws Exception { synchronized (this) { System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_SERVER_DEFAULT_DIR_ALLOWED.getVarName(), "false"); - AbstractTestRestApi.startUp(); + AbstractTestRestApi.startUp(DirAccessTest.class.getSimpleName()); HttpClient httpClient = new HttpClient(); GetMethod getMethod = new GetMethod(getUrlToTest() + "/app/"); httpClient.executeMethod(getMethod); @@ -43,7 +43,7 @@ public void testDirAccessForbidden() throws Exception { public void testDirAccessOk() throws Exception { synchronized (this) { System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_SERVER_DEFAULT_DIR_ALLOWED.getVarName(), "true"); - AbstractTestRestApi.startUp(); + AbstractTestRestApi.startUp(DirAccessTest.class.getSimpleName()); HttpClient httpClient = new HttpClient(); GetMethod getMethod = new GetMethod(getUrlToTest() + "/app/"); httpClient.executeMethod(getMethod); diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/security/SecurityUtilsTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/security/SecurityUtilsTest.java index 9d902c8099a..5bb41180e50 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/security/SecurityUtilsTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/security/SecurityUtilsTest.java @@ -17,18 +17,35 @@ package org.apache.zeppelin.security; import static org.junit.Assert.*; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.InetAddress; +import java.net.URISyntaxException; +import java.net.UnknownHostException; import org.apache.commons.configuration.ConfigurationException; import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.notebook.Notebook; +import org.apache.zeppelin.server.ZeppelinServer; import org.apache.zeppelin.utils.SecurityUtils; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import sun.security.acl.PrincipalImpl; -import java.net.URISyntaxException; -import java.net.UnknownHostException; -import java.net.InetAddress; - - +@RunWith(PowerMockRunner.class) +@PrepareForTest(org.apache.shiro.SecurityUtils.class) public class SecurityUtilsTest { + @Mock + org.apache.shiro.subject.Subject subject; + @Test public void isInvalid() throws URISyntaxException, UnknownHostException { assertFalse(SecurityUtils.isValidOrigin("http://127.0.1.1", ZeppelinConfiguration.create())); @@ -87,4 +104,54 @@ public void notAURIOrigin() throws URISyntaxException, UnknownHostException, Con assertFalse(SecurityUtils.isValidOrigin("test123", new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml")))); } + + + @Test + public void canGetPrincipalName() { + String expectedName = "java.security.Principal.getName()"; + setupPrincipalName(expectedName); + assertEquals(expectedName, SecurityUtils.getPrincipal()); + } + + @Test + public void testUsernameForceLowerCase() throws IOException, InterruptedException { + String expectedName = "java.security.Principal.getName()"; + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_USERNAME_FORCE_LOWERCASE + .getVarName(), String.valueOf(true)); + setupPrincipalName(expectedName); + assertEquals(expectedName.toLowerCase(), SecurityUtils.getPrincipal()); + + } + + private void setupPrincipalName(String expectedName) { + SecurityUtils.setIsEnabled(true); + PowerMockito.mockStatic(org.apache.shiro.SecurityUtils.class); + when(org.apache.shiro.SecurityUtils.getSubject()).thenReturn(subject); + when(subject.isAuthenticated()).thenReturn(true); + when(subject.getPrincipal()).thenReturn(new PrincipalImpl(expectedName)); + + Notebook notebook = Mockito.mock(Notebook.class); + try { + setFinalStatic(ZeppelinServer.class.getDeclaredField("notebook"), notebook); + when(ZeppelinServer.notebook.getConf()) + .thenReturn(new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml"))); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (ConfigurationException e) { + e.printStackTrace(); + } + } + + private void setFinalStatic(Field field, Object newValue) + throws NoSuchFieldException, IllegalAccessException { + field.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, newValue); + } + + } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/server/CorsFilterTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/server/CorsFilterTest.java index df2a6e92e0d..7ee99518f1b 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/server/CorsFilterTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/server/CorsFilterTest.java @@ -58,7 +58,7 @@ public Object answer(InvocationOnMock invocationOnMock) throws Throwable { count++; return null; } - }).when(mockResponse).addHeader(anyString(), anyString()); + }).when(mockResponse).setHeader(anyString(), anyString()); filter.doFilter(mockRequest, mockResponse, mockedFilterChain); Assert.assertTrue(headers[0].equals("http://localhost:8080")); @@ -82,7 +82,7 @@ public Object answer(InvocationOnMock invocationOnMock) throws Throwable { count++; return null; } - }).when(mockResponse).addHeader(anyString(), anyString()); + }).when(mockResponse).setHeader(anyString(), anyString()); filter.doFilter(mockRequest, mockResponse, mockedFilterChain); Assert.assertTrue(headers[0].equals("")); 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..0b68ca64b19 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; @@ -62,7 +63,7 @@ public class NotebookServerTest extends AbstractTestRestApi { @BeforeClass public static void init() throws Exception { - AbstractTestRestApi.startUp(); + AbstractTestRestApi.startUp(NotebookServerTest.class.getSimpleName()); gson = new Gson(); notebook = ZeppelinServer.notebook; notebookServer = ZeppelinServer.notebookWsServer; @@ -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,16 @@ 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); + } + // sleep for 1 second to make sure job running thread finish to fire event. See ZEPPELIN-3277 + Thread.sleep(1000); + // add angularObject interpreterGroup.getAngularObjectRegistry().add("object1", "value1", note1.getId(), null); @@ -199,7 +210,7 @@ public void bindAngularObjectToRemoteForParagraphs() throws Exception { final InterpreterGroup mdGroup = new InterpreterGroup("mdGroup"); mdGroup.setAngularObjectRegistry(mdRegistry); - when(paragraph.getCurrentRepl().getInterpreterGroup()).thenReturn(mdGroup); + when(paragraph.getBindedInterpreter().getInterpreterGroup()).thenReturn(mdGroup); final AngularObject ao1 = AngularObjectBuilder.build(varName, value, "noteId", "paragraphId"); @@ -247,7 +258,7 @@ public void bindAngularObjectToLocalForParagraphs() throws Exception { final InterpreterGroup mdGroup = new InterpreterGroup("mdGroup"); mdGroup.setAngularObjectRegistry(mdRegistry); - when(paragraph.getCurrentRepl().getInterpreterGroup()).thenReturn(mdGroup); + when(paragraph.getBindedInterpreter().getInterpreterGroup()).thenReturn(mdGroup); final AngularObject ao1 = AngularObjectBuilder.build(varName, value, "noteId", "paragraphId"); @@ -293,7 +304,7 @@ public void unbindAngularObjectFromRemoteForParagraphs() throws Exception { final InterpreterGroup mdGroup = new InterpreterGroup("mdGroup"); mdGroup.setAngularObjectRegistry(mdRegistry); - when(paragraph.getCurrentRepl().getInterpreterGroup()).thenReturn(mdGroup); + when(paragraph.getBindedInterpreter().getInterpreterGroup()).thenReturn(mdGroup); final AngularObject ao1 = AngularObjectBuilder.build(varName, value, "noteId", "paragraphId"); when(mdRegistry.removeAndNotifyRemoteProcess(varName, "noteId", "paragraphId")).thenReturn(ao1); @@ -338,7 +349,7 @@ public void unbindAngularObjectFromLocalForParagraphs() throws Exception { final InterpreterGroup mdGroup = new InterpreterGroup("mdGroup"); mdGroup.setAngularObjectRegistry(mdRegistry); - when(paragraph.getCurrentRepl().getInterpreterGroup()).thenReturn(mdGroup); + when(paragraph.getBindedInterpreter().getInterpreterGroup()).thenReturn(mdGroup); final AngularObject ao1 = AngularObjectBuilder.build(varName, value, "noteId", "paragraphId"); 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-server/src/test/resources/log4j.properties b/zeppelin-server/src/test/resources/log4j.properties index b0d1067bc46..5d4517d559f 100644 --- a/zeppelin-server/src/test/resources/log4j.properties +++ b/zeppelin-server/src/test/resources/log4j.properties @@ -27,13 +27,12 @@ log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c:%L - %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 @@ -43,5 +42,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.spark=DEBUG +log4j.logger.org.apache.hadoop=WARN +log4j.logger.org.apache.zeppelin.scheduler=DEBUG +log4j.logger.org.apache.zeppelin.interpreter.remote=DEBUG diff --git a/zeppelin-server/src/test/scala/org/apache/zeppelin/AbstractFunctionalSuite.scala b/zeppelin-server/src/test/scala/org/apache/zeppelin/AbstractFunctionalSuite.scala deleted file mode 100644 index 2f773c6fddc..00000000000 --- a/zeppelin-server/src/test/scala/org/apache/zeppelin/AbstractFunctionalSuite.scala +++ /dev/null @@ -1,84 +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 - -import org.apache.zeppelin.AbstractFunctionalSuite.SERVER_ADDRESS -import org.openqa.selenium.WebDriver -import org.openqa.selenium.chrome.ChromeDriver -import org.openqa.selenium.firefox.{FirefoxBinary, FirefoxDriver, FirefoxProfile} -import org.openqa.selenium.safari.SafariDriver -import org.scalatest.concurrent.Eventually._ -import org.scalatest.time._ -import org.scalatest.selenium.WebBrowser -import org.scalatest.{BeforeAndAfterAll, FunSuite, Suite} - -import scala.sys.process._ -import scala.util.Try - -object AbstractFunctionalSuite { - val SERVER_ADDRESS = "http://localhost:8080" -} - -class AbstractFunctionalSuite extends FunSuite with WebBrowser with BeforeAndAfterAll { - - implicit val webDriver = getDriver() - - override def beforeAll() = { - "../bin/zeppelin-daemon.sh start" ! - - eventually (timeout(Span(180, Seconds))) { - go to SERVER_ADDRESS - assert(find("welcome").isDefined) - } - } - - override def nestedSuites = - List[Suite](new WelcomePageSuite).toIndexedSeq - - override def afterAll() = { - "../bin/zeppelin-daemon.sh stop" ! - - webDriver.close() - } - - def getDriver(): WebDriver = { - val possibleDrivers = List[() => WebDriver](safari, chrome, firefox) - val createdDriver = possibleDrivers.map(driverFactory => Try(driverFactory.apply())).find(_.isSuccess) - createdDriver match { - case Some(driver) => driver.get - case None => throw new RuntimeException("Could not initialize any driver") - } - } - - def safari(): WebDriver = { - new SafariDriver() - } - - def chrome(): WebDriver = { - new ChromeDriver() - } - - def firefox(): WebDriver = { - val ffox: FirefoxBinary = new FirefoxBinary - if ("true" == System.getenv("TRAVIS")) { - ffox.setEnvironmentProperty("DISPLAY", ":99") - } - val profile: FirefoxProfile = new FirefoxProfile - new FirefoxDriver(ffox, profile) - } -} diff --git a/zeppelin-web/.bowerrc b/zeppelin-web/.bowerrc index dd7c6b18176..6afb9ba1343 100644 --- a/zeppelin-web/.bowerrc +++ b/zeppelin-web/.bowerrc @@ -1,4 +1,5 @@ { "directory": "bower_components", + "allow_root": true, "json": "bower.json" } 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/.npmrc b/zeppelin-web/.npmrc new file mode 100644 index 00000000000..5af86736167 --- /dev/null +++ b/zeppelin-web/.npmrc @@ -0,0 +1 @@ +unsafe-perm = true diff --git a/zeppelin-web/bower.json b/zeppelin-web/bower.json index a68c2e9ec8b..1e9c480dda5 100644 --- a/zeppelin-web/bower.json +++ b/zeppelin-web/bower.json @@ -14,9 +14,9 @@ "angular-resource": "1.5.7", "angular-bootstrap": "~2.5.0", "angular-websocket": "~1.0.13", - "ace-builds": "1.2.7", + "ace-builds": "1.3.2", "angular-ui-ace": "0.1.3", - "jquery.scrollTo": "~1.4.13", + "jquery.scrollTo": "~2.1.2", "nvd3": "~1.8.5", "angular-dragdrop": "~1.0.8", "perfect-scrollbar": "~0.5.4", @@ -32,7 +32,11 @@ "bootstrap3-dialog": "bootstrap-dialog#~1.34.7", "select2": "^4.0.3", "MathJax": "2.7.0", - "ngclipboard": "^1.1.1" + "ngclipboard": "^1.1.1", + "jsdiff": "3.3.0", + "ngInfiniteScroll": "^1.3.4", + "jszip": "2.6.1", + "excel-builder-js": "excelbuilder#2.0.0" }, "devDependencies": { "angular-mocks": "1.5.7" @@ -53,7 +57,7 @@ "src-noconflict/ext-language_tools.js", "src-noconflict/theme-chrome.js" ], - "version": "1.2.7", + "version": "1.3.2", "name": "ace-builds" }, "highlightjs": { @@ -66,7 +70,8 @@ } }, "resolutions": { - "ace-builds": "1.2.7", - "angular": ">=1.5.0 <1.6" + "ace-builds": "1.3.2", + "angular": ">=1.5.0 <1.6", + "jquery.scrollTo": "~2.1.2" } } diff --git a/zeppelin-web/e2e/home.spec.js b/zeppelin-web/e2e/home.spec.js index 7fc9499ed63..299fbb5adb5 100644 --- a/zeppelin-web/e2e/home.spec.js +++ b/zeppelin-web/e2e/home.spec.js @@ -1,4 +1,26 @@ describe('Home e2e Test', function() { + /*Common methods for interact with elements*/ + let clickOn = function(elem) { + browser.actions().mouseMove(elem).click().perform() + } + + let sendKeysToInput = function(input, keys) { + cleanInput(input) + input.sendKeys(keys) + } + + let cleanInput = function(inputElem) { + inputElem.sendKeys(protractor.Key.chord(protractor.Key.CONTROL, "a")) + inputElem.sendKeys(protractor.Key.BACK_SPACE) + } + + let scrollToElementAndClick = function(elem) { + browser.executeScript("arguments[0].scrollIntoView(false);", elem.getWebElement()) + browser.sleep(100) + clickOn(elem) + } + + //tests it('should have a welcome message', function() { browser.get('http://localhost:8080'); var welcomeElem = element(by.id('welcome')) @@ -15,4 +37,30 @@ describe('Home e2e Test', function() { var btn = element(by.cssContainingText('a', 'Create new note')) expect(btn.isPresent()).toBe(true) }) + + it('correct save permission in interpreter', function() { + var ownerName = 'admin' + var interpreterName = 'interpreter_e2e_test' + clickOn(element(by.xpath('//span[@class="username ng-binding"]'))) + clickOn(element(by.xpath('//a[@href="#/interpreter"]'))) + clickOn(element(by.xpath('//button[@ng-click="showAddNewSetting = !showAddNewSetting"]'))) + sendKeysToInput(element(by.xpath('//input[@id="newInterpreterSettingName"]')), interpreterName) + clickOn(element(by.xpath('//select[@ng-model="newInterpreterSetting.group"]'))) + browser.sleep(500) + browser.actions().sendKeys(protractor.Key.ARROW_DOWN).perform() + browser.actions().sendKeys(protractor.Key.ENTER).perform() + clickOn(element(by.xpath('//div[@ng-show="showAddNewSetting"]//input[@id="idShowPermission"]'))) + sendKeysToInput(element(by.xpath('//div[@ng-show="showAddNewSetting"]//input[@class="select2-search__field"]')), ownerName) + browser.sleep(500) + browser.actions().sendKeys(protractor.Key.ENTER).perform() + scrollToElementAndClick(element(by.xpath('//span[@ng-click="addNewInterpreterSetting()"]'))) + scrollToElementAndClick(element(by.xpath('//*[@id="' + interpreterName + '"]//span[@class="fa fa-pencil"]'))) + scrollToElementAndClick(element(by.xpath('//*[@id="' + interpreterName + '"]//button[@type="submit"]'))) + clickOn(element(by.xpath('//div[@class="bootstrap-dialog-footer-buttons"]//button[contains(text(), \'OK\')]'))) + browser.get('http://localhost:8080/#/interpreter'); + var text = element(by.xpath('//*[@id="' + interpreterName + '"]//li[contains(text(), \'admin\')]')).getText() + scrollToElementAndClick(element(by.xpath('//*[@id="' + interpreterName + '"]//span//span[@class="fa fa-trash"]'))) + clickOn(element(by.xpath('//div[@class="bootstrap-dialog-footer-buttons"]//button[contains(text(), \'OK\')]'))) + expect(text).toEqual(ownerName); + }) }) diff --git a/zeppelin-web/e2e/searchBlock.spec.js b/zeppelin-web/e2e/searchBlock.spec.js index 4a0ea48ca37..570673b838e 100644 --- a/zeppelin-web/e2e/searchBlock.spec.js +++ b/zeppelin-web/e2e/searchBlock.spec.js @@ -99,7 +99,7 @@ describe('Search block e2e Test', function() { waitVisibility(element(by.repeater('currentParagraph in note.paragraphs'))) browser.switchTo().activeElement().sendKeys(testData.textInFirstP) let addBelow = element( - by.xpath('//div[@class="new-paragraph" and @ng-click="insertNew(\'below\');"]')) + by.xpath('//div[@class="new-paragraph last-paragraph" and @ng-click="insertNew(\'below\');"]')) clickAndWait(addBelow) browser.switchTo().activeElement().sendKeys(testData.textInSecondP) } @@ -123,7 +123,6 @@ describe('Search block e2e Test', function() { } /*Tests*/ - it('shortcut works', function() { waitVisibility(element(by.repeater('currentParagraph in note.paragraphs'))) openSearchBoxByShortcut() diff --git a/zeppelin-web/karma.conf.js b/zeppelin-web/karma.conf.js index 1c7934629e2..5daceb91fff 100644 --- a/zeppelin-web/karma.conf.js +++ b/zeppelin-web/karma.conf.js @@ -86,6 +86,10 @@ module.exports = function(config) { 'bower_components/MathJax/MathJax.js', '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/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-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/package.json b/zeppelin-web/package.json index d44bbcd8dac..0c54eeac0d3 100644 --- a/zeppelin-web/package.json +++ b/zeppelin-web/package.json @@ -19,14 +19,15 @@ "dev:watch": "grunt watch-webpack-dev", "dev": "npm-run-all --parallel dev:server lint:watch dev:watch", "test:watch": "karma start karma.conf.js --single-run=false", - "pree2e": "webdriver-manager update --gecko false --versions.chrome=2.30", + "pree2e": "webdriver-manager update --gecko false --versions.chrome=2.35", "e2e": "protractor protractor.conf.js", "pretest": "npm rebuild phantomjs-prebuilt", "test": "karma start karma.conf.js" }, "dependencies": { - "angular-ui-grid": "^4.0.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", "grunt-angular-templates": "^0.5.7", "grunt-dom-munger": "^3.4.0", diff --git a/zeppelin-web/pom.xml b/zeppelin-web/pom.xml index 227f30b1743..cd7fcecdfea 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.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-web war - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: web Application @@ -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 @@ -144,7 +140,7 @@ ${web.e2e.disabled} - + 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 034de2a6ca6..826829a5aed 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', @@ -44,6 +44,7 @@ const requiredModules = [ 'ngResource', 'ngclipboard', 'angularViewportWatch', + 'infinite-scroll', 'ui.grid', 'ui.grid.exporter', 'ui.grid.edit', 'ui.grid.rowEdit', @@ -55,163 +56,170 @@ 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'); + angular.injector(['zeppelinWebApp']).get('loaderSrv').hideLoader(); // 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) { - $rootScope.ticket = angular.fromJson(response.data).body - - $rootScope.ticket.screenUsername = $rootScope.ticket.principal - if ($rootScope.ticket.principal.startsWith('#Pac4j')) { - let re = ', name=(.*?),' - $rootScope.ticket.screenUsername = $rootScope.ticket.principal.match(re)[1] + 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; + } 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) { + }); + }, 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..043a9adc9a5 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: _.escape(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 + title: 'Error on enabling ' + _.escape(name), + message: _.escape(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) { + message: _.escape(artifact), + 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 + title: 'Error on disabling ' + _.escape(name), + message: _.escape(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.html b/zeppelin-web/src/app/helium/helium.html index 9d0628c231f..c5f53009b21 100644 --- a/zeppelin-web/src/app/helium/helium.html +++ b/zeppelin-web/src/app/helium/helium.html @@ -28,9 +28,10 @@

    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 2cf84395558..7ae5e44d70c 100644 --- a/zeppelin-web/src/app/home/home.controller.js +++ b/zeppelin-web/src/app/home/home.controller.js @@ -12,140 +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.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.renameNote = function (nodeId, nodePath) { - vm.noteActionService.renameNote(nodeId, nodePath) - } + $scope.loadMoreNotes = function() { + vm.numberOfNotesDisplayed += 10; + }; - $scope.moveNoteToTrash = function (noteId) { - vm.noteActionService.moveNoteToTrash(noteId, false) - } + $scope.renameNote = function(nodeId, nodePath) { + vm.noteActionService.renameNote(nodeId, nodePath); + }; - $scope.moveFolderToTrash = function (folderId) { - vm.noteActionService.moveFolderToTrash(folderId) - } + $scope.moveNoteToTrash = function(noteId) { + vm.noteActionService.moveNoteToTrash(noteId, false); + }; - $scope.restoreNote = function (noteId) { - websocketMsgSrv.restoreNote(noteId) - } + $scope.moveFolderToTrash = function(folderId) { + vm.noteActionService.moveFolderToTrash(folderId); + }; - $scope.restoreFolder = function (folderId) { - websocketMsgSrv.restoreFolder(folderId) - } + $scope.restoreNote = function(noteId) { + websocketMsgSrv.restoreNote(noteId); + }; - $scope.restoreAll = function () { - vm.noteActionService.restoreAll() - } + $scope.restoreFolder = function(folderId) { + websocketMsgSrv.restoreFolder(folderId); + }; - $scope.renameFolder = function (node) { - vm.noteActionService.renameFolder(node.id) - } + $scope.restoreAll = function() { + vm.noteActionService.restoreAll(); + }; - $scope.removeNote = function (noteId) { - vm.noteActionService.removeNote(noteId, false) - } + $scope.renameFolder = function(node) { + vm.noteActionService.renameFolder(node.id); + }; - $scope.removeFolder = function (folderId) { - vm.noteActionService.removeFolder(folderId) - } + $scope.removeNote = function(noteId) { + vm.noteActionService.removeNote(noteId, false); + }; - $scope.emptyTrash = function () { - vm.noteActionService.emptyTrash() - } + $scope.removeFolder = function(folderId) { + vm.noteActionService.removeFolder(folderId); + }; - $scope.clearAllParagraphOutput = function (noteId) { - vm.noteActionService.clearAllParagraphOutput(noteId) - } + $scope.emptyTrash = function() { + vm.noteActionService.emptyTrash(); + }; + + $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/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 f276a222fad..ff1eb75f299 100644 --- a/zeppelin-web/src/app/home/notebook.html +++ b/zeppelin-web/src/app/home/notebook.html @@ -23,16 +23,16 @@

    Notebook

    Import note
    -
    +
    Create new note
    • -
      -
    • +
    • -
      -
    • +
    diff --git a/zeppelin-web/src/app/interpreter/interpreter-create.html b/zeppelin-web/src/app/interpreter/interpreter-create.html index 8bf29a91f43..3078d656b76 100644 --- a/zeppelin-web/src/app/interpreter/interpreter-create.html +++ b/zeppelin-web/src/app/interpreter/interpreter-create.html @@ -100,7 +100,7 @@
    Option
  • scoped per user @@ -117,7 +117,7 @@
    Option
  • isolated per user 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 dc3619eb183..ef6b8a5eb95 100644 --- a/zeppelin-web/src/app/interpreter/interpreter.controller.js +++ b/zeppelin-web/src/app/interpreter/interpreter.controller.js @@ -12,546 +12,555 @@ * 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') - .success(function (data, status, headers, config) { - $scope.interpreterSettings = data.body - checkDownloadingDependencies() - }).error(function (data, status, headers, config) { - if (status === 401) { + .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', status, 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').success(function (data, status, headers, config) { - $scope.availableInterpreters = data.body - }).error(function (data, status, headers, config) { - console.log('Error %o %o', status, 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') - .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 : ''); + }); + }; 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(); + for (let i = 0; i < setting.option.owners.length; i++) { + setting.option.owners[i] = setting.option.owners[i].trim(); } - 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) - .success(function (data, status, headers, config) { - $scope.interpreterSettings[index] = data.body - removeTMPSettings(index) - checkDownloadingDependencies() - thisConfirm.close() + .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'}) - form.$show() - 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) - .success(function (data, status, headers, config) { - 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) - }) + .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) - .success(function (data, status, headers, config) { - let index = _.findIndex($scope.interpreterSettings, {'id': settingId}) - $scope.interpreterSettings[index] = 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) - 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 ' + _.escape($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) - .success(function (data, status, headers, config) { - $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'}) - }) - } - - $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, @@ -562,94 +571,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 @@ -657,21 +666,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: '', @@ -682,102 +691,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) - .success(function (data, status, headers, config) { - getRepositories() - $scope.resetNewRepositorySetting() - angular.element('#repoModal').modal('hide') - }).error(function (data, status, headers, config) { - console.log('Error %o %o', headers, 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) - .success(function (data, status, headers, config) { - 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) - }) + .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: _.escape(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) - .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 + 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: _.escape(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 : ''); + }); + }; $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/interpreter.html b/zeppelin-web/src/app/interpreter/interpreter.html index c1d90cc28e0..a4ef4ea5c32 100644 --- a/zeppelin-web/src/app/interpreter/interpreter.html +++ b/zeppelin-web/src/app/interpreter/interpreter.html @@ -41,10 +41,12 @@

    - +
    + +
    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.css b/zeppelin-web/src/app/notebook/dynamic-forms/dynamic-forms.css new file mode 100644 index 00000000000..d15b240961c --- /dev/null +++ b/zeppelin-web/src/app/notebook/dynamic-forms/dynamic-forms.css @@ -0,0 +1,24 @@ +import './note-create.css' + +.dynamicForm { + margin-right: 20px; + margin-left: 20px; +} + +.dynamicForm.form-horizontal .form-group { + margin-right: 0; + margin-left: 0; +} + +.dynamicForm.form-horizontal .form-group label { + padding-left: 0; +} + +.dynamicForm.form-horizontal .form-group .checkbox-item { + padding-left: 0; + padding-right: 10px; +} + +.dynamicForm.form-horizontal .form-group .checkbox-item input { + margin-right: 2px; +} diff --git a/zeppelin-web/src/app/notebook/dynamic-forms/dynamic-forms.directive.html b/zeppelin-web/src/app/notebook/dynamic-forms/dynamic-forms.directive.html new file mode 100644 index 00000000000..4f3e7151db0 --- /dev/null +++ b/zeppelin-web/src/app/notebook/dynamic-forms/dynamic-forms.directive.html @@ -0,0 +1,86 @@ + +
    +
    + + + + + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    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..1b1043e8b75 --- /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/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-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index 47ec8d7abf0..a9358086859 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -14,12 +14,17 @@

    -
    - -

    {{noteName(note)}}

    +
    + +

    {{noteName(note)}}

    @@ -95,7 +100,7 @@

    - +

  • +
    + +
    + compare with +
    + + +
    + + +
    +
    +
    +
    +
    +

    + {{p.paragraph.id}}({{p.paragraph.title}}) + added + deleted + there + are differences + contents are + identical + {{p.firstString}} +

    +
    +
    +
    +
    + Please select a revision +
    +
    +
    +
    +
    + + +
    + Revision: {{currentFirstRevisionForCompare}} --> {{currentSecondRevisionForCompare}} +
    {{currentParagraphDiffDisplay.paragraph.text}}
    +
    {{currentParagraphDiffDisplay.paragraph.text}}
    +
    
    +  
    Nothing to display
    +
    + +
    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 c71c0f7f72a..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 @@ -12,40 +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 { - content = 'data:image/svg;charset=utf-8,' + BOM + encodeURIComponent(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() + const fileName = filename + '.' + extension; + 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); + a.style = 'display: none'; + a.href = url; + a.download = fileName; + a.click(); + window.URL.revokeObjectURL(url); } - } + }; } 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
    diff --git a/zeppelin-web/src/app/search/result-list.controller.js b/zeppelin-web/src/app/search/result-list.controller.js index cd7542d9c5c..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,19 +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')) - } - } + 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/result-list.html b/zeppelin-web/src/app/search/result-list.html index 67b0d7c6f71..804fc16724a 100644 --- a/zeppelin-web/src/app/search/result-list.html +++ b/zeppelin-web/src/app/search/result-list.html @@ -32,7 +32,7 @@

    onLoad: highlightSearchResults(note), require: ['ace/ext/language_tools'] }" - ng-model="_" + ng-model="note.searchResult" > 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/package.json b/zeppelin-web/src/app/spell/package.json index 7003e062b5b..e6d6a15e9ea 100644 --- a/zeppelin-web/src/app/spell/package.json +++ b/zeppelin-web/src/app/spell/package.json @@ -1,7 +1,7 @@ { "name": "zeppelin-spell", "description": "Zeppelin Spell Framework", - "version": "0.8.0-SNAPSHOT", + "version": "0.8.1-SNAPSHOT", "main": "index", "dependencies": { }, 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..71ed7b86507 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 = sortWithNumberSupport(Object.keys(cube)); /** 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 = sortWithNumberSupport(Object.keys(cube)); /** 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 = sortWithNumberSupport(Object.keys(key1NameSet)); /** keys should be sorted */ + let key2Names = sortWithNumberSupport(Object.keys(key2NameSet)); /** keys should be sorted */ return { cube: cube, @@ -939,67 +1027,69 @@ 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 = sortWithNumberSupport(Object.keys(selectorNameWithIndex)); + 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, key2NameWithIndex: key2NameWithIndex, transformed: selectorRows, - groupNames: Array.from(groupNameSet).sort(), + groupNames: sortWithNumberSupport(Array.from(groupNameSet)), 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 = sortWithNumberSupport(Object.keys(selectorNameWithIndex)); + 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(), + groupNames: sortWithNumberSupport(Array.from(groupNameSet)), 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(), - } + sortedSelectors: sortWithNumberSupport(Object.keys(selectorNameWithIndex)), + groupNames: sortWithNumberSupport(Array.from(groupNameSet)), + }; } 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 = sortWithNumberSupport(Object.keys(selectorNameWithIndex)); + const sortedSelectorNameWithIndex = getNameWithIndex(sortedSelectors); - const rows = new Array(sortedSelectors.length) + 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] + 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,41 @@ 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; + } +} + +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 90f569fade4..28ce67d5d7d 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,1767 @@ * 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') + + 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/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 7983d827265..368254d3f22 100644 --- a/zeppelin-web/src/app/tabledata/networkdata.js +++ b/zeppelin-web/src/app/tabledata/networkdata.js @@ -12,134 +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.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} + ? 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}; - 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 || {}) }) - keys = _.flatten(keys) - keys = _.uniq(keys).filter(function(key) { - return baseCols.indexOf(key) === -1 - }) - let columnNames = baseColumnNames.concat(_.map(keys, function(elem, i) { - return {name: elem, index: i + baseColumnNames.length, aggr: 'sum'} - })) + 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'}); + } + 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); + 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 = [] - entity.data = entity.data || {} + 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 - ? entity[name] : entity.data[name] - let parsedValue = value === null || value === undefined ? '' : value - col.push(parsedValue) - col2.push({key: name, value: parsedValue}) + 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); } - rows.push(col) + rows.push(col); } - this.comment = comment - 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 + 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 f8d98a89a3f..cd3a12f29c8 100644 --- a/zeppelin-web/src/app/tabledata/networkdata.test.js +++ b/zeppelin-web/src/app/tabledata/networkdata.test.js @@ -12,35 +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) - }) - - expect(nd.columns.length).toBe(2) - 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) - }) -}) + 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); + }); + + 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/tabledata/package.json b/zeppelin-web/src/app/tabledata/package.json index 2eec9090f91..1ac1d38ed15 100644 --- a/zeppelin-web/src/app/tabledata/package.json +++ b/zeppelin-web/src/app/tabledata/package.json @@ -1,7 +1,7 @@ { "name": "zeppelin-tabledata", "description": "tabledata api", - "version": "0.8.0-SNAPSHOT", + "version": "0.8.1-SNAPSHOT", "main": "tabledata", "dependencies": { "json3": "~3.3.1", 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 1c938ea8280..2baa6b5c8d8 100644 --- a/zeppelin-web/src/app/tabledata/pivot.js +++ b/zeppelin-web/src/app/tabledata/pivot.js @@ -12,175 +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) + 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) ? 1 : parseFloat(a)) : 0 - let varB = (b !== undefined) ? (isNaN(b) ? 1 : 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 varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0 - let varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0 - return Math.min(varA, varB) + min: function(a, b) { + 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 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)); + } }, - 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) + 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) ? 1 : parseFloat(a)) : 0 - let varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0 - return varA + varB - } - } + }; + + let isValidNumber = function(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]) { @@ -188,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]) { @@ -211,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, + }; } } } @@ -257,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/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;"> -
  • +
  • + diff --git a/zeppelin-web/src/app/tabledata/tabledata.js b/zeppelin-web/src/app/tabledata/tabledata.js index 3fe01b7791c..67c47be45ed 100644 --- a/zeppelin-web/src/app/tabledata/tabledata.js +++ b/zeppelin-web/src/app/tabledata/tabledata.js @@ -11,65 +11,72 @@ * 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; + const float64MaxDigits = 16; 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}) + let valueOfCol; + if (!(col[0] === '0' || col[0] === '+' || col.length > float64MaxDigits)) { + if (!isNaN(valueOfCol = parseFloat(col)) && isFinite(col)) { + col = valueOfCol; + } + } + 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 7e41de4becf..931cc2d7801 100644 --- a/zeppelin-web/src/app/tabledata/tabledata.test.js +++ b/zeppelin-web/src/app/tabledata/tabledata.test.js @@ -12,30 +12,120 @@ * limitations under the License. */ -import TableData from './tabledata.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' - }) - - expect(td.columns.length).toBe(2) - expect(td.rows.length).toBe(2) - expect(td.comment).toBe('hello') - }) -}) + 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'); + }); +}); + +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); + }); + + 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); + }); +}); 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 506b1c5f186..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,90 +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; + } else { + return; } - console.log('Render Graph Visualization') + console.log('Rendering the graph'); - let transformationConfig = this.transformation.getSetting().scope.config - console.log('cfg', transformationConfig) + 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('') + 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) => { - 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 + 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 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. - let tick = () => { + 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})`); + }; - let setOpacity = (scale) => { - let opacity = scale >= +transformationConfig.d3Graph.zoom.minScale ? 1 : 0 + const setOpacity = (scale) => { + 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); + }; - let zoom = d3.behavior.zoom() + 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) @@ -126,52 +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(); - 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') .empty() - .append(html.join('')) - } + .append(html.join('')); + }; - let drag = d3.behavior.drag() + 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(); + }); - 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]) @@ -185,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 - let link = container.append('svg:g') + 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') - 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') + .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})`) - let textPath = link.append('svg:path') + .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()) @@ -216,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 - let circle = container.append('svg:g') + 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()) @@ -228,36 +243,96 @@ 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) - let text = container.append('svg:g').selectAll('g') + .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) { - let transformationConfig = this.transformation.getSetting().scope.config - let 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() { + } + + _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; } } diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-linechart.js b/zeppelin-web/src/app/visualization/builtins/visualization-linechart.js index 959efc8bd92..df161b98aa8 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-linechart.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-linechart.js @@ -12,38 +12,39 @@ * limitations under the License. */ -import Nvd3ChartVisualization from './visualization-nvd3chart' -import PivotTransformation from '../../tabledata/pivot' +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, @@ -52,99 +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) { return self.xAxisTickFormat(d, self.xLabels) }) - chart.yAxis.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); + } else { + 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) { 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) - } + 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 { @@ -163,19 +178,35 @@ export default class LinechartVisualization extends Nvd3ChartVisualization { ng-click="save()" /> zoom + +
    + + + + `, 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..831bf95ccd6 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-table.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-table.js @@ -12,19 +12,26 @@ * 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, - isInputWidget, isOptionWidget, isCheckboxWidget, - isTextareaWidget, isBtnGroupWidget, - initializeTableConfig, resetTableOptionConfig, - DefaultTableColumnType, TableColumnType, updateColumnTypeState, + DefaultTableColumnType, + initializeTableConfig, + isBtnGroupWidget, + isCheckboxWidget, + isInputWidget, + isOptionWidget, + isTextareaWidget, parseTableOption, -} from './visualization-util' + resetTableOptionConfig, + TableColumnType, + updateColumnTypeState, + ValueType, + Widget, +} 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 +55,47 @@ 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 + 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) + 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, @@ -93,8 +106,10 @@ export default class TableVisualization extends Visualization { flatEntityAccess: true, fastWatch: false, treeRowHeaderAlwaysVisible: false, + exporterExcelFilename: 'myFile.xlsx', - columnDefs: columnNames.map(colName => { + columnDefs: columnNames.map((colName) => { + const self = this; return { displayName: colName, name: colName, @@ -111,7 +126,19 @@ 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 */ enableRowHashing: true, @@ -126,260 +153,311 @@ 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); + const gridApiId = this.getGridApiId(); + const scope = this.getScope(); + if(scope[gridApiId]!==undefined) { + scope[gridApiId].core.handleWindowResize(); + } } } 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) + 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); + }); + } + } - const config = this.config - const self = this // for closure + render(tableData) { + const gridElemId = this.getGridElemId(); + let gridElem = document.getElementById(gridElemId); + + const config = this.config; + const self = this; // for closure + const scope = this.getScope(); + // set gridApi for this elem + const gridApiId = this.getGridApiId(); + const gridOptions = this.createGridOptions(tableData, onRegisterApiCallback, config); + + const onRegisterApiCallback = (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); + }); + + // 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) }) + }; if (!gridElem) { // create, compile and append grid elem gridElem = angular.element( `
    `) - - 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 - - // set gridApi for this elem - const gridApiId = this.getGridApiId() - const onRegisterApiCallback = (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) }) - - // 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 + ui-grid-exporter>`); + + gridElem.css('height', this.targetEl.height() - 10); + gridElem = this._compile(gridElem)(scope); + this.targetEl.append(gridElem); + this.setDynamicGridOptions(gridOptions, config); + this.addColumnMenus(gridOptions); + scope[gridElemId] = gridOptions; + 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() + scope[gridElemId] = gridOptions; + 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 +471,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..7feb129bc0e 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,179 @@ 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)) + 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] - 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/package.json b/zeppelin-web/src/app/visualization/package.json index 51a18149082..9bb70d5dd1e 100644 --- a/zeppelin-web/src/app/visualization/package.json +++ b/zeppelin-web/src/app/visualization/package.json @@ -1,7 +1,7 @@ { "name": "zeppelin-vis", "description": "Visualization API", - "version": "0.8.0-SNAPSHOT", + "version": "0.8.1-SNAPSHOT", "main": "visualization", "dependencies": { "json3": "~3.3.1", diff --git a/zeppelin-web/src/app/visualization/visualization.js b/zeppelin-web/src/app/visualization/visualization.js index 82704e3a00b..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,32 +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'); } /** * Method will be invoked when visualization need to be destroyed. * Don't need to destroy this.targetEl. */ - destroy () { + destroy() { // override this } @@ -64,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/assets/styles/looknfeel/report.css b/zeppelin-web/src/assets/styles/looknfeel/report.css index 8c850efc95f..1bd1d060013 100644 --- a/zeppelin-web/src/assets/styles/looknfeel/report.css +++ b/zeppelin-web/src/assets/styles/looknfeel/report.css @@ -58,6 +58,10 @@ body { visibility: hidden; } +.noteAction .form-control-title > 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..a6a721db369 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-title > span { + visibility: visible; +} + .noteAction:hover span, .noteAction:hover button, .noteAction:hover form { 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..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 - let note2 = v2.value + 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/loader/loader.service.js b/zeppelin-web/src/components/loader/loader.service.js new file mode 100644 index 00000000000..589bb761faf --- /dev/null +++ b/zeppelin-web/src/components/loader/loader.service.js @@ -0,0 +1,27 @@ +/* + * 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. + */ + +angular.module('zeppelinWebApp').service('loaderSrv', LoaderService); + +function LoaderService() { + 'ngInject'; + + this.showLoader = function() { + angular.element('#pre-loader').fadeIn(); + }; + + this.hideLoader = function() { + angular.element('#pre-loader').fadeOut(); + }; +} 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/login/login.html b/zeppelin-web/src/components/login/login.html index d4a94a6f85c..fd98bc8c4e5 100644 --- a/zeppelin-web/src/components/login/login.html +++ b/zeppelin-web/src/components/login/login.html @@ -29,6 +29,7 @@
    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..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) { - if (angular.element(element).find('.expandable:visible').length > 1) { - angular.element(element).find('.expandable:visible').slideUp('slow') - angular.element(element).find('i.fa-folder-open').toggleClass('fa-folder fa-folder-open') + 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'); } 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') + 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-note-list-elem.html b/zeppelin-web/src/components/navbar/navbar-note-list-elem.html index cb36cfaca9d..911f1f1f3e8 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 @@ - + diff --git a/zeppelin-web/src/components/navbar/navbar.controller.js b/zeppelin-web/src/components/navbar/navbar.controller.js index 0ac2f18bb56..7665bf8e4fa 100644 --- a/zeppelin-web/src/components/navbar/navbar.controller.js +++ b/zeppelin-web/src/components/navbar/navbar.controller.js @@ -12,156 +12,249 @@ * 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' + '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 + 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; + let revisionSupported = false; - $scope.query = {q: ''} + $scope.query = {q: ''}; - initController() + initController(); - function getZeppelinVersion () { + 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' + function logout() { + let logoutURL = baseUrlSrv.getRestApiBase() + '/login/logout'; + + $http.post(logoutURL).then(function() {}, function(response) { + if (response.data) { + 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(); + }); + } else { + window.location.href = res['redirectURL'] + window.location.href; + } + return undefined; + } + } - // for firefox and safari - logoutURL = logoutURL.replace('//', '//false:false@') - $http.post(logoutURL).error(function () { // force authcBasic (if configured) to logout - $http.post(logoutURL).error(function () { - $rootScope.userName = '' - $rootScope.ticket.principal = '' - $rootScope.ticket.screenUsername = '' - $rootScope.ticket.ticket = '' - $rootScope.ticket.roles = '' + if (detectIE()) { + let outcome; + try { + outcome = document.execCommand('ClearAuthenticationCache'); + } catch (e) { + console.log(e); + } + if (!outcome) { + // Let's create an xmlhttp object + 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(''); + // x.abort() + return 1; // this is **speculative** "We are done." + } else { + // eslint-disable-next-line no-useless-return + return; + } + })(window.XMLHttpRequest ? new window.XMLHttpRequest() + // eslint-disable-next-line no-undef + : (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); + } + } else { + // for firefox and safari + logoutURL = logoutURL.replace('//', '//false:false@'); + } + + $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 search (searchTerm) { - $location.path('/search/' + searchTerm) + function detectIE() { + 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); + } + + 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 edge = ua.indexOf('Edge/'); + if (edge > 0) { + // Edge (IE 12+) => return version number + return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); + } + + // other browser + return false; + } + + 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.calculateTooltipPlacement = function (note) { + $scope.loadMoreNotes = function() { + vm.numberOfNotesDisplayed += 10; + }; + + $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'; + }; + + $scope.$on('configurationsInfo', function(scope, event) { + // Server send this parameter is String + if(event.configurations['isRevisionSupported']==='true') { + revisionSupported = true; + } + }); + + $rootScope.isRevisionSupported = function() { + return revisionSupported; + }; } 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/navbar/navbar.html b/zeppelin-web/src/components/navbar/navbar.html index 597ed511e68..8f6b603dba6 100644 --- a/zeppelin-web/src/components/navbar/navbar.html +++ b/zeppelin-web/src/components/navbar/navbar.html @@ -46,13 +46,13 @@
  • -
    -
  • +
  • -
    -
  • +
  • @@ -99,7 +99,6 @@
  • Interpreter
  • Notebook Repos
  • -
  • Credential
  • Helium
  • Configuration
  • 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..83cb6df4f67 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) { + message: 'The folder will be merged into ' + _.escape(newFolderId) + '. Are you sure?', + 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 21abbc046d8..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,70 +12,73 @@ * limitations under the License. */ -angular.module('zeppelinWebApp').factory('noteListFactory', NoteListFactory) +angular.module('zeppelinWebApp').factory('noteListFactory', NoteListFactory); -function NoteListFactory(TRASH_FOLDER_ID) { - 'ngInject' +function NoteListFactory(arrayOrderingSrv, TRASH_FOLDER_ID) { + '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) - } - } + 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 58d5d420b5c..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[0].name).toBe('A') - expect(folderList[0].id).toBe('000001') - expect(folderList[1].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() - }) -}) + 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-name-filter/note-name-filter.html b/zeppelin-web/src/components/note-name-filter/note-name-filter.html index 071cba4e3ec..e1bcc94813c 100644 --- a/zeppelin-web/src/components/note-name-filter/note-name-filter.html +++ b/zeppelin-web/src/components/note-name-filter/note-name-filter.html @@ -11,9 +11,11 @@ See the License for the specific language governing permissions and limitations under the License. --> +
    +
    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 db058bbc68b..dd4f17b6703 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, ngToast) { + '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) - 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({ @@ -105,92 +105,98 @@ function WebsocketEventFactory ($rootScope, $websocket, $location, baseUrlSrv) { closeByBackdrop: false, closeByKeyboard: false, title: 'Insufficient privileges', - message: data.info.toString(), - buttons: btn - }) + message: _.escape(data.info.toString()), + 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); } else if (op === 'INTERPRETER_BINDINGS') { - $rootScope.$broadcast('interpreterBindings', data) + $rootScope.$broadcast('interpreterBindings', data); + } else if (op === 'SAVE_NOTE_FORMS') { + $rootScope.$broadcast('saveNoteForms', data); } else if (op === 'ERROR_INFO') { BootstrapDialog.show({ closable: false, closeByBackdrop: false, closeByKeyboard: false, title: 'Details', - message: data.info.toString(), + message: _.escape(data.info.toString()), 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 if (op === 'NOTICE') { + ngToast.info(data.notice); } 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 0dc02c3bfdc..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,149 +195,179 @@ 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) { - 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, - 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, + }, + }); }, - getEditorSetting: function (paragraphId, replName) { + getNoteByRevisionForCompare: function(noteId, revisionId, position) { + websocketEvents.sendNewEvent({ + op: 'NOTE_REVISION_FOR_COMPARE', + data: { + noteId: noteId, + revisionId: revisionId, + position: position, + }, + }); + }, + + 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) { + websocketEvents.sendNewEvent({op: 'SAVE_NOTE_FORMS', + data: { + noteId: note.id, + noteParams: note.noteParams, + }, + }); + }, + + removeNoteForms: function(note, formName) { + websocketEvents.sendNewEvent({op: 'REMOVE_NOTE_FORMS', + data: { + noteId: note.id, + formName: formName, + }, + }); }, - } + }; } diff --git a/zeppelin-web/src/index.html b/zeppelin-web/src/index.html index 4b43179bf43..ca1c56fd18e 100644 --- a/zeppelin-web/src/index.html +++ b/zeppelin-web/src/index.html @@ -68,8 +68,52 @@ + +
    +
    +
    +
    Loading ...
    +
    +
    @@ -165,6 +209,10 @@ + + + + diff --git a/zeppelin-web/src/index.js b/zeppelin-web/src/index.js index 3cf052bba69..19d2d3e1ef8 100644 --- a/zeppelin-web/src/index.js +++ b/zeppelin-web/src/index.js @@ -13,63 +13,66 @@ */ // 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/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 './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/loader/loader.service'; +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'; diff --git a/zeppelin-web/webpack.config.js b/zeppelin-web/webpack.config.js index 6fba4b1a303..3c349240ca1 100644 --- a/zeppelin-web/webpack.config.js +++ b/zeppelin-web/webpack.config.js @@ -120,6 +120,7 @@ module.exports = function makeWebpackConfig () { * Reference: http://webpack.github.io/docs/configuration.html#devtool * Type of sourcemap to use per build type */ + config.devtool = 'eval-source-map'; if (isTest) { config.devtool = 'inline-source-map'; } else if (isProd) { @@ -127,7 +128,6 @@ module.exports = function makeWebpackConfig () { } else { config.devtool = 'eval-source-map'; } - config.devtool = 'source-map'; /** * Loaders @@ -196,14 +196,6 @@ module.exports = function makeWebpackConfig () { } ]}) }], - postLoaders: [ - { - // COVERAGE - test: /\.js$/, - exclude: /(node_modules|bower_components|\.test\.js)/, - loader: 'istanbul-instrumenter' - } - ] }; /** @@ -250,6 +242,17 @@ module.exports = function makeWebpackConfig () { }) ) } + + if (isTest) { + config.module.postLoaders = [ + { + // COVERAGE + test: /\.js$/, + exclude: /(node_modules|bower_components|\.test\.js)/, + loader: 'istanbul-instrumenter' + } + ] + } // Add build specific plugins if (isProd) { diff --git a/zeppelin-zengine/pom.xml b/zeppelin-zengine/pom.xml index 38b1e830068..8637f1e3577 100644 --- a/zeppelin-zengine/pom.xml +++ b/zeppelin-zengine/pom.xml @@ -23,33 +23,36 @@ zeppelin org.apache.zeppelin - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 .. org.apache.zeppelin zeppelin-zengine jar - 0.8.0-SNAPSHOT + 0.8.2-mapr-1912-r2 Zeppelin: Zengine Zeppelin Zengine - 3.4 + 2.7.0-mapr-1808 + 3.7 2.0 + 1.14.0 1.10.62 - 4.0.0 + 2.1.4 1.5.2 2.2.1 5.3.1 0.9.8 1.4.01 - 4.1.1.201511131810-r + 4.5.4.201711221230-r 1.3 0.27 + 0.32.0-alpha @@ -69,11 +72,6 @@ slf4j-log4j12 - - commons-configuration - commons-configuration - - commons-io commons-io @@ -119,27 +117,81 @@ - com.amazonaws - aws-java-sdk-s3 - ${aws.sdk.s3.version} + 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.microsoft.azure - azure-storage - ${azure.storage.version} + com.google.http-client + google-http-client-jackson2 + 1.23.0 com.fasterxml.jackson.core jackson-core - org.slf4j - slf4j-api + com.google.code.findbugs + jsr305 + + + + + com.amazonaws + aws-java-sdk-s3 + ${aws.sdk.s3.version} + - org.apache.commons - commons-lang3 + joda-time + joda-time @@ -170,6 +222,7 @@ com.google.guava guava + 20.0 @@ -212,12 +265,6 @@ - - xml-apis - xml-apis - ${xml.apis.version} - - org.eclipse.jgit org.eclipse.jgit @@ -256,11 +303,34 @@ 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 ${google.truth.version} test + + + com.google.guava + guava + + @@ -290,17 +360,18 @@ ${commons.lang3.version} - - org.mongodb - mongo-java-driver - 3.4.1 - org.apache.commons commons-compress 1.5 + + org.mongodb + mongo-java-driver + 3.4.1 + + @@ -315,8 +386,526 @@ maven-surefire-plugin always + + ${project.build.directory}/tmp + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + + test-jar + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + + + hadoop2 + + + true + + + + + org.apache.hadoop + hadoop-client + ${hadoop.version} + provided + + + com.sun.jersey + jersey-core + + + com.sun.jersey + jersey-json + + + com.sun.jersey + jersey-client + + + 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.eclipse.jgit + org.eclipse.jgit + + + com.jcraft + jsch + + + org.apache.commons + commons-compress + + + xml-apis + xml-apis + + + xerces + xercesImpl + + + com.google.guava + guava + + + + + + org.apache.hadoop + hadoop-common + ${hadoop.version} + tests + test + + + com.sun.jersey + jersey-core + + + com.sun.jersey + jersey-json + + + com.sun.jersey + jersey-client + + + 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.eclipse.jgit + org.eclipse.jgit + + + com.jcraft + jsch + + + org.apache.commons + commons-compress + + + xml-apis + xml-apis + + + xerces + xercesImpl + + + org.codehaus.jackson + jackson-mapper-asl + + + org.codehaus.jackson + jackson-core-asl + + + com.google.guava + guava + + + + + + org.apache.hadoop + hadoop-hdfs + ${hadoop.version} + tests + test + + + com.sun.jersey + jersey-json + + + com.sun.jersey + jersey-client + + + javax.servlet + servlet-api + + + org.apache.avro + avro + + + org.apache.jackrabbit + jackrabbit-webdav + + + io.netty + netty + + + commons-httpclient + commons-httpclient + + + org.eclipse.jgit + org.eclipse.jgit + + + com.jcraft + jsch + + + org.apache.commons + commons-compress + + + xml-apis + xml-apis + + + xerces + xercesImpl + + + com.google.guava + guava + + + + + + org.apache.hadoop + hadoop-yarn-server-tests + ${hadoop.version} + tests + test + + + com.sun.jersey + jersey-core + + + com.sun.jersey + jersey-client + + + 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.eclipse.jgit + org.eclipse.jgit + + + com.jcraft + jsch + + + org.apache.commons + commons-compress + + + xml-apis + xml-apis + + + xerces + xercesImpl + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + org.codehaus.jackson + jackson-xc + + + org.codehaus.jackson + jackson-mapper-asl + + + com.google.guava + guava + + + + + + org.apache.hadoop + hadoop-azure + ${hadoop.version} + provided + + + 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} + provided + + + com.amazonaws + aws-java-sdk + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + joda-time + joda-time + + + + + + + + + hadoop3 + + 3.0.0 + + + + org.apache.hadoop + hadoop-client-api + ${hadoop.version} + + + org.apache.hadoop + hadoop-client-runtime + ${hadoop.version} + + + org.apache.hadoop + hadoop-client-minicluster + ${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.amazonaws + aws-java-sdk + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + joda-time + joda-time + + + + + + 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..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 @@ -16,14 +16,17 @@ */ package org.apache.zeppelin.helium; +import com.google.gson.Gson; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterNotFoundException; +import org.apache.zeppelin.interpreter.InterpreterSettingManager; +import org.apache.zeppelin.interpreter.ManagedInterpreterGroup; +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 +50,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(); } @@ -93,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)); @@ -335,8 +344,10 @@ private Map getPackagePersistedConfig(String artifact) { public HeliumPackageSuggestion suggestApp(Paragraph paragraph) { HeliumPackageSuggestion suggestion = new HeliumPackageSuggestion(); - Interpreter intp = paragraph.getCurrentRepl(); - if (intp == null) { + Interpreter intp = null; + try { + intp = paragraph.getBindedInterpreter(); + } catch (InterpreterNotFoundException e) { return suggestion; } @@ -350,7 +361,7 @@ public HeliumPackageSuggestion suggestApp(Paragraph paragraph) { allResources = resourcePool.getAll(); } } else { - allResources = ResourcePoolUtils.getAllResources(); + allResources = interpreterSettingManager.getAllResources(); } for (List pkgs : allPackages.values()) { @@ -478,4 +489,40 @@ private static Map> createMixedConfig(Map resourceList = remoteInterpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction>() { + @Override + public List call(RemoteInterpreterService.Client client) throws Exception { + return client.resourcePoolGetAll(); + } + } + ); + Gson gson = new Gson(); + for (String res : resourceList) { + resourceSet.add(gson.fromJson(res, Resource.class)); + } + } + } + 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..50928bb7ab1 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 @@ -16,7 +16,6 @@ */ package org.apache.zeppelin.helium; -import org.apache.thrift.TException; import org.apache.zeppelin.interpreter.*; import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; @@ -79,8 +78,8 @@ public LoadApplication(ApplicationState appState, HeliumPackage pkg, Paragraph p public void run() { try { // get interpreter process - Interpreter intp = paragraph.getRepl(paragraph.getRequiredReplName()); - InterpreterGroup intpGroup = intp.getInterpreterGroup(); + Interpreter intp = paragraph.getBindedInterpreter(); + ManagedInterpreterGroup intpGroup = (ManagedInterpreterGroup) intp.getInterpreterGroup(); RemoteInterpreterProcess intpProcess = intpGroup.getRemoteInterpreterProcess(); if (intpProcess == null) { throw new ApplicationException("Target interpreter process is not running"); @@ -105,38 +104,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,44 +193,39 @@ 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( "Can't unload application status " + appsToUnload.getStatus()); } appStatusChange(paragraph, appsToUnload.getId(), ApplicationState.Status.UNLOADING); - Interpreter intp = paragraph.getCurrentRepl(); - if (intp == null) { - throw new ApplicationException("No interpreter found"); + Interpreter intp = null; + try { + intp = paragraph.getBindedInterpreter(); + } catch (InterpreterException e) { + throw new ApplicationException("No interpreter found", e); } RemoteInterpreterProcess intpProcess = - intp.getInterpreterGroup().getRemoteInterpreterProcess(); + ((ManagedInterpreterGroup) intp.getInterpreterGroup()).getRemoteInterpreterProcess(); if (intpProcess == null) { 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,46 +275,38 @@ 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( "Can't run application status " + app.getStatus()); } - Interpreter intp = paragraph.getCurrentRepl(); - if (intp == null) { - throw new ApplicationException("No interpreter found"); + Interpreter intp = null; + try { + intp = paragraph.getBindedInterpreter(); + } catch (InterpreterException e) { + throw new ApplicationException("No interpreter found", e); } RemoteInterpreterProcess intpProcess = - intp.getInterpreterGroup().getRemoteInterpreterProcess(); + ((ManagedInterpreterGroup) intp.getInterpreterGroup()).getRemoteInterpreterProcess(); 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()); } } } @@ -440,7 +421,13 @@ public void onNoteCreate(Note note) { @Override public void onUnbindInterpreter(Note note, InterpreterSetting setting) { for (Paragraph p : note.getParagraphs()) { - Interpreter currentInterpreter = p.getCurrentRepl(); + Interpreter currentInterpreter = null; + try { + currentInterpreter = p.getBindedInterpreter(); + } catch (InterpreterNotFoundException e) { + logger.warn("Not interpreter found", e); + return; + } List infos = setting.getInterpreterInfos(); for (InterpreterInfo info : infos) { if (currentInterpreter != null && 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; 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..7d1df9b6083 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ConfInterpreter.java @@ -0,0 +1,95 @@ +/* + * 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); + + protected String sessionId; + protected String interpreterGroupId; + protected InterpreterSetting interpreterSetting; + + + public ConfInterpreter(Properties properties, + String sessionId, + String interpreterGroupId, + InterpreterSetting interpreterSetting) { + super(properties); + this.sessionId = sessionId; + 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/InterpreterFactory.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java index 9403b4f34e2..20b1c2be7af 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterFactory.java @@ -17,288 +17,31 @@ 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.apache.commons.lang.StringUtils; 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; /** + * //TODO(zjffdu) considering to move to InterpreterSettingManager + * * 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; +public class InterpreterFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(InterpreterFactory.class); 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; + public InterpreterFactory(InterpreterSettingManager interpreterSettingManager) { 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"); + Preconditions.checkNotNull(group, "group should be not null"); for (InterpreterSetting setting : settings) { if (group.equals(setting.getName())) { return setting; @@ -307,117 +50,60 @@ private InterpreterSetting getInterpreterSettingByGroup(List 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) { + public Interpreter getInterpreter(String user, String noteId, String replName) + throws InterpreterNotFoundException { List settings = interpreterSettingManager.getInterpreterSettings(noteId); InterpreterSetting setting; Interpreter interpreter; if (settings == null || settings.size() == 0) { - return null; + throw new InterpreterNotFoundException("No interpreter is binded to this note: " + noteId); } - 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); + 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 = null; - String name = null; - group = replNameSplit[0]; - name = replNameSplit[1]; - + String group = replNameSplit[0]; + String name = replNameSplit[1]; setting = getInterpreterSettingByGroup(settings, group); - if (null != setting) { - interpreter = getInterpreter(user, noteId, setting, name); - + interpreter = setting.getInterpreter(user, noteId, name); if (null != interpreter) { return interpreter; } + throw new InterpreterNotFoundException("No such interpreter: " + replName); } - - throw new InterpreterException(replName + " interpreter not found"); - - } else { + throw new InterpreterNotFoundException("Interpreter " + group + + " is not binded to this note"); + } else if (replNameSplit.length == 1){ // 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); + 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) + // next, assume replName is 'group' of interpreter ('name' is omitted) // 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 setting.getDefaultInterpreter(user, noteId); + } else { + throw new InterpreterNotFoundException("Either no interpreter named " + replName + + " or it is not binded to this note"); } } - return null; + throw new InterpreterNotFoundException("No such interpreter " + replName + " for note " + + noteId); } - - 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/InterpreterInfoSaving.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java index ca688dcb6e7..3d9c2c348a3 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterInfoSaving.java @@ -19,22 +19,73 @@ 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.apache.zeppelin.common.JsonSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.sonatype.aether.repository.RemoteRepository; -import java.util.List; -import java.util.Map; +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 implements JsonSerializable { - private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + 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()); - public Map interpreterSettings; - public Map> interpreterBindings; - public List interpreterRepositories; + if (infoSaving != null && infoSaving.interpreterSettings != null) { + for (InterpreterSetting interpreterSetting : infoSaving.interpreterSettings.values()) { + 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); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterNotFoundException.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterNotFoundException.java new file mode 100644 index 00000000000..192e8221664 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterNotFoundException.java @@ -0,0 +1,22 @@ +package org.apache.zeppelin.interpreter; + +/** + * Exception for no interpreter is found + */ +public class InterpreterNotFoundException extends InterpreterException { + + public InterpreterNotFoundException() { + } + + public InterpreterNotFoundException(String message) { + super(message); + } + + public InterpreterNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public InterpreterNotFoundException(Throwable cause) { + super(cause); + } +} 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 752b4e28824..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 @@ -17,8 +17,40 @@ package org.apache.zeppelin.interpreter; -import java.util.Arrays; -import java.util.Collection; +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.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.launcher.InterpreterLaunchContext; +import org.apache.zeppelin.interpreter.launcher.InterpreterLauncher; +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; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; @@ -26,104 +58,314 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; 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; +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; /** - * Interpreter settings + * Represent one InterpreterSetting in the interpreter setting page */ public class InterpreterSetting { - private static final Logger logger = LoggerFactory.getLogger(InterpreterSetting.class); + 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; - // always be null in case of InterpreterSettingRef + // the original interpreter setting template name where it is created from 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; + //TODO(zjffdu) make the interpreter.json consistent with interpreter-setting.json /** - * properties can be either Map or - * Map + * properties can be either Properties or Map * properties should be: - * - Map when Interpreter instances are saved to - * `conf/interpreter.json` file - * - Map when Interpreters are registered + * - 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; + private Object properties = new 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; + + private List dependencies = new ArrayList<>(); + private InterpreterOption option = new InterpreterOption(); @SerializedName("runner") private InterpreterRunner interpreterRunner; - @Deprecated - private transient InterpreterGroupFactory interpreterGroupFactory; + /////////////////////////////////////////////////////////////////////////////////////////// + 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(); + + // TODO(zjffdu) ShellScriptLauncher is the only launcher implemention for now. It could be other + // 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; + /////////////////////////////////////////////////////////////////////////////////////////// + + + + private transient RecoveryStorage recoveryStorage; + /////////////////////////////////////////////////////////////////////////////////////////// + + /** + * 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 Builder setLifecycleManager(LifecycleManager lifecycleManager) { + interpreterSetting.lifecycleManager = lifecycleManager; + return this; + } + + public Builder setRecoveryStorage(RecoveryStorage recoveryStorage) { + interpreterSetting.recoveryStorage = recoveryStorage; + 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(); } - 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; + void postProcessing() { 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); + this.id = this.name; + 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. + } + } } /** - * Create interpreter from interpreterSettingRef + * Create interpreter from InterpreterSettingTemplate * - * @param o interpreterSetting from interpreterSettingRef + * @param o interpreterSetting from InterpreterSettingTemplate */ public InterpreterSetting(InterpreterSetting o) { - this(generateId(), o.getName(), o.getGroup(), o.getInterpreterInfos(), o.getProperties(), - o.getDependencies(), o.getOption(), o.getPath(), o.getInterpreterRunner()); + this(); + this.id = o.name; + 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(); + } + + private void createLauncher() { + if (group.equals("spark")) { + this.launcher = new SparkInterpreterLauncher(this.conf, this.recoveryStorage); + } else { + this.launcher = new ShellScriptLauncher(this.conf, this.recoveryStorage); + } + } + + 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 InterpreterSetting setAngularObjectRegistryListener(AngularObjectRegistryListener + angularObjectRegistryListener) { + this.angularObjectRegistryListener = angularObjectRegistryListener; + return this; + } + + public InterpreterSetting setAppEventListener(ApplicationEventListener appEventListener) { + this.appEventListener = appEventListener; + return this; + } + + public InterpreterSetting setRemoteInterpreterProcessListener(RemoteInterpreterProcessListener + remoteInterpreterProcessListener) { + this.remoteInterpreterProcessListener = remoteInterpreterProcessListener; + return this; + } + + public InterpreterSetting setDependencyResolver(DependencyResolver dependencyResolver) { + this.dependencyResolver = dependencyResolver; + return this; + } + + public InterpreterSetting setInterpreterSettingManager( + InterpreterSettingManager interpreterSettingManager) { + this.interpreterSettingManager = interpreterSettingManager; + return this; + } + + public InterpreterSetting setLifecycleManager(LifecycleManager lifecycleManager) { + this.lifecycleManager = lifecycleManager; + return this; + } + + public InterpreterSetting setRecoveryStorage(RecoveryStorage recoveryStorage) { + this.recoveryStorage = recoveryStorage; + return this; + } + + public RecoveryStorage getRecoveryStorage() { + return recoveryStorage; + } + + public LifecycleManager getLifecycleManager() { + return lifecycleManager; } public String getId() { @@ -138,10 +380,9 @@ public String getGroup() { return group; } - private String getInterpreterProcessKey(String user, String noteId) { - InterpreterOption option = getOption(); + private String getInterpreterGroupId(String user, String noteId) { String key; - if (getOption().isExistingProcess) { + if (option.isExistingProcess) { key = Constants.EXISTING_PROCESS; } else if (getOption().isProcess()) { key = (option.perUserIsolated() ? user : "") + ":" + (option.perNoteIsolated() ? noteId : ""); @@ -149,40 +390,11 @@ private String getInterpreterProcessKey(String user, String noteId) { 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); - } + //TODO(zjffdu) we encode interpreter setting id into groupId, this is not a good design + return id + ":" + key; } - String getInterpreterSessionKey(String user, String noteId) { - InterpreterOption option = getOption(); + private String getInterpreterSessionId(String user, String noteId) { String key; if (option.isExistingProcess()) { key = Constants.EXISTING_PROCESS; @@ -193,120 +405,160 @@ String getInterpreterSessionKey(String user, String noteId) { } else if (option.perNoteScoped()) { key = noteId; } else { - key = "shared_session"; + 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()); + public ManagedInterpreterGroup 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); + ManagedInterpreterGroup intpGroup = createInterpreterGroup(groupId); + interpreterGroups.put(groupId, intpGroup); + } + return interpreterGroups.get(groupId); + } finally { + interpreterGroupWriteLock.unlock();; + } + } + void removeInterpreterGroup(String groupId) { + try { interpreterGroupWriteLock.lock(); - logger.debug("create interpreter group with groupId:" + interpreterGroupId); - interpreterGroupRef.put(key, intpGroup); + this.interpreterGroups.remove(groupId); + } finally { interpreterGroupWriteLock.unlock(); } + } + + public ManagedInterpreterGroup getInterpreterGroup(String user, String noteId) { + String groupId = getInterpreterGroupId(user, noteId); try { interpreterGroupReadLock.lock(); - return interpreterGroupRef.get(key); + return interpreterGroups.get(groupId); } finally { - interpreterGroupReadLock.unlock(); + interpreterGroupReadLock.unlock();; } } - public Collection getAllInterpreterGroups() { + ManagedInterpreterGroup getInterpreterGroup(String groupId) { + return interpreterGroups.get(groupId); + } + + public ArrayList getAllInterpreterGroups() { try { interpreterGroupReadLock.lock(); - return new LinkedList<>(interpreterGroupRef.values()); + return new ArrayList(interpreterGroups.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); + Map getEditorFromSettingByClassName(String className) { + for (InterpreterInfo intpInfo : interpreterInfos) { + if (className.equals(intpInfo.getClassName())) { + if (intpInfo.getEditor() == null) { + break; + } + return intpInfo.getEditor(); } - groupToRemove.clear(); } + return DEFAULT_EDITOR; + } - //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 closeInterpreters(String user, String noteId) { + ManagedInterpreterGroup interpreterGroup = getInterpreterGroup(user, noteId); + if (interpreterGroup != null) { + String sessionId = getInterpreterSessionId(user, noteId); + interpreterGroup.close(sessionId); + } } - 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); - } + public void close() { + LOGGER.info("Close InterpreterSetting: " + name); + for (ManagedInterpreterGroup intpGroup : interpreterGroups.values()) { + intpGroup.close(); } + interpreterGroups.clear(); + this.runtimeInfosToBeCleared = null; + this.infos = null; } - void shutdownAndRemoveAllInterpreterGroups() { - for (InterpreterGroup interpreterGroup : interpreterGroupRef.values()) { - interpreterGroup.shutdown(); + 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; } - 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); + @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()) { + if (entry.getValue().getValue() != null) { + jProperties.setProperty(entry.getKey(), entry.getValue().getValue().toString()); } } - return p; + + 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 InterpreterSetting setConf(ZeppelinConfiguration conf) { + this.conf = conf; + return this; } public List getDependencies() { - if (dependencies == null) { - return new LinkedList<>(); - } return dependencies; } public void setDependencies(List dependencies) { this.dependencies = dependencies; + loadInterpreterDependencies(); } public InterpreterOption getOption() { - if (option == null) { - option = new InterpreterOption(); - } - return option; } @@ -314,35 +566,32 @@ public void setOption(InterpreterOption option) { this.option = option; } - public String getPath() { - return path; + public String getInterpreterDir() { + return interpreterDir; } - public void setPath(String path) { - this.path = path; + public void setInterpreterDir(String interpreterDir) { + this.interpreterDir = interpreterDir; } 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); } } + loadInterpreterDependencies(); } void setInterpreterOption(InterpreterOption interpreterOption) { this.option = interpreterOption; } - public void setProperties(Map p) { + public void setProperties(Properties p) { this.properties = p; } @@ -379,6 +628,10 @@ public void setErrorReason(String errorReason) { this.errorReason = errorReason; } + public void setInterpreterInfos(List interpreterInfos) { + this.interpreterInfos = interpreterInfos; + } + public void setInfos(Map infos) { this.infos = infos; } @@ -415,7 +668,189 @@ public void clearNoteIdAndParaMap() { runtimeInfosToBeCleared = null; } - // For backward compatibility of interpreter.json format after ZEPPELIN-2654 + + //////////////////////////// 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 interpreterGroupId, String sessionId) { + List interpreters = new ArrayList<>(); + List interpreterInfos = getInterpreterInfos(); + Properties intpProperties = getJavaProperties(); + for (InterpreterInfo info : interpreterInfos) { + Interpreter interpreter = new RemoteInterpreter(intpProperties, sessionId, + info.getClassName(), user, lifecycleManager); + if (info.isDefaultInterpreter()) { + interpreters.add(0, interpreter); + } else { + interpreters.add(interpreter); + } + LOGGER.info("Interpreter {} created for user: {}, sessionId: {}", + interpreter.getClassName(), user, sessionId); + } + + // TODO(zjffdu) this kind of hardcode is ugly. For now SessionConfInterpreter is used + // for livy, we could add new property in interpreter-setting.json when there's new interpreter + // require SessionConfInterpreter + if (group.equals("livy")) { + interpreters.add( + new SessionConfInterpreter(intpProperties, sessionId, interpreterGroupId, this)); + } else { + interpreters.add(new ConfInterpreter(intpProperties, sessionId, interpreterGroupId, this)); + } + return interpreters; + } + + synchronized RemoteInterpreterProcess createInterpreterProcess(String interpreterGroupId, + String userName, + Properties properties) + throws IOException { + if (launcher == null) { + createLauncher(); + } + InterpreterLaunchContext launchContext = new + InterpreterLaunchContext(properties, option, interpreterRunner, userName, + interpreterGroupId, id, group, name); + RemoteInterpreterProcess process = (RemoteInterpreterProcess) launcher.launch(launchContext); + process.setRemoteInterpreterEventPoller( + new RemoteInterpreterEventPoller(remoteInterpreterProcessListener, appEventListener)); + recoveryStorage.onInterpreterClientStart(process); + return process; + } + + List getOrCreateSession(String user, String noteId) { + ManagedInterpreterGroup 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(); + } + } + //TODO(zjffdu) It requires user can not create interpreter with name `conf`, + // conf is a reserved word of interpreter name + if (replName.equals("conf")) { + if (group.equals("livy")) { + return SessionConfInterpreter.class.getName(); + } else { + return ConfInterpreter.class.getName(); + } + } + return null; + } + + private ManagedInterpreterGroup createInterpreterGroup(String groupId) { + AngularObjectRegistry angularObjectRegistry; + ManagedInterpreterGroup interpreterGroup = new ManagedInterpreterGroup(groupId, this); + angularObjectRegistry = + new RemoteAngularObjectRegistry(groupId, angularObjectRegistryListener, interpreterGroup); + interpreterGroup.setAngularObjectRegistry(angularObjectRegistry); + 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); + Thread t = new Thread() { + public void run() { + try { + // dependencies to prevent library conflict + File localRepoDir = new File(conf.getInterpreterLocalRepoPath() + "/" + id); + 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"); @@ -434,26 +869,71 @@ public void convertPermissionsFromUsersToOwners(JsonObject jsonObject) { } // For backward compatibility of interpreter.json format after ZEPPELIN-2403 - public void convertFlatPropertiesToPropertiesWithWidgets() { - StringMap newProperties = new StringMap(); + 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)) { - StringMap newProperty = new StringMap(); - newProperty.put("name", entry.getKey()); - newProperty.put("value", entry.getValue()); - newProperty.put("type", InterpreterPropertyType.TEXTAREA.getValue()); + InterpreterProperty newProperty = new InterpreterProperty( + entry.getKey().toString(), + entry.getValue(), + InterpreterPropertyType.STRING.getValue()); newProperties.put(entry.getKey().toString(), newProperty); } else { // already converted - return; + 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.containsKey("type") ? stringMap.get("type").toString() : "string"); + + 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 if (value instanceof String) { + InterpreterProperty newProperty = new InterpreterProperty( + key, + value, + "string"); + + newProperties.put(newProperty.getName(), newProperty); + } 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()); + } - this.properties = newProperties; + public void waitForReady() throws InterruptedException { + while (getStatus().equals( + org.apache.zeppelin.interpreter.InterpreterSetting.Status.DOWNLOADING_DEPENDENCIES)) { + Thread.sleep(200); } } } 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 12545d6ba69..2a36a64559d 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 @@ -17,271 +17,299 @@ package org.apache.zeppelin.interpreter; -import java.io.BufferedReader; +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.common.collect.Sets; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import java.util.Set; +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.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.apache.zeppelin.storage.ConfigStorage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonatype.aether.repository.Proxy; +import org.sonatype.aether.repository.RemoteRepository; +import org.sonatype.aether.repository.Authentication; + 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.Arrays; 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 + * 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 { +public class InterpreterSettingManager implements InterpreterSettingManagerMBean { - private static final Logger logger = LoggerFactory.getLogger(InterpreterSettingManager.class); - private static final String SHARED_SESSION = "shared_session"; + 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 zeppelinConfiguration; + private final ZeppelinConfiguration conf; private final Path interpreterDirPath; - private final Path interpreterBindingPath; /** - * This is only references with default settings, name and properties - * key: InterpreterSetting.name + * This is only InterpreterSetting templates with default name and properties + * name --> InterpreterSetting */ - private final Map interpreterSettingsRef; + private final Map interpreterSettingTemplates = + Maps.newConcurrentMap(); /** * This is used by creating and running Interpreters - * key: InterpreterSetting.id <- This is becuase backward compatibility + * id --> InterpreterSetting + * TODO(zjffdu) change it to name --> InterpreterSetting */ - private final Map interpreterSettings; - private final Map> interpreterBindings; - - private final DependencyResolver dependencyResolver; - private final List interpreterRepositories; - - private final InterpreterOption defaultOption; - - private final Map cleanCl; + private final Map interpreterSettings = + Maps.newConcurrentMap(); - @Deprecated - private String[] interpreterClassList; - private String[] interpreterGroupOrderList; - private InterpreterGroupFactory interpreterGroupFactory; + /** + * 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; + private LifecycleManager lifecycleManager; + private RecoveryStorage recoveryStorage; + private ConfigStorage configStorage; + 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; + AngularObjectRegistryListener angularObjectRegistryListener, + RemoteInterpreterProcessListener + remoteInterpreterProcessListener, + ApplicationEventListener appEventListener) + throws IOException { + this(zeppelinConfiguration, new InterpreterOption(), + angularObjectRegistryListener, + remoteInterpreterProcessListener, + appEventListener, + ConfigStorage.getInstance(zeppelinConfiguration)); + } + + public InterpreterSettingManager(ZeppelinConfiguration conf, + InterpreterOption defaultOption, + AngularObjectRegistryListener angularObjectRegistryListener, + RemoteInterpreterProcessListener + remoteInterpreterProcessListener, + ApplicationEventListener appEventListener, + ConfigStorage configStorage) throws IOException { + this.conf = conf; + this.defaultOption = defaultOption; + this.interpreterDirPath = Paths.get(conf.getInterpreterDir()); + LOGGER.debug("InterpreterRootPath: {}", interpreterDirPath); + 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; + 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()); + + this.configStorage = configStorage; - 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(","); + init(); + } - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.setPrettyPrinting(); - this.gson = gsonBuilder.create(); - init(); + private void initInterpreterSetting(InterpreterSetting interpreterSetting) { + interpreterSetting.setConf(conf) + .setInterpreterSettingManager(this) + .setAngularObjectRegistryListener(angularObjectRegistryListener) + .setRemoteInterpreterProcessListener(remoteInterpreterProcessListener) + .setAppEventListener(appEventListener) + .setDependencyResolver(dependencyResolver) + .setLifecycleManager(lifecycleManager) + .setRecoveryStorage(recoveryStorage) + .postProcessing(); } /** - * Remember this method doesn't keep current connections after being called + * Load interpreter setting from interpreter.json */ - private void loadFromFile() { - if (!Files.exists(interpreterBindingPath)) { - // nothing to read + private void loadFromFile() throws IOException { + InterpreterInfoSaving infoSaving = + configStorage.loadInterpreterSettings(); + if (infoSaving == null) { + // it is fresh zeppelin instance if there's no interpreter.json, just create interpreter + // setting from interpreterSettingTemplates + for (InterpreterSetting interpreterSettingTemplate : interpreterSettingTemplates.values()) { + InterpreterSetting interpreterSetting = new InterpreterSetting(interpreterSettingTemplate); + initInterpreterSetting(interpreterSetting); + interpreterSettings.put(interpreterSetting.getId(), interpreterSetting); + } 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)); + + // update interpreter binding first as we change interpreter setting id in ZEPPELIN-3208. + Map> newBindingMap = new HashMap<>(); + for (Map.Entry> entry : infoSaving.interpreterBindings.entrySet()) { + String noteId = entry.getKey(); + List oldSettingIdList = entry.getValue(); + List newSettingIdList = new ArrayList<>(); + for (String oldId : oldSettingIdList) { + if (infoSaving.interpreterSettings.containsKey(oldId)) { + newSettingIdList.add(infoSaving.interpreterSettings.get(oldId).getName()); } - 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; + } + newBindingMap.put(noteId, newSettingIdList); + } + interpreterBindings.putAll(newBindingMap); + + //TODO(zjffdu) still ugly (should move all to InterpreterInfoSaving) + for (InterpreterSetting savedInterpreterSetting : infoSaving.interpreterSettings.values()) { + savedInterpreterSetting.setProperties(InterpreterSetting.convertInterpreterProperties( + savedInterpreterSetting.getProperties() + )); + initInterpreterSetting(savedInterpreterSetting); + + 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())); + Map savedProperties = InterpreterSetting + .convertInterpreterProperties(savedInterpreterSetting.getProperties()); + for (Map.Entry entry : savedProperties.entrySet()) { + // only merge properties whose value is not empty + if (entry.getValue().getValue() != null && ! + StringUtils.isBlank(entry.getValue().toString())) { + mergedProperties.put(entry.getKey(), entry.getValue()); + } } - String depClassPath = interpreterSettingObject.getPath(); - setting.setPath(depClassPath); - - for (InterpreterInfo info : infos) { - if (info.getEditor() == null) { - Map editor = getEditorFromSettingByClassName(interpreterSettingObject, - info.getClassName()); - info.setEditor(editor); + savedInterpreterSetting.setProperties(mergedProperties); + // merge InterpreterInfo + savedInterpreterSetting.setInterpreterInfos( + interpreterSettingTemplate.getInterpreterInfos()); + savedInterpreterSetting.setInterpreterRunner( + interpreterSettingTemplate.getInterpreterRunner()); + } else { + LOGGER.warn("No InterpreterSetting Template found for InterpreterSetting: " + + savedInterpreterSetting.getGroup() + ", but it is found in interpreter.json, " + + "it would be skipped."); + // also delete its binding + for (Map.Entry> entry : interpreterBindings.entrySet()) { + List ids = entry.getValue(); + Iterator iter = ids.iterator(); + while (iter.hasNext()) { + if (iter.next().equals(savedInterpreterSetting.getId())) { + iter.remove(); + } } } - - setting.setInterpreterGroupFactory(interpreterGroupFactory); - - loadInterpreterDependencies(setting); - interpreterSettings.put(k, setting); + continue; } - interpreterBindings.putAll(infoSaving.interpreterBindings); - - if (infoSaving.interpreterRepositories != null) { - for (RemoteRepository repo : infoSaving.interpreterRepositories) { - if (!dependencyResolver.getRepos().contains(repo)) { - this.interpreterRepositories.add(repo); - } + // 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()); } } - } catch (IOException e) { - e.printStackTrace(); + savedInterpreterSetting.postProcessing(); + LOGGER.info("Create Interpreter Setting {} from interpreter.json", + savedInterpreterSetting.getName()); + interpreterSettings.put(savedInterpreterSetting.getId(), savedInterpreterSetting); } - } - 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); + if (infoSaving.interpreterRepositories != null) { + for (RemoteRepository repo : infoSaving.interpreterRepositories) { + if (!dependencyResolver.getRepos().contains(repo)) { + this.interpreterRepositories.add(repo); + } + } - 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); + // force interpreter dependencies loading once the + // repositories have been loaded. + for (InterpreterSetting setting : interpreterSettings.values()) { + setting.setDependencies(setting.getDependencies()); } } - - 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; + public void saveToFile() throws IOException { + InterpreterInfoSaving info = new InterpreterInfoSaving(); + info.interpreterBindings = interpreterBindings; + info.interpreterSettings = Maps.newHashMap(interpreterSettings); + info.interpreterRepositories = interpreterRepositories; + configStorage.save(info); } - private void init() throws InterpreterException, IOException, RepositoryException { - String interpreterJson = zeppelinConfiguration.getInterpreterJson(); - ClassLoader cl = Thread.currentThread().getContextClassLoader(); + 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() { @@ -291,234 +319,160 @@ 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}/ * 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 - } - } + LOGGER.warn("No interpreter-setting.json found in " + interpreterDirString); } } } - } - - 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()); + } else { + LOGGER.warn("InterpreterDir {} doesn't exist", interpreterDirPath); } loadFromFile(); + saveToFile(); + } - // 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(); - } + public RemoteInterpreterProcessListener getRemoteInterpreterProcessListener() { + return remoteInterpreterProcessListener; + } - for (String settingId : interpreterSettings.keySet()) { - InterpreterSetting setting = interpreterSettings.get(settingId); - logger.info("InterpreterSetting group {} : id={}, name={}", setting.getGroup(), settingId, - setting.getName()); - } + public ApplicationEventListener getAppEventListener() { + return appEventListener; } private boolean registerInterpreterFromResource(ClassLoader cl, String interpreterDir, - String interpreterJson) throws IOException, RepositoryException { + String interpreterJson) throws IOException { URL[] urls = recursiveBuildLibList(new File(interpreterDir)); - ClassLoader tempClassLoader = new URLClassLoader(urls, cl); + ClassLoader tempClassLoader = new URLClassLoader(urls, null); - Enumeration interpreterSettings = tempClassLoader.getResources(interpreterJson); - if (!interpreterSettings.hasMoreElements()) { + URL url = tempClassLoader.getResource(interpreterJson); + if (url == null) { 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); - } - } + + 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, RepositoryException { + throws IOException { Path interpreterJsonPath = Paths.get(interpreterDir, interpreterJson); if (Files.exists(interpreterJsonPath)) { - logger.debug("Reading {}", interpreterJsonPath); + LOGGER.debug("Reading interpreter-setting.json from file {}", interpreterJsonPath); List registeredInterpreterList = - getInterpreterListFromJson(interpreterJsonPath); - registerInterpreters(registeredInterpreterList, interpreterDir); + getInterpreterListFromJson(new FileInputStream(interpreterJsonPath.toFile())); + registerInterpreterSetting(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 { + 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 - 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); - } - + 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: {}", + interpreterSettingTemplate.getName()); + interpreterSettingTemplates.put(interpreterSettingTemplate.getName(), + interpreterSettingTemplate); + } + + @VisibleForTesting public InterpreterSetting getDefaultInterpreterSetting(String noteId) { - return getDefaultInterpreterSetting(getInterpreterSettings(noteId)); + List allInterpreterSettings = getInterpreterSettings(noteId); + return allInterpreterSettings.size() > 0 ? allInterpreterSettings.get(0) : null; } 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); + List settings = new ArrayList<>(); + 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; } - private List getNoteInterpreterSettingBinding(String noteId) { - LinkedList bindings = new LinkedList<>(); - List settingIds = interpreterBindings.get(noteId); - if (settingIds != null) { - bindings.addAll(settingIds); + public InterpreterSetting getInterpreterSettingByName(String name) { + try { + for (InterpreterSetting setting : interpreterSettings.values()) { + if (setting.getName().equals(name)) { + return setting; + } + } + throw new RuntimeException("No such interpreter setting: " + name); + } finally { } - 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())); + public ManagedInterpreterGroup getInterpreterGroupById(String groupId) { + for (InterpreterSetting setting : interpreterSettings.values()) { + ManagedInterpreterGroup interpreterGroup = setting.getInterpreterGroup(groupId); + if (interpreterGroup != null) { + return interpreterGroup; + } } - return properties; + 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; @@ -532,130 +486,168 @@ public Map getEditorSetting(Interpreter interpreter, String user group = replNameSplit[0]; } // when replName is 'name' of interpreter - if (defaultSettingName.equals(intpSetting.getName())) { - editor = getEditorFromSettingByClassName(intpSetting, interpreter.getClassName()); + if (intpSetting.getName().equals(defaultSettingName)) { + 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 = getEditorFromSettingByClassName(intpSetting, interpreter.getClassName()); + 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"); + 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) { + 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); + } - if (className.equals(intpInfo.getClassName())) { - if (intpInfo.getEditor() == null) { - break; + private ResourceSet getAllResourcesExcept(String interpreterGroupExcludsion) { + ResourceSet resourceSet = new ResourceSet(); + for (ManagedInterpreterGroup 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 intpInfo.getEditor(); } } - return DEFAULT_EDITOR; + return resourceSet; } - 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); - } - } + public RecoveryStorage getRecoveryStorage() { + return recoveryStorage; + } - // 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())); - } + public void removeResourcesBelongsToParagraph(String noteId, String paragraphId) { + for (ManagedInterpreterGroup 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)); + } - 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); - } + if (noteId != null) { + resourceSet = resourceSet.filterByNoteId(noteId); } - }; - t.start(); + 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 + * 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())); - } + 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 { - interpreterSettings.put(setting.getId(), setting); } + 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(); - } + } + }; + t.start(); } /** @@ -663,220 +655,96 @@ public void run() { * 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; - } + public List getInterpreterSettingIds() { + List settingIdList = new ArrayList<>(); + for (InterpreterSetting interpreterSetting : get()) { + settingIdList.add(interpreterSetting.getId()); } - return false; + 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."); } - InterpreterSetting setting = createFromInterpreterSettingRef(group); + // 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.setInterpreterGroupFactory(interpreterGroupFactory); + initInterpreterSetting(setting); 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; + @VisibleForTesting + public void addInterpreterSetting(InterpreterSetting interpreterSetting) { + interpreterSettingTemplates.put(interpreterSetting.getName(), interpreterSetting); + initInterpreterSetting(interpreterSetting); + interpreterSettings.put(interpreterSetting.getId(), interpreterSetting); } /** * map interpreter ids into noteId * + * @param user user name * @param noteId note id - * @param ids InterpreterSetting id list + * @param settingIdList 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<>(); + public void setInterpreterBinding(String user, String noteId, List settingIdList) + throws IOException { + List unBindedSettingIdList = new LinkedList<>(); - synchronized (interpreterSettings) { - List oldSettings = interpreterBindings.get(noteId); - if (oldSettings != null) { - for (String oldSettingId : oldSettings) { - if (!settingList.contains(oldSettingId)) { - unBindedSettings.add(oldSettingId); - } + List oldSettingIdList = interpreterBindings.get(noteId); + if (oldSettingIdList != null) { + for (String oldSettingId : oldSettingIdList) { + if (!settingIdList.contains(oldSettingId)) { + unBindedSettingIdList.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, ""); - } - } + interpreterBindings.put(noteId, settingIdList); + saveToFile(); - 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; + 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); + } } - - 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 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); - if (settings == null || settings.size() == 0) { - return; - } - - logger.info("closeNote: {}", noteId); for (InterpreterSetting setting : settings) { - removeInterpretersForNote(setting, user, noteId); + setting.closeInterpreters(user, noteId); } } - public Map getAvailableInterpreterSettings() { - return interpreterSettingsRef; + public Map getInterpreterSettingTemplates() { + return interpreterSettingTemplates; } private URL[] recursiveBuildLibList(File path) throws MalformedURLException { @@ -914,124 +782,93 @@ public void removeRepository(String id) throws IOException { } 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(); + setInterpreterBinding(user, noteId, new ArrayList()); + interpreterBindings.remove(noteId); } - /** - * Change interpreter property and restart - */ - public void setPropertyAndRestart(String id, InterpreterOption option, + /** Change interpreter properties 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"); + List dependencies) + throws InterpreterException, IOException { + 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 new IOException(e); } + } else { + throw new InterpreterException("Interpreter setting id " + id + " not found"); } } - public void restart(String settingId, String noteId, String user) { + // restart in note page + public void restart(String settingId, String noteId, String user) throws InterpreterException { 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"); - } + 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); + intpSetting.closeInterpreters(user, noteId); + } else { + throw new InterpreterException("Interpreter setting id " + settingId + " not found"); } } - public void restart(String id) { - restart(id, "", "anonymous"); + public void restart(String id) throws InterpreterException { + interpreterSettings.get(id).close(); } - 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 id) { + return interpreterSettings.get(id); } - public InterpreterSetting get(String name) { - synchronized (interpreterSettings) { - return interpreterSettings.get(name); + @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 { - 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(); - } + // 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); + 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(); } + saveToFile(); } - File localRepoDir = new File(zeppelinConfiguration.getInterpreterLocalRepoPath() + "/" + id); + File localRepoDir = new File(conf.getInterpreterLocalRepoPath() + "/" + id); FileUtils.deleteDirectory(localRepoDir); } @@ -1039,98 +876,81 @@ public void remove(String id) throws IOException { * 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()); + 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; } - nameInterpreterSettingMap.get(group).add(interpreterSetting); - } - - for (String groupName : interpreterGroupOrderList) { - List interpreterSettingList = - nameInterpreterSettingMap.remove(groupName); - if (null != interpreterSettingList) { - for (InterpreterSetting interpreterSetting : interpreterSettingList) { - orderedSettings.add(interpreterSetting); - } + 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; } - } - - 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) { + if (i < j) { + return -1; + } else if (i > j) { + return 1; + } else { return o1.getName().compareTo(o2.getName()); } - }); - - orderedSettings.addAll(settings); + } + }); + return orderedSettings; + } - return orderedSettings; + @VisibleForTesting + public List getSettingIds() { + List settingIds = new ArrayList<>(); + for (InterpreterSetting interpreterSetting : get()) { + settingIds.add(interpreterSetting.getId()); } + return settingIds; } - public void close(InterpreterSetting interpreterSetting) { - interpreterSetting.closeAndRemoveAllInterpreterGroups(); + 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.closeAndRemoveAllInterpreterGroups(); - } - }; - t.start(); - closeThreads.add(t); - } + for (final InterpreterSetting intpSetting : interpreterSettings.values()) { + 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); + 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); + @Override + public Set getRunningInterpreters() { + Set runningInterpreters = Sets.newHashSet(); + for (Map.Entry entry : interpreterSettings.entrySet()) { + for (ManagedInterpreterGroup mig : entry.getValue().getAllInterpreterGroups()) { + if (null != mig.getRemoteInterpreterProcess()) { + runningInterpreters.add(entry.getKey()); + } } } + return runningInterpreters; } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerMBean.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerMBean.java new file mode 100644 index 00000000000..36271a13897 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerMBean.java @@ -0,0 +1,27 @@ +/* + * 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.Set; + +/** + * MBean for InterpreterSettingManager + */ +public interface InterpreterSettingManagerMBean { + Set getRunningInterpreters(); +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/LifecycleManager.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/LifecycleManager.java new file mode 100644 index 00000000000..f36cb0db182 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/LifecycleManager.java @@ -0,0 +1,32 @@ +/* + * 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; + + +/** + * Interface for managing the lifecycle of interpreters + */ +public interface LifecycleManager { + + void onInterpreterProcessStarted(ManagedInterpreterGroup interpreterGroup); + + void onInterpreterUse(ManagedInterpreterGroup interpreterGroup, + String sessionId); + +} 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 new file mode 100644 index 00000000000..ecbaf16a277 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java @@ -0,0 +1,164 @@ +/* + * 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.RemoteInterpreterProcess; +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.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Properties; + +/** + * ManagedInterpreterGroup runs under zeppelin server + */ +public class ManagedInterpreterGroup extends InterpreterGroup { + + private static final Logger LOGGER = LoggerFactory.getLogger(ManagedInterpreterGroup.class); + + private InterpreterSetting interpreterSetting; + private RemoteInterpreterProcess remoteInterpreterProcess; // attached remote interpreter process + + /** + * Create InterpreterGroup with given id and interpreterSetting, used in ZeppelinServer + * @param id + * @param interpreterSetting + */ + ManagedInterpreterGroup(String id, InterpreterSetting interpreterSetting) { + super(id); + this.interpreterSetting = interpreterSetting; + } + + public InterpreterSetting getInterpreterSetting() { + return interpreterSetting; + } + + public synchronized RemoteInterpreterProcess getOrCreateInterpreterProcess(String userName, + Properties properties) + throws IOException { + if (remoteInterpreterProcess == null) { + LOGGER.info("Create InterpreterProcess for InterpreterGroup: " + getId()); + remoteInterpreterProcess = interpreterSetting.createInterpreterProcess(id, userName, + properties); + remoteInterpreterProcess.start(userName); + interpreterSetting.getLifecycleManager().onInterpreterProcessStarted(this); + 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; + } + + + /** + * Close all interpreter instances in this group + */ + public synchronized void close() { + LOGGER.info("Close InterpreterGroup: " + id); + for (String sessionId : sessions.keySet()) { + close(sessionId); + } + } + + /** + * Close all interpreter instances in this session + * @param sessionId + */ + public synchronized void close(String sessionId) { + LOGGER.info("Close Session: " + sessionId + " for interpreter setting: " + + interpreterSetting.getName()); + 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(); + try { + interpreterSetting.getRecoveryStorage().onInterpreterClientStop(remoteInterpreterProcess); + } catch (IOException e) { + LOGGER.error("Fail to store recovery data", e); + } + remoteInterpreterProcess = null; + } + } + } + + private void close(Collection interpreters) { + if (interpreters == null) { + return; + } + + for (Interpreter interpreter : interpreters) { + Scheduler scheduler = interpreter.getScheduler(); + for (Job job : scheduler.getJobsRunning()) { + job.abort(); + job.setStatus(Job.Status.ABORT); + LOGGER.info("Job " + job.getJobName() + " aborted "); + } + for (Job job : scheduler.getJobsWaiting()) { + job.abort(); + job.setStatus(Job.Status.ABORT); + LOGGER.info("Job " + job.getJobName() + " aborted "); + } + + try { + interpreter.close(); + } catch (InterpreterException e) { + LOGGER.warn("Fail to close interpreter " + interpreter.getClassName(), e); + } + //TODO(zjffdu) move the close of schedule to Interpreter + if (null != scheduler) { + SchedulerFactory.singleton().removeScheduler(scheduler.getName()); + } + } + } + + public synchronized List getOrCreateSession(String user, String sessionId) { + if (sessions.containsKey(sessionId)) { + return sessions.get(sessionId); + } else { + List interpreters = interpreterSetting.createInterpreters(user, id, 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; + } + } + +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/SessionConfInterpreter.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/SessionConfInterpreter.java new file mode 100644 index 00000000000..f1fcdb40441 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/SessionConfInterpreter.java @@ -0,0 +1,74 @@ +/* + * 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.apache.zeppelin.interpreter.remote.RemoteInterpreter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.StringReader; +import java.util.List; +import java.util.Properties; + +/** + * ConfInterpreter for session level + */ +public class SessionConfInterpreter extends ConfInterpreter { + + private static Logger LOGGER = LoggerFactory.getLogger(SessionConfInterpreter.class); + + public SessionConfInterpreter(Properties properties, + String sessionId, + String interpreterGroupId, + InterpreterSetting interpreterSetting) { + super(properties, sessionId, interpreterGroupId, interpreterSetting); + } + + @Override + public InterpreterResult interpret(String st, InterpreterContext context) + throws InterpreterException { + try { + Properties finalProperties = new Properties(); + finalProperties.putAll(this.properties); + Properties updatedProperties = new Properties(); + updatedProperties.load(new StringReader(st)); + finalProperties.putAll(updatedProperties); + LOGGER.debug("Properties for Session: " + sessionId + ": " + finalProperties); + + List interpreters = + interpreterSetting.getInterpreterGroup(interpreterGroupId).get(sessionId); + for (Interpreter intp : interpreters) { + // only check the RemoteInterpreter, ConfInterpreter itself will be ignored here. + if (intp instanceof RemoteInterpreter) { + RemoteInterpreter remoteInterpreter = (RemoteInterpreter) intp; + if (remoteInterpreter.isOpened()) { + return new InterpreterResult(InterpreterResult.Code.ERROR, + "Can not change interpreter session properties after this session is started"); + } + remoteInterpreter.setProperties(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)); + } + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/install/InstallInterpreter.java b/zeppelin-zengine/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-zengine/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/launcher/ShellScriptLauncher.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncher.java new file mode 100644 index 00000000000..e107fb7edec --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncher.java @@ -0,0 +1,100 @@ +/* + * 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.launcher; + +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, RecoveryStorage recoveryStorage) { + super(zConf, recoveryStorage); + } + + @Override + 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.getInterpreterDir() + "/" + groupName, localRepoPath, + buildEnvFromProperties(context), connectTimeout, name, option.isUserImpersonate()); + } + } + + protected Map buildEnvFromProperties(InterpreterLaunchContext context) { + Map env = new HashMap<>(); + for (Object key : context.getProperties().keySet()) { + if (RemoteInterpreterUtils.isEnvString((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 new file mode 100644 index 00000000000..c751cf0585a --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncher.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.interpreter.launcher; + +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; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * Spark specific launcher. + */ +public class SparkInterpreterLauncher extends ShellScriptLauncher { + + private static final Logger LOGGER = LoggerFactory.getLogger(SparkInterpreterLauncher.class); + + public SparkInterpreterLauncher(ZeppelinConfiguration zConf, RecoveryStorage recoveryStorage) { + super(zConf, recoveryStorage); + } + + @Override + protected Map buildEnvFromProperties(InterpreterLaunchContext context) { + Map env = new HashMap(); + Properties sparkProperties = new Properties(); + String sparkMaster = getSparkMaster(properties); + for (String key : properties.stringPropertyNames()) { + if (RemoteInterpreterUtils.isEnvString(key)) { + env.put(key, properties.getProperty(key)); + } + if (isSparkConf(key, properties.getProperty(key))) { + sparkProperties.setProperty(key, toShellFormat(properties.getProperty(key))); + } + } + + setupPropertiesForPySpark(sparkProperties); + setupPropertiesForSparkR(sparkProperties); + if (isYarnMode() && getDeployMode().equals("cluster")) { + env.put("ZEPPELIN_SPARK_YARN_CLUSTER", "true"); + } + + StringBuilder sparkConfBuilder = new StringBuilder(); + if (sparkMaster != null) { + sparkConfBuilder.append(" --master " + sparkMaster); + } + if (isYarnMode() && getDeployMode().equals("cluster")) { + List files = new ArrayList<>(); + if (sparkProperties.containsKey("spark.files")) { + files.add(sparkProperties.getProperty("spark.files")); + } + files.add(zConf.getConfDir() + "/log4j_yarn_cluster.properties"); + files.add(zConf.getZeppelinHome() + "/interpreter/lib/python/mpl_config.py"); + files.add(zConf.getZeppelinHome() + "/interpreter/lib/python/backend_zinline.py"); + sparkProperties.put("spark.files", StringUtils.join(files, ",")); + } + 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()); + + // set these env in the order of + // 1. interpreter-setting + // 2. zeppelin-env.sh + // It is encouraged to set env in interpreter setting, but just for backward compatability, + // we also fallback to zeppelin-env.sh if it is not specified in interpreter setting. + for (String envName : new String[]{"SPARK_HOME", "SPARK_CONF_DIR", "HADOOP_CONF_DIR"}) { + String envValue = getEnv(envName); + if (envValue != null) { + env.put(envName, envValue); + } + } + + 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)) { + env.put("ZEPPELIN_SERVER_KERBEROS_KEYTAB", keytab); + env.put("ZEPPELIN_SERVER_KERBEROS_PRINCIPAL", principal); + LOGGER.info("Run Spark under secure mode with keytab: " + keytab + + ", principal: " + principal); + } else { + LOGGER.info("Run Spark under non-secure mode as no keytab and principal is specified"); + } + LOGGER.debug("buildEnvFromProperties: " + env); + return env; + + } + + + /** + * get environmental variable in the following order + * + * 1. interpreter setting + * 2. zeppelin-env.sh + * + */ + private String getEnv(String envName) { + String env = properties.getProperty(envName); + if (env == null) { + env = System.getenv(envName); + } + return env; + } + + private boolean isSparkConf(String key, String value) { + return !StringUtils.isEmpty(key) && key.startsWith("spark.") && !StringUtils.isEmpty(value); + } + + private void setupPropertiesForPySpark(Properties sparkProperties) { + if (isYarnMode()) { + sparkProperties.setProperty("spark.yarn.isPython", "true"); + } + } + + private void mergeSparkProperty(Properties sparkProperties, String propertyName, + String propertyValue) { + if (sparkProperties.containsKey(propertyName)) { + String oldPropertyValue = sparkProperties.getProperty(propertyName); + sparkProperties.setProperty(propertyName, oldPropertyValue + "," + propertyValue); + } else { + sparkProperties.setProperty(propertyName, propertyValue); + } + } + + private void setupPropertiesForSparkR(Properties sparkProperties) { + String sparkHome = getEnv("SPARK_HOME"); + File sparkRBasePath = null; + if (sparkHome == null) { + if (!getSparkMaster(properties).startsWith("local")) { + throw new RuntimeException("SPARK_HOME is not specified in interpreter-setting" + + " for non-local mode, if you specify it in zeppelin-env.sh, please move that into " + + " interpreter setting"); + } + String zeppelinHome = zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME); + sparkRBasePath = new File(zeppelinHome, + "interpreter" + File.separator + "spark" + File.separator + "R"); + } else { + sparkRBasePath = new File(sparkHome, "R" + File.separator + "lib"); + } + + File sparkRPath = new File(sparkRBasePath, "sparkr.zip"); + if (sparkRPath.exists() && sparkRPath.isFile()) { + mergeSparkProperty(sparkProperties, "spark.yarn.dist.archives", + sparkRPath.getAbsolutePath() + "#sparkr"); + } else { + LOGGER.warn("sparkr.zip is not found, SparkR may not work."); + } + } + + /** + * Order to look for spark master + * 1. master in interpreter setting + * 2. spark.master interpreter setting + * 3. use local[*] + * @param properties + * @return + */ + private String getSparkMaster(Properties properties) { + String master = properties.getProperty("master"); + if (master == null) { + master = properties.getProperty("spark.master"); + if (master == null) { + master = "local[*]"; + } + } + return master; + } + + private String getDeployMode() { + String master = getSparkMaster(properties); + if (master.equals("yarn-client")) { + return "client"; + } else if (master.equals("yarn-cluster")) { + return "cluster"; + } else if (master.startsWith("local")) { + return "client"; + } else { + String deployMode = properties.getProperty("spark.submit.deployMode"); + if (deployMode == null) { + throw new RuntimeException("master is set as yarn, but spark.submit.deployMode " + + "is not specified"); + } + if (!deployMode.equals("client") && !deployMode.equals("cluster")) { + throw new RuntimeException("Invalid value for spark.submit.deployMode: " + deployMode); + } + return deployMode; + } + } + + private boolean isYarnMode() { + return getSparkMaster(properties).startsWith("yarn"); + } + + 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 + "'"; + } + } + +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/lifecycle/NullLifecycleManager.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/lifecycle/NullLifecycleManager.java new file mode 100644 index 00000000000..5a62d22b465 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/lifecycle/NullLifecycleManager.java @@ -0,0 +1,43 @@ +/* + * 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.lifecycle; + +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.LifecycleManager; +import org.apache.zeppelin.interpreter.ManagedInterpreterGroup; + +/** + * Do nothing for the lifecycle of interpreter. User need to explicitly start/stop interpreter. + */ +public class NullLifecycleManager implements LifecycleManager { + + public NullLifecycleManager(ZeppelinConfiguration zConf) { + + } + + @Override + public void onInterpreterProcessStarted(ManagedInterpreterGroup interpreterGroup) { + + } + + @Override + public void onInterpreterUse(ManagedInterpreterGroup interpreterGroup, String sessionId) { + + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/lifecycle/TimeoutLifecycleManager.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/lifecycle/TimeoutLifecycleManager.java new file mode 100644 index 00000000000..f9cd9c74716 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/lifecycle/TimeoutLifecycleManager.java @@ -0,0 +1,77 @@ +package org.apache.zeppelin.interpreter.lifecycle; + +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.LifecycleManager; +import org.apache.zeppelin.interpreter.ManagedInterpreterGroup; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + + +/** + * This lifecycle manager would close interpreter after it is timeout. By default, it is timeout + * after no using in 1 hour. + *

    + * For now, this class only manage the lifecycle of interpreter group (will close interpreter + * process after timeout). Managing the lifecycle of interpreter session could be done in future + * if necessary. + */ +public class TimeoutLifecycleManager implements LifecycleManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(TimeoutLifecycleManager.class); + + // ManagerInterpreterGroup -> LastTimeUsing timestamp + private Map interpreterGroups = new ConcurrentHashMap<>(); + + private long checkInterval; + private long timeoutThreshold; + + private ScheduledExecutorService checkScheduler; + + public TimeoutLifecycleManager(ZeppelinConfiguration zConf) { + this.checkInterval = zConf.getLong(ZeppelinConfiguration.ConfVars + .ZEPPELIN_INTERPRETER_LIFECYCLE_MANAGER_TIMEOUT_CHECK_INTERVAL); + this.timeoutThreshold = zConf.getLong( + ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_LIFECYCLE_MANAGER_TIMEOUT_THRESHOLD); + this.checkScheduler = Executors.newScheduledThreadPool(1); + this.checkScheduler.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + long now = System.currentTimeMillis(); + for (Map.Entry entry : interpreterGroups.entrySet()) { + ManagedInterpreterGroup interpreterGroup = entry.getKey(); + Long lastTimeUsing = entry.getValue(); + if ((now - lastTimeUsing) > timeoutThreshold) { + LOGGER.info("InterpreterGroup {} is timeout.", interpreterGroup.getId()); + try { + interpreterGroup.close(); + } catch (Exception e) { + LOGGER.warn("Fail to close interpreterGroup: " + interpreterGroup.getId(), e); + } + interpreterGroups.remove(entry.getKey()); + } + } + } + }, checkInterval, checkInterval, MILLISECONDS); + LOGGER.info("TimeoutLifecycleManager is started with checkinterval: " + checkInterval + + ", timeoutThreshold: " + timeoutThreshold); + } + + @Override + public void onInterpreterProcessStarted(ManagedInterpreterGroup interpreterGroup) { + LOGGER.info("Process of InterpreterGroup {} is started", interpreterGroup.getId()); + interpreterGroups.put(interpreterGroup, System.currentTimeMillis()); + } + + @Override + public void onInterpreterUse(ManagedInterpreterGroup interpreterGroup, String sessionId) { + LOGGER.debug("InterpreterGroup {} is used in session {}", interpreterGroup.getId(), sessionId); + interpreterGroups.put(interpreterGroup, System.currentTimeMillis()); + } +} 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..9b1b6cb666f --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/recovery/FileSystemRecoveryStorage.java @@ -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.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 = 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); + } + + @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-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/AppendOutputBuffer.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/AppendOutputBuffer.java similarity index 100% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/AppendOutputBuffer.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/AppendOutputBuffer.java diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunner.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunner.java similarity index 100% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunner.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunner.java index 03d919187e4..2a88dc204b6 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunner.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunner.java @@ -17,6 +17,9 @@ package org.apache.zeppelin.interpreter.remote; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -24,9 +27,6 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * This thread sends paragraph's append-data * periodically, rather than continously, with diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/ClientFactory.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/ClientFactory.java similarity index 100% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/ClientFactory.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/ClientFactory.java diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/InterpreterContextRunnerPool.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/InterpreterContextRunnerPool.java similarity index 96% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/InterpreterContextRunnerPool.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/InterpreterContextRunnerPool.java index 064abd5370d..76538245795 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/InterpreterContextRunnerPool.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/InterpreterContextRunnerPool.java @@ -82,7 +82,7 @@ public void run(String noteId, String paragraphId) { } } - throw new InterpreterException("Can not run paragraph " + paragraphId + " on " + noteId); + throw new RuntimeException("Can not run paragraph " + paragraphId + " on " + noteId); } } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObject.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObject.java similarity index 87% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObject.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObject.java index c1f9b94a6e9..62c8efd2010 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObject.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObject.java @@ -20,17 +20,18 @@ import org.apache.zeppelin.display.AngularObject; import org.apache.zeppelin.display.AngularObjectListener; import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.ManagedInterpreterGroup; /** * Proxy for AngularObject that exists in remote interpreter process */ public class RemoteAngularObject extends AngularObject { - private transient InterpreterGroup interpreterGroup; + private transient ManagedInterpreterGroup interpreterGroup; RemoteAngularObject(String name, Object o, String noteId, String paragraphId, - InterpreterGroup interpreterGroup, - AngularObjectListener listener) { + ManagedInterpreterGroup interpreterGroup, + AngularObjectListener listener) { super(name, o, noteId, paragraphId, listener); this.interpreterGroup = interpreterGroup; } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java index 0ac71165348..924901bbf1b 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectRegistry.java @@ -17,29 +17,28 @@ package org.apache.zeppelin.interpreter.remote; -import java.util.List; - -import org.apache.thrift.TException; +import com.google.gson.Gson; 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.ManagedInterpreterGroup; 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 */ public class RemoteAngularObjectRegistry extends AngularObjectRegistry { Logger logger = LoggerFactory.getLogger(RemoteAngularObjectRegistry.class); - private InterpreterGroup interpreterGroup; + private ManagedInterpreterGroup interpreterGroup; public RemoteAngularObjectRegistry(String interpreterId, - AngularObjectRegistryListener listener, - InterpreterGroup interpreterGroup) { + AngularObjectRegistryListener listener, + ManagedInterpreterGroup interpreterGroup) { super(interpreterId, listener); this.interpreterGroup = interpreterGroup; } @@ -56,31 +55,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 +88,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-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 12e0caa435d..34ed804044f 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 @@ -17,160 +17,81 @@ package org.apache.zeppelin.interpreter.remote; -import java.util.*; - -import org.apache.commons.lang3.StringUtils; +import com.google.common.annotations.VisibleForTesting; +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.helium.ApplicationEventListener; import org.apache.zeppelin.display.Input; -import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.ConfInterpreter; +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.InterpreterResult; +import org.apache.zeppelin.interpreter.LifecycleManager; +import org.apache.zeppelin.interpreter.ManagedInterpreterGroup; 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.RemoteScheduler; 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; +import java.io.IOException; +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 final RemoteInterpreterProcessListener remoteInterpreterProcessListener; - private final ApplicationEventListener applicationEventListener; - private Gson gson = new Gson(); - private String interpreterRunner; - private String interpreterPath; - private String localRepoPath; + private static final Logger LOGGER = LoggerFactory.getLogger(RemoteInterpreter.class); + private static final Gson gson = new Gson(); + + private String className; - private String sessionKey; + private String sessionId; 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; - } + private RemoteInterpreterProcess interpreterProcess; + private volatile boolean isOpened = false; + private volatile boolean isCreated = false; + private LifecycleManager lifecycleManager; /** - * Connect to existing process + * Remote interpreter and manage interpreter 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); + public RemoteInterpreter(Properties properties, + String sessionId, + String className, + String userName, + LifecycleManager lifecycleManager) { + super(properties); + this.sessionId = sessionId; 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; + this.setUserName(userName); + this.lifecycleManager = lifecycleManager; } - 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 + "\'"; - } + public boolean isOpened() { + return isOpened; } - static boolean isSparkConf(String key, String value) { - return !StringUtils.isEmpty(key) && key.startsWith("spark.") && !StringUtils.isEmpty(value); + @VisibleForTesting + public void setOpened(boolean opened) { + isOpened = opened; } @Override @@ -178,202 +99,123 @@ public String getClassName() { return className; } - private boolean connectToExistingProcess() { - return host != null && port > 0; + public String getSessionId() { + return this.sessionId; } - 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 RemoteInterpreterProcess getOrCreateInterpreterProcess() throws IOException { + if (this.interpreterProcess != null) { + return this.interpreterProcess; } + ManagedInterpreterGroup intpGroup = getInterpreterGroup(); + this.interpreterProcess = intpGroup.getOrCreateInterpreterProcess(getUserName(), properties); + return interpreterProcess; } - 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); - } + public ManagedInterpreterGroup getInterpreterGroup() { + return (ManagedInterpreterGroup) super.getInterpreterGroup(); + } - 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); + @Override + public void open() throws InterpreterException { + 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(this.getUserName(), sessionId)) { + try { + if (!(interpreter instanceof ConfInterpreter)) { + ((RemoteInterpreter) interpreter).internal_create(); + } + } catch (IOException e) { + throw new InterpreterException(e); + } } - } 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); + interpreterProcess.callRemoteFunction(new RemoteInterpreterProcess.RemoteFunction() { + @Override + public Void call(Client client) throws Exception { + LOGGER.info("Open RemoteInterpreter {}", getClassName()); + // open interpreter here instead of in the jobRun method in RemoteInterpreterServer + // 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; + this.lifecycleManager.onInterpreterUse(this.getInterpreterGroup(), sessionId); } } - 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); - } + private void internal_create() throws IOException { + synchronized (this) { + if (!isCreated) { + this.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) getProperties(), getUserName()); + return null; + } + }); + isCreated = true; } } } + @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(); + public void close() throws InterpreterException { + if (isOpened) { + RemoteInterpreterProcess interpreterProcess = null; + try { + interpreterProcess = getOrCreateInterpreterProcess(); + } catch (IOException e) { + throw new InterpreterException(e); } - 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); + interpreterProcess.callRemoteFunction(new RemoteInterpreterProcess.RemoteFunction() { + @Override + public Void call(Client client) throws Exception { + client.close(sessionId, className); + return null; } - } - } - } - - 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; + }); + isOpened = false; + this.lifecycleManager.onInterpreterUse(this.getInterpreterGroup(), sessionId); + } else { + LOGGER.warn("close is called when RemoterInterpreter is not opened for " + className); } } @Override - public InterpreterResult interpret(String st, InterpreterContext context) { - if (logger.isDebugEnabled()) { - logger.debug("st:\n{}", st); + public InterpreterResult interpret(final String st, final InterpreterContext context) + throws InterpreterException { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("st:\n{}", st); } - FormType form = getFormType(); - RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); - Client client = null; + final FormType form = getFormType(); + RemoteInterpreterProcess interpreterProcess = null; try { - client = interpreterProcess.getClient(); - } catch (Exception e1) { - throw new InterpreterException(e1); + interpreterProcess = getOrCreateInterpreterProcess(); + } catch (IOException e) { + throw new InterpreterException(e); } - 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 @@ -382,165 +224,191 @@ public InterpreterResult interpret(String st, InterpreterContext context) { interpreterContextRunnerPool.clear(noteId); interpreterContextRunnerPool.addAll(noteId, runners); } + this.lifecycleManager.onInterpreterUse(this.getInterpreterGroup(), sessionId); + 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(); + GUI currentNoteGUI = context.getNoteGui(); + if (form == FormType.NATIVE) { + GUI remoteGui = GUI.fromJson(remoteResult.getGui()); + GUI remoteNoteGui = GUI.fromJson(remoteResult.getNoteGui()); + currentGUI.clear(); + currentGUI.setParams(remoteGui.getParams()); + currentGUI.setForms(remoteGui.getForms()); + currentNoteGUI.setParams(remoteNoteGui.getParams()); + currentNoteGUI.setForms(remoteNoteGui.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; + } + } + ); - 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); + public void cancel(final InterpreterContext context) throws InterpreterException { + if (!isOpened) { + LOGGER.warn("Cancel is called when RemoterInterpreter is not opened for " + className); + return; } - - boolean broken = false; + RemoteInterpreterProcess interpreterProcess = null; try { - client.cancel(sessionKey, className, convert(context)); - } catch (TException e) { - broken = true; + interpreterProcess = getOrCreateInterpreterProcess(); + } catch (IOException e) { throw new InterpreterException(e); - } finally { - interpreterProcess.releaseClient(client, broken); } + this.lifecycleManager.onInterpreterUse(this.getInterpreterGroup(), sessionId); + 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() { - open(); - + public FormType getFormType() throws InterpreterException { if (formType != null) { return formType; } - RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); - Client client = null; - try { - client = interpreterProcess.getClient(); - } catch (Exception e1) { - throw new InterpreterException(e1); + // it is possible to call getFormType before it is opened + synchronized (this) { + if (!isOpened) { + open(); + } } - - boolean broken = false; + RemoteInterpreterProcess interpreterProcess = null; try { - formType = FormType.valueOf(client.getFormType(sessionKey, className)); - return formType; - } catch (TException e) { - broken = true; + interpreterProcess = getOrCreateInterpreterProcess(); + } catch (IOException e) { throw new InterpreterException(e); - } finally { - interpreterProcess.releaseClient(client, broken); } + this.lifecycleManager.onInterpreterUse(this.getInterpreterGroup(), sessionId); + 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(InterpreterContext context) { - RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); - if (interpreterProcess == null || !interpreterProcess.isRunning()) { + public int getProgress(final InterpreterContext context) throws InterpreterException { + if (!isOpened) { + LOGGER.warn("getProgress is called when RemoterInterpreter is not opened for " + className); return 0; } - - Client client = null; - try { - client = interpreterProcess.getClient(); - } catch (Exception e1) { - throw new InterpreterException(e1); - } - - boolean broken = false; + RemoteInterpreterProcess interpreterProcess = null; try { - return client.getProgress(sessionKey, className, convert(context)); - } catch (TException e) { - broken = true; + interpreterProcess = getOrCreateInterpreterProcess(); + } catch (IOException e) { throw new InterpreterException(e); - } finally { - interpreterProcess.releaseClient(client, broken); } + this.lifecycleManager.onInterpreterUse(this.getInterpreterGroup(), sessionId); + 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(String buf, int cursor, - InterpreterContext interpreterContext) { - RemoteInterpreterProcess interpreterProcess = getInterpreterProcess(); - Client client = null; - try { - client = interpreterProcess.getClient(); - } catch (Exception e1) { - throw new InterpreterException(e1); + public List completion(final String buf, final int cursor, + final InterpreterContext interpreterContext) + throws InterpreterException { + if (!isOpened) { + open(); } - - boolean broken = false; + RemoteInterpreterProcess interpreterProcess = null; try { - List completion = client.completion(sessionKey, className, buf, cursor, - convert(interpreterContext)); - return completion; - } catch (TException e) { - broken = true; + interpreterProcess = getOrCreateInterpreterProcess(); + } catch (IOException e) { throw new InterpreterException(e); - } finally { - interpreterProcess.releaseClient(client, broken); } + this.lifecycleManager.onInterpreterUse(this.getInterpreterGroup(), sessionId); + return interpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction>() { + @Override + public List call(Client client) throws Exception { + return client.completion(sessionId, className, buf, cursor, + convert(interpreterContext)); + } + }); } - @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); + 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 = null; + try { + interpreterProcess = getOrCreateInterpreterProcess(); + } catch (IOException e) { + throw new RuntimeException(e); } + this.lifecycleManager.onInterpreterUse(this.getInterpreterGroup(), sessionId); + return interpreterProcess.callRemoteFunction( + new RemoteInterpreterProcess.RemoteFunction() { + @Override + public String call(Client client) throws Exception { + return client.getStatus(sessionId, jobId); + } + }); } - private String getInterpreterGroupKey(InterpreterGroup interpreterGroup) { - return interpreterGroup.getId(); + + @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() + "-" + getInterpreterGroup().getId() + "-" + sessionId, + SchedulerFactory.singleton().getExecutor(), + sessionId, + this, + SchedulerFactory.singleton(), + maxConcurrency); + return SchedulerFactory.singleton().createOrGetScheduler(s); } 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())); + ic.getParagraphTitle(), ic.getParagraphText(), gson.toJson(ic.getAuthenticationInfo()), + gson.toJson(ic.getConfig()), ic.getGui().toJson(), gson.toJson(ic.getNoteGui()), + gson.toJson(ic.getRunners())); } private InterpreterResult convert(RemoteInterpreterResult result) { @@ -557,41 +425,25 @@ private InterpreterResult convert(RemoteInterpreterResult result) { /** * Push local angular object registry to * remote interpreter. This method should be - * call ONLY inside the init() method + * call ONLY once when the first Interpreter is created */ - void pushAngularObjectRegistryToRemote(Client client) throws TException { + 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" + + 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; + @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-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java similarity index 73% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java index 6927b3b6687..abda81e03c4 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterEventPoller.java @@ -26,6 +26,7 @@ import org.apache.zeppelin.interpreter.InterpreterContextRunner; import org.apache.zeppelin.interpreter.InterpreterGroup; import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.ManagedInterpreterGroup; import org.apache.zeppelin.interpreter.RemoteZeppelinServerResource; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterEvent; import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterEventType; @@ -38,8 +39,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.util.LinkedList; import java.util.List; @@ -62,7 +61,7 @@ public class RemoteInterpreterEventPoller extends Thread { private volatile boolean shutdown; private RemoteInterpreterProcess interpreterProcess; - private InterpreterGroup interpreterGroup; + private ManagedInterpreterGroup interpreterGroup; Gson gson = new Gson(); @@ -78,13 +77,12 @@ public void setInterpreterProcess(RemoteInterpreterProcess interpreterProcess) { this.interpreterProcess = interpreterProcess; } - public void setInterpreterGroup(InterpreterGroup interpreterGroup) { + public void setInterpreterGroup(ManagedInterpreterGroup interpreterGroup) { this.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 +98,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(); @@ -193,11 +179,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 = @@ -260,7 +246,6 @@ public void run() { listener.onParaInfosReceived(noteId, paraId, settingId, paraInfos); } } - logger.debug("Event from remote process {}", event.getType()); } catch (Exception e) { logger.error("Can't handle event " + event, e); } @@ -268,7 +253,11 @@ public void run() { try { clearUnreadEvents(interpreterProcess.getClient()); } catch (Exception e1) { - logger.error("Can't get RemoteInterpreterEvent", e1); + if (shutdown) { + logger.error("Can not get RemoteInterpreterEvent because it is shutdown."); + } else { + logger.error("Can't get RemoteInterpreterEvent", e1); + } } if (appendFuture != null) { appendFuture.cancel(true); @@ -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 (ManagedInterpreterGroup 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) { + ManagedInterpreterGroup 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; + } + + 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(InvokeResourceMethodEventMessage message) { - ResourceId resourceId = message.resourceId; - InterpreterGroup intpGroup = InterpreterGroup.getByInterpreterGroupId( - resourceId.getResourcePoolId()); + private Object invokeResourceMethod(final InvokeResourceMethodEventMessage message) { + final ResourceId resourceId = message.resourceId; + ManagedInterpreterGroup 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-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java index 1fb9b90771c..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 @@ -17,10 +17,23 @@ package org.apache.zeppelin.interpreter.remote; -import org.apache.commons.exec.*; +import com.google.common.annotations.VisibleForTesting; +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.zeppelin.helium.ApplicationEventListener; -import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.thrift.TException; +import org.apache.thrift.server.TServer; +import org.apache.thrift.server.TThreadPoolServer; +import org.apache.thrift.transport.TServerSocket; +import org.apache.thrift.transport.TTransportException; +import org.apache.zeppelin.interpreter.thrift.CallbackInfo; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterCallbackService; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,6 +41,7 @@ import java.io.IOException; import java.io.OutputStream; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; /** * This class manages start / stop of remote interpreter process @@ -36,55 +50,47 @@ public class RemoteInterpreterManagedProcess extends RemoteInterpreterProcess implements ExecuteResultHandler { private static final Logger logger = LoggerFactory.getLogger( RemoteInterpreterManagedProcess.class); - private final String interpreterRunner; + private final String interpreterRunner; + private final String callbackPortRange; + private final String interpreterPortRange; private DefaultExecutor executor; private ExecuteWatchdog watchdog; - boolean running = false; + private AtomicBoolean running = new AtomicBoolean(false); + private TServer callbackServer; + private String host = null; private int port = -1; private final String interpreterDir; private final String localRepoDir; - private final String interpreterGroupName; + private final String interpreterSettingName; + private final boolean isUserImpersonated; private Map env; public RemoteInterpreterManagedProcess( String intpRunner, + String callbackPortRange, + String interpreterPortRange, String intpDir, String localRepoDir, Map env, int connectTimeout, - RemoteInterpreterProcessListener listener, - ApplicationEventListener appListener, - String interpreterGroupName) { - super(new RemoteInterpreterEventPoller(listener, appListener), - connectTimeout); + String interpreterSettingName, + boolean isUserImpersonated) { + super(connectTimeout); this.interpreterRunner = intpRunner; + this.callbackPortRange = callbackPortRange; + this.interpreterPortRange = interpreterPortRange; this.env = env; this.interpreterDir = intpDir; this.localRepoDir = localRepoDir; - this.interpreterGroupName = interpreterGroupName; - } - - RemoteInterpreterManagedProcess(String intpRunner, - String intpDir, - String localRepoDir, - Map env, - RemoteInterpreterEventPoller remoteInterpreterEventPoller, - int connectTimeout, - String interpreterGroupName) { - super(remoteInterpreterEventPoller, - connectTimeout); - this.interpreterRunner = intpRunner; - this.env = env; - this.interpreterDir = intpDir; - this.localRepoDir = localRepoDir; - this.interpreterGroupName = interpreterGroupName; + this.interpreterSettingName = interpreterSettingName; + this.isUserImpersonated = isUserImpersonated; } @Override public String getHost() { - return "localhost"; + return host; } @Override @@ -93,27 +99,79 @@ 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; + TServerSocket tSocket = null; try { - port = RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces(); + tSocket = RemoteInterpreterUtils.createTServerSocket(callbackPortRange); + callbackPort = tSocket.getServerSocket().getLocalPort(); + callbackHost = RemoteInterpreterUtils.findAvailableHostAddress(); } catch (IOException e1) { - throw new InterpreterException(e1); + throw new RuntimeException(e1); + } + + logger.info("Thrift server for callback will start. Port: {}", callbackPort); + try { + callbackServer = new TThreadPoolServer( + new TThreadPoolServer.Args(tSocket).processor( + new RemoteInterpreterCallbackService.Processor<>( + new RemoteInterpreterCallbackService.Iface() { + @Override + public void callback(CallbackInfo callbackInfo) throws TException { + logger.info("RemoteInterpreterServer Registered: {}", callbackInfo); + host = callbackInfo.getHost(); + port = callbackInfo.getPort(); + running.set(true); + synchronized (running) { + running.notify(); + } + } + }))); + // Start thrift server to receive callbackInfo from RemoteInterpreterServer; + new Thread(new Runnable() { + @Override + public void run() { + callbackServer.serve(); + } + }).start(); + + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + if (callbackServer.isServing()) { + callbackServer.stop(); + } + } + })); + + while (!callbackServer.isServing()) { + logger.debug("callbackServer is not serving"); + Thread.sleep(500); + } + logger.debug("callbackServer is serving now"); + } catch (InterruptedException e) { + logger.warn("", e); } CommandLine cmdLine = CommandLine.parse(interpreterRunner); cmdLine.addArgument("-d", false); cmdLine.addArgument(interpreterDir, false); + cmdLine.addArgument("-c", false); + cmdLine.addArgument(callbackHost, false); cmdLine.addArgument("-p", false); - cmdLine.addArgument(Integer.toString(port), false); - if (isUserImpersonate && !userName.equals("anonymous")) { + cmdLine.addArgument(Integer.toString(callbackPort), false); + cmdLine.addArgument("-r", false); + cmdLine.addArgument(interpreterPortRange, false); + if (isUserImpersonated && !userName.equals("anonymous")) { cmdLine.addArgument("-u", false); cmdLine.addArgument(userName, false); } cmdLine.addArgument("-l", false); cmdLine.addArgument(localRepoDir, false); cmdLine.addArgument("-g", false); - cmdLine.addArgument(interpreterGroupName, false); + cmdLine.addArgument(interpreterSettingName, false); executor = new DefaultExecutor(); @@ -131,71 +189,99 @@ public void start(String userName, Boolean isUserImpersonate) { logger.info("Run interpreter process {}", cmdLine); executor.execute(cmdLine, procEnv, this); - running = true; } catch (IOException e) { - running = false; - throw new InterpreterException(e); + running.set(false); + throw new RuntimeException(e); } - - long startTime = System.currentTimeMillis(); - while (System.currentTimeMillis() - startTime < getConnectTimeout()) { - if (!running) { - try { - cmdOut.flush(); - } catch (IOException e) { - // nothing to do + try { + synchronized (running) { + if (!running.get()) { + running.wait(getConnectTimeout() * 2); } - throw new InterpreterException(new String(cmdOut.toByteArray())); } - - try { - if (RemoteInterpreterUtils.checkIfRemoteEndpointAccessible("localhost", port)) { - break; - } else { - try { - Thread.sleep(500); - } catch (InterruptedException e) { - logger.error("Exception in RemoteInterpreterProcess while synchronized reference " + - "Thread.sleep", e); - } - } - } catch (Exception e) { - if (logger.isDebugEnabled()) { - logger.debug("Remote interpreter not yet accessible at localhost:" + port); - } + if (!running.get()) { + callbackServer.stop(); + throw new RuntimeException(new String(cmdOut.toByteArray())); } + } catch (InterruptedException e) { + logger.error("Remote interpreter is not accessible"); } processOutput.setOutputStream(null); } public void stop() { + // shutdown EventPoller first. + this.getRemoteInterpreterEventPoller().shutdown(); + if (callbackServer.isServing()) { + callbackServer.stop(); + } if (isRunning()) { - logger.info("kill interpreter process"); + 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(); } executor = null; watchdog = null; - running = false; + running.set(false); logger.info("Remote process terminated"); } @Override public void onProcessComplete(int exitValue) { logger.info("Interpreter process exited {}", exitValue); - running = false; + running.set(false); } @Override public void onProcessFailed(ExecuteException e) { logger.info("Interpreter process failed {}", e); - running = false; + running.set(false); + } + + @VisibleForTesting + public Map getEnv() { + return env; + } + + @VisibleForTesting + public String getLocalRepoDir() { + return localRepoDir; + } + + @VisibleForTesting + public String getInterpreterDir() { + return interpreterDir; + } + + public String getInterpreterSettingName() { + return interpreterSettingName; + } + + @VisibleForTesting + public String getInterpreterRunner() { + return interpreterRunner; + } + + @VisibleForTesting + public boolean isUserImpersonated() { + return isUserImpersonated; } public boolean isRunning() { - return running; + return running.get(); } private static class ProcessLogOutputStream extends LogOutputStream { 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 new file mode 100644 index 00000000000..08653ae390f --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java @@ -0,0 +1,157 @@ +/* + * 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 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.launcher.InterpreterClient; +import org.apache.zeppelin.interpreter.thrift.RemoteInterpreterService.Client; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract class for interpreter process + */ +public abstract class RemoteInterpreterProcess implements InterpreterClient { + private static final Logger logger = LoggerFactory.getLogger(RemoteInterpreterProcess.class); + + private GenericObjectPool clientPool; + private RemoteInterpreterEventPoller remoteInterpreterEventPoller; + private final InterpreterContextRunnerPool interpreterContextRunnerPool; + private int connectTimeout; + + public RemoteInterpreterProcess( + int connectTimeout) { + this.interpreterContextRunnerPool = new InterpreterContextRunnerPool(); + this.connectTimeout = connectTimeout; + } + + public RemoteInterpreterEventPoller getRemoteInterpreterEventPoller() { + return remoteInterpreterEventPoller; + } + + public void setRemoteInterpreterEventPoller(RemoteInterpreterEventPoller eventPoller) { + this.remoteInterpreterEventPoller = eventPoller; + } + + public int getConnectTimeout() { + return connectTimeout; + } + + public synchronized Client getClient() throws Exception { + if (clientPool == null || clientPool.isClosed()) { + clientPool = new GenericObjectPool<>(new ClientFactory(getHost(), getPort())); + } + return clientPool.borrowObject(); + } + + private void releaseClient(Client client) { + releaseClient(client, false); + } + + private void releaseClient(Client client, boolean broken) { + if (broken) { + releaseBrokenClient(client); + } else { + try { + clientPool.returnObject(client); + } catch (Exception e) { + logger.warn("exception occurred during releasing thrift client", e); + } + } + } + + private void releaseBrokenClient(Client client) { + try { + clientPool.invalidateObject(client); + } catch (Exception e) { + logger.warn("exception occurred during releasing thrift client", e); + } + } + + /** + * Called when angular object is updated in client side to propagate + * change to the remote process + * @param name + * @param o + */ + public void updateRemoteAngularObject(String name, String noteId, String paragraphId, Object o) { + Client client = null; + try { + client = getClient(); + } catch (NullPointerException e) { + // remote process not started + logger.info("NullPointerException in RemoteInterpreterProcess while " + + "updateRemoteAngularObject getClient, remote process not started", e); + return; + } catch (Exception e) { + logger.error("Can't update angular object", e); + } + + boolean broken = false; + try { + Gson gson = new Gson(); + client.angularObjectUpdate(name, noteId, paragraphId, gson.toJson(o)); + } catch (TException e) { + broken = true; + logger.error("Can't update angular object", e); + } catch (NullPointerException e) { + logger.error("Remote interpreter process not started", e); + return; + } finally { + if (client != null) { + releaseClient(client, broken); + } + } + } + + 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 RuntimeException(e); + } catch (Exception e1) { + throw new RuntimeException(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/RemoteInterpreterProcessListener.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessListener.java similarity index 95% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessListener.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessListener.java index 0e9dc5128dc..8b23bf287fc 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessListener.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcessListener.java @@ -41,5 +41,5 @@ public interface RemoteWorksEventListener { public void onError(); } public void onParaInfosReceived(String noteId, String paragraphId, - String interpreterSettingId, Map metaInfos); + String interpreterSettingId, Map metaInfos); } 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 bb176bea590..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 @@ -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,15 +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, - RemoteInterpreterProcessListener listener, - ApplicationEventListener appListener, String host, int port ) { - super(connectTimeout, listener, appListener); + super(connectTimeout); + this.interpreterSettingName = interpreterSettingName; this.host = host; this.port = port; } @@ -51,13 +53,35 @@ public int getPort() { } @Override - public void start(String userName, Boolean isUserImpersonate) { + public String getInterpreterSettingName() { + return interpreterSettingName; + } + + @Override + public void start(String userName) { // assume process is externally managed. nothing to do } @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/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/CredentialInjector.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/CredentialInjector.java new file mode 100644 index 00000000000..7f7226ffa75 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/CredentialInjector.java @@ -0,0 +1,112 @@ +/* + * 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.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResultMessage; +import org.apache.zeppelin.user.UserCredentials; +import org.apache.zeppelin.user.UsernamePassword; + +/** + * Class for replacing {user.>credentialkey<} and + * {password.>credentialkey<} tags with the matching credentials from + * zeppelin + */ +class CredentialInjector { + + private Set passwords = new HashSet<>(); + private final UserCredentials creds; + private static final Pattern userpattern = Pattern.compile("\\{user\\.([^\\}]+)\\}"); + private static final Pattern passwordpattern = Pattern.compile("\\{password\\.([^\\}]+)\\}"); + + + public CredentialInjector(UserCredentials creds) { + this.creds = creds; + } + + public String replaceCredentials(String code) { + if (code == null) { + return null; + } + String replaced = code; + Matcher matcher = userpattern.matcher(replaced); + while (matcher.find()) { + String key = matcher.group(1); + UsernamePassword usernamePassword = creds.getUsernamePassword(key); + if (usernamePassword != null) { + String value = usernamePassword.getUsername(); + String quotedValue = Matcher.quoteReplacement(value); + replaced = matcher.replaceFirst(quotedValue); + matcher = userpattern.matcher(replaced); + } + } + matcher = passwordpattern.matcher(replaced); + while (matcher.find()) { + String key = matcher.group(1); + UsernamePassword usernamePassword = creds.getUsernamePassword(key); + if (usernamePassword != null) { + passwords.add(usernamePassword.getPassword()); + String value = usernamePassword.getPassword(); + String quotedValue = Matcher.quoteReplacement(value); + replaced = matcher.replaceFirst(quotedValue); + matcher = passwordpattern.matcher(replaced); + } + } + return replaced; + } + + public InterpreterResult hidePasswords(InterpreterResult ret) { + if (ret == null) { + return null; + } + return new InterpreterResult(ret.code(), replacePasswords(ret.message())); + } + + private List replacePasswords(List original) { + List replaced = new ArrayList<>(); + for (InterpreterResultMessage msg : original) { + switch(msg.getType()) { + case HTML: + case TEXT: + case TABLE: { + String replacedMessages = replacePasswords(msg.getData()); + replaced.add(new InterpreterResultMessage(msg.getType(), replacedMessages)); + break; + } + default: + replaced.add(msg); + } + } + return replaced; + } + + private String replacePasswords(String str) { + String result = str; + for (String password : passwords) { + result = result.replace(password, "###"); + } + return result; + } + +} 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..b8cdbabf75f --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FileSystemStorage.java @@ -0,0 +1,201 @@ +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.hadoop.security.UserGroupInformation.AuthenticationMethod; +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); + + // only do UserGroupInformation.loginUserFromKeytab one time, otherwise you will still get + // your ticket expired. + static { + if (isKerberosSecurityEnabled()) { + ZeppelinConfiguration zConf = ZeppelinConfiguration.create(); + 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 RuntimeException("keytab and principal can not be empty, keytab: " + keytab + + ", principal: " + principal); + } + try { + UserGroupInformation.loginUserFromKeytab(principal, keytab); + } catch (IOException e) { + throw new RuntimeException("Fail to login via keytab:" + keytab + + ", principal:" + principal, e); + } + } + } + + private ZeppelinConfiguration zConf; + private Configuration hadoopConf; + private boolean isSecurityEnabled; + private FileSystem fs; + + 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 + // non-hadoop filesystem api + // disable caching for file:// scheme to avoid getting LocalFS which does CRC checks + this.hadoopConf.setBoolean("fs.file.impl.disable.cache", true); + this.hadoopConf.set("fs.file.impl", RawLocalFileSystem.class.getName()); + this.isSecurityEnabled = UserGroupInformation.isSecurityEnabled(); + + try { + this.fs = FileSystem.get(new URI(path), this.hadoopConf); + } catch (URISyntaxException e) { + throw new IOException(e); + } + } + + public FileSystem getFs() { + return fs; + } + + public Path makeQualified(Path path) { + return fs.makeQualified(path); + } + + public boolean exists(final Path path) throws IOException { + return callHdfsOperation(new HdfsOperation() { + + @Override + public Boolean call() throws IOException { + return fs.exists(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) { + 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(); + } + } + + 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; + } + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Folder.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Folder.java index 18535506776..afd5229f3c5 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Folder.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Folder.java @@ -17,6 +17,7 @@ package org.apache.zeppelin.notebook; +import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -216,6 +217,25 @@ public List getNotesRecursively() { return notes; } + public List getNotesRecursively(Set userAndRoles, + NotebookAuthorization notebookAuthorization) { + final Set entities = Sets.newHashSet(); + if (userAndRoles != null) { + entities.addAll(userAndRoles); + } + + List notes = new ArrayList<>(); + for (Note note : getNotes()) { + if (notebookAuthorization.isOwner(note.getId(), entities)) { + notes.add(note); + } + } + for (Folder child : children.values()) { + notes.addAll(child.getNotesRecursively(userAndRoles, notebookAuthorization)); + } + return notes; + } + public int countNotes() { return notes.size(); } 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..7a7b992f977 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 @@ -19,29 +19,43 @@ import static java.lang.String.format; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import java.io.IOException; -import java.io.Serializable; -import java.util.*; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; - -import com.google.common.annotations.VisibleForTesting; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import org.apache.commons.lang.StringUtils; import org.apache.zeppelin.common.JsonSerializable; +import org.apache.zeppelin.completer.CompletionType; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.display.AngularObject; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.Input; -import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.InterpreterFactory; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterInfo; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResultMessage; +import org.apache.zeppelin.interpreter.InterpreterSetting; +import org.apache.zeppelin.interpreter.InterpreterSettingManager; import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.notebook.repo.NotebookRepo; +import org.apache.zeppelin.notebook.repo.NotebookRepoSync; +import org.apache.zeppelin.notebook.repo.NotebookRepoWithVersionControl; 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; @@ -50,9 +64,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.base.Preconditions; -import com.google.gson.Gson; - /** * Binded interpreters for a note */ @@ -63,7 +74,8 @@ public class Note implements ParagraphJobListener, JsonSerializable { .setPrettyPrinting() .setDateFormat("yyyy-MM-dd HH:mm:ss.SSS") .registerTypeAdapter(Date.class, new NotebookImportDeserializer()) - .registerTypeAdapterFactory(Input.TypeAdapterFactory).create(); + .registerTypeAdapterFactory(Input.TypeAdapterFactory) + .create(); // threadpool for delayed persist of note private static final ScheduledThreadPoolExecutor delayedPersistThreadPool = @@ -77,6 +89,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(); @@ -88,6 +103,7 @@ public class Note implements ParagraphJobListener, JsonSerializable { private transient NotebookRepo repo; private transient SearchService index; private transient ScheduledFuture delayedPersist; + private transient Object delayedPersistLock = new Object(); private transient NoteEventListener noteEventListener; private transient Credentials credentials; private transient NoteNameListener noteNameListener; @@ -106,6 +122,7 @@ public class Note implements ParagraphJobListener, JsonSerializable { public Note() { + generateId(); } public Note(NotebookRepo repo, InterpreterFactory factory, @@ -125,11 +142,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); @@ -165,6 +177,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(); @@ -232,7 +260,7 @@ public void setNoteNameListener(NoteNameListener listener) { this.noteNameListener = listener; } - void setInterpreterFactory(InterpreterFactory factory) { + public void setInterpreterFactory(InterpreterFactory factory) { this.factory = factory; synchronized (paragraphs) { for (Paragraph p : paragraphs) { @@ -243,11 +271,6 @@ void setInterpreterFactory(InterpreterFactory factory) { void setInterpreterSettingManager(InterpreterSettingManager interpreterSettingManager) { this.interpreterSettingManager = interpreterSettingManager; - synchronized (paragraphs) { - for (Paragraph p : paragraphs) { - p.setInterpreterSettingManager(interpreterSettingManager); - } - } } public void initializeJobListenerForParagraph(Paragraph paragraph) { @@ -281,6 +304,27 @@ void setNotebookRepo(NotebookRepo repo) { this.repo = repo; } + public Boolean isCronSupported(ZeppelinConfiguration config) { + if (config.isZeppelinNotebookCronEnable()) { + config.getZeppelinNotebookCronFolders(); + if (config.getZeppelinNotebookCronFolders() == null) { + return true; + } else { + for (String folder : config.getZeppelinNotebookCronFolders().split(",")) { + folder = folder.replaceAll("\\*", "\\.*").replaceAll("\\?", "\\."); + if (getName().matches(folder)) { + return true; + } + } + } + } + return false; + } + + public void setCronSupported(ZeppelinConfiguration config) { + getConfig().put("isZeppelinNotebookCronEnable", isCronSupported(config)); + } + public void setIndex(SearchService index) { this.index = index; } @@ -310,21 +354,26 @@ public Paragraph addNewParagraph(AuthenticationInfo authenticationInfo) { * * @param srcParagraph source paragraph */ - void addCloneParagraph(Paragraph srcParagraph) { + void addCloneParagraph(Paragraph srcParagraph, AuthenticationInfo subject) { // Keep paragraph original ID - final Paragraph newParagraph = new Paragraph(srcParagraph.getId(), this, this, factory, - interpreterSettingManager); + final Paragraph newParagraph = new Paragraph(srcParagraph.getId(), this, this, factory); Map config = new HashMap<>(srcParagraph.getConfig()); Map param = srcParagraph.settings.getParams(); LinkedHashMap form = srcParagraph.settings.getForms(); + logger.debug("srcParagraph user: " + srcParagraph.getUser()); + + newParagraph.setAuthenticationInfo(subject); newParagraph.setConfig(config); newParagraph.settings.setParams(param); newParagraph.settings.setForms(form); newParagraph.setText(srcParagraph.getText()); newParagraph.setTitle(srcParagraph.getTitle()); + + logger.debug("newParagraph user: " + newParagraph.getUser()); + try { Gson gson = new Gson(); @@ -357,7 +406,7 @@ public Paragraph insertNewParagraph(int index, AuthenticationInfo authentication } private Paragraph createParagraph(int index, AuthenticationInfo authenticationInfo) { - Paragraph p = new Paragraph(this, this, factory, interpreterSettingManager); + Paragraph p = new Paragraph(this, this, factory); p.setAuthenticationInfo(authenticationInfo); setParagraphMagic(p, index); return p; @@ -384,7 +433,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()) { @@ -515,6 +564,10 @@ public boolean isLastParagraph(String paragraphId) { return true; } + public int getParagraphCount() { + return paragraphs.size(); + } + public Paragraph getParagraph(String paragraphId) { synchronized (paragraphs) { for (Paragraph p : paragraphs) { @@ -574,74 +627,60 @@ private Map populateParagraphInfo(Paragraph p) { private void setParagraphMagic(Paragraph p, int index) { if (paragraphs.size() > 0) { - String magic; + String replName; if (index == 0) { - magic = paragraphs.get(0).getMagic(); + replName = paragraphs.get(0).getIntpText(); } else { - magic = paragraphs.get(index - 1).getMagic(); + replName = paragraphs.get(index - 1).getIntpText(); } - if (StringUtils.isNotEmpty(magic)) { - p.setText(magic + "\n"); + if (p.isValidInterpreter(replName) && StringUtils.isNotEmpty(replName)) { + p.setText("%" + replName + "\n"); } } } /** - * Run all paragraphs sequentially. + * Run all paragraphs sequentially. Only used for CronJob */ public synchronized void runAll() { String cronExecutingUser = (String) getConfig().get("cronExecutingUser"); + String cronExecutingRoles = (String) getConfig().get("cronExecutingRoles"); if (null == cronExecutingUser) { cronExecutingUser = "anonymous"; } - AuthenticationInfo authenticationInfo = new AuthenticationInfo(); - authenticationInfo.setUser(cronExecutingUser); - runAll(authenticationInfo); + AuthenticationInfo authenticationInfo = new AuthenticationInfo( + cronExecutingUser, + StringUtils.isEmpty(cronExecutingRoles) ? null : cronExecutingRoles, + null); + runAll(authenticationInfo, true); } - public void runAll(AuthenticationInfo authenticationInfo) { + public void runAll(AuthenticationInfo authenticationInfo, boolean blocking) { for (Paragraph p : getParagraphs()) { if (!p.isEnabled()) { continue; } p.setAuthenticationInfo(authenticationInfo); - run(p.getId()); + if (!run(p.getId(), blocking)) { + logger.warn("Skip running the remain notes because paragraph {} fails", p.getId()); + break; + } } } + public boolean run(String paragraphId) { + return run(paragraphId, false); + } + /** * Run a single paragraph. * * @param paragraphId ID of paragraph */ - public void run(String paragraphId) { + public boolean run(String paragraphId, boolean blocking) { Paragraph p = getParagraph(paragraphId); p.setListener(jobListenerFactory.getParagraphJobListener(this)); - - if (p.isBlankParagraph()) { - logger.info("skip to run blank paragraph. {}", p.getId()); - p.setStatus(Job.Status.FINISHED); - return; - } - - p.clearRuntimeInfo(null); - String requiredReplName = p.getRequiredReplName(); - Interpreter intp = factory.getInterpreter(p.getUser(), getId(), requiredReplName); - - if (intp == null) { - String intpExceptionMsg = - p.getJobName() + "'s Interpreter " + requiredReplName + " not found"; - InterpreterException intpException = new InterpreterException(intpExceptionMsg); - InterpreterResult intpResult = - new InterpreterResult(InterpreterResult.Code.ERROR, intpException.getMessage()); - p.setReturn(intpResult, intpException); - p.setStatus(Job.Status.ERROR); - throw intpException; - } - if (p.getConfig().get("enabled") == null || (Boolean) p.getConfig().get("enabled")) { - p.setAuthenticationInfo(p.getAuthenticationInfo()); - intp.getScheduler().submit(p); - } + return p.execute(blocking); } /** @@ -659,6 +698,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) == '/') { @@ -674,6 +729,23 @@ public List completion(String paragraphId, String buffer, return p.completion(buffer, cursor); } + public List getInterpreterCompletion() { + List completion = new LinkedList(); + for (InterpreterSetting intp : interpreterSettingManager.getInterpreterSettings(getId())) { + List intInfo = intp.getInterpreterInfos(); + if (intInfo.size() > 1) { + for (InterpreterInfo info : intInfo) { + String name = intp.getName() + "." + info.getName(); + completion.add(new InterpreterCompletion(name, name, CompletionType.setting.name())); + } + } else { + completion.add(new InterpreterCompletion(intp.getName(), intp.getName(), + CompletionType.setting.name())); + } + } + return completion; + } + public List getParagraphs() { synchronized (paragraphs) { return new LinkedList<>(paragraphs); @@ -690,8 +762,10 @@ private void snapshotAngularObjectRegistry(String user) { for (InterpreterSetting setting : settings) { InterpreterGroup intpGroup = setting.getInterpreterGroup(user, id); - AngularObjectRegistry registry = intpGroup.getAngularObjectRegistry(); - angularObjects.put(intpGroup.getId(), registry.getAllWithGlobal(id)); + if (intpGroup != null) { + AngularObjectRegistry registry = intpGroup.getAngularObjectRegistry(); + angularObjects.put(intpGroup.getId(), registry.getAllWithGlobal(id)); + } } } @@ -704,6 +778,9 @@ private void removeAllAngularObjectInParagraph(String user, String paragraphId) } for (InterpreterSetting setting : settings) { + if (setting.getInterpreterGroup(user, id) == null) { + continue; + } InterpreterGroup intpGroup = setting.getInterpreterGroup(user, id); AngularObjectRegistry registry = intpGroup.getAngularObjectRegistry(); @@ -780,7 +857,7 @@ public Note getUserNote(String user) { } private void startDelayedPersistTimer(int maxDelaySec, final AuthenticationInfo subject) { - synchronized (this) { + synchronized (delayedPersistLock) { if (delayedPersist != null) { return; } @@ -800,11 +877,10 @@ public void run() { } private void stopDelayedPersistTimer() { - synchronized (this) { + synchronized (delayedPersistLock) { if (delayedPersist == null) { return; } - delayedPersist.cancel(false); } } @@ -899,6 +975,11 @@ void setNoteEventListener(NoteEventListener noteEventListener) { this.noteEventListener = noteEventListener; } + boolean hasInterpreterBinded() { + return !interpreterSettingManager.getInterpreterSettings(getId()).isEmpty(); + } + + @Override public String toJson() { return gson.toJson(this); } @@ -906,13 +987,27 @@ public String toJson() { public static Note fromJson(String json) { Note note = gson.fromJson(json, Note.class); convertOldInput(note); - note.resetRuntimeInfos(); + note.postProcessParagraphs(); return note; } - public void resetRuntimeInfos() { + 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/Notebook.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java index 07febf17a1f..59ac45bf542 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,8 +17,11 @@ package org.apache.zeppelin.notebook; +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 java.io.IOException; -import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -30,42 +33,38 @@ 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 com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.stream.JsonReader; -import org.apache.zeppelin.interpreter.*; -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.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterFactory; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterSetting; +import org.apache.zeppelin.interpreter.InterpreterSettingManager; 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; -import org.apache.zeppelin.resource.ResourcePoolUtils; +import org.apache.zeppelin.notebook.repo.NotebookRepoWithVersionControl; +import org.apache.zeppelin.notebook.repo.NotebookRepoWithVersionControl.Revision; import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.scheduler.SchedulerFactory; import org.apache.zeppelin.search.SearchService; import org.apache.zeppelin.user.AuthenticationInfo; import org.apache.zeppelin.user.Credentials; +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; /** * Collection of Notes. @@ -85,8 +84,9 @@ public class Notebook implements NoteEventListener { private final FolderView folders = new FolderView(); private ZeppelinConfiguration conf; private StdSchedulerFactory quertzSchedFact; - private org.quartz.Scheduler quartzSched; + org.quartz.Scheduler quartzSched; private JobListenerFactory jobListenerFactory; + private NotebookRepo notebookRepo; private SearchService noteSearchService; private NotebookAuthorization notebookAuthorization; @@ -140,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.getDefaultInterpreterSettingList(), subject); + note = createNote(interpreterSettingManager.getInterpreterSettingIds(), subject); } else { note = createNote(null, subject); } @@ -204,13 +204,15 @@ public Note importNote(String sourceJson, String noteName, AuthenticationInfo su Note oldNote = Note.fromJson(sourceJson); convertFromSingleResultToMultipleResultsFormat(oldNote); newNote = createNote(subject); - if (noteName != null) + if (noteName != null) { newNote.setName(noteName); - else + } else { newNote.setName(oldNote.getName()); + } + newNote.setCronSupported(getConf()); List paragraphs = oldNote.getParagraphs(); for (Paragraph p : paragraphs) { - newNote.addCloneParagraph(p); + newNote.addCloneParagraph(p, subject); } notebookAuthorization.setNewNotePermissions(newNote.getId(), subject); @@ -244,13 +246,14 @@ public Note cloneNote(String sourceNoteId, String newNoteName, AuthenticationInf } else { newNote.setName("Note " + newNote.getId()); } + newNote.setCronSupported(getConf()); // Copy the interpreter bindings List boundInterpreterSettingsIds = getBindedInterpreterSettingsIds(sourceNote.getId()); bindInterpretersToNote(subject.getUser(), newNote.getId(), boundInterpreterSettingsIds); List paragraphs = sourceNote.getParagraphs(); for (Paragraph p : paragraphs) { - newNote.addCloneParagraph(p); + newNote.addCloneParagraph(p, subject); } noteSearchService.addIndexDoc(newNote); @@ -270,8 +273,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 +282,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 +316,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(); } } @@ -338,43 +342,45 @@ 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(); - if (registry instanceof RemoteAngularObjectRegistry) { - // remove paragraph scope object - for (Paragraph p : note.getParagraphs()) { - ((RemoteAngularObjectRegistry) registry).removeAllAndNotifyRemoteProcess(id, p.getId()); - - // remove app scope object - List appStates = p.getAllApplicationStates(); - if (appStates != null) { - for (ApplicationState app : appStates) { - ((RemoteAngularObjectRegistry) registry) - .removeAllAndNotifyRemoteProcess(id, app.getId()); + InterpreterGroup interpreterGroup = settings.getInterpreterGroup(subject.getUser(), id); + if (interpreterGroup != null) { + AngularObjectRegistry registry = interpreterGroup.getAngularObjectRegistry(); + if (registry instanceof RemoteAngularObjectRegistry) { + // remove paragraph scope object + for (Paragraph p : note.getParagraphs()) { + ((RemoteAngularObjectRegistry) registry).removeAllAndNotifyRemoteProcess(id, p.getId()); + + // remove app scope object + List appStates = p.getAllApplicationStates(); + if (appStates != null) { + for (ApplicationState app : appStates) { + ((RemoteAngularObjectRegistry) registry) + .removeAllAndNotifyRemoteProcess(id, app.getId()); + } } } - } - // remove note scope object - ((RemoteAngularObjectRegistry) registry).removeAllAndNotifyRemoteProcess(id, null); - } else { - // remove paragraph scope object - for (Paragraph p : note.getParagraphs()) { - registry.removeAll(id, p.getId()); - - // remove app scope object - List appStates = p.getAllApplicationStates(); - if (appStates != null) { - for (ApplicationState app : appStates) { - registry.removeAll(id, app.getId()); + // remove note scope object + ((RemoteAngularObjectRegistry) registry).removeAllAndNotifyRemoteProcess(id, null); + } else { + // remove paragraph scope object + for (Paragraph p : note.getParagraphs()) { + registry.removeAll(id, p.getId()); + + // remove app scope object + List appStates = p.getAllApplicationStates(); + if (appStates != null) { + for (ApplicationState app : appStates) { + registry.removeAll(id, app.getId()); + } } } + // remove note scope object + registry.removeAll(id, null); } - // remove note scope object - registry.removeAll(id, null); } } - ResourcePoolUtils.removeResourcesBelongsToNote(id); + interpreterSettingManager.removeResourcesBelongsToNote(id); fireNoteRemoveEvent(note); @@ -387,22 +393,40 @@ public void removeNote(String id, AuthenticationInfo subject) { public Revision checkpointNote(String noteId, String checkpointMessage, AuthenticationInfo subject) throws IOException { - return notebookRepo.checkpoint(noteId, checkpointMessage, subject); + if (((NotebookRepoSync) notebookRepo).isRevisionSupportedInDefaultRepo()) { + return ((NotebookRepoWithVersionControl) notebookRepo) + .checkpoint(noteId, checkpointMessage, subject); + } else { + return null; + + } } - public List listRevisionHistory(String noteId, - AuthenticationInfo subject) { - return notebookRepo.revisionHistory(noteId, subject); + public List listRevisionHistory(String noteId, AuthenticationInfo subject) { + if (((NotebookRepoSync) notebookRepo).isRevisionSupportedInDefaultRepo()) { + return ((NotebookRepoWithVersionControl) notebookRepo).revisionHistory(noteId, subject); + } else { + return null; + } } public Note setNoteRevision(String noteId, String revisionId, AuthenticationInfo subject) throws IOException { - return notebookRepo.setNoteRevision(noteId, revisionId, subject); + if (((NotebookRepoSync) notebookRepo).isRevisionSupportedInDefaultRepo()) { + return ((NotebookRepoWithVersionControl) notebookRepo) + .setNoteRevision(noteId, revisionId, subject); + } else { + return null; + } } - + public Note getNoteByRevision(String noteId, String revisionId, AuthenticationInfo subject) throws IOException { - return notebookRepo.get(noteId, revisionId, subject); + if (((NotebookRepoSync) notebookRepo).isRevisionSupportedInDefaultRepo()) { + return ((NotebookRepoWithVersionControl) notebookRepo).get(noteId, revisionId, subject); + } else { + return null; + } } public void convertFromSingleResultToMultipleResultsFormat(Note note) { @@ -447,6 +471,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); } } @@ -479,6 +522,7 @@ public Note loadNoteFromRepo(String id, AuthenticationInfo subject) { note.setJobListenerFactory(jobListenerFactory); note.setNotebookRepo(notebookRepo); + note.setCronSupported(getConf()); Map angularObjectSnapshot = new HashMap<>(); @@ -522,7 +566,7 @@ public Note loadNoteFromRepo(String id, AuthenticationInfo subject) { List settings = interpreterSettingManager.get(); for (InterpreterSetting setting : settings) { InterpreterGroup intpGroup = setting.getInterpreterGroup(subject.getUser(), note.getId()); - if (intpGroup.getId().equals(snapshot.getIntpGroupId())) { + if (intpGroup != null && intpGroup.getId().equals(snapshot.getIntpGroupId())) { AngularObjectRegistry registry = intpGroup.getAngularObjectRegistry(); String noteId = snapshot.getAngularObject().getNoteId(); String paragraphId = snapshot.getAngularObject().getParagraphId(); @@ -608,6 +652,11 @@ public List getNotesUnderFolder(String folderId) { return folders.getFolder(folderId).getNotesRecursively(); } + public List getNotesUnderFolder(String folderId, + Set userAndRoles) { + return folders.getFolder(folderId).getNotesRecursively(userAndRoles, notebookAuthorization); + } + public List getAllNotes() { synchronized (notes) { List noteList = new ArrayList<>(notes.values()); @@ -829,7 +878,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) { @@ -873,21 +922,30 @@ 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; } + if (!note.isCronSupported(notebook.getConf())) { + logger.warn("execution of the cron job is skipped cron is not enabled from " + + "Zeppelin server"); + return; + } + + note.runAll(); + 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); @@ -895,7 +953,12 @@ public void execute(JobExecutionContext context) throws JobExecutionException { if (releaseResource) { for (InterpreterSetting setting : notebook.getInterpreterSettingManager() .getInterpreterSettings(note.getId())) { - notebook.getInterpreterSettingManager().restart(setting.getId()); + try { + notebook.getInterpreterSettingManager().restart(setting.getId(), noteId, + cronExecutingUser != null ? cronExecutingUser : "anonymous"); + } catch (InterpreterException e) { + logger.error("Fail to restart interpreter: " + setting.getId(), e); + } } } } @@ -906,7 +969,7 @@ public void refreshCron(String id) { synchronized (notes) { Note note = notes.get(id); - if (note == null) { + if (note == null || note.isTrash()) { return; } Map config = note.getConfig(); @@ -914,6 +977,12 @@ public void refreshCron(String id) { return; } + if (!note.isCronSupported(getConf())) { + logger.warn("execution of the cron job is skipped cron is not enabled from " + + "Zeppelin server"); + return; + } + String cronExpr = (String) note.getConfig().get("cron"); if (cronExpr == null || cronExpr.trim().length() == 0) { return; @@ -948,7 +1017,7 @@ public void refreshCron(String id) { } } - private void removeCron(String id) { + public void removeCron(String id) { try { quartzSched.deleteJob(new JobKey(id, "note")); } catch (SchedulerException e) { @@ -999,6 +1068,16 @@ private void fireUnbindInterpreter(Note note, InterpreterSetting setting) { } } + public Boolean isRevisionSupported() { + if (notebookRepo instanceof NotebookRepoSync) { + return ((NotebookRepoSync) notebookRepo).isRevisionSupportedInDefaultRepo(); + } else if (notebookRepo instanceof NotebookRepoWithVersionControl) { + return true; + } else { + return false; + } + } + @Override public void onParagraphRemove(Paragraph p) { for (NotebookEventListener listener : notebookEventListeners) { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NotebookAuthorization.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NotebookAuthorization.java index 500f068775a..137af651aa9 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NotebookAuthorization.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NotebookAuthorization.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -35,6 +34,8 @@ import org.apache.commons.lang.StringUtils; import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; +import org.apache.zeppelin.storage.ConfigStorage; import org.apache.zeppelin.user.AuthenticationInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +53,8 @@ public class NotebookAuthorization { private static final Logger LOG = LoggerFactory.getLogger(NotebookAuthorization.class); private static NotebookAuthorization instance = null; /* - * { "note1": { "owners": ["u1"], "readers": ["u1", "u2"], "writers": ["u1"] }, "note2": ... } } + * { "note1": { "owners": ["u1"], "readers": ["u1", "u2"], "runners": ["u2"], + * "writers": ["u1"] }, "note2": ... } } */ private static Map>> authInfo = new HashMap<>(); /* @@ -60,8 +62,8 @@ public class NotebookAuthorization { */ private static Map> userRoles = new HashMap<>(); private static ZeppelinConfiguration conf; - private static Gson gson; - private static String filePath; + + private static ConfigStorage configStorage; private NotebookAuthorization() {} @@ -69,11 +71,8 @@ public static NotebookAuthorization init(ZeppelinConfiguration config) { if (instance == null) { instance = new NotebookAuthorization(); conf = config; - filePath = conf.getNotebookAuthorizationPath(); - GsonBuilder builder = new GsonBuilder(); - builder.setPrettyPrinting(); - gson = builder.create(); try { + configStorage = ConfigStorage.getInstance(config); loadFromFile(); } catch (IOException e) { LOG.error("Error loading NotebookAuthorization", e); @@ -92,26 +91,10 @@ public static NotebookAuthorization getInstance() { } private static void loadFromFile() throws IOException { - File settingFile = new File(filePath); - LOG.info(settingFile.getAbsolutePath()); - if (!settingFile.exists()) { - // nothing to read - return; + NotebookAuthorizationInfoSaving info = configStorage.loadNotebookAuthorization(); + if (info != null) { + authInfo = info.authInfo; } - FileInputStream fis = new FileInputStream(settingFile); - InputStreamReader isr = new InputStreamReader(fis); - BufferedReader bufferedReader = new BufferedReader(isr); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = bufferedReader.readLine()) != null) { - sb.append(line); - } - isr.close(); - fis.close(); - - String json = sb.toString(); - NotebookAuthorizationInfoSaving info = NotebookAuthorizationInfoSaving.fromJson(json); - authInfo = info.authInfo; } public void setRoles(String user, Set roles) { @@ -132,32 +115,19 @@ public Set getRoles(String user) { } private void saveToFile() { - String jsonString; - synchronized (authInfo) { NotebookAuthorizationInfoSaving info = new NotebookAuthorizationInfoSaving(); info.authInfo = authInfo; - jsonString = gson.toJson(info); - } - - try { - File settingFile = new File(filePath); - if (!settingFile.exists()) { - settingFile.createNewFile(); + try { + configStorage.save(info); + } catch (IOException e) { + LOG.error("Error saving notebook authorization file", e); } - - FileOutputStream fos = new FileOutputStream(settingFile, false); - OutputStreamWriter out = new OutputStreamWriter(fos); - out.append(jsonString); - out.close(); - fos.close(); - } catch (IOException e) { - LOG.error("Error saving notebook authorization file: " + e.getMessage()); } } public boolean isPublic() { - return conf.isNotebokPublic(); + return conf.isNotebookPublic(); } private Set validateUser(Set users) { @@ -177,6 +147,7 @@ public void setOwners(String noteId, Set entities) { noteAuthInfo = new LinkedHashMap(); noteAuthInfo.put("owners", new LinkedHashSet(entities)); noteAuthInfo.put("readers", new LinkedHashSet()); + noteAuthInfo.put("runners", new LinkedHashSet()); noteAuthInfo.put("writers", new LinkedHashSet()); } else { noteAuthInfo.put("owners", new LinkedHashSet(entities)); @@ -192,6 +163,7 @@ public void setReaders(String noteId, Set entities) { noteAuthInfo = new LinkedHashMap(); noteAuthInfo.put("owners", new LinkedHashSet()); noteAuthInfo.put("readers", new LinkedHashSet(entities)); + noteAuthInfo.put("runners", new LinkedHashSet()); noteAuthInfo.put("writers", new LinkedHashSet()); } else { noteAuthInfo.put("readers", new LinkedHashSet(entities)); @@ -200,6 +172,23 @@ public void setReaders(String noteId, Set entities) { saveToFile(); } + public void setRunners(String noteId, Set entities) { + Map> noteAuthInfo = authInfo.get(noteId); + entities = validateUser(entities); + if (noteAuthInfo == null) { + noteAuthInfo = new LinkedHashMap(); + noteAuthInfo.put("owners", new LinkedHashSet()); + noteAuthInfo.put("readers", new LinkedHashSet()); + noteAuthInfo.put("runners", new LinkedHashSet(entities)); + noteAuthInfo.put("writers", new LinkedHashSet()); + } else { + noteAuthInfo.put("runners", new LinkedHashSet(entities)); + } + authInfo.put(noteId, noteAuthInfo); + saveToFile(); + } + + public void setWriters(String noteId, Set entities) { Map> noteAuthInfo = authInfo.get(noteId); entities = validateUser(entities); @@ -207,6 +196,7 @@ public void setWriters(String noteId, Set entities) { noteAuthInfo = new LinkedHashMap(); noteAuthInfo.put("owners", new LinkedHashSet()); noteAuthInfo.put("readers", new LinkedHashSet()); + noteAuthInfo.put("runners", new LinkedHashSet()); noteAuthInfo.put("writers", new LinkedHashSet(entities)); } else { noteAuthInfo.put("writers", new LinkedHashSet(entities)); @@ -215,6 +205,21 @@ public void setWriters(String noteId, Set entities) { saveToFile(); } + /* + * If case conversion is enforced, then change entity names to lower case + */ + private Set checkCaseAndConvert(Set entities) { + if (conf.isUsernameForceLowerCase()) { + Set set2 = new HashSet(); + for (String name : entities) { + set2.add(name.toLowerCase()); + } + return set2; + } else { + return entities; + } + } + public Set getOwners(String noteId) { Map> noteAuthInfo = authInfo.get(noteId); Set entities = null; @@ -224,6 +229,8 @@ public Set getOwners(String noteId) { entities = noteAuthInfo.get("owners"); if (entities == null) { entities = new HashSet<>(); + } else { + entities = checkCaseAndConvert(entities); } } return entities; @@ -238,6 +245,24 @@ public Set getReaders(String noteId) { entities = noteAuthInfo.get("readers"); if (entities == null) { entities = new HashSet<>(); + } else { + entities = checkCaseAndConvert(entities); + } + } + return entities; + } + + public Set getRunners(String noteId) { + Map> noteAuthInfo = authInfo.get(noteId); + Set entities = null; + if (noteAuthInfo == null) { + entities = new HashSet<>(); + } else { + entities = noteAuthInfo.get("runners"); + if (entities == null) { + entities = new HashSet<>(); + } else { + entities = checkCaseAndConvert(entities); } } return entities; @@ -252,23 +277,44 @@ public Set getWriters(String noteId) { entities = noteAuthInfo.get("writers"); if (entities == null) { entities = new HashSet<>(); + } else { + entities = checkCaseAndConvert(entities); } } return entities; } public boolean isOwner(String noteId, Set entities) { - return isMember(entities, getOwners(noteId)); + return isMember(entities, getOwners(noteId)) || isAdmin(entities); } public boolean isWriter(String noteId, Set entities) { - return isMember(entities, getWriters(noteId)) || isMember(entities, getOwners(noteId)); + return isMember(entities, getWriters(noteId)) || + isMember(entities, getOwners(noteId)) || + isAdmin(entities); } public boolean isReader(String noteId, Set entities) { return isMember(entities, getReaders(noteId)) || - isMember(entities, getOwners(noteId)) || - isMember(entities, getWriters(noteId)); + isMember(entities, getOwners(noteId)) || + isMember(entities, getWriters(noteId)) || + isMember(entities, getRunners(noteId)) || + isAdmin(entities); + } + + public boolean isRunner(String noteId, Set entities) { + return isMember(entities, getRunners(noteId)) || + isMember(entities, getWriters(noteId)) || + isMember(entities, getOwners(noteId)) || + isAdmin(entities); + } + + private boolean isAdmin(Set entities) { + String adminRole = conf.getString(ConfVars.ZEPPELIN_OWNER_ROLE); + if (StringUtils.isBlank(adminRole)) { + return false; + } + return entities.contains(adminRole); } // return true if b is empty or if (a intersection b) is non-empty @@ -311,6 +357,17 @@ public boolean hasReadAuthorization(Set userAndRoles, String noteId) { return isReader(noteId, userAndRoles); } + public boolean hasRunAuthorization(Set userAndRoles, String noteId) { + if (conf.isAnonymousAllowed()) { + LOG.debug("Zeppelin runs in anonymous mode, everybody is runner"); + return true; + } + if (userAndRoles == null) { + return false; + } + return isRunner(noteId, userAndRoles); + } + public void removeNote(String noteId) { authInfo.remove(noteId); saveToFile(); @@ -337,13 +394,16 @@ public void setNewNotePermissions(String noteId, AuthenticationInfo subject) { owners.add(subject.getUser()); setOwners(noteId, owners); } else { - // add current user to owners, readers, writers - private note + // add current user to owners, readers, runners, writers - private note Set entities = getOwners(noteId); entities.add(subject.getUser()); setOwners(noteId, entities); entities = getReaders(noteId); entities.add(subject.getUser()); setReaders(noteId, entities); + entities = getRunners(noteId); + entities.add(subject.getUser()); + setRunners(noteId, entities); entities = getWriters(noteId); entities.add(subject.getUser()); setWriters(noteId, entities); 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..f5a3c229800 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,63 +17,88 @@ package org.apache.zeppelin.notebook; -import com.google.common.collect.Maps; -import com.google.common.base.Strings; -import org.apache.commons.lang.StringUtils; +import java.io.IOException; +import java.security.SecureRandom; +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.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + 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.Constants; +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.InterpreterNotFoundException; +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.ManagedInterpreterGroup; +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. */ public class Paragraph extends Job implements Cloneable, JsonSerializable { - private static final long serialVersionUID = -6328572073497992016L; - private static Logger logger = LoggerFactory.getLogger(Paragraph.class); - private transient InterpreterFactory factory; - private transient InterpreterSettingManager interpreterSettingManager; + private static Pattern REPL_PATTERN = Pattern.compile("(\\s*)%([\\w\\.]+).*", Pattern.DOTALL); + + private transient InterpreterFactory interpreterFactory; + private transient Interpreter interpreter; private transient Note note; private transient AuthenticationInfo authenticationInfo; private transient Map userParagraphMap = Maps.newHashMap(); // personalized - String title; - String text; - String user; - Date dateUpdated; - private Map config; // paragraph configs like isOpen, colWidth, etc - public GUI settings; // form and parameter settings + private String title; + private String text; // text is composed of intpText and scriptText. + private transient String intpText; + private transient String scriptText; + private String user; + private Date dateUpdated; + // paragraph configs like isOpen, colWidth, etc + private Map config = new HashMap<>(); + public GUI settings = new GUI(); // form and parameter settings // 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; /** @@ -84,42 +109,23 @@ public class Paragraph extends Job implements Cloneable, JsonSerializable { @VisibleForTesting Paragraph() { super(generateId(), null); - config = new HashMap<>(); - settings = new GUI(); } public Paragraph(String paragraphId, Note note, JobListener listener, - InterpreterFactory factory, InterpreterSettingManager interpreterSettingManager) { + InterpreterFactory interpreterFactory) { super(paragraphId, generateId(), listener); this.note = note; - this.factory = factory; - this.interpreterSettingManager = interpreterSettingManager; - title = null; - text = null; - authenticationInfo = null; - user = null; - dateUpdated = null; - settings = new GUI(); - config = new HashMap<>(); - } - - public Paragraph(Note note, JobListener listener, InterpreterFactory factory, - InterpreterSettingManager interpreterSettingManager) { + this.interpreterFactory = interpreterFactory; + } + + public Paragraph(Note note, JobListener listener, InterpreterFactory interpreterFactory) { super(generateId(), listener); this.note = note; - this.factory = factory; - this.interpreterSettingManager = interpreterSettingManager; - title = null; - text = null; - authenticationInfo = null; - dateUpdated = null; - settings = new GUI(); - config = new HashMap<>(); + this.interpreterFactory = interpreterFactory; } private static String generateId() { - return "paragraph_" + System.currentTimeMillis() + "_" + new Random(System.currentTimeMillis()) - .nextInt(); + return "paragraph_" + System.currentTimeMillis() + "_" + new SecureRandom().nextInt(); } public Map getUserParagraphMap() { @@ -134,15 +140,20 @@ public Paragraph getUserParagraph(String user) { } @Override - public void setResult(Object results) { + public synchronized void setResult(Object results) { this.results = results; } public Paragraph cloneParagraphForUser(String user) { Paragraph p = new Paragraph(); + p.interpreterFactory = interpreterFactory; + p.note = note; p.settings.setParams(Maps.newHashMap(settings.getParams())); p.settings.setForms(Maps.newLinkedHashMap(settings.getForms())); p.setConfig(Maps.newHashMap(config)); + if (getAuthenticationInfo() != null) { + p.setAuthenticationInfo(getAuthenticationInfo()); + } p.setTitle(getTitle()); p.setText(getText()); p.setResult(getReturn()); @@ -169,8 +180,25 @@ public String getText() { } public void setText(String newText) { + // strip white space from the beginning this.text = newText; this.dateUpdated = new Date(); + parseText(); + } + + public void parseText() { + // parse text to get interpreter component + if (this.text != null) { + Matcher matcher = REPL_PATTERN.matcher(this.text); + if (matcher.matches()) { + String headingSpace = matcher.group(1); + this.intpText = matcher.group(2); + this.scriptText = this.text.substring(headingSpace.length() + intpText.length() + 1).trim(); + } else { + this.intpText = ""; + this.scriptText = this.text.trim(); + } + } } public AuthenticationInfo getAuthenticationInfo() { @@ -190,6 +218,14 @@ public void setTitle(String title) { this.title = title; } + public String getIntpText() { + return intpText; + } + + public String getScriptText() { + return scriptText; + } + public void setNote(Note note) { this.note = note; } @@ -203,115 +239,60 @@ public boolean isEnabled() { return enabled == null || enabled.booleanValue(); } - public String getRequiredReplName() { - return getRequiredReplName(text); + public Interpreter getBindedInterpreter() throws InterpreterNotFoundException { + return this.interpreterFactory.getInterpreter(user, note.getId(), intpText); } - public static String getRequiredReplName(String text) { - if (text == null) { - return null; - } - - String trimmed = text.trim(); - if (!trimmed.startsWith("%")) { - return null; - } + public void setInterpreter(Interpreter interpreter) { + this.interpreter = interpreter; + } - // get script head - int scriptHeadIndex = 0; - for (int i = 0; i < trimmed.length(); i++) { - char ch = trimmed.charAt(i); - if (Character.isWhitespace(ch) || ch == '(' || ch == '\n') { - break; + public List completion(String buffer, int cursor) { + String lines[] = buffer.split(System.getProperty("line.separator")); + if (lines.length > 0 && lines[0].startsWith("%") && cursor <= lines[0].trim().length()) { + int idx = lines[0].indexOf(' '); + if (idx < 0 || (idx > 0 && cursor <= idx)) { + return note.getInterpreterCompletion(); } - scriptHeadIndex = i; } - if (scriptHeadIndex < 1) { - return null; - } - String head = text.substring(1, scriptHeadIndex + 1); - return head; - } - - public String getScriptBody() { - return getScriptBody(text); - } - - public static String getScriptBody(String text) { - if (text == null) { + try { + this.interpreter = getBindedInterpreter(); + } catch (InterpreterNotFoundException e) { return null; } + setText(buffer); - String magic = getRequiredReplName(text); - if (magic == null) { - return text; - } - - String trimmed = text.trim(); - if (magic.length() + 1 >= trimmed.length()) { - return ""; - } - return trimmed.substring(magic.length() + 1).trim(); - } + cursor = calculateCursorPosition(buffer, cursor); - public Interpreter getRepl(String name) { - return factory.getInterpreter(user, note.getId(), name); - } - - public Interpreter getCurrentRepl() { - return getRepl(getRequiredReplName()); - } + InterpreterContext interpreterContext = getInterpreterContextWithoutRunner(null); - public List getInterpreterCompletion() { - List completion = new LinkedList(); - for (InterpreterSetting intp : interpreterSettingManager.getInterpreterSettings(note.getId())) { - List intInfo = intp.getInterpreterInfos(); - if (intInfo.size() > 1) { - for (InterpreterInfo info : intInfo) { - String name = intp.getName() + "." + info.getName(); - completion.add(new InterpreterCompletion(name, name, CompletionType.setting.name())); - } + try { + if (this.interpreter != null) { + return this.interpreter.completion(this.scriptText, cursor, interpreterContext); } else { - completion.add(new InterpreterCompletion(intp.getName(), intp.getName(), - CompletionType.setting.name())); + return null; } + } catch (InterpreterException e) { + throw new RuntimeException("Fail to get completion", e); } - return completion; } - public List completion(String buffer, int cursor) { - String lines[] = buffer.split(System.getProperty("line.separator")); - if (lines.length > 0 && lines[0].startsWith("%") && cursor <= lines[0].trim().length()) { - - int idx = lines[0].indexOf(' '); - if (idx < 0 || (idx > 0 && cursor <= idx)) { - return getInterpreterCompletion(); - } - } + public int calculateCursorPosition(String buffer, int cursor) { + // scriptText trimmed - String replName = getRequiredReplName(buffer); - if (replName != null && cursor > replName.length()) { - cursor -= replName.length() + 1; + if (this.scriptText.isEmpty()) { + return 0; } - - String body = getScriptBody(buffer); - Interpreter repl = getRepl(replName); - if (repl == null) { - return null; + int countCharactersBeforeScript = buffer.indexOf(this.scriptText); + if (countCharactersBeforeScript > 0) { + cursor -= countCharactersBeforeScript; } - InterpreterContext interpreterContext = getInterpreterContextWithoutRunner(null); - - List completion = repl.completion(body, cursor, interpreterContext); - return completion; + return cursor; } public void setInterpreterFactory(InterpreterFactory factory) { - this.factory = factory; - } - - public void setInterpreterSettingManager(InterpreterSettingManager interpreterSettingManager) { - this.interpreterSettingManager = interpreterSettingManager; + this.interpreterFactory = factory; } public InterpreterResult getResult() { @@ -319,7 +300,7 @@ public InterpreterResult getResult() { } @Override - public Object getReturn() { + public synchronized Object getReturn() { return results; } @@ -329,12 +310,14 @@ public Object getPreviousResultFormat() { @Override public int progress() { - String replName = getRequiredReplName(); - Interpreter repl = getRepl(replName); - if (repl != null) { - return repl.getProgress(getInterpreterContext(null)); - } else { - return 0; + try { + if (this.interpreter != null) { + return this.interpreter.getProgress(getInterpreterContext(null)); + } else { + return 0; + } + } catch (InterpreterException e) { + throw new RuntimeException("Fail to get progress", e); } } @@ -353,30 +336,66 @@ private boolean hasPermission(List userAndRoles, List intpUsersA } public boolean isBlankParagraph() { - return Strings.isNullOrEmpty(getText()) || getText().trim().equals(getMagic()); + return Strings.isNullOrEmpty(scriptText); } + public boolean execute(boolean blocking) { + if (isBlankParagraph()) { + logger.info("skip to run blank paragraph. {}", getId()); + setStatus(Job.Status.FINISHED); + return true; + } + + clearRuntimeInfo(null); + try { + this.interpreter = getBindedInterpreter(); + setStatus(Status.READY); + if (getConfig().get("enabled") == null || (Boolean) getConfig().get("enabled")) { + setAuthenticationInfo(getAuthenticationInfo()); + interpreter.getScheduler().submit(this); + } + + if (blocking) { + while (!getStatus().isCompleted()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + return getStatus() == Status.FINISHED; + } else { + return true; + } + } catch (InterpreterNotFoundException e) { + InterpreterResult intpResult = + new InterpreterResult(InterpreterResult.Code.ERROR); + setReturn(intpResult, e); + setStatus(Job.Status.ERROR); + throw new RuntimeException(e); + } + } @Override protected Object jobRun() throws Throwable { - String replName = getRequiredReplName(); - Interpreter repl = getRepl(replName); - logger.info("run paragraph {} using {} " + repl, getId(), replName); - if (repl == null) { - logger.error("Can not find interpreter name " + repl); - throw new RuntimeException("Can not find interpreter for " + getRequiredReplName()); - } - InterpreterSetting intp = getInterpreterSettingById(repl.getInterpreterGroup().getId()); - while (intp.getStatus().equals( - org.apache.zeppelin.interpreter.InterpreterSetting.Status.DOWNLOADING_DEPENDENCIES)) { - Thread.sleep(200); - } - if (this.noteHasUser() && this.noteHasInterpreters()) { - if (intp != null && interpreterHasUser(intp) - && isUserAuthorizedToAccessInterpreter(intp.getOption()) == false) { - logger.error("{} has no permission for {} ", authenticationInfo.getUser(), repl); + logger.info("Run paragraph [paragraph_id: {}, interpreter: {}, note_id: {}, user: {}]", + getId(), intpText, note.getId(), authenticationInfo.getUser()); + this.interpreter = getBindedInterpreter(); + if (this.interpreter == null) { + logger.error("Can not find interpreter name " + intpText); + throw new RuntimeException("Can not find interpreter for " + intpText); + } + InterpreterSetting interpreterSetting = ((ManagedInterpreterGroup) + interpreter.getInterpreterGroup()).getInterpreterSetting(); + if (interpreterSetting != null) { + interpreterSetting.waitForReady(); + } + if (this.hasUser() && this.note.hasInterpreterBinded()) { + if (interpreterSetting != null && interpreterHasUser(interpreterSetting) + && isUserAuthorizedToAccessInterpreter(interpreterSetting.getOption()) == false) { + logger.error("{} has no permission for {} ", authenticationInfo.getUser(), intpText); return new InterpreterResult(Code.ERROR, - authenticationInfo.getUser() + " has no permission for " + getRequiredReplName()); + authenticationInfo.getUser() + " has no permission for " + intpText); } } @@ -384,28 +403,56 @@ && isUserAuthorizedToAccessInterpreter(intp.getOption()) == false) { p.setText(getText()); } - String script = getScriptBody(); // inject form - if (repl.getFormType() == FormType.NATIVE) { + String script = this.scriptText; + if (interpreter.getFormType() == FormType.NATIVE) { settings.clear(); - } else if (repl.getFormType() == FormType.SIMPLE) { - String scriptBody = getScriptBody(); + } else if (interpreter.getFormType() == FormType.SIMPLE) { // inputs will be built from script body - LinkedHashMap inputs = Input.extractSimpleQueryForm(scriptBody); - + LinkedHashMap inputs = Input.extractSimpleQueryForm(script, false); + LinkedHashMap noteInputs = Input.extractSimpleQueryForm(script, true); final AngularObjectRegistry angularRegistry = - repl.getInterpreterGroup().getAngularObjectRegistry(); - - scriptBody = extractVariablesFromAngularRegistry(scriptBody, inputs, angularRegistry); + interpreter.getInterpreterGroup().getAngularObjectRegistry(); + 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 { InterpreterContext context = getInterpreterContext(); InterpreterContext.set(context); - InterpreterResult ret = repl.interpret(script, context); + UserCredentials creds = context.getAuthenticationInfo().getUserCredentials(); + + boolean shouldInjectCredentials = Boolean.parseBoolean( + interpreter.getProperty(Constants.INJECT_CREDENTIALS, "false")); + InterpreterResult ret = null; + if (shouldInjectCredentials) { + CredentialInjector credinjector = new CredentialInjector(creds); + String code = credinjector.replaceCredentials(script); + ret = interpreter.interpret(code, context); + ret = credinjector.hidePasswords(ret); + } else { + 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(); @@ -429,16 +476,13 @@ && isUserAuthorizedToAccessInterpreter(intp.getOption()) == false) { } } - private boolean noteHasUser() { + private boolean hasUser() { return this.user != null; } - private boolean noteHasInterpreters() { - return !interpreterSettingManager.getInterpreterSettings(note.getId()).isEmpty(); - } - - private boolean interpreterHasUser(InterpreterSetting intp) { - return intp.getOption().permissionIsSet() && intp.getOption().getOwners() != null; + private boolean interpreterHasUser(InterpreterSetting interpreterSetting) { + return interpreterSetting.getOption().permissionIsSet() && + interpreterSetting.getOption().getOwners() != null; } private boolean isUserAuthorizedToAccessInterpreter(InterpreterOption intpOpt) { @@ -446,26 +490,12 @@ private boolean isUserAuthorizedToAccessInterpreter(InterpreterOption intpOpt) { intpOpt.getOwners()); } - private InterpreterSetting getInterpreterSettingById(String id) { - InterpreterSetting setting = null; - for (InterpreterSetting i : interpreterSettingManager.getInterpreterSettings(note.getId())) { - if (id.startsWith(i.getId())) { - setting = i; - break; - } - } - return setting; - } - @Override protected boolean jobAbort() { - Interpreter repl = getRepl(getRequiredReplName()); - if (repl == null) { - // when interpreters are already destroyed + if (interpreter == null) { return true; } - - Scheduler scheduler = repl.getScheduler(); + Scheduler scheduler = interpreter.getScheduler(); if (scheduler == null) { return true; } @@ -474,7 +504,11 @@ protected boolean jobAbort() { if (job != null) { job.setStatus(Status.ABORT); } else { - repl.cancel(getInterpreterContextWithoutRunner(null)); + try { + interpreter.cancel(getInterpreterContextWithoutRunner(null)); + } catch (InterpreterException e) { + throw new RuntimeException(e); + } } return true; } @@ -522,11 +556,9 @@ private InterpreterContext getInterpreterContextWithoutRunner(InterpreterOutput AngularObjectRegistry registry = null; ResourcePool resourcePool = null; - 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(); + if (this.interpreter != null) { + registry = this.interpreter.getInterpreterGroup().getAngularObjectRegistry(); + resourcePool = this.interpreter.getInterpreterGroup().getResourcePool(); } List runners = new LinkedList<>(); @@ -543,9 +575,9 @@ private InterpreterContext getInterpreterContextWithoutRunner(InterpreterOutput } InterpreterContext interpreterContext = - new InterpreterContext(note.getId(), getId(), getRequiredReplName(), this.getTitle(), - this.getText(), this.getAuthenticationInfo(), this.getConfig(), this.settings, registry, - resourcePool, runners, output); + new InterpreterContext(note.getId(), getId(), intpText, this.getTitle(), + this.getText(), this.getAuthenticationInfo(), this.getConfig(), this.settings, + getNoteGui(), registry, resourcePool, runners, output); return interpreterContext; } @@ -553,11 +585,9 @@ private InterpreterContext getInterpreterContext(InterpreterOutput output) { AngularObjectRegistry registry = null; ResourcePool resourcePool = null; - 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(); + if (this.interpreter != null) { + registry = this.interpreter.getInterpreterGroup().getAngularObjectRegistry(); + resourcePool = this.interpreter.getInterpreterGroup().getResourcePool(); } List runners = new LinkedList<>(); @@ -575,14 +605,13 @@ private InterpreterContext getInterpreterContext(InterpreterOutput output) { } InterpreterContext interpreterContext = - new InterpreterContext(note.getId(), getId(), getRequiredReplName(), this.getTitle(), - this.getText(), this.getAuthenticationInfo(), this.getConfig(), this.settings, registry, - resourcePool, runners, output); + new InterpreterContext(note.getId(), getId(), intpText, this.getTitle(), + 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()); } @@ -604,7 +633,7 @@ public ParagraphRunner(Note note, String noteId, String paragraphId) { @Override public void run() { - note.run(getParagraphId()); + note.run(getParagraphId(), false); } } @@ -686,25 +715,10 @@ String extractVariablesFromAngularRegistry(String scriptBody, Map return scriptBody; } - public String getMagic() { - String magic = StringUtils.EMPTY; - String text = getText(); - if (text != null && text.startsWith("%")) { - magic = text.split("\\s+")[0]; - if (isValidInterpreter(magic.substring(1))) { - return magic; - } else { - return StringUtils.EMPTY; - } - } - return magic; - } - - private boolean isValidInterpreter(String replName) { + public boolean isValidInterpreter(String replName) { try { - return factory.getInterpreter(user, note.getId(), replName) != null; - } catch (InterpreterException e) { - // ignore this exception, it would be recaught when running paragraph. + return interpreterFactory.getInterpreter(user, note.getId(), replName) != null; + } catch (InterpreterNotFoundException e) { return false; } } @@ -732,7 +746,7 @@ public void updateRuntimeInfos(String label, String tooltip, Map * @param settingId */ public void clearRuntimeInfo(String settingId) { - if (settingId != null) { + if (settingId != null && runtimeInfos != null) { Set keys = runtimeInfos.keySet(); if (keys.size() > 0) { List infosToRemove = new ArrayList<>(); @@ -763,6 +777,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) { @@ -822,6 +843,7 @@ public int hashCode() { return result1; } + @Override public String toJson() { return Note.getGson().toJson(this); } 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..3b010889003 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 @@ -207,26 +195,6 @@ public void remove(String noteId, AuthenticationInfo subject) throws IOException public void close() { } - @Override - public Revision checkpoint(String noteId, String checkpointMsg, AuthenticationInfo subject) - throws IOException { - // no-op - LOG.warn("Checkpoint feature isn't supported in {}", this.getClass().toString()); - return Revision.EMPTY; - } - - @Override - public Note get(String noteId, String revId, AuthenticationInfo subject) throws IOException { - LOG.warn("Get note revision feature isn't supported in {}", this.getClass().toString()); - return null; - } - - @Override - public List revisionHistory(String noteId, AuthenticationInfo subject) { - LOG.warn("Get Note revisions feature isn't supported in {}", this.getClass().toString()); - return Collections.emptyList(); - } - @Override public List getSettings(AuthenticationInfo subject) { LOG.warn("Method not implemented"); @@ -238,10 +206,4 @@ public void updateSettings(Map settings, AuthenticationInfo subj LOG.warn("Method not implemented"); } - @Override - public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subject) - throws IOException { - // Auto-generated method stub - return null; - } } 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 new file mode 100644 index 00000000000..bfc11788207 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/FileSystemNotebookRepo.java @@ -0,0 +1,99 @@ +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.FileSystemStorage; +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.net.URI; +import java.net.URISyntaxException; +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 FileSystemNotebookRepo implements NotebookRepo { + private static final Logger LOGGER = LoggerFactory.getLogger(FileSystemNotebookRepo.class); + + private FileSystemStorage fs; + private Path notebookDir; + + public FileSystemNotebookRepo(ZeppelinConfiguration zConf) throws IOException { + 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 + public List list(AuthenticationInfo subject) throws IOException { + 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 { + 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 { + 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 { + this.fs.delete(new Path(notebookDir.toString() + "/" + noteId)); + } + + @Override + public void close() { + LOGGER.warn("close is not implemented for HdfsNotebookRepo"); + } + + @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"); + } + +} 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..6d622987ff0 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GCSNotebookRepo.java @@ -0,0 +1,208 @@ +/* + * 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 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/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..829f2c16c09 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,11 +47,12 @@ * * 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 */ -public class GitNotebookRepo extends VFSNotebookRepo { +class GitNotebookRepo extends VFSNotebookRepo implements NotebookRepoWithVersionControl { private static final Logger LOG = LoggerFactory.getLogger(GitNotebookRepo.class); private String localPath; @@ -177,7 +178,7 @@ public void close() { } //DI replacements for Tests - Git getGit() { + protected Git getGit() { return git; } @@ -186,3 +187,4 @@ void setGit(Git git) { } } + 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..03a0f1654ac 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); } /** @@ -221,33 +203,6 @@ public void close() { mongo.close(); } - @Override - public Revision checkpoint(String noteId, String checkpointMsg, AuthenticationInfo subject) - throws IOException { - // no-op - LOG.warn("Checkpoint feature isn't supported in {}", this.getClass().toString()); - return Revision.EMPTY; - } - - @Override - public Note get(String noteId, String revId, AuthenticationInfo subject) throws IOException { - LOG.warn("Get note revision feature isn't supported in {}", this.getClass().toString()); - return null; - } - - @Override - public List revisionHistory(String noteId, AuthenticationInfo subject) { - LOG.warn("Get Note revisions feature isn't supported in {}", this.getClass().toString()); - return Collections.emptyList(); - } - - @Override - public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subject) - throws IOException { - // Auto-generated method stub - return null; - } - @Override public List getSettings(AuthenticationInfo subject) { LOG.warn("Method not implemented"); @@ -258,4 +213,5 @@ public List getSettings(AuthenticationInfo subject) { public void updateSettings(Map settings, AuthenticationInfo subject) { LOG.warn("Method not implemented"); } + } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepo.java index 6599d087576..3f25dbf54c3 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepo.java @@ -73,47 +73,6 @@ public interface NotebookRepo { * Versioning API (optional, preferred to have). */ - /** - * chekpoint (set revision) for notebook. - * @param noteId Id of the Notebook - * @param checkpointMsg message description of the checkpoint - * @return Rev - * @throws IOException - */ - @ZeppelinApi public Revision checkpoint(String noteId, String checkpointMsg, - AuthenticationInfo subject) throws IOException; - - /** - * Get particular revision of the Notebook. - * - * @param noteId Id of the Notebook - * @param rev revision of the Notebook - * @return a Notebook - * @throws IOException - */ - @ZeppelinApi public Note get(String noteId, String revId, AuthenticationInfo subject) - throws IOException; - - /** - * List of revisions of the given Notebook. - * - * @param noteId id of the Notebook - * @return list of revisions - */ - @ZeppelinApi public List revisionHistory(String noteId, AuthenticationInfo subject); - - /** - * Set note to particular revision. - * - * @param noteId Id of the Notebook - * @param rev revision of the Notebook - * @return a Notebook - * @throws IOException - */ - @ZeppelinApi - public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subject) - throws IOException; - /** * Get NotebookRepo settings got the given user. * @@ -130,25 +89,4 @@ public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subj */ @ZeppelinApi public void updateSettings(Map settings, AuthenticationInfo subject); - /** - * Represents the 'Revision' a point in life of the notebook - */ - static class Revision { - public static final Revision EMPTY = new Revision(StringUtils.EMPTY, StringUtils.EMPTY, 0); - - public String id; - public String message; - public int time; - - public Revision(String revId, String message, int time) { - this.id = revId; - this.message = message; - this.time = time; - } - - public static boolean isEmpty(Revision revision) { - return revision == null || EMPTY.equals(revision); - } - } - } 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..6a2daef65bc 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 @@ -17,17 +17,7 @@ package org.apache.zeppelin.notebook.repo; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - +import com.google.common.collect.Lists; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; import org.apache.zeppelin.notebook.Note; @@ -38,12 +28,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.collect.Lists; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.*; /** * Notebook repository sync with remote storage */ -public class NotebookRepoSync implements NotebookRepo { +public class NotebookRepoSync implements NotebookRepoWithVersionControl { private static final Logger LOG = LoggerFactory.getLogger(NotebookRepoSync.class); private static final int maxRepoNum = 2; private static final String pushKey = "pushNoteIds"; @@ -81,6 +74,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) { @@ -284,6 +278,7 @@ private boolean emptyNoteAcl(String noteId) { NotebookAuthorization notebookAuthorization = NotebookAuthorization.getInstance(); return notebookAuthorization.getOwners(noteId).isEmpty() && notebookAuthorization.getReaders(noteId).isEmpty() + && notebookAuthorization.getRunners(noteId).isEmpty() && notebookAuthorization.getWriters(noteId).isEmpty(); } @@ -299,6 +294,9 @@ private void makePrivate(String noteId, AuthenticationInfo subject) { users = notebookAuthorization.getReaders(noteId); users.add(subject.getUser()); notebookAuthorization.setReaders(noteId, users); + users = notebookAuthorization.getRunners(noteId); + users.add(subject.getUser()); + notebookAuthorization.setRunners(noteId, users); users = notebookAuthorization.getWriters(noteId); users.add(subject.getUser()); notebookAuthorization.setWriters(noteId, users); @@ -319,7 +317,7 @@ int getMaxRepoNum() { return maxRepoNum; } - NotebookRepo getRepo(int repoIndex) throws IOException { + public NotebookRepo getRepo(int repoIndex) throws IOException { if (repoIndex < 0 || repoIndex >= getRepoCount()) { throw new IOException("Requested storage index " + repoIndex + " isn't initialized," + " repository count is " + getRepoCount()); @@ -433,6 +431,21 @@ public void close() { } } + public Boolean isRevisionSupportedInDefaultRepo() { + return isRevisionSupportedInRepo(0); + } + + public Boolean isRevisionSupportedInRepo(int repoIndex) { + try { + if (getRepo(repoIndex) instanceof NotebookRepoWithVersionControl) { + return true; + } + } catch (IOException e) { + LOG.error("Error getting default repo", e); + } + return false; + } + //checkpoint to all available storages @Override public Revision checkpoint(String noteId, String checkpointMsg, AuthenticationInfo subject) @@ -445,7 +458,11 @@ public Revision checkpoint(String noteId, String checkpointMsg, AuthenticationIn Revision rev = null; for (int i = 0; i < repoBound; i++) { try { - allRepoCheckpoints.add(getRepo(i).checkpoint(noteId, checkpointMsg, subject)); + if (isRevisionSupportedInRepo(i)) { + allRepoCheckpoints + .add(((NotebookRepoWithVersionControl) getRepo(i)) + .checkpoint(noteId, checkpointMsg, subject)); + } } catch (IOException e) { LOG.warn("Couldn't checkpoint in {} storage with index {} for note {}", getRepo(i).getClass().toString(), i, noteId); @@ -472,7 +489,9 @@ public Revision checkpoint(String noteId, String checkpointMsg, AuthenticationIn public Note get(String noteId, String revId, AuthenticationInfo subject) { Note revisionNote = null; try { - revisionNote = getRepo(0).get(noteId, revId, subject); + if (isRevisionSupportedInDefaultRepo()) { + revisionNote = ((NotebookRepoWithVersionControl) getRepo(0)).get(noteId, revId, subject); + } } catch (IOException e) { LOG.error("Failed to get revision {} of note {}", revId, noteId, e); } @@ -483,7 +502,9 @@ public Note get(String noteId, String revId, AuthenticationInfo subject) { public List revisionHistory(String noteId, AuthenticationInfo subject) { List revisions = Collections.emptyList(); try { - revisions = getRepo(0).revisionHistory(noteId, subject); + if (isRevisionSupportedInDefaultRepo()) { + revisions = ((NotebookRepoWithVersionControl) getRepo(0)).revisionHistory(noteId, subject); + } } catch (IOException e) { LOG.error("Failed to list revision history", e); } @@ -518,7 +539,10 @@ public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subj Note currentNote = null, revisionNote = null; for (int i = 0; i < repoBound; i++) { try { - currentNote = getRepo(i).setNoteRevision(noteId, revId, subject); + if (isRevisionSupportedInRepo(i)) { + currentNote = ((NotebookRepoWithVersionControl) getRepo(i)) + .setNoteRevision(noteId, revId, subject); + } } catch (IOException e) { // already logged currentNote = null; @@ -530,4 +554,5 @@ public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subj } return revisionNote; } + } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoWithVersionControl.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoWithVersionControl.java new file mode 100644 index 00000000000..05c846eac00 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoWithVersionControl.java @@ -0,0 +1,97 @@ +/* + * 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.commons.lang.StringUtils; +import org.apache.zeppelin.annotation.ZeppelinApi; +import org.apache.zeppelin.notebook.Note; +import org.apache.zeppelin.notebook.NoteInfo; +import org.apache.zeppelin.user.AuthenticationInfo; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Notebook repository (persistence layer) abstraction + */ +public interface NotebookRepoWithVersionControl extends NotebookRepo { + + /** + * chekpoint (set revision) for notebook. + * @param noteId Id of the Notebook + * @param checkpointMsg message description of the checkpoint + * @return Rev + * @throws IOException + */ + @ZeppelinApi public Revision checkpoint(String noteId, String checkpointMsg, + AuthenticationInfo subject) throws IOException; + + /** + * Get particular revision of the Notebook. + * + * @param noteId Id of the Notebook + * @param revId revision of the Notebook + * @return a Notebook + * @throws IOException + */ + @ZeppelinApi public Note get(String noteId, String revId, AuthenticationInfo subject) + throws IOException; + + /** + * List of revisions of the given Notebook. + * + * @param noteId id of the Notebook + * @return list of revisions + */ + @ZeppelinApi public List revisionHistory(String noteId, AuthenticationInfo subject); + + /** + * Set note to particular revision. + * + * @param noteId Id of the Notebook + * @param revId revision of the Notebook + * @return a Notebook + * @throws IOException + */ + @ZeppelinApi + public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subject) + throws IOException; + + /** + * Represents the 'Revision' a point in life of the notebook + */ + static class Revision { + public static final Revision EMPTY = new Revision(StringUtils.EMPTY, StringUtils.EMPTY, 0); + + public String id; + public String message; + public int time; + + public Revision(String revId, String message, int time) { + this.id = revId; + this.message = message; + this.time = time; + } + + public static boolean isEmpty(Revision revision) { + return revision == null || EMPTY.equals(revision); + } + } + +} 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 16b270cd8e3..c54c48be07f 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 @@ -42,6 +42,8 @@ import org.slf4j.LoggerFactory; import com.amazonaws.AmazonClientException; +import com.amazonaws.ClientConfiguration; +import com.amazonaws.ClientConfigurationFactory; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.services.s3.AmazonS3; @@ -88,43 +90,40 @@ 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 AWSCredentialsProvider credentialsProvider = new DefaultAWSCredentialsProviderChain(); - CryptoConfiguration cryptoConf = null; + CryptoConfiguration cryptoConf = new CryptoConfiguration(); String keyRegion = conf.getS3KMSKeyRegion(); if (StringUtils.isNotBlank(keyRegion)) { - cryptoConf = new CryptoConfiguration(); cryptoConf.setAwsKmsRegion(Region.getRegion(Regions.fromName(keyRegion))); } + + ClientConfiguration cliConf = createClientConfiguration(); // see if we should be encrypting data in S3 String kmsKeyID = conf.getS3KMSKeyID(); if (kmsKeyID != null) { // use the AWS KMS to encrypt data KMSEncryptionMaterialsProvider emp = new KMSEncryptionMaterialsProvider(kmsKeyID); - if (cryptoConf != null) { - this.s3client = new AmazonS3EncryptionClient(credentialsProvider, emp, cryptoConf); - } else { - this.s3client = new AmazonS3EncryptionClient(credentialsProvider, emp); - } + this.s3client = new AmazonS3EncryptionClient(credentialsProvider, emp, cliConf, cryptoConf); } else if (conf.getS3EncryptionMaterialsProviderClass() != null) { // use a custom encryption materials provider class EncryptionMaterialsProvider emp = createCustomProvider(conf); - this.s3client = new AmazonS3EncryptionClient(credentialsProvider, emp); + this.s3client = new AmazonS3EncryptionClient(credentialsProvider, emp, cliConf, cryptoConf); } else { // regular S3 - this.s3client = new AmazonS3Client(credentialsProvider); + this.s3client = new AmazonS3Client(credentialsProvider, cliConf); } // set S3 endpoint to use - s3client.setEndpoint(conf.getEndpoint()); + s3client.setEndpoint(conf.getS3Endpoint()); } /** @@ -154,6 +153,22 @@ private EncryptionMaterialsProvider createCustomProvider(ZeppelinConfiguration c return emp; } + /** + * Create AWS client configuration and return it. + * @return AWS client configuration + */ + private ClientConfiguration createClientConfiguration() { + ClientConfigurationFactory configFactory = new ClientConfigurationFactory(); + ClientConfiguration config = configFactory.getConfig(); + + String s3SignerOverride = conf.getS3SignerOverride(); + if (StringUtils.isNotBlank(s3SignerOverride)) { + config.setSignerOverride(s3SignerOverride); + } + + return config; + } + @Override public List list(AuthenticationInfo subject) throws IOException { List infos = new LinkedList<>(); @@ -190,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 { @@ -270,26 +276,6 @@ public void close() { //no-op } - @Override - public Revision checkpoint(String noteId, String checkpointMsg, AuthenticationInfo subject) - throws IOException { - // no-op - LOG.warn("Checkpoint feature isn't supported in {}", this.getClass().toString()); - return Revision.EMPTY; - } - - @Override - public Note get(String noteId, String revId, AuthenticationInfo subject) throws IOException { - LOG.warn("Get note revision feature isn't supported in {}", this.getClass().toString()); - return null; - } - - @Override - public List revisionHistory(String noteId, AuthenticationInfo subject) { - LOG.warn("Get Note revisions feature isn't supported in {}", this.getClass().toString()); - return Collections.emptyList(); - } - @Override public List getSettings(AuthenticationInfo subject) { LOG.warn("Method not implemented"); @@ -301,10 +287,4 @@ public void updateSettings(Map settings, AuthenticationInfo subj LOG.warn("Method not implemented"); } - @Override - public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subject) - throws IOException { - // Auto-generated method stub - return null; - } } 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 4006d13cb32..bd0d4b2cd10 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,26 +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); -// note.setReplLoader(replLoader); -// note.jobListenerFactory = jobListenerFactory; - - 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 { @@ -218,6 +193,7 @@ protected FileObject getRootDir() throws IOException { @Override public synchronized void save(Note note, AuthenticationInfo subject) throws IOException { + LOG.info("Saving note:" + note.getId()); String json = note.toJson(); FileObject rootDir = getRootDir(); @@ -262,26 +238,6 @@ public void close() { //no-op } - @Override - public Revision checkpoint(String noteId, String checkpointMsg, AuthenticationInfo subject) - throws IOException { - // no-op - LOG.warn("Checkpoint feature isn't supported in {}", this.getClass().toString()); - return Revision.EMPTY; - } - - @Override - public Note get(String noteId, String revId, AuthenticationInfo subject) throws IOException { - LOG.warn("Get note revision feature isn't supported in {}", this.getClass().toString()); - return null; - } - - @Override - public List revisionHistory(String noteId, AuthenticationInfo subject) { - LOG.warn("Get Note revisions feature isn't supported in {}", this.getClass().toString()); - return Collections.emptyList(); - } - @Override public List getSettings(AuthenticationInfo subject) { NotebookRepoSettingsInfo repoSetting = NotebookRepoSettingsInfo.newInstance(); @@ -320,11 +276,5 @@ public void updateSettings(Map settings, AuthenticationInfo subj } } - @Override - public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subject) - throws IOException { - // Auto-generated method stub - return null; - } - } + diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/zeppelinhub/ZeppelinHubRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/zeppelinhub/ZeppelinHubRepo.java index 89c1dd16573..7c6bd353178 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/zeppelinhub/ZeppelinHubRepo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/zeppelinhub/ZeppelinHubRepo.java @@ -27,7 +27,7 @@ import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.NoteInfo; -import org.apache.zeppelin.notebook.repo.NotebookRepo; +import org.apache.zeppelin.notebook.repo.NotebookRepoWithVersionControl; import org.apache.zeppelin.notebook.repo.NotebookRepoSettingsInfo; import org.apache.zeppelin.notebook.repo.zeppelinhub.model.Instance; import org.apache.zeppelin.notebook.repo.zeppelinhub.model.UserTokenContainer; @@ -48,7 +48,7 @@ /** * ZeppelinHub repo class. */ -public class ZeppelinHubRepo implements NotebookRepo { +public class ZeppelinHubRepo implements NotebookRepoWithVersionControl { private static final Logger LOG = LoggerFactory.getLogger(ZeppelinHubRepo.class); private static final String DEFAULT_SERVER = "https://www.zeppelinhub.com"; static final String ZEPPELIN_CONF_PROP_NAME_SERVER = "zeppelinhub.api.address"; diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/zeppelinhub/security/Authentication.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/zeppelinhub/security/Authentication.java index fd5142bd486..38d8b5005ac 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/zeppelinhub/security/Authentication.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/zeppelinhub/security/Authentication.java @@ -47,7 +47,6 @@ public class Authentication implements Runnable { // Cipher is an AES in CBC mode private static final String CIPHER_ALGORITHM = "AES"; private static final String CIPHER_MODE = "AES/CBC/PKCS5PADDING"; - private static final String KEY = "AbtEr99DxsWWbJkP"; private static final int ivSize = 16; private static final String ZEPPELIN_CONF_ANONYMOUS_ALLOWED = "zeppelin.anonymous.allowed"; @@ -198,7 +197,7 @@ private Map login(String authKey, String endpoint) { private Key generateKey() { try { KeyGenerator kgen = KeyGenerator.getInstance(CIPHER_ALGORITHM); - kgen.init(128, new SecureRandom(toBytes(KEY))); + kgen.init(128, new SecureRandom()); SecretKey secretKey = kgen.generateKey(); byte[] enCodeFormat = secretKey.getEncoded(); return new SecretKeySpec(enCodeFormat, CIPHER_ALGORITHM); 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 06c83e13403..9a320cf5527 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 @@ -152,6 +152,10 @@ public static enum OP { SET_NOTE_REVISION, // [c-s] set current notebook head to this revision // @param noteId // @param revisionId + NOTE_REVISION_FOR_COMPARE, // [c-s] get certain revision of note for compare + // @param noteId + // @param revisionId + // @param position APP_APPEND_OUTPUT, // [s-c] append output APP_UPDATE_OUTPUT, // [s-c] update (replace) output APP_LOAD, // [s-c] on app load @@ -178,8 +182,11 @@ 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 + NOTICE // [s-c] Notice } private static final Gson gson = new Gson(); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/utility/IdHashes.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/utility/IdHashes.java index 98aaac8a756..7b0d804de94 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/utility/IdHashes.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/utility/IdHashes.java @@ -18,9 +18,9 @@ package org.apache.zeppelin.notebook.utility; import java.math.BigInteger; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; -import java.util.Random; /** * Generate Tiny ID. @@ -71,6 +71,6 @@ private static String encode(Long value) { } public static String generateId() { - return encode(System.currentTimeMillis() + new Random().nextInt()); + return encode(System.currentTimeMillis() + new SecureRandom().nextInt()); } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/RemoteScheduler.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/scheduler/RemoteScheduler.java similarity index 73% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/RemoteScheduler.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/scheduler/RemoteScheduler.java index f9ddc4e99c2..a529416e477 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/RemoteScheduler.java +++ b/zeppelin-zengine/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; } @@ -133,7 +132,15 @@ public Collection getJobsRunning() { List ret = new LinkedList<>(); synchronized (queue) { for (Job job : running) { - ret.add(job); + if (job.getStatus() == Status.RUNNING) { + ret.add(job); + } else { + logger.error( + "Tried to add {} to list of running jobs, but job status is {}", + job.getJobName(), + job.getStatus().toString() + ); + } } } return ret; @@ -167,14 +174,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 +217,7 @@ public void run() { } Status newStatus = getStatus(); - if (newStatus == null) { // unknown + if (newStatus == Status.UNKNOWN) { // unknown continue; } @@ -231,7 +239,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 +249,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) { + public synchronized Status getStatus() { + 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 +325,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.debug("Job ABORT, " + job.getId() + ", " + job.getErrorMessage()); + job.setStatus(Status.ERROR); + } else if (jobResult != null && jobResult instanceof InterpreterResult + && ((InterpreterResult) jobResult).code() == Code.ERROR) { + logger.debug("Job Error, " + job.getId() + ", " + job.getErrorMessage()); + job.setStatus(Status.ERROR); + } else { + logger.debug("Job Finished, " + job.getId() + ", Result: " + job.getReturn()); + job.setStatus(Status.FINISHED); } synchronized (queue) { - job.setStatus(lastStatus); - if (listener != null) { listener.jobFinished(scheduler, job); } @@ -374,25 +363,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 +372,16 @@ public void afterStatusChange(Job job, Status before, Status after) { return; } else if (after == Status.RUNNING) { jobSubmittedRemotely = true; + job.setStatus(Status.RUNNING); } } 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) { + job.setStatus(Status.RUNNING); } } } 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 new file mode 100644 index 00000000000..b3175e59f3c --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/ConfigStorage.java @@ -0,0 +1,105 @@ +/* + * 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 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; +import org.apache.zeppelin.util.ReflectionUtils; + +import java.io.IOException; + +/** + * Interface for storing zeppelin configuration. + * + * 1. interpreter-setting.json + * 2. helium.json + * 3. notebook-authorization.json + * 4. credentials.json + * + */ +public abstract class ConfigStorage { + + private static ConfigStorage instance; + + protected ZeppelinConfiguration zConf; + + public static synchronized ConfigStorage getInstance(ZeppelinConfiguration zConf) + throws IOException { + if (instance == null) { + instance = createConfigStorage(zConf); + } + return instance; + } + + private static ConfigStorage createConfigStorage(ZeppelinConfiguration zConf) throws IOException { + String configStorageClass = + zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONFIG_STORAGE_CLASS); + return ReflectionUtils.createClazzInstance(configStorageClass, + new Class[] {ZeppelinConfiguration.class}, new Object[] {zConf}); + } + + + public ConfigStorage(ZeppelinConfiguration zConf) { + this.zConf = zConf; + } + + public abstract void save(InterpreterInfoSaving settingInfos) throws IOException; + + public abstract InterpreterInfoSaving loadInterpreterSettings() throws IOException; + + public abstract void save(NotebookAuthorizationInfoSaving authorizationInfoSaving) + throws IOException; + + public abstract NotebookAuthorizationInfoSaving loadNotebookAuthorization() throws IOException; + + 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 new file mode 100644 index 00000000000..20c19b65cb7 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/FileSystemConfigStorage.java @@ -0,0 +1,112 @@ +/* + * 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 com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.apache.hadoop.fs.Path; +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.FileSystemStorage; +import org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving; +import org.apache.zeppelin.user.CredentialsInfoSaving; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * It could be used either local file system or hadoop distributed file system, + * because FileSystem support both local file system and hdfs. + * + */ +public class FileSystemConfigStorage extends ConfigStorage { + + private static final Logger LOGGER = LoggerFactory.getLogger(FileSystemConfigStorage.class); + + private FileSystemStorage fs; + private Path interpreterSettingPath; + private Path authorizationPath; + private Path credentialPath; + + public FileSystemConfigStorage(ZeppelinConfiguration zConf) throws IOException { + super(zConf); + 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())); + } + + @Override + public void save(InterpreterInfoSaving settingInfos) throws IOException { + LOGGER.info("Save Interpreter Settings to " + interpreterSettingPath); + fs.writeFile(settingInfos.toJson(), interpreterSettingPath, false); + } + + @Override + public InterpreterInfoSaving loadInterpreterSettings() throws IOException { + if (!fs.exists(interpreterSettingPath)) { + LOGGER.warn("Interpreter Setting file {} is not existed", interpreterSettingPath); + return null; + } + LOGGER.info("Load Interpreter Setting from file: " + interpreterSettingPath); + String json = fs.readFile(interpreterSettingPath); + return buildInterpreterInfoSaving(json); + } + + public void save(NotebookAuthorizationInfoSaving authorizationInfoSaving) throws IOException { + LOGGER.info("Save notebook authorization to file: " + authorizationPath); + fs.writeFile(authorizationInfoSaving.toJson(), authorizationPath, false); + } + + @Override + public NotebookAuthorizationInfoSaving loadNotebookAuthorization() throws IOException { + if (!fs.exists(authorizationPath)) { + LOGGER.warn("Notebook Authorization file {} is not existed", authorizationPath); + return null; + } + LOGGER.info("Load notebook authorization from file: " + authorizationPath); + String json = this.fs.readFile(authorizationPath); + return NotebookAuthorizationInfoSaving.fromJson(json); + } + + @Override + public String loadCredentials() throws IOException { + if (!fs.exists(credentialPath)) { + LOGGER.warn("Credential file {} is not existed", credentialPath); + return null; + } + LOGGER.info("Load Credential from file: " + credentialPath); + return this.fs.readFile(credentialPath); + } + + @Override + public void saveCredentials(String credentials) throws IOException { + LOGGER.info("Save Credentials to file: " + credentialPath); + fs.writeFile(credentials, credentialPath, false); + } + +} 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..464d6ce212d --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/LocalConfigStorage.java @@ -0,0 +1,111 @@ +/* + * 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 { + LOGGER.info("Save Interpreter Setting to " + interpreterSettingPath.getAbsolutePath()); + 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-interpreter/src/main/java/org/apache/zeppelin/user/Credentials.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/user/Credentials.java similarity index 85% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/user/Credentials.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/user/Credentials.java index e80a89f0e7a..61f7fff1290 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/user/Credentials.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/user/Credentials.java @@ -20,18 +20,22 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import org.apache.zeppelin.common.JsonSerializable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; import java.nio.file.Files; import java.nio.file.attribute.PosixFilePermission; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; @@ -47,7 +51,21 @@ public class Credentials { private Boolean credentialsPersist = true; File credentialsFile; - public Credentials(Boolean credentialsPersist, String credentialsPath) { + private Encryptor encryptor; + + /** + * Wrapper fro user credentials. It can load credentials from a file if credentialsPath is + * supplied, and will encrypt the file if an encryptKey is supplied. + * + * @param credentialsPersist + * @param credentialsPath + * @param encryptKey + */ + public Credentials(Boolean credentialsPersist, String credentialsPath, String encryptKey) { + if (encryptKey != null) { + this.encryptor = new Encryptor(encryptKey); + } + this.credentialsPersist = credentialsPersist; if (credentialsPath != null) { credentialsFile = new File(credentialsPath); @@ -119,6 +137,11 @@ private void loadFromFile() { fis.close(); String json = sb.toString(); + + if (encryptor != null) { + json = encryptor.decrypt(json); + } + CredentialsInfoSaving info = CredentialsInfoSaving.fromJson(json); this.credentialsMap = info.credentialsMap; } catch (IOException e) { @@ -146,6 +169,11 @@ private void saveToFile() throws IOException { FileOutputStream fos = new FileOutputStream(credentialsFile, false); OutputStreamWriter out = new OutputStreamWriter(fos); + + if (encryptor != null) { + jsonString = encryptor.encrypt(jsonString); + } + out.append(jsonString); out.close(); fos.close(); diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/user/CredentialsInfoSaving.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/user/CredentialsInfoSaving.java similarity index 100% rename from zeppelin-interpreter/src/main/java/org/apache/zeppelin/user/CredentialsInfoSaving.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/user/CredentialsInfoSaving.java diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/user/Encryptor.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/user/Encryptor.java new file mode 100644 index 00000000000..ee240906d13 --- /dev/null +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/user/Encryptor.java @@ -0,0 +1,77 @@ +/* + * 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.user; + +import org.bouncycastle.crypto.BufferedBlockCipher; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.paddings.ZeroBytePadding; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.util.encoders.Base64; + +import java.io.IOException; + +/** + * Encrypt/decrypt arrays of bytes! + */ +public class Encryptor { + private final BufferedBlockCipher encryptCipher; + private final BufferedBlockCipher decryptCipher; + + public Encryptor(String encryptKey) { + encryptCipher = new PaddedBufferedBlockCipher(new AESEngine(), new ZeroBytePadding()); + encryptCipher.init(true, new KeyParameter(encryptKey.getBytes())); + + decryptCipher = new PaddedBufferedBlockCipher(new AESEngine(), new ZeroBytePadding()); + decryptCipher.init(false, new KeyParameter(encryptKey.getBytes())); + } + + + public String encrypt(String inputString) throws IOException { + byte[] input = inputString.getBytes(); + byte[] result = new byte[encryptCipher.getOutputSize(input.length)]; + int size = encryptCipher.processBytes(input, 0, input.length, result, 0); + + try { + size += encryptCipher.doFinal(result, size); + + byte[] out = new byte[size]; + System.arraycopy(result, 0, out, 0, size); + return new String(Base64.encode(out)); + } catch (InvalidCipherTextException e) { + throw new IOException("Cannot encrypt: " + e.getMessage(), e); + } + } + + public String decrypt(String base64Input) throws IOException { + byte[] input = Base64.decode(base64Input); + byte[] result = new byte[decryptCipher.getOutputSize(input.length)]; + int size = decryptCipher.processBytes(input, 0, input.length, result, 0); + + try { + size += decryptCipher.doFinal(result, size); + + byte[] out = new byte[size]; + System.arraycopy(result, 0, out, 0, size); + return new String(out); + } catch (InvalidCipherTextException e) { + throw new IOException("Cannot decrypt: " + e.getMessage(), e); + } + } +} 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/conf/ZeppelinConfigurationTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/conf/ZeppelinConfigurationTest.java index 3cc1022c2e7..1771fd39d40 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/conf/ZeppelinConfigurationTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/conf/ZeppelinConfigurationTest.java @@ -17,84 +17,99 @@ package org.apache.zeppelin.conf; import junit.framework.Assert; - import org.apache.commons.configuration.ConfigurationException; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; - import org.junit.Before; import org.junit.Test; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.net.MalformedURLException; import java.util.List; -/** - * Created by joelz on 8/19/15. - */ public class ZeppelinConfigurationTest { - @Before - public void clearSystemVariables() { - System.clearProperty(ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName()); - } - - @Test - public void getAllowedOrigins2Test() throws MalformedURLException, ConfigurationException { - - ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/test-zeppelin-site2.xml")); - List origins = conf.getAllowedOrigins(); - Assert.assertEquals(2, origins.size()); - Assert.assertEquals("http://onehost:8080", origins.get(0)); - Assert.assertEquals("http://otherhost.com", origins.get(1)); - } - - @Test - public void getAllowedOrigins1Test() throws MalformedURLException, ConfigurationException { - - ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/test-zeppelin-site1.xml")); - List origins = conf.getAllowedOrigins(); - Assert.assertEquals(1, origins.size()); - Assert.assertEquals("http://onehost:8080", origins.get(0)); - } - - @Test - public void getAllowedOriginsNoneTest() throws MalformedURLException, ConfigurationException { - - ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml")); - List origins = conf.getAllowedOrigins(); - Assert.assertEquals(1, origins.size()); - } - - @Test - public void isWindowsPathTestTrue() throws ConfigurationException { - - ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml")); - Boolean isIt = conf.isWindowsPath("c:\\test\\file.txt"); - Assert.assertTrue(isIt); - } - - @Test - public void isWindowsPathTestFalse() throws ConfigurationException { - - ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml")); - Boolean isIt = conf.isWindowsPath("~/test/file.xml"); - Assert.assertFalse(isIt); - } - - @Test - public void getNotebookDirTest() throws ConfigurationException { - - ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml")); - String notebookLocation = conf.getNotebookDir(); - Assert.assertEquals("notebook", notebookLocation); - } - - @Test - public void isNotebookPublicTest() throws ConfigurationException { - - ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml")); - boolean isIt = conf.isNotebokPublic(); - assertTrue(isIt); - } + @Before + public void clearSystemVariables() { + System.clearProperty(ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName()); + } + + @Test + public void getAllowedOrigins2Test() throws MalformedURLException, ConfigurationException { + + ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/test-zeppelin-site2.xml")); + List origins = conf.getAllowedOrigins(); + Assert.assertEquals(2, origins.size()); + Assert.assertEquals("http://onehost:8080", origins.get(0)); + Assert.assertEquals("http://otherhost.com", origins.get(1)); + } + + @Test + public void getAllowedOrigins1Test() throws MalformedURLException, ConfigurationException { + + ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/test-zeppelin-site1.xml")); + List origins = conf.getAllowedOrigins(); + Assert.assertEquals(1, origins.size()); + Assert.assertEquals("http://onehost:8080", origins.get(0)); + } + + @Test + public void getAllowedOriginsNoneTest() throws MalformedURLException, ConfigurationException { + + ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml")); + List origins = conf.getAllowedOrigins(); + Assert.assertEquals(1, origins.size()); + } + + @Test + public void isWindowsPathTestTrue() throws ConfigurationException { + + ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml")); + Boolean isIt = conf.isWindowsPath("c:\\test\\file.txt"); + Assert.assertTrue(isIt); + } + + @Test + public void isWindowsPathTestFalse() throws ConfigurationException { + + ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml")); + Boolean isIt = conf.isWindowsPath("~/test/file.xml"); + Assert.assertFalse(isIt); + } + + @Test + public void getNotebookDirTest() throws ConfigurationException { + + ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml")); + String notebookLocation = conf.getNotebookDir(); + Assert.assertEquals("notebook", notebookLocation); + } + + @Test + public void isNotebookPublicTest() throws ConfigurationException { + + ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml")); + boolean isIt = conf.isNotebookPublic(); + assertTrue(isIt); + } + + @Test + public void getPathTest() throws ConfigurationException { + System.setProperty(ConfVars.ZEPPELIN_HOME.getVarName(), "/usr/lib/zeppelin"); + ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml")); + Assert.assertEquals("/usr/lib/zeppelin", conf.getZeppelinHome()); + Assert.assertEquals("/usr/lib/zeppelin/conf", conf.getConfDir()); + } + + @Test + public void getConfigFSPath() throws ConfigurationException { + System.setProperty(ConfVars.ZEPPELIN_HOME.getVarName(), "/usr/lib/zeppelin"); + System.setProperty(ConfVars.ZEPPELIN_CONFIG_FS_DIR.getVarName(), "conf"); + ZeppelinConfiguration conf = new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml")); + assertEquals("/usr/lib/zeppelin/conf", conf.getConfigFSDir()); + + System.setProperty(ConfVars.ZEPPELIN_CONFIG_STORAGE_CLASS.getVarName(), "org.apache.zeppelin.storage.FileSystemConfigStorage"); + assertEquals("conf", conf.getConfigFSDir()); + } } 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..03de533a2b6 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,15 +16,20 @@ */ 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.interpreter.*; -import org.apache.zeppelin.interpreter.mock.MockInterpreter1; -import org.apache.zeppelin.interpreter.mock.MockInterpreter2; -import org.apache.zeppelin.notebook.*; +import org.apache.zeppelin.interpreter.AbstractInterpreterTest; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterNotFoundException; +import org.apache.zeppelin.interpreter.InterpreterResultMessage; +import org.apache.zeppelin.interpreter.InterpreterSetting; +import org.apache.zeppelin.notebook.ApplicationState; +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.Paragraph; +import org.apache.zeppelin.notebook.ParagraphJobListener; import org.apache.zeppelin.notebook.repo.VFSNotebookRepo; import org.apache.zeppelin.scheduler.Job; import org.apache.zeppelin.scheduler.SchedulerFactory; @@ -35,24 +40,17 @@ 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 java.util.LinkedList; import java.util.List; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; 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 +58,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,12 +75,12 @@ public void setUp() throws Exception { conf, notebookRepo, schedulerFactory, - factory, + interpreterFactory, interpreterSettingManager, this, search, notebookAuthorization, - new Credentials(false, null)); + new Credentials(false, null, null)); heliumAppFactory.setNotebook(notebook); @@ -124,16 +91,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 +108,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 +154,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 +194,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); @@ -273,8 +231,13 @@ public void testInterpreterUnbindOfNullReplParagraph() throws IOException { p1.setText("%fake "); // make sure that p1's repl is null - Interpreter intp = p1.getCurrentRepl(); - assertEquals(intp, null); + Interpreter intp = null; + try { + intp = p1.getBindedInterpreter(); + fail("Should throw InterpreterNotFoundException"); + } catch (InterpreterNotFoundException e) { + + } // Unbind all interpreter from note // NullPointerException shouldn't occur here @@ -286,7 +249,7 @@ public void testInterpreterUnbindOfNullReplParagraph() throws IOException { @Test - public void testUnloadOnInterpreterRestart() throws IOException { + public void testUnloadOnInterpreterRestart() throws IOException, InterpreterException { // given HeliumPackage pkg1 = new HeliumPackage(HeliumType.APPLICATION, "name1", @@ -297,7 +260,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/AbstractInterpreterTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java new file mode 100644 index 00000000000..16c8c1d8cec --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/AbstractInterpreterTest.java @@ -0,0 +1,77 @@ +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.apache.zeppelin.notebook.Note; +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); + + protected InterpreterSettingManager interpreterSettingManager; + protected InterpreterFactory interpreterFactory; + protected File zeppelinHome; + 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 + zeppelinHome = new File(".."); + LOGGER.info("ZEPPELIN_HOME: " + zeppelinHome.getAbsolutePath()); + interpreterDir = new File(zeppelinHome, "interpreter_" + getClass().getSimpleName()); + confDir = new File(zeppelinHome, "conf_" + getClass().getSimpleName()); + notebookDir = new File(zeppelinHome, "notebook_" + getClass().getSimpleName()); + + 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_HOME.getVarName(), zeppelinHome.getAbsolutePath()); + 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_GROUP_ORDER.getVarName(), "test,mock1,mock2,mock_resource_pool"); + + 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(interpreterDir); + FileUtils.deleteDirectory(confDir); + FileUtils.deleteDirectory(notebookDir); + } + + protected Note createNote() { + return new Note(null, interpreterFactory, interpreterSettingManager, null, null, null, null); + } +} 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/mock/MockInterpreter11.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/DoubleEchoInterpreter.java similarity index 56% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/DoubleEchoInterpreter.java index 5b9e8022145..be3d5be4cc3 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter11.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/DoubleEchoInterpreter.java @@ -15,69 +15,45 @@ * limitations under the License. */ -package org.apache.zeppelin.interpreter.mock; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +package org.apache.zeppelin.interpreter; + 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; -public class MockInterpreter11 extends Interpreter{ - Map vars = new HashMap<>(); +public class DoubleEchoInterpreter extends Interpreter { - public MockInterpreter11(Properties property) { + public DoubleEchoInterpreter(Properties property) { super(property); } - boolean open; @Override public void open() { - open = true; + } @Override public void close() { - open = false; - } - public boolean isOpen() { - return open; } @Override public InterpreterResult interpret(String st, InterpreterContext context) { - return new InterpreterResult(InterpreterResult.Code.SUCCESS, "repl11: "+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-zengine/src/test/java/org/apache/zeppelin/interpreter/EchoInterpreter.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/EchoInterpreter.java new file mode 100644 index 00000000000..cf1713ff1ba --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/EchoInterpreter.java @@ -0,0 +1,65 @@ +/* + * 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.Properties; + +/** + * 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() { + + } + + @Override + public InterpreterResult interpret(String st, InterpreterContext context) { + if (Boolean.parseBoolean(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) { + + } + + @Override + public FormType getFormType() { + return FormType.NATIVE; + } + + @Override + public int getProgress(InterpreterContext context) { + return 0; + } +} 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 index aaa8864e8cd..2fef05d8482 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterFactoryTest.java @@ -17,481 +17,67 @@ 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 java.io.IOException; 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(); +public class InterpreterFactoryTest extends AbstractInterpreterTest { - 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 + public void testGetFactory() throws IOException, InterpreterException { + // no default interpreter because there's no interpreter setting binded to this note 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()); - } - } + interpreterFactory.getInterpreter("user1", "note1", ""); + fail("Should throw InterpreterNotFoundException"); + } catch (InterpreterNotFoundException e) { - - @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()); - } } - } + 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 because mock1 is the default interpreter group - @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); + assertEquals(EchoInterpreter.class.getName(), remoteInterpreter.getClassName()); - 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()); + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "test") instanceof RemoteInterpreter); + remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test"); + assertEquals(EchoInterpreter.class.getName(), remoteInterpreter.getClassName()); - interpreterSettingManager.setInterpreters("user", "note", new ArrayList() {{ - add(setting1.getId()); - add(setting2.getId()); - }}); + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "test2") instanceof RemoteInterpreter); + remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test2"); + assertEquals(EchoInterpreter.class.getName(), remoteInterpreter.getClassName()); - assertEquals("className1", factory.getInterpreter("user1", "note", "test-group1").getClassName()); - assertEquals("className1", factory.getInterpreter("user1", "note", "group1").getClassName()); + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "test2.double_echo") instanceof RemoteInterpreter); + remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test2.double_echo"); + assertEquals(DoubleEchoInterpreter.class.getName(), remoteInterpreter.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() { + public void testUnknownRepl1() throws IOException { + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); 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()); + interpreterFactory.getInterpreter("user1", "note1", "test.unknown_repl"); + fail("should fail due to no such interpreter"); + } catch (InterpreterNotFoundException e) { + assertEquals("No such interpreter: test.unknown_repl", 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()); - } + public void testUnknownRepl2() throws IOException { + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); + try { + interpreterFactory.getInterpreter("user1", "note1", "unknown_repl"); + fail("should fail due to no such interpreter"); + } catch (InterpreterNotFoundException e) { + assertEquals("Either no interpreter named unknown_repl or it is not binded to this note", e.getMessage()); } - 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/InterpreterSettingManagerTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java new file mode 100644 index 00000000000..444f36688d2 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java @@ -0,0 +1,363 @@ +/* + * 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.lifecycle.NullLifecycleManager; +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 static org.junit.Assert.assertEquals; +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 InterpreterSettingManagerTest extends AbstractInterpreterTest { + + @Test + public void testInitInterpreterSettingManager() throws IOException, RepositoryException { + assertEquals(5, interpreterSettingManager.get().size()); + InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("test"); + assertEquals("test", interpreterSetting.getName()); + assertEquals("test", interpreterSetting.getGroup()); + assertTrue(interpreterSetting.getLifecycleManager() instanceof NullLifecycleManager); + assertEquals(3, interpreterSetting.getInterpreterInfos().size()); + // 3 other builtin properties: + // * zeppelin.interpreter.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()); + + // verify interpreter binding + List interpreterSettingIds = interpreterSettingManager.getInterpreterBinding("2C6793KRV"); + assertEquals(2, interpreterSettingIds.size()); + assertEquals("test", interpreterSettingIds.get(0)); + assertEquals("test2", interpreterSettingIds.get(1)); + + // Load it again + InterpreterSettingManager interpreterSettingManager2 = new InterpreterSettingManager(conf, + mock(AngularObjectRegistryListener.class), mock(RemoteInterpreterProcessListener.class), mock(ApplicationEventListener.class)); + assertEquals(5, interpreterSettingManager2.get().size()); + interpreterSetting = interpreterSettingManager2.getByName("test"); + assertEquals("test", interpreterSetting.getName()); + assertEquals("test", interpreterSetting.getGroup()); + assertEquals(3, 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, InterpreterException { + // 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(6, 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(6, 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()); + + // remove interpreter setting + interpreterSettingManager.remove(interpreterSetting.getId()); + assertEquals(5, interpreterSettingManager.get().size()); + + // load it again + InterpreterSettingManager interpreterSettingManager3 = new InterpreterSettingManager(new ZeppelinConfiguration(), + mock(AngularObjectRegistryListener.class), mock(RemoteInterpreterProcessListener.class), mock(ApplicationEventListener.class)); + assertEquals(5, 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, InterpreterNotFoundException { + 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, InterpreterNotFoundException { + 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, InterpreterNotFoundException { + 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()); + + } + + @Test + public void testGetEditor() throws IOException, InterpreterNotFoundException { + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getInterpreterSettingIds()); + Interpreter echoInterpreter = interpreterFactory.getInterpreter("user1", "note1", "test.echo"); + // get editor setting from interpreter-setting.json + Map editor = interpreterSettingManager.getEditorSetting(echoInterpreter, "user1", "note1", "test.echo"); + assertEquals("java", editor.get("language")); + + // when editor setting doesn't exit, return the default editor + Interpreter mock1Interpreter = interpreterFactory.getInterpreter("user1", "note1", "mock1"); + editor = interpreterSettingManager.getEditorSetting(mock1Interpreter,"user1", "note1", "mock1"); + assertEquals("text", editor.get("language")); + } + + @Test + public void testRestartShared() throws InterpreterException { + InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("test"); + interpreterSetting.getOption().setPerUser("shared"); + interpreterSetting.getOption().setPerNote("shared"); + + interpreterSetting.getOrCreateSession("user1", "note1"); + interpreterSetting.getOrCreateInterpreterGroup("user2", "note2"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + + interpreterSettingManager.restart(interpreterSetting.getId(), "user1", "note1"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void testRestartPerUserIsolated() throws InterpreterException { + InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("test"); + interpreterSetting.getOption().setPerUser("isolated"); + interpreterSetting.getOption().setPerNote("shared"); + + interpreterSetting.getOrCreateSession("user1", "note1"); + interpreterSetting.getOrCreateSession("user2", "note2"); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); + + interpreterSettingManager.restart(interpreterSetting.getId(), "note1", "user1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void testRestartPerNoteIsolated() throws InterpreterException { + InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("test"); + interpreterSetting.getOption().setPerUser("shared"); + interpreterSetting.getOption().setPerNote("isolated"); + + interpreterSetting.getOrCreateSession("user1", "note1"); + interpreterSetting.getOrCreateSession("user2", "note2"); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); + + interpreterSettingManager.restart(interpreterSetting.getId(), "note1", "user1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + } + + @Test + public void testRestartPerUserScoped() throws InterpreterException { + InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("test"); + interpreterSetting.getOption().setPerUser("scoped"); + interpreterSetting.getOption().setPerNote("shared"); + + interpreterSetting.getOrCreateSession("user1", "note1"); + interpreterSetting.getOrCreateSession("user2", "note2"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + interpreterSettingManager.restart(interpreterSetting.getId(), "note1", "user1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + } + + @Test + public void testRestartPerNoteScoped() throws InterpreterException { + InterpreterSetting interpreterSetting = interpreterSettingManager.getByName("test"); + interpreterSetting.getOption().setPerUser("shared"); + interpreterSetting.getOption().setPerNote("scoped"); + + interpreterSetting.getOrCreateSession("user1", "note1"); + interpreterSetting.getOrCreateSession("user2", "note2"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + + interpreterSettingManager.restart(interpreterSetting.getId(), "note1", "user1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + } +} 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 index 1aab7572758..e3e47d3b5ce 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingTest.java @@ -1,327 +1,402 @@ -package org.apache.zeppelin.interpreter; +/* + * 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 java.util.ArrayList; -import java.util.List; -import java.util.Properties; +package org.apache.zeppelin.interpreter; import org.junit.Test; -import org.apache.zeppelin.dep.Dependency; -import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; +import static org.junit.Assert.assertNull; public class InterpreterSettingTest { @Test - public void sharedModeCloseandRemoveInterpreterGroupTest() { + public void testCreateInterpreters() { 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()); + 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 perUserScopedModeCloseAndRemoveInterpreterGroupTest() { + public void testSharedMode() { 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); + interpreterOption.setPerUser(InterpreterOption.SHARED); + 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()); - 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()); + // create default interpreter user1 and note2 + interpreterSetting.getDefaultInterpreter("user1", "note2"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - // Check if non-existed key works or not - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); - assertEquals(1, interpreterSetting.getInterpreterGroup("user2","note1").size()); + // only 1 session is created, this session is shared across users and notes + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user2"); + interpreterSetting.closeInterpreters("note1", "user1"); assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); } @Test - public void perUserIsolatedModeCloseAndRemoveInterpreterGroupTest() { + public void testPerUserScopedMode() { 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); + interpreterOption.setPerUser(InterpreterOption.SCOPED); + 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()); - assertEquals(2, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user2", "note1").size()); + // create interpreter for user2 and note1 + interpreterSetting.getDefaultInterpreter("user2", "note1"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); - assertEquals(1, interpreterSetting.getInterpreterGroup("user2","note1").size()); + 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.closeAndRemoveInterpreterGroup("note1", "user2"); + interpreterSetting.closeInterpreters("user2", "note1"); assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); } @Test - public void perNoteScopedModeCloseAndRemoveInterpreterGroupTest() { + public void testPerNoteScopedMode() { 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); + 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.getInterpreterGroup("user1", "note1").size()); - assertEquals(2, interpreterSetting.getInterpreterGroup("user1", "note2").size()); + assertEquals(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note2").size()); + 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()); - // Check if non-existed key works or not - interpreterSetting.closeAndRemoveInterpreterGroup("note1", "user1"); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note2").size()); + interpreterSetting.closeInterpreters("user1", "note2"); + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + } - interpreterSetting.closeAndRemoveInterpreterGroup("note2", "user1"); + @Test + public void testPerUserIsolatedMode() { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.ISOLATED); + 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 perNoteIsolatedModeCloseAndRemoveInterpreterGroupTest() { + public void testPerNoteIsolatedMode() { 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); + 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()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note2").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.closeAndRemoveInterpreterGroup("note1", "user1"); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note2").size()); + interpreterSetting.closeInterpreters("user1", "note1"); assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - - interpreterSetting.closeAndRemoveInterpreterGroup("note2", "user1"); + interpreterSetting.closeInterpreters("user1", "note2"); assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); } @Test - public void perNoteScopedModeRemoveInterpreterGroupWhenNoteIsRemoved() { + public void testPerUserIsolatedPerNoteScopedMode() { InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.ISOLATED); 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); + 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()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); - // This method will be called when remove note - interpreterSetting.closeAndRemoveInterpreterGroup("note1",""); + // close session for user2 + interpreterSetting.closeInterpreters("user2", "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() { + public void testPerUserIsolatedPerNoteIsolatedMode() { InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.ISOLATED); 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); + 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()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); - // This method will be called when remove note - interpreterSetting.closeAndRemoveInterpreterGroup("note1",""); + // close one session for user2 and note2 + interpreterSetting.closeInterpreters("user2", "note2"); 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() { + public void testPerUserScopedPerNoteScopedMode() { 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); - + interpreterOption.setPerNote(InterpreterOption.SCOPED); + 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.getInterpreterGroup("user1", "note1").size()); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - // This method will be called when remove note - interpreterSetting.closeAndRemoveInterpreterGroup("note1",""); + // create interpreter for user1 and note2 + interpreterSetting.getDefaultInterpreter("user1", "note2"); 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(2, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); + // create interpreter for user2 and note1 + interpreterSetting.getDefaultInterpreter("user2", "note1"); assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - assertEquals(1, interpreterSetting.getInterpreterGroup("user1", "note1").size()); + assertEquals(3, interpreterSetting.getAllInterpreterGroups().get(0).getSessionNum()); - // This method will be called when remove note - interpreterSetting.closeAndRemoveInterpreterGroup("note1",""); + // create interpreter for user2 and note2 + interpreterSetting.getDefaultInterpreter("user2", "note2"); assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); - // Be careful that getInterpreterGroup makes interpreterGroup if it doesn't exist - assertEquals(1, interpreterSetting.getInterpreterGroup("user1","note1").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-zengine/src/test/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroupTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroupTest.java new file mode 100644 index 00000000000..aa7374991b2 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroupTest.java @@ -0,0 +1,89 @@ +/* + * 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 ManagedInterpreterGroupTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(ManagedInterpreterGroupTest.class); + + private InterpreterSetting interpreterSetting; + + @Before + public void setUp() throws IOException, RepositoryException { + InterpreterOption interpreterOption = new InterpreterOption(); + interpreterOption.setPerUser(InterpreterOption.SCOPED); + 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() { + ManagedInterpreterGroup interpreterGroup = new ManagedInterpreterGroup("group_1", interpreterSetting); + assertEquals(0, interpreterGroup.getSessionNum()); + + // create session_1 + List interpreters = interpreterGroup.getOrCreateSession("user1", "session_1"); + assertEquals(3, 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(3, 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-zengine/src/test/java/org/apache/zeppelin/interpreter/MiniHadoopCluster.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/MiniHadoopCluster.java new file mode 100644 index 00000000000..b0799ae6062 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/MiniHadoopCluster.java @@ -0,0 +1,115 @@ +package org.apache.zeppelin.interpreter; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hdfs.MiniDFSCluster; +import org.apache.hadoop.yarn.conf.YarnConfiguration; +import org.apache.hadoop.yarn.server.MiniYARNCluster; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + + +/** + * + * Util class for creating a Mini Hadoop cluster in local machine to test scenarios that needs + * hadoop cluster. + */ +public class MiniHadoopCluster { + + private static Logger LOGGER = LoggerFactory.getLogger(MiniHadoopCluster.class); + + private Configuration hadoopConf; + private MiniDFSCluster dfsCluster; + private MiniYARNCluster yarnCluster; + private String configPath = new File("target/tests/hadoop_conf").getAbsolutePath(); + + @BeforeClass + public void start() throws IOException { + LOGGER.info("Starting MiniHadoopCluster ..."); + this.hadoopConf = new Configuration(); + new File(configPath).mkdirs(); + // start MiniDFSCluster + this.dfsCluster = new MiniDFSCluster.Builder(hadoopConf) + .numDataNodes(2) + .format(true) + .waitSafeMode(true) + .build(); + this.dfsCluster.waitActive(); + saveConfig(hadoopConf, configPath + "/core-site.xml"); + + // start MiniYarnCluster + YarnConfiguration baseConfig = new YarnConfiguration(hadoopConf); + baseConfig.set("yarn.nodemanager.disk-health-checker.max-disk-utilization-per-disk-percentage", "95"); + this.yarnCluster = new MiniYARNCluster(getClass().getName(), 2, + 1, 1); + yarnCluster.init(baseConfig); + + // Install a shutdown hook for stop the service and kill all running applications. + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + yarnCluster.stop(); + } + }); + + yarnCluster.start(); + + // Workaround for YARN-2642. + Configuration yarnConfig = yarnCluster.getConfig(); + long start = System.currentTimeMillis(); + while (System.currentTimeMillis() - start < 30 * 1000) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new IOException(e); + } + if (!yarnConfig.get(YarnConfiguration.RM_ADDRESS).split(":")[1].equals("0")) { + break; + } + } + if (yarnConfig.get(YarnConfiguration.RM_ADDRESS).split(":")[1].equals("0")) { + throw new IOException("RM not up yes"); + } + + LOGGER.info("RM address in configuration is " + yarnConfig.get(YarnConfiguration.RM_ADDRESS)); + saveConfig(yarnConfig,configPath + "/yarn-site.xml"); + } + + protected void saveConfig(Configuration conf, String dest) throws IOException { + Configuration redacted = new Configuration(conf); + // This setting references a test class that is not available when using a real Spark + // installation, so remove it from client configs. + redacted.unset("net.topology.node.switch.mapping.impl"); + + FileOutputStream out = new FileOutputStream(dest); + try { + redacted.writeXml(out); + } finally { + out.close(); + } + LOGGER.info("Save configuration to " + dest); + } + + @AfterClass + public void stop() { + if (this.yarnCluster != null) { + this.yarnCluster.stop(); + } + if (this.dfsCluster != null) { + this.dfsCluster.shutdown(); + } + } + + public String getConfigPath() { + return configPath; + } + + public MiniYARNCluster getYarnCluster() { + return yarnCluster; + } +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/MiniZeppelin.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/MiniZeppelin.java new file mode 100644 index 00000000000..923ae5a022f --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/MiniZeppelin.java @@ -0,0 +1,68 @@ +package org.apache.zeppelin.interpreter; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; + +import static org.mockito.Mockito.mock; + +public class MiniZeppelin { + + protected static final Logger LOGGER = LoggerFactory.getLogger(MiniZeppelin.class); + + protected InterpreterSettingManager interpreterSettingManager; + protected InterpreterFactory interpreterFactory; + protected File zeppelinHome; + private File confDir; + private File notebookDir; + protected ZeppelinConfiguration conf; + + public void start() throws IOException { + zeppelinHome = new File(".."); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_HOME.getVarName(), + zeppelinHome.getAbsolutePath()); + confDir = new File(zeppelinHome, "conf_" + getClass().getSimpleName()); + notebookDir = new File(zeppelinHome, "notebook_" + getClass().getSimpleName()); + confDir.mkdirs(); + notebookDir.mkdirs(); + LOGGER.info("ZEPPELIN_HOME: " + zeppelinHome.getAbsolutePath()); + FileUtils.copyFile(new File(zeppelinHome, "conf/log4j.properties"), new File(confDir, "log4j.properties")); + FileUtils.copyFile(new File(zeppelinHome, "conf/log4j_yarn_cluster.properties"), new File(confDir, "log4j_yarn_cluster.properties")); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONF_DIR.getVarName(), confDir.getAbsolutePath()); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), notebookDir.getAbsolutePath()); + conf = new ZeppelinConfiguration(); + interpreterSettingManager = new InterpreterSettingManager(conf, + mock(AngularObjectRegistryListener.class), mock(RemoteInterpreterProcessListener.class), mock(ApplicationEventListener.class)); + interpreterFactory = new InterpreterFactory(interpreterSettingManager); + } + + public void stop() throws IOException { + interpreterSettingManager.close(); + FileUtils.deleteDirectory(confDir); + FileUtils.deleteDirectory(notebookDir); + } + + public File getZeppelinHome() { + return zeppelinHome; + } + + public File getZeppelinConfDir() { + return confDir; + } + + public InterpreterFactory getInterpreterFactory() { + return interpreterFactory; + } + + public InterpreterSettingManager getInterpreterSettingManager() { + return interpreterSettingManager; + } +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SessionConfInterpreterTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SessionConfInterpreterTest.java new file mode 100644 index 00000000000..7baa2e261ce --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SessionConfInterpreterTest.java @@ -0,0 +1,69 @@ +/* + * 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.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SessionConfInterpreterTest { + + @Test + public void testUserSessionConfInterpreter() throws InterpreterException { + + InterpreterSetting mockInterpreterSetting = mock(InterpreterSetting.class); + ManagedInterpreterGroup mockInterpreterGroup = mock(ManagedInterpreterGroup.class); + when(mockInterpreterSetting.getInterpreterGroup("group_1")).thenReturn(mockInterpreterGroup); + + Properties properties = new Properties(); + properties.setProperty("property_1", "value_1"); + properties.setProperty("property_2", "value_2"); + SessionConfInterpreter confInterpreter = new SessionConfInterpreter( + properties, "session_1", "group_1", mockInterpreterSetting); + + RemoteInterpreter remoteInterpreter = + new RemoteInterpreter(properties, "session_1", "clasName", "user1", null); + List interpreters = new ArrayList<>(); + interpreters.add(confInterpreter); + interpreters.add(remoteInterpreter); + when(mockInterpreterGroup.get("session_1")).thenReturn(interpreters); + + InterpreterResult result = + confInterpreter.interpret("property_1\tupdated_value_1\nproperty_3\tvalue_3", + mock(InterpreterContext.class)); + assertEquals(InterpreterResult.Code.SUCCESS, result.code); + assertEquals(3, remoteInterpreter.getProperties().size()); + assertEquals("updated_value_1", remoteInterpreter.getProperty("property_1")); + assertEquals("value_2", remoteInterpreter.getProperty("property_2")); + assertEquals("value_3", remoteInterpreter.getProperty("property_3")); + + remoteInterpreter.setOpened(true); + result = + confInterpreter.interpret("property_1\tupdated_value_1\nproperty_3\tvalue_3", + mock(InterpreterContext.class)); + assertEquals(InterpreterResult.Code.ERROR, result.code); + } +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SleepInterpreter.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SleepInterpreter.java new file mode 100644 index 00000000000..7a904c6bfb5 --- /dev/null +++ b/zeppelin-zengine/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(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/SparkDownloadUtils.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkDownloadUtils.java new file mode 100644 index 00000000000..0455dd85c7e --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkDownloadUtils.java @@ -0,0 +1,111 @@ +package org.apache.zeppelin.interpreter; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SparkDownloadUtils { + private static Logger LOGGER = LoggerFactory.getLogger(SparkDownloadUtils.class); + + private static String downloadFolder = System.getProperty("user.home") + "/.cache/spark"; + + static { + try { + FileUtils.forceMkdir(new File(downloadFolder)); + } catch (IOException e) { + throw new RuntimeException("Fail to create downloadFolder: " + downloadFolder, e); + } + } + + + public static String downloadSpark(String version) { + File targetSparkHomeFolder = new File(downloadFolder + "/spark-" + version + "-bin-hadoop2.6"); + if (targetSparkHomeFolder.exists()) { + LOGGER.info("Skip to download spark as it is already downloaded."); + return targetSparkHomeFolder.getAbsolutePath(); + } + // Try mirrors a few times until one succeeds + boolean downloaded = false; + for (int i = 0; i < 3; i++) { + try { + String preferredMirror = IOUtils.toString(new URL("https://www.apache.org/dyn/closer.lua?preferred=true")); + File downloadFile = new File(downloadFolder + "/spark-" + version + "-bin-hadoop2.6.tgz"); + String downloadURL = preferredMirror + "/spark/spark-" + version + "/spark-" + version + "-bin-hadoop2.6.tgz"; + runShellCommand(new String[] {"wget", downloadURL, "-P", downloadFolder}); + runShellCommand(new String[]{"tar", "-xvf", downloadFile.getAbsolutePath(), "-C", downloadFolder}); + downloaded = true; + break; + } catch (Exception e) { + LOGGER.warn("Failed to download Spark", e); + } + } + + // fallback to use apache archive + // https://archive.apache.org/dist/spark/spark-1.6.3/spark-1.6.3-bin-hadoop2.6.tgz + if (!downloaded) { + File downloadFile = new File(downloadFolder + "/spark-" + version + "-bin-hadoop2.6.tgz"); + String downloadURL = + "https://archive.apache.org/dist/spark/spark-" + + version + + "/spark-" + + version + + "-bin-hadoop2.6.tgz"; + try { + runShellCommand(new String[] {"wget", downloadURL, "-P", downloadFolder}); + runShellCommand( + new String[] {"tar", "-xvf", downloadFile.getAbsolutePath(), "-C", downloadFolder}); + } catch (Exception e) { + throw new RuntimeException("Fail to download spark " + version, e); + } + } + return targetSparkHomeFolder.getAbsolutePath(); + } + + private static void runShellCommand(String[] commands) throws IOException, InterruptedException { + LOGGER.info("Starting shell commands: " + StringUtils.join(commands, " ")); + Process process = Runtime.getRuntime().exec(commands); + StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream()); + StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream()); + errorGobbler.start(); + outputGobbler.start(); + if (process.waitFor() != 0) { + throw new IOException("Fail to run shell commands: " + StringUtils.join(commands, " ")); + } + LOGGER.info("Complete shell commands: " + StringUtils.join(commands, " ")); + } + + private static class StreamGobbler extends Thread { + InputStream is; + + // reads everything from is until empty. + StreamGobbler(InputStream is) { + this.is = is; + } + + public void run() { + try { + InputStreamReader isr = new InputStreamReader(is); + BufferedReader br = new BufferedReader(isr); + String line = null; + long startTime = System.currentTimeMillis(); + while ( (line = br.readLine()) != null) { + // logging per 5 seconds + if ((System.currentTimeMillis() - startTime) > 5000) { + LOGGER.info(line); + startTime = System.currentTimeMillis(); + } + } + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + } +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest.java new file mode 100644 index 00000000000..6782b470f32 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest.java @@ -0,0 +1,193 @@ +package org.apache.zeppelin.interpreter; + +import org.apache.commons.io.IOUtils; +import org.apache.hadoop.yarn.api.protocolrecords.GetApplicationsRequest; +import org.apache.hadoop.yarn.api.protocolrecords.GetApplicationsResponse; +import org.apache.hadoop.yarn.api.records.YarnApplicationState; +import org.apache.hadoop.yarn.exceptions.YarnException; +import org.apache.maven.model.Model; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.EnumSet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public abstract class SparkIntegrationTest { + private static Logger LOGGER = LoggerFactory.getLogger(SparkIntegrationTest.class); + + private static MiniHadoopCluster hadoopCluster; + private static MiniZeppelin zeppelin; + private static InterpreterFactory interpreterFactory; + private static InterpreterSettingManager interpreterSettingManager; + + private String sparkVersion; + private String sparkHome; + + public SparkIntegrationTest(String sparkVersion) { + LOGGER.info("Testing SparkVersion: " + sparkVersion); + this.sparkVersion = sparkVersion; + this.sparkHome = SparkDownloadUtils.downloadSpark(sparkVersion); + } + + @BeforeClass + public static void setUp() throws IOException { + hadoopCluster = new MiniHadoopCluster(); + hadoopCluster.start(); + + zeppelin = new MiniZeppelin(); + zeppelin.start(); + interpreterFactory = zeppelin.getInterpreterFactory(); + interpreterSettingManager = zeppelin.getInterpreterSettingManager(); + } + + @AfterClass + public static void tearDown() throws IOException { + if (zeppelin != null) { + zeppelin.stop(); + } + if (hadoopCluster != null) { + hadoopCluster.stop(); + } + } + + private void testInterpreterBasics() throws IOException, InterpreterException, XmlPullParserException { + // add jars & packages for testing + InterpreterSetting sparkInterpreterSetting = interpreterSettingManager.getInterpreterSettingByName("spark"); + sparkInterpreterSetting.setProperty("spark.jars.packages", "com.maxmind.geoip2:geoip2:2.5.0"); + MavenXpp3Reader reader = new MavenXpp3Reader(); + Model model = reader.read(new FileReader("pom.xml")); + sparkInterpreterSetting.setProperty("spark.jars", new File("target/zeppelin-zengine-" + model.getVersion() + ".jar").getAbsolutePath()); + + // test SparkInterpreter + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getInterpreterSettingIds()); + Interpreter sparkInterpreter = interpreterFactory.getInterpreter("user1", "note1", "spark.spark"); + + InterpreterContext context = new InterpreterContext.Builder().setNoteId("note1").setParagraphId("paragraph_1").build(); + InterpreterResult interpreterResult = sparkInterpreter.interpret("sc.version", context); + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code); + String detectedSparkVersion = interpreterResult.message().get(0).getData(); + assertTrue(detectedSparkVersion +" doesn't contain " + this.sparkVersion, detectedSparkVersion.contains(this.sparkVersion)); + interpreterResult = sparkInterpreter.interpret("sc.range(1,10).sum()", context); + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code); + assertTrue(interpreterResult.msg.get(0).getData().contains("45")); + + // test jars & packages can be loaded correctly + interpreterResult = sparkInterpreter.interpret("import org.apache.zeppelin.interpreter.install.InstallInterpreter\n" + + "import com.maxmind.geoip2._", context); + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code()); + + // test PySparkInterpreter + Interpreter pySparkInterpreter = interpreterFactory.getInterpreter("user1", "note1", "spark.pyspark"); + interpreterResult = pySparkInterpreter.interpret("sqlContext.createDataFrame([(1,'a'),(2,'b')], ['id','name']).registerTempTable('test')", context); + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code); + + // test IPySparkInterpreter + Interpreter ipySparkInterpreter = interpreterFactory.getInterpreter("user1", "note1", "spark.ipyspark"); + interpreterResult = ipySparkInterpreter.interpret("sqlContext.table('test').show()", context); + assertEquals(interpreterResult.toString(), InterpreterResult.Code.SUCCESS, interpreterResult.code); + + // test SparkSQLInterpreter + Interpreter sqlInterpreter = interpreterFactory.getInterpreter("user1", "note1", "spark.sql"); + interpreterResult = sqlInterpreter.interpret("select count(1) as c from test", context); + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code); + assertEquals(InterpreterResult.Type.TABLE, interpreterResult.message().get(0).getType()); + assertEquals("c\n2\n", interpreterResult.message().get(0).getData()); + + // test SparkRInterpreter + Interpreter sparkrInterpreter = interpreterFactory.getInterpreter("user1", "note1", "spark.r"); + if (isSpark2()) { + interpreterResult = sparkrInterpreter.interpret("df <- as.DataFrame(faithful)\nhead(df)", context); + } else { + interpreterResult = sparkrInterpreter.interpret("df <- createDataFrame(sqlContext, faithful)\nhead(df)", context); + } + assertEquals(InterpreterResult.Code.SUCCESS, interpreterResult.code); + assertEquals(InterpreterResult.Type.TEXT, interpreterResult.message().get(0).getType()); + assertTrue(interpreterResult.message().get(0).getData().contains("eruptions waiting")); + } + + @Test + public void testLocalMode() throws IOException, YarnException, InterpreterException, InterruptedException, XmlPullParserException { + InterpreterSetting sparkInterpreterSetting = interpreterSettingManager.getInterpreterSettingByName("spark"); + sparkInterpreterSetting.setProperty("master", "local[*]"); + sparkInterpreterSetting.setProperty("SPARK_HOME", sparkHome); + sparkInterpreterSetting.setProperty("ZEPPELIN_CONF_DIR", zeppelin.getZeppelinConfDir().getAbsolutePath()); + sparkInterpreterSetting.setProperty("zeppelin.spark.useHiveContext", "false"); + sparkInterpreterSetting.setProperty("zeppelin.pyspark.useIPython", "false"); + sparkInterpreterSetting.setProperty("spark.pyspark.python", getPythonExec()); + + testInterpreterBasics(); + + // no yarn application launched + GetApplicationsRequest request = GetApplicationsRequest.newInstance(EnumSet.of(YarnApplicationState.RUNNING)); + GetApplicationsResponse response = hadoopCluster.getYarnCluster().getResourceManager().getClientRMService().getApplications(request); + assertEquals(0, response.getApplicationList().size()); + + interpreterSettingManager.close(); + } + + @Test + public void testYarnClientMode() throws IOException, YarnException, InterruptedException, InterpreterException, XmlPullParserException { + InterpreterSetting sparkInterpreterSetting = interpreterSettingManager.getInterpreterSettingByName("spark"); + sparkInterpreterSetting.setProperty("master", "yarn-client"); + sparkInterpreterSetting.setProperty("HADOOP_CONF_DIR", hadoopCluster.getConfigPath()); + sparkInterpreterSetting.setProperty("SPARK_HOME", sparkHome); + sparkInterpreterSetting.setProperty("ZEPPELIN_CONF_DIR", zeppelin.getZeppelinConfDir().getAbsolutePath()); + sparkInterpreterSetting.setProperty("zeppelin.spark.useHiveContext", "false"); + sparkInterpreterSetting.setProperty("zeppelin.pyspark.useIPython", "false"); + sparkInterpreterSetting.setProperty("spark.pyspark.python", getPythonExec()); + sparkInterpreterSetting.setProperty("spark.driver.memory", "512m"); + + testInterpreterBasics(); + + // 1 yarn application launched + GetApplicationsRequest request = GetApplicationsRequest.newInstance(EnumSet.of(YarnApplicationState.RUNNING)); + GetApplicationsResponse response = hadoopCluster.getYarnCluster().getResourceManager().getClientRMService().getApplications(request); + assertEquals(1, response.getApplicationList().size()); + + interpreterSettingManager.close(); + } + + @Test + public void testYarnClusterMode() throws IOException, YarnException, InterruptedException, InterpreterException, XmlPullParserException { + InterpreterSetting sparkInterpreterSetting = interpreterSettingManager.getInterpreterSettingByName("spark"); + sparkInterpreterSetting.setProperty("master", "yarn-cluster"); + sparkInterpreterSetting.setProperty("HADOOP_CONF_DIR", hadoopCluster.getConfigPath()); + sparkInterpreterSetting.setProperty("SPARK_HOME", sparkHome); + sparkInterpreterSetting.setProperty("ZEPPELIN_CONF_DIR", zeppelin.getZeppelinConfDir().getAbsolutePath()); + sparkInterpreterSetting.setProperty("zeppelin.spark.useHiveContext", "false"); + sparkInterpreterSetting.setProperty("zeppelin.pyspark.useIPython", "false"); + sparkInterpreterSetting.setProperty("spark.pyspark.python", getPythonExec()); + sparkInterpreterSetting.setProperty("spark.driver.memory", "512m"); + + testInterpreterBasics(); + + // 1 yarn application launched + GetApplicationsRequest request = GetApplicationsRequest.newInstance(EnumSet.of(YarnApplicationState.RUNNING)); + GetApplicationsResponse response = hadoopCluster.getYarnCluster().getResourceManager().getClientRMService().getApplications(request); + assertEquals(1, response.getApplicationList().size()); + + interpreterSettingManager.close(); + } + + private boolean isSpark2() { + return this.sparkVersion.startsWith("2."); + } + + private String getPythonExec() throws IOException, InterruptedException { + Process process = Runtime.getRuntime().exec(new String[]{"which", "python"}); + if (process.waitFor() != 0) { + throw new RuntimeException("Fail to run command: which python."); + } + return IOUtils.toString(process.getInputStream()).trim(); + } +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest16.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest16.java new file mode 100644 index 00000000000..ffe31f732ac --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest16.java @@ -0,0 +1,40 @@ +/* + * 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.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +@RunWith(value = Parameterized.class) +public class SparkIntegrationTest16 extends SparkIntegrationTest{ + + public SparkIntegrationTest16(String sparkVersion) { + super(sparkVersion); + } + + @Parameterized.Parameters + public static List data() { + return Arrays.asList(new Object[][]{ + {"1.6.3"} + }); + } + +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest20.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest20.java new file mode 100644 index 00000000000..27e70856555 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest20.java @@ -0,0 +1,40 @@ +/* + * 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.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +@RunWith(value = Parameterized.class) +public class SparkIntegrationTest20 extends SparkIntegrationTest{ + + public SparkIntegrationTest20(String sparkVersion) { + super(sparkVersion); + } + + @Parameterized.Parameters + public static List data() { + return Arrays.asList(new Object[][]{ + {"2.0.2"} + }); + } + +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest21.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest21.java new file mode 100644 index 00000000000..423852803d2 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest21.java @@ -0,0 +1,40 @@ +/* + * 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.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +@RunWith(value = Parameterized.class) +public class SparkIntegrationTest21 extends SparkIntegrationTest{ + + public SparkIntegrationTest21(String sparkVersion) { + super(sparkVersion); + } + + @Parameterized.Parameters + public static List data() { + return Arrays.asList(new Object[][]{ + {"2.1.3"} + }); + } + +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest22.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest22.java new file mode 100644 index 00000000000..02ab5dc822a --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest22.java @@ -0,0 +1,40 @@ +/* + * 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.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +@RunWith(value = Parameterized.class) +public class SparkIntegrationTest22 extends SparkIntegrationTest{ + + public SparkIntegrationTest22(String sparkVersion) { + super(sparkVersion); + } + + @Parameterized.Parameters + public static List data() { + return Arrays.asList(new Object[][]{ + {"2.2.2"} + }); + } + +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest23.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest23.java new file mode 100644 index 00000000000..7aa69ecaea3 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest23.java @@ -0,0 +1,40 @@ +/* + * 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.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +@RunWith(value = Parameterized.class) +public class SparkIntegrationTest23 extends SparkIntegrationTest{ + + public SparkIntegrationTest23(String sparkVersion) { + super(sparkVersion); + } + + @Parameterized.Parameters + public static List data() { + return Arrays.asList(new Object[][]{ + {"2.3.2"} + }); + } + +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest24.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest24.java new file mode 100644 index 00000000000..3b825b7c670 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/SparkIntegrationTest24.java @@ -0,0 +1,40 @@ +/* + * 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.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +@RunWith(value = Parameterized.class) +public class SparkIntegrationTest24 extends SparkIntegrationTest{ + + public SparkIntegrationTest24(String sparkVersion) { + super(sparkVersion); + } + + @Parameterized.Parameters + public static List data() { + return Arrays.asList(new Object[][]{ + {"2.4.0"} + }); + } + +} 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 new file mode 100644 index 00000000000..b7557ada982 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/ShellScriptLauncherTest.java @@ -0,0 +1,55 @@ +/* + * 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.launcher; + +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.InterpreterOption; +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; +import static org.junit.Assert.assertTrue; + +public class ShellScriptLauncherTest { + + @Test + public void testLauncher() throws IOException { + ZeppelinConfiguration zConf = new ZeppelinConfiguration(); + 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(); + 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; + assertEquals("name", interpreterProcess.getInterpreterSettingName()); + assertEquals(".//interpreter/groupName", interpreterProcess.getInterpreterDir()); + assertEquals(".//local-repo/groupId", interpreterProcess.getLocalRepoDir()); + 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 new file mode 100644 index 00000000000..e8df17496cb --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/launcher/SparkInterpreterLauncherTest.java @@ -0,0 +1,206 @@ +/* + * 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.launcher; + +import org.apache.commons.io.FileUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.InterpreterOption; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreterManagedProcess; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.Path; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class SparkInterpreterLauncherTest { + + @Test + public void testLocalMode() throws IOException { + ZeppelinConfiguration zConf = new ZeppelinConfiguration(); + SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf, null); + Properties properties = new Properties(); + properties.setProperty("SPARK_HOME", "/user/spark"); + properties.setProperty("property_1", "value_1"); + properties.setProperty("master", "local[*]"); + properties.setProperty("spark.files", "file_1"); + properties.setProperty("spark.jars", "jar_1"); + + InterpreterOption option = new InterpreterOption(); + 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; + assertEquals("spark", interpreterProcess.getInterpreterSettingName()); + assertTrue(interpreterProcess.getInterpreterDir().endsWith("/interpreter/spark")); + assertTrue(interpreterProcess.getLocalRepoDir().endsWith("/local-repo/groupId")); + assertEquals(zConf.getInterpreterRemoteRunnerPath(), interpreterProcess.getInterpreterRunner()); + assertTrue(interpreterProcess.getEnv().size() >= 2); + assertEquals("/user/spark", interpreterProcess.getEnv().get("SPARK_HOME")); + assertEquals(" --master local[*] --conf spark.files='file_1' --conf spark.jars='jar_1'", interpreterProcess.getEnv().get("ZEPPELIN_SPARK_CONF")); + } + + @Test + public void testYarnClientMode_1() throws IOException { + ZeppelinConfiguration zConf = new ZeppelinConfiguration(); + SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf, null); + Properties properties = new Properties(); + properties.setProperty("SPARK_HOME", "/user/spark"); + properties.setProperty("property_1", "value_1"); + properties.setProperty("master", "yarn-client"); + properties.setProperty("spark.files", "file_1"); + properties.setProperty("spark.jars", "jar_1"); + + InterpreterOption option = new InterpreterOption(); + 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; + assertEquals("spark", interpreterProcess.getInterpreterSettingName()); + assertTrue(interpreterProcess.getInterpreterDir().endsWith("/interpreter/spark")); + assertTrue(interpreterProcess.getLocalRepoDir().endsWith("/local-repo/groupId")); + assertEquals(zConf.getInterpreterRemoteRunnerPath(), interpreterProcess.getInterpreterRunner()); + assertTrue(interpreterProcess.getEnv().size() >= 2); + assertEquals("/user/spark", interpreterProcess.getEnv().get("SPARK_HOME")); + assertEquals(" --master yarn-client --conf spark.files='file_1' --conf spark.jars='jar_1' --conf spark.yarn.isPython=true", interpreterProcess.getEnv().get("ZEPPELIN_SPARK_CONF")); + } + + @Test + public void testYarnClientMode_2() throws IOException { + ZeppelinConfiguration zConf = new ZeppelinConfiguration(); + SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf, null); + Properties properties = new Properties(); + properties.setProperty("SPARK_HOME", "/user/spark"); + properties.setProperty("property_1", "value_1"); + properties.setProperty("master", "yarn"); + properties.setProperty("spark.submit.deployMode", "client"); + properties.setProperty("spark.files", "file_1"); + properties.setProperty("spark.jars", "jar_1"); + + InterpreterOption option = new InterpreterOption(); + 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; + assertEquals("spark", interpreterProcess.getInterpreterSettingName()); + assertTrue(interpreterProcess.getInterpreterDir().endsWith("/interpreter/spark")); + assertTrue(interpreterProcess.getLocalRepoDir().endsWith("/local-repo/groupId")); + assertEquals(zConf.getInterpreterRemoteRunnerPath(), interpreterProcess.getInterpreterRunner()); + assertTrue(interpreterProcess.getEnv().size() >= 2); + assertEquals("/user/spark", interpreterProcess.getEnv().get("SPARK_HOME")); + assertEquals(" --master yarn --conf spark.files='file_1' --conf spark.jars='jar_1' --conf spark.submit.deployMode='client' --conf spark.yarn.isPython=true", interpreterProcess.getEnv().get("ZEPPELIN_SPARK_CONF")); + } + + @Test + public void testYarnClusterMode_1() throws IOException { + ZeppelinConfiguration zConf = new ZeppelinConfiguration(); + SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf, null); + Properties properties = new Properties(); + properties.setProperty("SPARK_HOME", "/user/spark"); + properties.setProperty("property_1", "value_1"); + properties.setProperty("master", "yarn-cluster"); + properties.setProperty("spark.files", "file_1"); + properties.setProperty("spark.jars", "jar_1"); + + InterpreterOption option = new InterpreterOption(); + 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; + assertEquals("spark", interpreterProcess.getInterpreterSettingName()); + assertTrue(interpreterProcess.getInterpreterDir().endsWith("/interpreter/spark")); + assertTrue(interpreterProcess.getLocalRepoDir().endsWith("/local-repo/groupId")); + assertEquals(zConf.getInterpreterRemoteRunnerPath(), interpreterProcess.getInterpreterRunner()); + assertTrue(interpreterProcess.getEnv().size() >= 3); + assertEquals("/user/spark", interpreterProcess.getEnv().get("SPARK_HOME")); + assertEquals("true", interpreterProcess.getEnv().get("ZEPPELIN_SPARK_YARN_CLUSTER")); + assertEquals(" --master yarn-cluster --conf spark.files='file_1',.//conf/log4j_yarn_cluster.properties --conf spark.jars='jar_1' --conf spark.yarn.isPython=true", interpreterProcess.getEnv().get("ZEPPELIN_SPARK_CONF")); + } + + @Test + public void testYarnClusterMode_2() throws IOException { + ZeppelinConfiguration zConf = new ZeppelinConfiguration(); + SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf, null); + Properties properties = new Properties(); + properties.setProperty("SPARK_HOME", "/user/spark"); + properties.setProperty("property_1", "value_1"); + properties.setProperty("master", "yarn"); + properties.setProperty("spark.submit.deployMode", "cluster"); + properties.setProperty("spark.files", "file_1"); + properties.setProperty("spark.jars", "jar_1"); + + InterpreterOption option = new InterpreterOption(); + option.setUserImpersonate(true); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "user1", "intpGroupId", "groupId", "spark", "spark"); + Path localRepoPath = Paths.get(zConf.getInterpreterLocalRepoPath(), context.getInterpreterSettingId()); + FileUtils.deleteDirectory(localRepoPath.toFile()); + Files.createDirectories(localRepoPath); + Files.createFile(Paths.get(localRepoPath.toAbsolutePath().toString(), "test.jar")); + + InterpreterClient client = launcher.launch(context); + assertTrue( client instanceof RemoteInterpreterManagedProcess); + RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; + assertEquals("spark", interpreterProcess.getInterpreterSettingName()); + assertTrue(interpreterProcess.getInterpreterDir().endsWith("/interpreter/spark")); + assertTrue(interpreterProcess.getLocalRepoDir().endsWith("/local-repo/groupId")); + assertEquals(zConf.getInterpreterRemoteRunnerPath(), interpreterProcess.getInterpreterRunner()); + assertTrue(interpreterProcess.getEnv().size() >= 3); + assertEquals("/user/spark", interpreterProcess.getEnv().get("SPARK_HOME")); + assertEquals("true", interpreterProcess.getEnv().get("ZEPPELIN_SPARK_YARN_CLUSTER")); + assertEquals(" --master yarn --conf spark.files='file_1',.//conf/log4j_yarn_cluster.properties --conf spark.jars='jar_1' --conf spark.submit.deployMode='cluster' --conf spark.yarn.isPython=true --proxy-user user1", interpreterProcess.getEnv().get("ZEPPELIN_SPARK_CONF")); + Files.deleteIfExists(Paths.get(localRepoPath.toAbsolutePath().toString(), "test.jar")); + FileUtils.deleteDirectory(localRepoPath.toFile()); + } + + @Test + public void testYarnClusterMode_3() throws IOException { + ZeppelinConfiguration zConf = new ZeppelinConfiguration(); + SparkInterpreterLauncher launcher = new SparkInterpreterLauncher(zConf, null); + Properties properties = new Properties(); + properties.setProperty("SPARK_HOME", "/user/spark"); + properties.setProperty("property_1", "value_1"); + properties.setProperty("master", "yarn"); + properties.setProperty("spark.submit.deployMode", "cluster"); + properties.setProperty("spark.files", "file_1"); + properties.setProperty("spark.jars", "jar_1"); + + InterpreterOption option = new InterpreterOption(); + option.setUserImpersonate(true); + InterpreterLaunchContext context = new InterpreterLaunchContext(properties, option, null, "user1", "intpGroupId", "groupId", "spark", "spark"); + Path localRepoPath = Paths.get(zConf.getInterpreterLocalRepoPath(), context.getInterpreterSettingId()); + FileUtils.deleteDirectory(localRepoPath.toFile()); + Files.createDirectories(localRepoPath); + + InterpreterClient client = launcher.launch(context); + assertTrue(client instanceof RemoteInterpreterManagedProcess); + RemoteInterpreterManagedProcess interpreterProcess = (RemoteInterpreterManagedProcess) client; + assertEquals("spark", interpreterProcess.getInterpreterSettingName()); + assertTrue(interpreterProcess.getInterpreterDir().endsWith("/interpreter/spark")); + assertTrue(interpreterProcess.getLocalRepoDir().endsWith("/local-repo/groupId")); + assertEquals(zConf.getInterpreterRemoteRunnerPath(), interpreterProcess.getInterpreterRunner()); + assertTrue(interpreterProcess.getEnv().size() >= 3); + assertEquals("/user/spark", interpreterProcess.getEnv().get("SPARK_HOME")); + assertEquals("true", interpreterProcess.getEnv().get("ZEPPELIN_SPARK_YARN_CLUSTER")); + assertEquals(" --master yarn --conf spark.files='file_1',.//conf/log4j_yarn_cluster.properties --conf spark.jars='jar_1' --conf spark.submit.deployMode='cluster' --conf spark.yarn.isPython=true --proxy-user user1", interpreterProcess.getEnv().get("ZEPPELIN_SPARK_CONF")); + FileUtils.deleteDirectory(localRepoPath.toFile()); + } +} 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 new file mode 100644 index 00000000000..1041502a16d --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/lifecycle/TimeoutLifecycleManagerTest.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.lifecycle; + +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.interpreter.AbstractInterpreterTest; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterContextRunner; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterSetting; +import org.apache.zeppelin.interpreter.remote.RemoteInterpreter; +import org.apache.zeppelin.scheduler.Job; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TimeoutLifecycleManagerTest extends AbstractInterpreterTest { + + @Override + public void setUp() throws Exception { + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_LIFECYCLE_MANAGER_CLASS.getVarName(), + TimeoutLifecycleManager.class.getName()); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_LIFECYCLE_MANAGER_TIMEOUT_CHECK_INTERVAL.getVarName(), "1000"); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_INTERPRETER_LIFECYCLE_MANAGER_TIMEOUT_THRESHOLD.getVarName(), "10000"); + super.setUp(); + } + + @Test + public void testTimeout_1() throws InterpreterException, InterruptedException, IOException { + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "test.echo") instanceof RemoteInterpreter); + RemoteInterpreter remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test.echo"); + assertFalse(remoteInterpreter.isOpened()); + InterpreterSetting interpreterSetting = interpreterSettingManager.getInterpreterSettingByName("test"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + Thread.sleep(15*1000); + // InterpreterGroup is not removed after 15 seconds, as TimeoutLifecycleManager only manage it after it is started + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + + InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + new GUI(), null, null, new ArrayList(), null); + remoteInterpreter.interpret("hello world", context); + assertTrue(remoteInterpreter.isOpened()); + + Thread.sleep(15 * 1000); + // interpreterGroup is timeout, so is removed. + assertEquals(0, interpreterSetting.getAllInterpreterGroups().size()); + assertFalse(remoteInterpreter.isOpened()); + } + + @Test + public void testTimeout_2() throws InterpreterException, InterruptedException, IOException { + interpreterSettingManager.setInterpreterBinding("user1", "note1", interpreterSettingManager.getSettingIds()); + assertTrue(interpreterFactory.getInterpreter("user1", "note1", "test.sleep") instanceof RemoteInterpreter); + final RemoteInterpreter remoteInterpreter = (RemoteInterpreter) interpreterFactory.getInterpreter("user1", "note1", "test.sleep"); + + // simulate how zeppelin submit paragraph + remoteInterpreter.getScheduler().submit(new Job("test-job", null) { + @Override + public Object getReturn() { + return null; + } + + @Override + public int progress() { + return 0; + } + + @Override + public Map info() { + return null; + } + + @Override + protected Object jobRun() throws Throwable { + InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), + new GUI(), null, null, new ArrayList(), null); + return remoteInterpreter.interpret("100000", context); + } + + @Override + protected boolean jobAbort() { + return false; + } + + @Override + public void setResult(Object results) { + + } + }); + + while(!remoteInterpreter.isOpened()) { + Thread.sleep(1000); + LOGGER.info("Wait for interpreter to be started"); + } + + InterpreterSetting interpreterSetting = interpreterSettingManager.getInterpreterSettingByName("test"); + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + + Thread.sleep(15 * 1000); + // interpreterGroup is not timeout because getStatus is called periodically. + assertEquals(1, interpreterSetting.getAllInterpreterGroups().size()); + assertTrue(remoteInterpreter.isOpened()); + } +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java index b16e9371bf8..500c4f72094 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java +++ b/zeppelin-zengine/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,12 +24,27 @@ import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; -public class MockInterpreter1 extends Interpreter{ -Map vars = new HashMap<>(); +import java.lang.management.ManagementFactory; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +public class MockInterpreter1 extends Interpreter { + + private static AtomicInteger IdGenerator = new AtomicInteger(); + + private int object_id; + private String pid; + Map vars = new HashMap<>(); public MockInterpreter1(Properties property) { super(property); + this.object_id = IdGenerator.getAndIncrement(); + this.pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; } + boolean open; @@ -59,7 +69,7 @@ public InterpreterResult interpret(String st, InterpreterContext context) { if ("getId".equals(st)) { // get unique id of this interpreter instance - result = new InterpreterResult(InterpreterResult.Code.SUCCESS, "" + this.hashCode()); + result = new InterpreterResult(InterpreterResult.Code.SUCCESS, "" + this.object_id + "-" + this.pid); } else if (st.startsWith("sleep")) { try { Thread.sleep(Integer.parseInt(st.split(" ")[1])); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter2.java b/zeppelin-zengine/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-zengine/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/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()); + } + +} diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/AppendOutputRunnerTest.java b/zeppelin-zengine/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-zengine/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-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java index f7404e35cb0..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 @@ -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,23 @@ 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(); + 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,21 +86,22 @@ public void setUp() throws Exception { new AuthenticationInfo(), new HashMap(), new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), + new GUI(), + 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 - public void testAngularObjectInterpreterSideCRUD() throws InterruptedException { + public void testAngularObjectInterpreterSideCRUD() throws InterruptedException, InterpreterException { InterpreterResult ret = intp.interpret("get", context); Thread.sleep(500); // waitFor eventpoller pool event String[] result = ret.message().get(0).getData().split(" "); @@ -139,7 +134,7 @@ public void testAngularObjectInterpreterSideCRUD() throws InterruptedException { } @Test - public void testAngularObjectRemovalOnZeppelinServerSide() throws InterruptedException { + public void testAngularObjectRemovalOnZeppelinServerSide() throws InterruptedException, InterpreterException { // test if angularobject removal from server side propagate to interpreter process's registry. // will happen when notebook is removed. @@ -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); @@ -164,7 +159,7 @@ public void testAngularObjectRemovalOnZeppelinServerSide() throws InterruptedExc } @Test - public void testAngularObjectAddOnZeppelinServerSide() throws InterruptedException { + public void testAngularObjectAddOnZeppelinServerSide() throws InterruptedException, InterpreterException { // test if angularobject add from server side propagate to interpreter process's registry. // will happen when zeppelin server loads notebook and restore the object into registry @@ -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/RemoteInterpreterOutputTestStream.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterOutputTestStream.java index 3f865cb370d..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 @@ -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,31 @@ 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(); + + 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 +76,15 @@ private InterpreterContext createInterpreterContext() { new AuthenticationInfo(), new HashMap(), new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), + new GUI(), + null, null, new LinkedList(), null); } @Test - public void testInterpreterResultOnly() { - RemoteInterpreter intp = createMockInterpreter(); + public void testInterpreterResultOnly() throws InterpreterException { + 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()); @@ -112,8 +99,8 @@ public void testInterpreterResultOnly() { } @Test - public void testInterpreterOutputStreamOnly() { - RemoteInterpreter intp = createMockInterpreter(); + public void testInterpreterOutputStreamOnly() throws InterpreterException { + 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()); @@ -124,8 +111,8 @@ public void testInterpreterOutputStreamOnly() { } @Test - public void testInterpreterResultOutputStreamMixed() { - RemoteInterpreter intp = createMockInterpreter(); + public void testInterpreterResultOutputStreamMixed() throws InterpreterException { + 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()); @@ -133,8 +120,8 @@ public void testInterpreterResultOutputStreamMixed() { } @Test - public void testOutputType() { - RemoteInterpreter intp = createMockInterpreter(); + public void testOutputType() throws InterpreterException { + 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-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 index 95235e51a97..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 @@ -17,839 +17,379 @@ 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.display.Input; +import org.apache.zeppelin.display.ui.OptionInput; 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.apache.zeppelin.interpreter.remote.mock.GetAngularObjectSizeInterpreter; +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 org.mockito.Mockito; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; public class RemoteInterpreterTest { 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 InterpreterSetting interpreterSetting; @Before public void setUp() throws Exception { - intpGroup = new InterpreterGroup(); - env = new HashMap<>(); - env.put("ZEPPELIN_CLASSPATH", new File("./target/test-classes").getAbsolutePath()); + InterpreterOption interpreterOption = new InterpreterOption(); + + 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()); + InterpreterInfo interpreterInfo5 = new InterpreterInfo(GetAngularObjectSizeInterpreter.class.getName(), "angular_obj",false, new HashMap()); + List interpreterInfos = new ArrayList<>(); + interpreterInfos.add(interpreterInfo1); + interpreterInfos.add(interpreterInfo2); + interpreterInfos.add(interpreterInfo3); + interpreterInfos.add(interpreterInfo4); + interpreterInfos.add(interpreterInfo5); + 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(); + interpreterSetting.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"); - } + @Test + public void testSharedMode() throws InterpreterException, IOException { + 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; + + 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); + 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(); + } - 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); + 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 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()); + public void testScopedMode() throws InterpreterException, IOException { + 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; + + 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); + 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 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)); - + public void testIsolatedMode() throws InterpreterException, IOException { + 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; + + 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); + 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()); - 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()); + public void testExecuteIncorrectPrecode() throws TTransportException, IOException, InterpreterException { + interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); + 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(), new GUI(), + null, null, new ArrayList(), null); + assertEquals(Code.ERROR, interpreter1.interpret("10", context1).code()); } @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()); + public void testExecuteCorrectPrecode() throws TTransportException, IOException, InterpreterException { + interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); + 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(), new GUI(), + null, null, new ArrayList(), null); + assertEquals(Code.SUCCESS, interpreter1.interpret("10", context1).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(); + public void testRemoteInterperterErrorStatus() throws TTransportException, IOException, InterpreterException { + 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(), new GUI(), + null, null, new ArrayList(), null); + assertEquals(Code.ERROR, remoteInterpreter1.interpret("hello", context1).code()); } @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)); - } - + public void testFIFOScheduler() throws InterruptedException, InterpreterException { + 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(), 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 - protected boolean jobAbort() { - return false; + public void run() { + try { + assertEquals(Code.SUCCESS, interpreter1.interpret("100", context1).code()); + } catch (InterpreterException e) { + e.printStackTrace(); + fail(); + } } - }; - 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; - } - + Thread thread2 = new Thread() { @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; + public void run() { + try { + assertEquals(Code.SUCCESS, interpreter1.interpret("100", context1).code()); + } catch (InterpreterException e) { + e.printStackTrace(); + fail(); } - - }); - } - - // 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); - } - } - + thread1.start(); + thread2.start(); + thread1.join(); + thread2.join(); 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()); + assertTrue((end - start) >= 200); } @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; - } - + public void testParallelScheduler() throws InterruptedException, InterpreterException { + 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(), 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 - 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)); + public void run() { + try { + assertEquals(Code.SUCCESS, interpreter1.interpret("100", context1).code()); + } catch (InterpreterException e) { + e.printStackTrace(); + fail(); + } } - + }; + Thread thread2 = new Thread() { @Override - protected boolean jobAbort() { - return false; + public void run() { + try { + assertEquals(Code.SUCCESS, interpreter1.interpret("100", context1).code()); + } catch (InterpreterException e) { + e.printStackTrace(); + fail(); + } } - }; - 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()); - + long start = System.currentTimeMillis(); + thread1.start(); + thread2.start(); + thread1.join(); + thread2.join(); + long end = System.currentTimeMillis(); + assertTrue((end - start) <= 200); } @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()); + interpreterSetting.getOption().setPerUser(InterpreterOption.SHARED); + Interpreter interpreter1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); + Interpreter interpreter2 = interpreterSetting.getInterpreter("user1", "note1", "echo"); + assertEquals(interpreter1.getInterpreterGroup(), interpreter2.getInterpreterGroup()); + assertEquals(interpreter1.getScheduler(), interpreter2.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()); + interpreterSetting.getOption().setPerUser(InterpreterOption.SCOPED); + Interpreter interpreter1_user1 = interpreterSetting.getInterpreter("user1", "note1", "sleep"); + Interpreter interpreter2_user1 = interpreterSetting.getInterpreter("user1", "note1", "echo"); + assertEquals(interpreter1_user1.getInterpreterGroup(), interpreter2_user1.getInterpreterGroup()); + assertEquals(interpreter1_user1.getScheduler(), interpreter2_user1.getScheduler()); + + Interpreter interpreter1_user2 = interpreterSetting.getInterpreter("user2", "note1", "sleep"); + Interpreter interpreter2_user2 = interpreterSetting.getInterpreter("user2", "note1", "echo"); + assertEquals(interpreter1_user2.getInterpreterGroup(), interpreter2_user2.getInterpreterGroup()); + assertEquals(interpreter1_user2.getScheduler(), interpreter2_user2.getScheduler()); + + // scheduler is shared in session but not across session + assertNotEquals(interpreter1_user1.getScheduler(), interpreter1_user2.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); + final AngularObjectRegistry registry = new AngularObjectRegistry("spark", null); + registry.add("name_1", "value_1", "note_1", "paragraphId_1"); + registry.add("name_2", "value_2", "node_2", "paragraphId_2"); + Interpreter interpreter = interpreterSetting.getInterpreter("user1", "note1", "angular_obj"); + interpreter.getInterpreterGroup().setAngularObjectRegistry(registry); - //When - intr.pushAngularObjectRegistryToRemote(client); + final InterpreterContext context = new InterpreterContext("noteId", "paragraphId", "repl", + "title", "text", AuthenticationInfo.ANONYMOUS, new HashMap(), new GUI(), new GUI(), + null, null, new ArrayList(), null); - //Then - Mockito.verify(client).angularRegistryPush(expected); + InterpreterResult result = interpreter.interpret("dummy", context); + assertEquals(Code.SUCCESS, result.code()); + assertEquals("2", result.message().get(0).getData()); } @Test @@ -864,112 +404,44 @@ public void testEnvStringPattern() { } @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(); + public void testEnvironmentAndProperty() throws InterpreterException { + 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(), 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()); } @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); - } + public void testConvertDynamicForms() throws InterpreterException { + GUI gui = new GUI(); + OptionInput.ParamOption[] paramOptions = { + new OptionInput.ParamOption("value1", "param1"), + new OptionInput.ParamOption("value2", "param2") }; - 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)); + List defaultValues = new ArrayList(); + defaultValues.add("default1"); + defaultValues.add("default2"); + gui.checkbox("checkbox_id", defaultValues, paramOptions); + gui.select("select_id", "default", paramOptions); + gui.textbox("textbox_id"); + 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, new GUI(), + null, null, new ArrayList(), null); + + interpreter.interpret("text", context); + assertArrayEquals(expected.values().toArray(), gui.getForms().values().toArray()); } } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/GetAngularObjectSizeInterpreter.java similarity index 65% rename from zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/GetAngularObjectSizeInterpreter.java index 1b1306a78f0..6d6495f8b39 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/mock/MockInterpreter1.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/GetAngularObjectSizeInterpreter.java @@ -14,62 +14,50 @@ * 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; +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.thrift.InterpreterCompletion; -import org.apache.zeppelin.scheduler.Scheduler; -import org.apache.zeppelin.scheduler.SchedulerFactory; -public class MockInterpreter1 extends Interpreter{ - Map vars = new HashMap<>(); +import java.util.Properties; + +public class GetAngularObjectSizeInterpreter extends Interpreter { - public MockInterpreter1(Properties property) { + public GetAngularObjectSizeInterpreter(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, + "" + context.getAngularObjectRegistry().getRegistry().size()); } @Override public void cancel(InterpreterContext context) { + } @Override public FormType getFormType() { - return FormType.SIMPLE; + return FormType.NATIVE; } @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-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterEnv.java b/zeppelin-zengine/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-zengine/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-zengine/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-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterA.java index 50d988875bb..dbd2df77f51 100644 --- a/zeppelin-zengine/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,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; @@ -52,8 +51,9 @@ public String getLastStatement() { } @Override - public InterpreterResult interpret(String st, InterpreterContext context) { - if (property.containsKey("progress")) { + public InterpreterResult interpret(String st, InterpreterContext context) + throws InterpreterException { + if (getProperties().containsKey("progress")) { context.setProgress(Integer.parseInt(getProperty("progress"))); } try { diff --git a/zeppelin-zengine/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 index d4b26ad186d..ec89241136e 100644 --- a/zeppelin-zengine/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,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-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java deleted file mode 100644 index 7103335ac33..00000000000 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/interpreter/remote/mock/MockInterpreterB.java +++ /dev/null @@ -1,126 +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.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.InterpreterResult.Code; -import org.apache.zeppelin.interpreter.WrappedInterpreter; -import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; -import org.apache.zeppelin.scheduler.Scheduler; - -public class MockInterpreterB extends Interpreter { - - public MockInterpreterB(Properties property) { - super(property); - } - - @Override - public void open() { - //new RuntimeException().printStackTrace(); - } - - @Override - public void close() { - } - - @Override - public InterpreterResult interpret(String st, InterpreterContext context) { - MockInterpreterA intpA = getInterpreterA(); - String intpASt = intpA.getLastStatement(); - long timeToSleep = Long.parseLong(st); - if (intpASt != null) { - timeToSleep += Long.parseLong(intpASt); - } - try { - Thread.sleep(timeToSleep); - } catch (NumberFormatException | InterruptedException e) { - throw new InterpreterException(e); - } - return new InterpreterResult(Code.SUCCESS, Long.toString(timeToSleep)); - } - - @Override - public void cancel(InterpreterContext context) { - - } - - @Override - public FormType getFormType() { - return FormType.NATIVE; - } - - @Override - public int getProgress(InterpreterContext context) { - return 0; - } - - @Override - public List completion(String buf, int cursor, - InterpreterContext interpreterContext) { - return null; - } - - public MockInterpreterA getInterpreterA() { - InterpreterGroup interpreterGroup = getInterpreterGroup(); - synchronized (interpreterGroup) { - for (List interpreters : interpreterGroup.values()) { - boolean belongsToSameNoteGroup = false; - MockInterpreterA a = null; - for (Interpreter intp : interpreters) { - if (intp.getClassName().equals(MockInterpreterA.class.getName())) { - Interpreter p = intp; - while (p instanceof WrappedInterpreter) { - p = ((WrappedInterpreter) p).getInnerInterpreter(); - } - a = (MockInterpreterA) p; - } - - Interpreter p = intp; - while (p instanceof WrappedInterpreter) { - p = ((WrappedInterpreter) p).getInnerInterpreter(); - } - if (this == p) { - belongsToSameNoteGroup = true; - } - } - if (belongsToSameNoteGroup) { - return a; - } - } - } - return null; - } - - @Override - public Scheduler getScheduler() { - MockInterpreterA intpA = getInterpreterA(); - if (intpA != null) { - return intpA.getScheduler(); - } - return null; - } - -} diff --git a/zeppelin-zengine/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 index 349315c8ed1..7a5321ae3a4 100644 --- a/zeppelin-zengine/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,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; @@ -49,7 +52,8 @@ public String getLastStatement() { } @Override - public InterpreterResult interpret(String st, InterpreterContext context) { + public InterpreterResult interpret(String st, InterpreterContext context) + throws InterpreterException { String[] ret = st.split(":"); try { if (ret[1] != null) { diff --git a/zeppelin-zengine/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 index c4ff6abf6f4..ee9f15cb787 100644 --- a/zeppelin-zengine/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,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/notebook/CredentialInjectorTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/CredentialInjectorTest.java new file mode 100644 index 00000000000..9b0c93aa933 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/CredentialInjectorTest.java @@ -0,0 +1,86 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResult.Code; +import org.apache.zeppelin.user.UserCredentials; +import org.apache.zeppelin.user.UsernamePassword; +import org.junit.Test; + +public class CredentialInjectorTest { + + private static final String TEMPLATE = + "val jdbcUrl = \"jdbc:mysql://localhost/emp?user={user.mysql}&password={password.mysql}\""; + private static final String CORRECT_REPLACED = + "val jdbcUrl = \"jdbc:mysql://localhost/emp?user=username&password=pwd\""; + + private static final String ANSWER = + "jdbcUrl: String = jdbc:mysql://localhost/employees?user=username&password=pwd"; + private static final String HIDDEN = + "jdbcUrl: String = jdbc:mysql://localhost/employees?user=username&password=###"; + + @Test + public void replaceCredentials() { + UserCredentials userCredentials = mock(UserCredentials.class); + UsernamePassword usernamePassword = new UsernamePassword("username", "pwd"); + when(userCredentials.getUsernamePassword("mysql")).thenReturn(usernamePassword); + CredentialInjector testee = new CredentialInjector(userCredentials); + String actual = testee.replaceCredentials(TEMPLATE); + assertEquals(CORRECT_REPLACED, actual); + + InterpreterResult ret = new InterpreterResult(Code.SUCCESS, ANSWER); + InterpreterResult hiddenResult = testee.hidePasswords(ret); + assertEquals(1, hiddenResult.message().size()); + assertEquals(HIDDEN, hiddenResult.message().get(0).getData()); + } + + @Test + public void replaceCredentialNoTexts() { + UserCredentials userCredentials = mock(UserCredentials.class); + CredentialInjector testee = new CredentialInjector(userCredentials); + String actual = testee.replaceCredentials(null); + assertNull(actual); + } + + @Test + public void replaceCredentialsNotExisting() { + UserCredentials userCredentials = mock(UserCredentials.class); + CredentialInjector testee = new CredentialInjector(userCredentials); + String actual = testee.replaceCredentials(TEMPLATE); + assertEquals(TEMPLATE, actual); + + InterpreterResult ret = new InterpreterResult(Code.SUCCESS, ANSWER); + InterpreterResult hiddenResult = testee.hidePasswords(ret); + assertEquals(1, hiddenResult.message().size()); + assertEquals(ANSWER, hiddenResult.message().get(0).getData()); + } + + @Test + public void hidePasswordsNoResult() { + UserCredentials userCredentials = mock(UserCredentials.class); + CredentialInjector testee = new CredentialInjector(userCredentials); + assertNull(testee.hidePasswords(null)); + } + +} 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/NoteTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteTest.java index 69c1f8627fe..824fe10d653 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteTest.java @@ -23,6 +23,7 @@ import org.apache.zeppelin.display.ui.TextBox; import org.apache.zeppelin.interpreter.Interpreter; import org.apache.zeppelin.interpreter.InterpreterFactory; +import org.apache.zeppelin.interpreter.InterpreterNotFoundException; import org.apache.zeppelin.interpreter.InterpreterResult; import org.apache.zeppelin.interpreter.InterpreterSettingManager; import org.apache.zeppelin.notebook.repo.NotebookRepo; @@ -73,7 +74,7 @@ public class NoteTest { private AuthenticationInfo anonymous = new AuthenticationInfo("anonymous"); @Test - public void runNormalTest() { + public void runNormalTest() throws InterpreterNotFoundException { when(interpreterFactory.getInterpreter(anyString(), anyString(), eq("spark"))).thenReturn(interpreter); when(interpreter.getScheduler()).thenReturn(scheduler); @@ -87,7 +88,7 @@ public void runNormalTest() { ArgumentCaptor pCaptor = ArgumentCaptor.forClass(Paragraph.class); verify(scheduler, only()).submit(pCaptor.capture()); - verify(interpreterFactory, times(2)).getInterpreter(anyString(), anyString(), eq("spark")); + verify(interpreterFactory, times(1)).getInterpreter(anyString(), anyString(), eq("spark")); assertEquals("Paragraph text", pText, pCaptor.getValue().getText()); } @@ -101,7 +102,7 @@ public void addParagraphWithEmptyReplNameTest() { } @Test - public void addParagraphWithLastReplNameTest() { + public void addParagraphWithLastReplNameTest() throws InterpreterNotFoundException { when(interpreterFactory.getInterpreter(anyString(), anyString(), eq("spark"))).thenReturn(interpreter); Note note = new Note(repo, interpreterFactory, interpreterSettingManager, jobListenerFactory, index, credentials, noteEventListener); @@ -113,7 +114,7 @@ public void addParagraphWithLastReplNameTest() { } @Test - public void insertParagraphWithLastReplNameTest() { + public void insertParagraphWithLastReplNameTest() throws InterpreterNotFoundException { when(interpreterFactory.getInterpreter(anyString(), anyString(), eq("spark"))).thenReturn(interpreter); Note note = new Note(repo, interpreterFactory, interpreterSettingManager, jobListenerFactory, index, credentials, noteEventListener); @@ -125,7 +126,7 @@ public void insertParagraphWithLastReplNameTest() { } @Test - public void insertParagraphWithInvalidReplNameTest() { + public void insertParagraphWithInvalidReplNameTest() throws InterpreterNotFoundException { when(interpreterFactory.getInterpreter(anyString(), anyString(), eq("invalid"))).thenReturn(null); Note note = new Note(repo, interpreterFactory, interpreterSettingManager, jobListenerFactory, index, credentials, noteEventListener); @@ -144,7 +145,7 @@ public void insertParagraphwithUser() { } @Test - public void clearAllParagraphOutputTest() { + public void clearAllParagraphOutputTest() throws InterpreterNotFoundException { when(interpreterFactory.getInterpreter(anyString(), anyString(), eq("md"))).thenReturn(interpreter); when(interpreter.getScheduler()).thenReturn(scheduler); @@ -247,7 +248,6 @@ public void testNoteJson() { note.getInfo().put("info_1", "value_1"); String pText = "%spark sc.version"; Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - p.dateUpdated = new Date(); p.setText(pText); p.setResult("1.6.2"); p.settings.getForms().put("textbox_1", new TextBox("name", "default_name")); 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..c1f8993d10b 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,26 @@ 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.mock.MockInterpreter1; -import org.apache.zeppelin.interpreter.mock.MockInterpreter2; +import org.apache.zeppelin.interpreter.AbstractInterpreterTest; +import org.apache.zeppelin.interpreter.InterpreterException; +import org.apache.zeppelin.interpreter.InterpreterFactory; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterNotFoundException; +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.notebook.repo.FileSystemNotebookRepo; +import org.apache.zeppelin.notebook.repo.GitHubNotebookRepo; +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; @@ -51,23 +46,42 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.quartz.JobKey; import org.quartz.SchedulerException; +import org.quartz.impl.matchers.GroupMatcher; import org.slf4j.Logger; 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,64 +89,63 @@ 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"); + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_ENABLE.getVarName(), "true"); + 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()); + credentials = new Credentials(conf.credentialsPersist(), conf.getCredentialsPath(), null); - 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 testRevisionSupported() throws IOException, SchedulerException { + NotebookRepo notebookRepo; + Notebook notebook; + + notebookRepo = new VFSNotebookRepo(conf); + notebook = new Notebook(conf, notebookRepo, schedulerFactory, interpreterFactory, + interpreterSettingManager, this, null, + notebookAuthorization, credentials); + assertFalse("Revision is not supported in VFSNotebookRepo", notebook.isRevisionSupported()); + + notebookRepo = new GitHubNotebookRepo(conf); + notebook = new Notebook(conf, notebookRepo, schedulerFactory, interpreterFactory, + interpreterSettingManager, this, null, + notebookAuthorization, credentials); + assertTrue("Revision is supported in GitHubNotebookRepo", notebook.isRevisionSupported()); + + notebookRepo = new FileSystemNotebookRepo(conf); + notebook = new Notebook(conf, notebookRepo, schedulerFactory, interpreterFactory, + interpreterSettingManager, this, null, + notebookAuthorization, credentials); + assertFalse("Revision is not supported in FileSystemNotebookRepo", + notebook.isRevisionSupported()); } @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); Map config = p1.getConfig(); config.put("enabled", true); p1.setConfig(config); - p1.setText("hello world"); + p1.setText("%mock1 hello world"); p1.setAuthenticationInfo(anonymous); note.run(p1.getId()); while(p1.isTerminated()==false || p1.getResult()==null) Thread.yield(); @@ -162,7 +175,12 @@ public void testReloadAndSetInterpreter() throws IOException { // then interpreter factory should be injected into all the paragraphs Note note = notebook.getAllNotes().get(0); - assertNull(note.getParagraphs().get(0).getRepl(null)); + try { + note.getParagraphs().get(0).getBindedInterpreter(); + fail("Should throw InterpreterNotFoundException"); + } catch (InterpreterNotFoundException e) { + + } } @Test @@ -202,8 +220,8 @@ public void testReloadAllNotes() throws IOException { copiedNote.getParagraphs().get(0).getText()); assertEquals(notes.get(1).getParagraphs().get(0).settings, copiedNote.getParagraphs().get(0).settings); - assertEquals(notes.get(1).getParagraphs().get(0).title, - copiedNote.getParagraphs().get(0).title); + assertEquals(notes.get(1).getParagraphs().get(0).getTitle(), + copiedNote.getParagraphs().get(0).getTitle()); // delete the notebook for (String note : noteNames) { @@ -244,7 +262,7 @@ public void testLoadAllNotes() { fail("Subject is non-emtpy anonymous, shouldn't fail"); } } - + @Test public void testPersist() throws IOException, SchedulerException, RepositoryException { Note note = notebook.createNote(anonymous); @@ -259,7 +277,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()); @@ -286,7 +304,7 @@ public void testClearParagraphOutput() throws IOException, SchedulerException{ Map config = p1.getConfig(); config.put("enabled", true); p1.setConfig(config); - p1.setText("hello world"); + p1.setText("%mock1 hello world"); p1.setAuthenticationInfo(anonymous); note.run(p1.getId()); @@ -316,34 +334,29 @@ 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); Map config1 = p1.getConfig(); config1.put("enabled", true); p1.setConfig(config1); - p1.setText("p1"); + p1.setText("%mock1 p1"); // p2 Paragraph p2 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); Map config2 = p2.getConfig(); config2.put("enabled", false); p2.setConfig(config2); - p2.setText("p2"); + p2.setText("%mock1 p2"); // p3 Paragraph p3 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); - p3.setText("p3"); + p3.setText("%mock1 p3"); // when note.runAll(); - // wait for finish - while(p3.isTerminated() == false || p3.getResult() == null) { - Thread.yield(); - } - assertEquals("repl1: p1", p1.getResult().message().get(0).getData()); assertNull(p2.getResult()); assertEquals("repl1: p3", p3.getResult().message().get(0).getData()); @@ -355,7 +368,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<>(); @@ -370,20 +383,60 @@ public void testSchedule() throws InterruptedException, IOException { config.put("cron", "* * * * * ?"); note.setConfig(config); notebook.refreshCron(note.getId()); - Thread.sleep(1 * 1000); + Thread.sleep(2 * 1000); // remove cron scheduler. config.put("cron", null); note.setConfig(config); notebook.refreshCron(note.getId()); - Thread.sleep(1000); + Thread.sleep(2 * 1000); dateFinished = p.getDateFinished(); assertNotNull(dateFinished); - Thread.sleep(1 * 1000); + Thread.sleep(2 * 1000); assertEquals(dateFinished, p.getDateFinished()); 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; @@ -417,23 +470,103 @@ private void executeNewParagraphByCron(Note note, String cron) { notebook.refreshCron(note.getId()); } + @Test + public void testScheduleDisabled() throws InterruptedException, IOException { + + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_ENABLE.getVarName(), "false"); + try { + final int timeout = 10; + final String everySecondCron = "* * * * * ?"; + final CountDownLatch jobsToExecuteCount = new CountDownLatch(5); + final Note note = notebook.createNote(anonymous); + + executeNewParagraphByCron(note, everySecondCron); + afterStatusChangedListener = new StatusChangedListener() { + @Override + public void onStatusChanged(Job job, Status before, Status after) { + if (after == Status.FINISHED) { + jobsToExecuteCount.countDown(); + } + } + }; + + //This job should not run because "ZEPPELIN_NOTEBOOK_CRON_ENABLE" is set to false + assertFalse(jobsToExecuteCount.await(timeout, TimeUnit.SECONDS)); + + terminateScheduledNote(note); + afterStatusChangedListener = null; + } finally { + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_ENABLE.getVarName(), "true"); + } + } + + @Test + public void testScheduleDisabledWithName() throws InterruptedException, IOException { + + System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_FOLDERS.getVarName(), "System/*"); + try { + final int timeout = 10; + final String everySecondCron = "* * * * * ?"; + final CountDownLatch jobsToExecuteCount = new CountDownLatch(5); + final Note note = notebook.createNote(anonymous); + + executeNewParagraphByCron(note, everySecondCron); + afterStatusChangedListener = new StatusChangedListener() { + @Override + public void onStatusChanged(Job job, Status before, Status after) { + if (after == Status.FINISHED) { + jobsToExecuteCount.countDown(); + } + } + }; + + //This job should not run because it's name does not matches "ZEPPELIN_NOTEBOOK_CRON_FOLDERS" + assertFalse(jobsToExecuteCount.await(timeout, TimeUnit.SECONDS)); + + terminateScheduledNote(note); + afterStatusChangedListener = null; + + final Note noteNameSystem = notebook.createNote(anonymous); + noteNameSystem.setName("System/test1"); + final CountDownLatch jobsToExecuteCountNameSystem = new CountDownLatch(5); + + executeNewParagraphByCron(noteNameSystem, everySecondCron); + afterStatusChangedListener = new StatusChangedListener() { + @Override + public void onStatusChanged(Job job, Status before, Status after) { + if (after == Status.FINISHED) { + jobsToExecuteCountNameSystem.countDown(); + } + } + }; + + //This job should run because it's name contains "System/" + assertTrue(jobsToExecuteCountNameSystem.await(timeout, TimeUnit.SECONDS)); + + terminateScheduledNote(noteNameSystem); + afterStatusChangedListener = null; + } finally { + System.clearProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_FOLDERS.getVarName()); + } + } + private void terminateScheduledNote(Note note) { note.getConfig().remove("cron"); notebook.refreshCron(note.getId()); notebook.removeNote(note.getId(), anonymous); } - - @Test - public void testAutoRestartInterpreterAfterSchedule() throws InterruptedException, IOException{ + + // @Test + public void testAutoRestartInterpreterAfterSchedule() throws InterruptedException, IOException, InterpreterNotFoundException { // 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); - p.setText("sleep 1000"); + p.setText("%mock1 sleep 1000"); Paragraph p2 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); p2.setConfig(config); @@ -448,21 +581,17 @@ public void testAutoRestartInterpreterAfterSchedule() throws InterruptedExceptio notebook.refreshCron(note.getId()); - MockInterpreter1 mock1 = ((MockInterpreter1) (((ClassloaderInterpreter) - ((LazyOpenInterpreter) factory.getInterpreter(anonymous.getUser(), note.getId(), "mock1")).getInnerInterpreter()) - .getInnerInterpreter())); + RemoteInterpreter mock1 = (RemoteInterpreter) interpreterFactory.getInterpreter(anonymous.getUser(), note.getId(), "mock1"); - MockInterpreter2 mock2 = ((MockInterpreter2) (((ClassloaderInterpreter) - ((LazyOpenInterpreter) factory.getInterpreter(anonymous.getUser(), note.getId(), "mock2")).getInnerInterpreter()) - .getInnerInterpreter())); + RemoteInterpreter mock2 = (RemoteInterpreter) interpreterFactory.getInterpreter(anonymous.getUser(), note.getId(), "mock2"); // wait until interpreters are started - while (!mock1.isOpen() || !mock2.isOpen()) { + while (!mock1.isOpened() || !mock2.isOpened()) { Thread.yield(); } // wait until interpreters are closed - while (mock1.isOpen() || mock2.isOpen()) { + while (mock1.isOpened() || mock2.isOpened()) { Thread.yield(); } @@ -477,20 +606,121 @@ public void testAutoRestartInterpreterAfterSchedule() throws InterruptedExceptio notebook.removeNote(note.getId(), anonymous); } + @Test + public void testCronWithReleaseResourceClosesOnlySpecificInterpreters() + throws IOException, InterruptedException, InterpreterNotFoundException { + // 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 testCronNoteInTrash() throws InterruptedException, IOException, SchedulerException { + Note note = notebook.createNote(anonymous); + note.setName("~Trash/NotCron"); + + Map config = note.getConfig(); + config.put("enabled", true); + config.put("cron", "* * * * * ?"); + note.setConfig(config); + + final int jobsBeforeRefresh = notebook.quartzSched.getJobKeys(GroupMatcher.anyGroup()).size(); + notebook.refreshCron(note.getId()); + final int jobsAfterRefresh = notebook.quartzSched.getJobKeys(GroupMatcher.anyGroup()).size(); + + assertEquals(jobsBeforeRefresh, jobsAfterRefresh); + + // remove cron scheduler. + config.remove("cron"); + notebook.refreshCron(note.getId()); + notebook.removeNote(note.getId(), anonymous); + } + @Test 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"; p.setText(simpleText); note.runAll(); - while (p.isTerminated() == false || p.getResult() == null) { - Thread.yield(); - } String exportedNoteJson = notebook.exportNote(note.getId()); @@ -500,7 +730,7 @@ public void testExportAndImportNote() throws IOException, CloneNotSupportedExcep // Test assertEquals(p.getId(), p2.getId()); - assertEquals(p.text, p2.text); + assertEquals(p.getText(), p2.getText()); assertEquals(p.getResult().message().get(0).getData(), p2.getResult().message().get(0).getData()); // Verify import note with subject @@ -520,12 +750,11 @@ 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"); note.runAll(); - while(p.isTerminated()==false || p.getResult()==null) Thread.yield(); p.setStatus(Status.RUNNING); Note cloneNote = notebook.cloneNote(note.getId(), "clone note", anonymous); @@ -534,7 +763,7 @@ public void testCloneNote() throws IOException, CloneNotSupportedException, // Keep same ParagraphId assertEquals(cp.getId(), p.getId()); - assertEquals(cp.text, p.text); + assertEquals(cp.getText(), p.getText()); assertEquals(cp.getResult().message().get(0).getData(), p.getResult().message().get(0).getData()); // Verify clone note with subject @@ -554,7 +783,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,14 +795,12 @@ 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"); note.runAll(); - while (p.isTerminated() == false || p.getResult() == null) { - Thread.yield(); - } + // Force paragraph to have String type object p.setResult("Exception"); @@ -582,7 +809,7 @@ public void testCloneNoteWithExceptionResult() throws IOException, CloneNotSuppo // Keep same ParagraphId assertEquals(cp.getId(), p.getId()); - assertEquals(cp.text, p.text); + assertEquals(cp.getText(), p.getText()); assertNull(cp.getResult()); notebook.removeNote(note.getId(), anonymous); notebook.removeNote(cloneNote.getId(), anonymous); @@ -591,28 +818,26 @@ 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"); + p1.setText("%mock1 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 +845,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 +878,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); @@ -684,13 +909,13 @@ public void testAngularObjectRemovalOnParagraphRemove() throws InterruptedExcept @Test public void testAngularObjectRemovalOnInterpreterRestart() throws InterruptedException, - IOException { + IOException, InterpreterException { // 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 +925,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); } @@ -721,6 +945,8 @@ public void testPermissions() throws IOException { new HashSet<>(Arrays.asList("user2"))), true); assertEquals(notebookAuthorization.isReader(note.getId(), new HashSet<>(Arrays.asList("user2"))), true); + assertEquals(notebookAuthorization.isRunner(note.getId(), + new HashSet<>(Arrays.asList("user2"))), true); assertEquals(notebookAuthorization.isWriter(note.getId(), new HashSet<>(Arrays.asList("user2"))), true); @@ -728,6 +954,8 @@ public void testPermissions() throws IOException { new HashSet<>(Arrays.asList("user1"))); notebookAuthorization.setReaders(note.getId(), new HashSet<>(Arrays.asList("user1", "user2"))); + notebookAuthorization.setRunners(note.getId(), + new HashSet<>(Arrays.asList("user3"))); notebookAuthorization.setWriters(note.getId(), new HashSet<>(Arrays.asList("user1"))); @@ -737,21 +965,26 @@ public void testPermissions() throws IOException { new HashSet<>(Arrays.asList("user1"))), true); assertEquals(notebookAuthorization.isReader(note.getId(), - new HashSet<>(Arrays.asList("user3"))), false); + new HashSet<>(Arrays.asList("user4"))), false); assertEquals(notebookAuthorization.isReader(note.getId(), new HashSet<>(Arrays.asList("user2"))), true); + assertEquals(notebookAuthorization.isRunner(note.getId(), + new HashSet<>(Arrays.asList("user3"))), true); + assertEquals(notebookAuthorization.isRunner(note.getId(), + new HashSet<>(Arrays.asList("user2"))), false); + assertEquals(notebookAuthorization.isWriter(note.getId(), new HashSet<>(Arrays.asList("user2"))), false); assertEquals(notebookAuthorization.isWriter(note.getId(), new HashSet<>(Arrays.asList("user1"))), true); - // Test clearing of permssions + // Test clearing of permissions notebookAuthorization.setReaders(note.getId(), Sets.newHashSet()); assertEquals(notebookAuthorization.isReader(note.getId(), new HashSet<>(Arrays.asList("user2"))), true); assertEquals(notebookAuthorization.isReader(note.getId(), - new HashSet<>(Arrays.asList("user3"))), true); + new HashSet<>(Arrays.asList("user4"))), true); notebook.removeNote(note.getId(), anonymous); } @@ -764,56 +997,60 @@ public void testAuthorizationRoles() throws IOException { // set admin roles for both user1 and user2 notebookAuthorization.setRoles(user1, roles); notebookAuthorization.setRoles(user2, roles); - + Note note = notebook.createNote(new AuthenticationInfo(user1)); - - // check that user1 is owner, reader and writer + + // check that user1 is owner, reader, runner and writer assertEquals(notebookAuthorization.isOwner(note.getId(), Sets.newHashSet(user1)), true); assertEquals(notebookAuthorization.isReader(note.getId(), Sets.newHashSet(user1)), true); + assertEquals(notebookAuthorization.isRunner(note.getId(), + Sets.newHashSet(user2)), true); assertEquals(notebookAuthorization.isWriter(note.getId(), Sets.newHashSet(user1)), true); - + // since user1 and user2 both have admin role, user2 will be reader and writer as well assertEquals(notebookAuthorization.isOwner(note.getId(), Sets.newHashSet(user2)), false); assertEquals(notebookAuthorization.isReader(note.getId(), Sets.newHashSet(user2)), true); + assertEquals(notebookAuthorization.isRunner(note.getId(), + Sets.newHashSet(user2)), true); assertEquals(notebookAuthorization.isWriter(note.getId(), Sets.newHashSet(user2)), true); - + // check that user1 has note listed in his workbench Set user1AndRoles = notebookAuthorization.getRoles(user1); user1AndRoles.add(user1); List user1Notes = notebook.getAllNotes(user1AndRoles); assertEquals(user1Notes.size(), 1); assertEquals(user1Notes.get(0).getId(), note.getId()); - - // check that user2 has note listed in his workbech because of admin role + + // check that user2 has note listed in his workbench because of admin role Set user2AndRoles = notebookAuthorization.getRoles(user2); user2AndRoles.add(user2); List user2Notes = notebook.getAllNotes(user2AndRoles); assertEquals(user2Notes.size(), 1); assertEquals(user2Notes.get(0).getId(), note.getId()); } - + @Test public void testAbortParagraphStatusOnInterpreterRestart() throws InterruptedException, - IOException { + IOException, InterpreterException { 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); - p1.setText("sleep 1000"); + p1.setText("%mock1 sleep 1000"); Paragraph p2 = note.addNewParagraph(anonymous); - p2.setText("sleep 1000"); + p2.setText("%mock1 sleep 1000"); Paragraph p3 = note.addNewParagraph(anonymous); - p3.setText("sleep 1000"); + p3.setText("%mock1 sleep 1000"); - note.runAll(); + note.runAll(AuthenticationInfo.ANONYMOUS, false); // wait until first paragraph finishes and second paragraph starts while (p1.getStatus() != Status.FINISHED || p2.getStatus() != Status.RUNNING) Thread.yield(); @@ -823,9 +1060,9 @@ public void testAbortParagraphStatusOnInterpreterRestart() throws InterruptedExc assertEquals(Status.PENDING, p3.getStatus()); // restart interpreter - interpreterSettingManager.restart(interpreterSettingManager.getInterpreterSettings(note.getId()).get(0).getId()); + interpreterSettingManager.restart(interpreterSettingManager.getInterpreterSettingByName("mock1").getId()); - // make sure three differnt status aborted well. + // make sure three different status aborted well. assertEquals(Status.FINISHED, p1.getStatus()); assertEquals(Status.ABORT, p2.getStatus()); assertEquals(Status.ABORT, p3.getStatus()); @@ -834,11 +1071,11 @@ public void testAbortParagraphStatusOnInterpreterRestart() throws InterruptedExc } @Test - public void testPerSessionInterpreterCloseOnNoteRemoval() throws IOException { + public void testPerSessionInterpreterCloseOnNoteRemoval() throws IOException, InterpreterException { // create a notes Note note1 = notebook.createNote(anonymous); Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); - p1.setText("getId"); + p1.setText("%mock1 getId"); p1.setAuthenticationInfo(anonymous); // restart interpreter with per user session enabled @@ -855,7 +1092,7 @@ public void testPerSessionInterpreterCloseOnNoteRemoval() throws IOException { notebook.removeNote(note1.getId(), anonymous); note1 = notebook.createNote(anonymous); p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); - p1.setText("getId"); + p1.setText("%mock1 getId"); p1.setAuthenticationInfo(anonymous); note1.run(p1.getId()); @@ -866,7 +1103,7 @@ public void testPerSessionInterpreterCloseOnNoteRemoval() throws IOException { } @Test - public void testPerSessionInterpreter() throws IOException { + public void testPerSessionInterpreter() throws IOException, InterpreterException { // create two notes Note note1 = notebook.createNote(anonymous); Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -874,9 +1111,9 @@ public void testPerSessionInterpreter() throws IOException { Note note2 = notebook.createNote(anonymous); Paragraph p2 = note2.addNewParagraph(AuthenticationInfo.ANONYMOUS); - p1.setText("getId"); + p1.setText("%mock1 getId"); p1.setAuthenticationInfo(anonymous); - p2.setText("getId"); + p2.setText("%mock1 getId"); p2.setAuthenticationInfo(anonymous); // run per note session disabled @@ -910,7 +1147,7 @@ public void testPerSessionInterpreter() throws IOException { @Test - public void testPerNoteSessionInterpreter() throws IOException { + public void testPerNoteSessionInterpreter() throws IOException, InterpreterException { // create two notes Note note1 = notebook.createNote(anonymous); Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); @@ -918,9 +1155,9 @@ public void testPerNoteSessionInterpreter() throws IOException { Note note2 = notebook.createNote(anonymous); Paragraph p2 = note2.addNewParagraph(AuthenticationInfo.ANONYMOUS); - p1.setText("getId"); + p1.setText("%mock1 getId"); p1.setAuthenticationInfo(anonymous); - p2.setText("getId"); + p2.setText("%mock1 getId"); p2.setAuthenticationInfo(anonymous); // shared mode. @@ -935,8 +1172,7 @@ public void testPerNoteSessionInterpreter() throws IOException { // restart interpreter with scoped mode enabled for (InterpreterSetting setting : notebook.getInterpreterSettingManager().getInterpreterSettings(note1.getId())) { setting.getOption().setPerNote(InterpreterOption.SCOPED); - notebook.getInterpreterSettingManager().restart(setting.getId(), note1.getId(), anonymous.getUser()); - notebook.getInterpreterSettingManager().restart(setting.getId(), note2.getId(), anonymous.getUser()); + notebook.getInterpreterSettingManager().restart(setting.getId()); } // run per note session enabled @@ -951,8 +1187,7 @@ public void testPerNoteSessionInterpreter() throws IOException { // restart interpreter with isolated mode enabled for (InterpreterSetting setting : notebook.getInterpreterSettingManager().getInterpreterSettings(note1.getId())) { setting.getOption().setPerNote(InterpreterOption.ISOLATED); - notebook.getInterpreterSettingManager().restart(setting.getId(), note1.getId(), anonymous.getUser()); - notebook.getInterpreterSettingManager().restart(setting.getId(), note2.getId(), anonymous.getUser()); + setting.getInterpreterSettingManager().restart(setting.getId()); } // run per note process enabled @@ -969,12 +1204,12 @@ public void testPerNoteSessionInterpreter() throws IOException { } @Test - public void testPerSessionInterpreterCloseOnUnbindInterpreterSetting() throws IOException { + public void testPerSessionInterpreterCloseOnUnbindInterpreterSetting() throws IOException, InterpreterException { // create a notes Note note1 = notebook.createNote(anonymous); Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); p1.setAuthenticationInfo(anonymous); - p1.setText("getId"); + p1.setText("%mock1 getId"); // restart interpreter with per note session enabled for (InterpreterSetting setting : interpreterSettingManager.getInterpreterSettings(note1.getId())) { @@ -1045,7 +1280,7 @@ public void onParagraphStatusChange(Paragraph p, Status status) { Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS); assertEquals(1, onParagraphCreate.get()); - note1.addCloneParagraph(p1); + note1.addCloneParagraph(p1, AuthenticationInfo.ANONYMOUS); assertEquals(2, onParagraphCreate.get()); note1.removeParagraph(anonymous.getUser(), p1.getId()); @@ -1094,6 +1329,7 @@ public void testGetAllNotes() throws Exception { notebook.getNotebookAuthorization().setOwners(note1.getId(), Sets.newHashSet("user1")); notebook.getNotebookAuthorization().setWriters(note1.getId(), Sets.newHashSet("user1")); + notebook.getNotebookAuthorization().setRunners(note1.getId(), Sets.newHashSet("user1")); notebook.getNotebookAuthorization().setReaders(note1.getId(), Sets.newHashSet("user1")); assertEquals(1, notebook.getAllNotes(Sets.newHashSet("anonymous")).size()); assertEquals(2, notebook.getAllNotes(Sets.newHashSet("user1")).size()); @@ -1101,6 +1337,7 @@ public void testGetAllNotes() throws Exception { notebook.getNotebookAuthorization().setOwners(note2.getId(), Sets.newHashSet("user2")); notebook.getNotebookAuthorization().setWriters(note2.getId(), Sets.newHashSet("user2")); notebook.getNotebookAuthorization().setReaders(note2.getId(), Sets.newHashSet("user2")); + notebook.getNotebookAuthorization().setRunners(note2.getId(), Sets.newHashSet("user2")); assertEquals(0, notebook.getAllNotes(Sets.newHashSet("anonymous")).size()); assertEquals(1, notebook.getAllNotes(Sets.newHashSet("user1")).size()); assertEquals(1, notebook.getAllNotes(Sets.newHashSet("user2")).size()); @@ -1126,14 +1363,20 @@ public void testGetAllNotesWithDifferentPermissions() throws IOException { notes2 = notebook.getAllNotes(user2); assertEquals(notes1.size(), 1); assertEquals(notes2.size(), 1); - + notebook.getNotebookAuthorization().setReaders(note.getId(), Sets.newHashSet("user1")); //note is public since writers empty notes1 = notebook.getAllNotes(user1); notes2 = notebook.getAllNotes(user2); assertEquals(notes1.size(), 1); assertEquals(notes2.size(), 1); - + + notebook.getNotebookAuthorization().setRunners(note.getId(), Sets.newHashSet("user1")); + notes1 = notebook.getAllNotes(user1); + notes2 = notebook.getAllNotes(user2); + assertEquals(notes1.size(), 1); + assertEquals(notes2.size(), 1); + notebook.getNotebookAuthorization().setWriters(note.getId(), Sets.newHashSet("user1")); notes1 = notebook.getAllNotes(user1); notes2 = notebook.getAllNotes(user2); @@ -1145,19 +1388,19 @@ public void testGetAllNotesWithDifferentPermissions() throws IOException { public void testPublicPrivateNewNote() throws IOException, SchedulerException { HashSet user1 = Sets.newHashSet("user1"); HashSet user2 = Sets.newHashSet("user2"); - + // case of public note - assertTrue(conf.isNotebokPublic()); + assertTrue(conf.isNotebookPublic()); assertTrue(notebookAuthorization.isPublic()); - + List notes1 = notebook.getAllNotes(user1); List notes2 = notebook.getAllNotes(user2); assertEquals(notes1.size(), 0); assertEquals(notes2.size(), 0); - + // user1 creates note Note notePublic = notebook.createNote(new AuthenticationInfo("user1")); - + // both users have note notes1 = notebook.getAllNotes(user1); notes2 = notebook.getAllNotes(user2); @@ -1165,25 +1408,26 @@ public void testPublicPrivateNewNote() throws IOException, SchedulerException { assertEquals(notes2.size(), 1); assertEquals(notes1.get(0).getId(), notePublic.getId()); assertEquals(notes2.get(0).getId(), notePublic.getId()); - + // user1 is only owner assertEquals(notebookAuthorization.getOwners(notePublic.getId()).size(), 1); assertEquals(notebookAuthorization.getReaders(notePublic.getId()).size(), 0); + assertEquals(notebookAuthorization.getRunners(notePublic.getId()).size(), 0); assertEquals(notebookAuthorization.getWriters(notePublic.getId()).size(), 0); // case of private note System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_PUBLIC.getVarName(), "false"); ZeppelinConfiguration conf2 = ZeppelinConfiguration.create(); - assertFalse(conf2.isNotebokPublic()); + assertFalse(conf2.isNotebookPublic()); // notebook authorization reads from conf, so no need to re-initilize assertFalse(notebookAuthorization.isPublic()); - + // check that still 1 note per user notes1 = notebook.getAllNotes(user1); notes2 = notebook.getAllNotes(user2); assertEquals(notes1.size(), 1); assertEquals(notes2.size(), 1); - + // create private note Note notePrivate = notebook.createNote(new AuthenticationInfo("user1")); @@ -1193,17 +1437,39 @@ public void testPublicPrivateNewNote() throws IOException, SchedulerException { assertEquals(notes1.size(), 2); assertEquals(notes2.size(), 1); assertEquals(true, notes1.contains(notePrivate)); - + // user1 have all rights assertEquals(notebookAuthorization.getOwners(notePrivate.getId()).size(), 1); assertEquals(notebookAuthorization.getReaders(notePrivate.getId()).size(), 1); + assertEquals(notebookAuthorization.getRunners(notePrivate.getId()).size(), 1); assertEquals(notebookAuthorization.getWriters(notePrivate.getId()).size(), 1); - + //set back public to true System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_PUBLIC.getVarName(), "true"); ZeppelinConfiguration.create(); } + @Test + public void testCloneImportCheck() throws IOException { + Note sourceNote = notebook.createNote(new AuthenticationInfo("user")); + sourceNote.setName("TestNote"); + + assertEquals("TestNote",sourceNote.getName()); + + Paragraph sourceParagraph = sourceNote.addNewParagraph(AuthenticationInfo.ANONYMOUS); + assertEquals("anonymous", sourceParagraph.getUser()); + + Note destNote = notebook.createNote(new AuthenticationInfo("user")); + destNote.setName("ClonedNote"); + assertEquals("ClonedNote",destNote.getName()); + + List paragraphs = sourceNote.getParagraphs(); + for (Paragraph p : paragraphs) { + destNote.addCloneParagraph(p, AuthenticationInfo.ANONYMOUS); + assertEquals("anonymous", p.getUser()); + } + } + private void delete(File file){ if(file.isFile()) file.delete(); else if(file.isDirectory()){ @@ -1256,4 +1522,5 @@ public void afterStatusChange(Job job, Status before, Status after) { private interface StatusChangedListener { void onStatusChanged(Job job, Status before, Status after); } + } 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..d2b38c8906b 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 @@ -22,7 +22,6 @@ 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; @@ -32,17 +31,21 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.google.common.collect.Lists; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; + +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; import org.apache.zeppelin.display.Input; +import org.apache.zeppelin.interpreter.AbstractInterpreterTest; +import org.apache.zeppelin.interpreter.Constants; 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; import org.apache.zeppelin.interpreter.InterpreterResult.Code; @@ -50,67 +53,115 @@ 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.interpreter.ManagedInterpreterGroup; 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.apache.zeppelin.user.UserCredentials; +import org.apache.zeppelin.user.UsernamePassword; import org.junit.Test; - -import java.util.HashMap; -import java.util.Map; -import org.mockito.ArgumentCaptor; import org.mockito.Mockito; -public class ParagraphTest { +import com.google.common.collect.Lists; + +public class ParagraphTest extends AbstractInterpreterTest { + @Test public void scriptBodyWithReplName() { - String text = "%spark(1234567"; - assertEquals("(1234567", Paragraph.getScriptBody(text)); - - text = "%table 1234567"; - assertEquals("1234567", Paragraph.getScriptBody(text)); + Note note = createNote(); + Paragraph paragraph = new Paragraph(note, null, interpreterFactory); + paragraph.setText("%test(1234567"); + assertEquals("test", paragraph.getIntpText()); + assertEquals("(1234567", paragraph.getScriptText()); + + paragraph.setText("%test 1234567"); + assertEquals("test", paragraph.getIntpText()); + assertEquals("1234567", paragraph.getScriptText()); } @Test public void scriptBodyWithoutReplName() { - String text = "12345678"; - assertEquals(text, Paragraph.getScriptBody(text)); + Note note = createNote(); + Paragraph paragraph = new Paragraph(note, null, interpreterFactory); + paragraph.setText("1234567"); + assertEquals("", paragraph.getIntpText()); + assertEquals("1234567", paragraph.getScriptText()); } @Test public void replNameAndNoBody() { - String text = "%md"; - assertEquals("md", Paragraph.getRequiredReplName(text)); - assertEquals("", Paragraph.getScriptBody(text)); + Note note = createNote(); + Paragraph paragraph = new Paragraph(note, null, interpreterFactory); + paragraph.setText("%test"); + assertEquals("test", paragraph.getIntpText()); + assertEquals("", paragraph.getScriptText()); } - + @Test public void replSingleCharName() { - String text = "%r a"; - assertEquals("r", Paragraph.getRequiredReplName(text)); - assertEquals("a", Paragraph.getScriptBody(text)); + Note note = createNote(); + Paragraph paragraph = new Paragraph(note, null, interpreterFactory); + paragraph.setText("%r a"); + assertEquals("r", paragraph.getIntpText()); + assertEquals("a", paragraph.getScriptText()); } @Test - public void replNameEndsWithWhitespace() { - String text = "%md\r\n###Hello"; - assertEquals("md", Paragraph.getRequiredReplName(text)); - - text = "%md\t###Hello"; - assertEquals("md", Paragraph.getRequiredReplName(text)); - - text = "%md\u000b###Hello"; - assertEquals("md", Paragraph.getRequiredReplName(text)); - - text = "%md\f###Hello"; - assertEquals("md", Paragraph.getRequiredReplName(text)); - - text = "%md\n###Hello"; - assertEquals("md", Paragraph.getRequiredReplName(text)); + public void replInvalid() { + Note note = createNote(); + Paragraph paragraph = new Paragraph(note, null, interpreterFactory); + paragraph.setText("foo %r"); + assertEquals("", paragraph.getIntpText()); + assertEquals("foo %r", paragraph.getScriptText()); + + paragraph.setText("foo%r"); + assertEquals("", paragraph.getIntpText()); + assertEquals("foo%r", paragraph.getScriptText()); + + paragraph.setText("% foo"); + assertEquals("", paragraph.getIntpText()); + assertEquals("% foo", paragraph.getScriptText()); + } - text = "%md ###Hello"; - assertEquals("md", Paragraph.getRequiredReplName(text)); + @Test + public void replNameEndsWithWhitespace() { + Note note = createNote(); + Paragraph paragraph = new Paragraph(note, null, interpreterFactory); + paragraph.setText("%test\r\n###Hello"); + assertEquals("test", paragraph.getIntpText()); + assertEquals("###Hello", paragraph.getScriptText()); + + paragraph.setText("%test\t###Hello"); + assertEquals("test", paragraph.getIntpText()); + assertEquals("###Hello", paragraph.getScriptText()); + + paragraph.setText("%test\u000b###Hello"); + assertEquals("test", paragraph.getIntpText()); + assertEquals("###Hello", paragraph.getScriptText()); + + paragraph.setText("%test\f###Hello"); + assertEquals("test", paragraph.getIntpText()); + assertEquals("###Hello", paragraph.getScriptText()); + + paragraph.setText("%test\n###Hello"); + assertEquals("test", paragraph.getIntpText()); + assertEquals("###Hello", paragraph.getScriptText()); + + paragraph.setText("%test ###Hello"); + assertEquals("test", paragraph.getIntpText()); + assertEquals("###Hello", paragraph.getScriptText()); + + paragraph.setText(" %test ###Hello"); + assertEquals("test", paragraph.getIntpText()); + assertEquals("###Hello", paragraph.getScriptText()); + + paragraph.setText("\n\r%test ###Hello"); + assertEquals("test", paragraph.getIntpText()); + assertEquals("###Hello", paragraph.getScriptText()); + + paragraph.setText("%\r\n###Hello"); + assertEquals("", paragraph.getIntpText()); + assertEquals("%\r\n###Hello", paragraph.getScriptText()); } @Test @@ -128,7 +179,7 @@ public void should_extract_variable_from_angular_object_registry() throws Except final String scriptBody = "My name is ${name} and I am ${age=20} years old. " + "My occupation is ${ job = engineer | developer | artists}"; - final Paragraph paragraph = new Paragraph(note, null, null, null); + final Paragraph paragraph = new Paragraph(note, null, null); final String paragraphId = paragraph.getId(); final AngularObject nameAO = AngularObjectBuilder.build("name", "DuyHai DOAN", noteId, @@ -154,7 +205,7 @@ public void should_extract_variable_from_angular_object_registry() throws Except @Test public void returnDefaultParagraphWithNewUser() { - Paragraph p = new Paragraph("para_1", null, null, null, null); + Paragraph p = new Paragraph("para_1", null, null, null); Object defaultValue = "Default Value"; p.setResult(defaultValue); Paragraph newUserParagraph = p.getUserParagraph("new_user"); @@ -164,18 +215,15 @@ public void returnDefaultParagraphWithNewUser() { @Test public void returnUnchangedResultsWithDifferentUser() throws Throwable { - InterpreterSettingManager mockInterpreterSettingManager = mock(InterpreterSettingManager.class); Note mockNote = mock(Note.class); when(mockNote.getCredentials()).thenReturn(mock(Credentials.class)); - Paragraph spyParagraph = spy(new Paragraph("para_1", mockNote, null, null, mockInterpreterSettingManager)); - - doReturn("spy").when(spyParagraph).getRequiredReplName(); - + Paragraph spyParagraph = spy(new Paragraph("para_1", mockNote, null, null)); Interpreter mockInterpreter = mock(Interpreter.class); - doReturn(mockInterpreter).when(spyParagraph).getRepl(anyString()); + spyParagraph.setInterpreter(mockInterpreter); + doReturn(mockInterpreter).when(spyParagraph).getBindedInterpreter(); - InterpreterGroup mockInterpreterGroup = mock(InterpreterGroup.class); + ManagedInterpreterGroup mockInterpreterGroup = mock(ManagedInterpreterGroup.class); when(mockInterpreter.getInterpreterGroup()).thenReturn(mockInterpreterGroup); when(mockInterpreterGroup.getId()).thenReturn("mock_id_1"); when(mockInterpreterGroup.getAngularObjectRegistry()).thenReturn(mock(AngularObjectRegistry.class)); @@ -188,12 +236,9 @@ 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); - - doReturn("spy script body").when(spyParagraph).getScriptBody(); when(mockInterpreter.getFormType()).thenReturn(FormType.NONE); @@ -228,8 +273,81 @@ 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()); + // 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) { + paragraph.setText(data.getLeft()); + Integer actual = paragraph.calculateCursorPosition(data.getLeft(), data.getMiddle()); + assertEquals(data.getRight(), actual); + } + } + + @Test + public void credentialReplacement() throws Throwable { + Note mockNote = mock(Note.class); + Credentials creds = mock(Credentials.class); + when(mockNote.getCredentials()).thenReturn(creds); + Paragraph spyParagraph = spy(new Paragraph("para_1", mockNote, null, null)); + UserCredentials uc = mock(UserCredentials.class); + when(creds.getUserCredentials(anyString())).thenReturn(uc); + UsernamePassword up = new UsernamePassword("user", "pwd"); + when(uc.getUsernamePassword("ent")).thenReturn(up ); + + Interpreter mockInterpreter = mock(Interpreter.class); + spyParagraph.setInterpreter(mockInterpreter); + doReturn(mockInterpreter).when(spyParagraph).getBindedInterpreter(); + + ManagedInterpreterGroup mockInterpreterGroup = mock(ManagedInterpreterGroup.class); + when(mockInterpreter.getInterpreterGroup()).thenReturn(mockInterpreterGroup); + when(mockInterpreterGroup.getId()).thenReturn("mock_id_1"); + when(mockInterpreterGroup.getAngularObjectRegistry()).thenReturn(mock(AngularObjectRegistry.class)); + when(mockInterpreterGroup.getResourcePool()).thenReturn(mock(ResourcePool.class)); + when(mockInterpreter.getFormType()).thenReturn(FormType.NONE); + ParagraphJobListener mockJobListener = mock(ParagraphJobListener.class); + doReturn(mockJobListener).when(spyParagraph).getListener(); + doNothing().when(mockJobListener).onOutputUpdateAll(Mockito.any(), Mockito.anyList()); + + InterpreterResult mockInterpreterResult = mock(InterpreterResult.class); + when(mockInterpreter.interpret(anyString(), Mockito.any())).thenReturn(mockInterpreterResult); + when(mockInterpreterResult.code()).thenReturn(Code.SUCCESS); + + AuthenticationInfo user1 = new AuthenticationInfo("user1"); + spyParagraph.setAuthenticationInfo(user1); + + spyParagraph.setText("val x = \"usr={user.ent}&pass={password.ent}\""); + + // Credentials should only be injected when it is enabled for an interpreter + when(mockInterpreter.getProperty(Constants.INJECT_CREDENTIALS, "false")).thenReturn("false"); + spyParagraph.jobRun(); + verify(mockInterpreter).interpret(eq("val x = \"usr={user.ent}&pass={password.ent}\""), any(InterpreterContext.class)); + when(mockInterpreter.getProperty(Constants.INJECT_CREDENTIALS, "false")).thenReturn("true"); + spyParagraph.jobRun(); + verify(mockInterpreter).interpret(eq("val x = \"usr=user&pass=pwd\""), any(InterpreterContext.class)); } } diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/FileSystemNotebookRepoTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/FileSystemNotebookRepoTest.java new file mode 100644 index 00000000000..79eb387706b --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/FileSystemNotebookRepoTest.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 FileSystemNotebookRepoTest { + + private ZeppelinConfiguration zConf; + private Configuration hadoopConf; + private FileSystem fs; + private FileSystemNotebookRepo hdfsNotebookRepo; + private String notebookDir; + private AuthenticationInfo authInfo = AuthenticationInfo.ANONYMOUS; + + @Before + public void setUp() throws IOException { + notebookDir = Files.createTempDirectory("FileSystemNotebookRepoTest").toFile().getAbsolutePath(); + zConf = new ZeppelinConfiguration(); + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), notebookDir); + hadoopConf = new Configuration(); + fs = FileSystem.get(hadoopConf); + hdfsNotebookRepo = new FileSystemNotebookRepo(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()); + } +} 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) {} + } +} 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..7aac2eeb99d --- /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 { + NotebookRepoWithVersionControl.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 + NotebookRepoWithVersionControl.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 + NotebookRepoWithVersionControl.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 954636cdbfc..0ce2a27acae 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 @@ -18,6 +18,7 @@ package org.apache.zeppelin.notebook.repo; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; import java.io.File; import java.io.IOException; @@ -28,10 +29,11 @@ import org.apache.commons.lang.StringUtils; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; +import org.apache.zeppelin.interpreter.InterpreterFactory; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.NoteInfo; import org.apache.zeppelin.notebook.Paragraph; -import org.apache.zeppelin.notebook.repo.NotebookRepo.Revision; +import org.apache.zeppelin.notebook.repo.NotebookRepoWithVersionControl.Revision; import org.apache.zeppelin.user.AuthenticationInfo; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; @@ -68,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) ); @@ -141,6 +153,7 @@ public void showNotebookHistoryMultipleNotesTest() throws IOException { //modify, save and checkpoint first note Note note = notebookRepo.get(TEST_NOTE_ID, null); + note.setInterpreterFactory(mock(InterpreterFactory.class)); Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); Map config = p.getConfig(); config.put("enabled", true); @@ -156,6 +169,7 @@ public void showNotebookHistoryMultipleNotesTest() throws IOException { //modify, save and checkpoint second note note = notebookRepo.get(TEST_NOTE_ID2, null); + note.setInterpreterFactory(mock(InterpreterFactory.class)); p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); config = p.getConfig(); config.put("enabled", false); @@ -182,6 +196,7 @@ public void addCheckpointTest() throws IOException { // add changes to note Note note = notebookRepo.get(TEST_NOTE_ID, null); + note.setInterpreterFactory(mock(InterpreterFactory.class)); Paragraph p = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); Map config = p.getConfig(); config.put("enabled", true); @@ -221,6 +236,7 @@ public void getRevisionTest() throws IOException { // add paragraph and save Note note = notebookRepo.get(TEST_NOTE_ID, null); + note.setInterpreterFactory(mock(InterpreterFactory.class)); Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS); Map config = p1.getConfig(); config.put("enabled", true); @@ -240,6 +256,7 @@ public void getRevisionTest() throws IOException { // get current note note = notebookRepo.get(TEST_NOTE_ID, null); + note.setInterpreterFactory(mock(InterpreterFactory.class)); assertThat(note.getParagraphs().size()).isEqualTo(paragraphCount_2); // add one more paragraph and save @@ -249,6 +266,7 @@ public void getRevisionTest() throws IOException { p2.setText("get revision when modified note test text"); notebookRepo.save(note, null); note = notebookRepo.get(TEST_NOTE_ID, null); + note.setInterpreterFactory(mock(InterpreterFactory.class)); int paragraphCount_3 = note.getParagraphs().size(); assertThat(paragraphCount_3).isEqualTo(paragraphCount_2 + 1); @@ -276,6 +294,7 @@ public void getRevisionFailTest() throws IOException { // get current note Note note = notebookRepo.get(TEST_NOTE_ID, null); + note.setInterpreterFactory(mock(InterpreterFactory.class)); assertThat(note.getParagraphs().size()).isEqualTo(paragraphCount_1); // add one more paragraph and save @@ -293,6 +312,7 @@ public void getRevisionFailTest() throws IOException { // get current note note = notebookRepo.get(TEST_NOTE_ID, null); + note.setInterpreterFactory(mock(InterpreterFactory.class)); assertThat(note.getParagraphs().size()).isEqualTo(paragraphCount_2); // test for absent revision @@ -311,6 +331,7 @@ public void setRevisionTest() throws IOException { // get current note Note note = notebookRepo.get(TEST_NOTE_ID, null); + note.setInterpreterFactory(mock(InterpreterFactory.class)); int paragraphCount_1 = note.getParagraphs().size(); LOG.info("initial paragraph count: {}", paragraphCount_1); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncInitializationTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncInitializationTest.java index 53c052a43d2..a91b19645e1 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncInitializationTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncInitializationTest.java @@ -142,7 +142,7 @@ public void initEmptyStorageTest() throws IOException { NotebookRepoSync notebookRepoSync = new NotebookRepoSync(conf); // check initialization of one default storage assertEquals(notebookRepoSync.getRepoCount(), 1); - assertTrue(notebookRepoSync.getRepo(0) instanceof VFSNotebookRepo); + assertTrue(notebookRepoSync.getRepo(0) instanceof NotebookRepoWithVersionControl); } @Test @@ -154,6 +154,6 @@ public void initOneDummyStorageTest() throws IOException { NotebookRepoSync notebookRepoSync = new NotebookRepoSync(conf); // check initialization of one default storage instead of invalid one assertEquals(notebookRepoSync.getRepoCount(), 1); - assertTrue(notebookRepoSync.getRepo(0) instanceof VFSNotebookRepo); + assertTrue(notebookRepoSync.getRepo(0) instanceof NotebookRepo); } } \ No newline at end of file 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..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 @@ -33,15 +33,18 @@ 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; 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; @@ -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(); @@ -87,20 +90,25 @@ 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(); - this.schedulerFactory = new SchedulerFactory(); + ConfigStorage.reset(); + + 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); - credentials = new Credentials(conf.credentialsPersist(), conf.getCredentialsPath()); + credentials = new Credentials(conf.credentialsPersist(), conf.getCredentialsPath(), null); notebookSync = new Notebook(conf, notebookRepoSync, schedulerFactory, factory, interpreterSettingManager, this, search, notebookAuthorization, credentials); anonymous = new AuthenticationInfo("anonymous"); @@ -110,19 +118,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 +138,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 +148,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 +175,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 +292,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(); @@ -330,32 +338,34 @@ public void testSyncWithAcl() throws IOException { assertEquals(true, authInfo.isOwner(note.getId(), entity)); assertEquals(1, authInfo.getOwners(note.getId()).size()); assertEquals(0, authInfo.getReaders(note.getId()).size()); + assertEquals(0, authInfo.getRunners(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()); assertEquals(true, authInfo.isOwner(note.getId(), entity)); assertEquals(1, authInfo.getOwners(note.getId()).size()); assertEquals(0, authInfo.getReaders(note.getId()).size()); + assertEquals(0, authInfo.getRunners(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); @@ -364,17 +374,20 @@ public void testSyncWithAcl() throws IOException { authInfo.removeNote(note.getId()); assertEquals(0, authInfo.getOwners(note.getId()).size()); assertEquals(0, authInfo.getReaders(note.getId()).size()); + assertEquals(0, authInfo.getRunners(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()); assertEquals(1, notebookRepoSync.list(1, null).size()); assertEquals(1, authInfo.getOwners(note.getId()).size()); assertEquals(1, authInfo.getReaders(note.getId()).size()); + assertEquals(1, authInfo.getRunners(note.getId()).size()); assertEquals(1, authInfo.getWriters(note.getId()).size()); assertEquals(true, authInfo.isOwner(note.getId(), entity)); assertEquals(true, authInfo.isReader(note.getId(), entity)); + assertEquals(true, authInfo.isRunner(note.getId(), entity)); assertEquals(true, authInfo.isWriter(note.getId(), entity)); } @@ -423,5 +436,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..d62c308ca5e 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,25 +22,14 @@ 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.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.mock.MockInterpreter1; + +import org.apache.zeppelin.interpreter.AbstractInterpreterTest; + import org.apache.zeppelin.notebook.JobListenerFactory; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.Notebook; @@ -58,60 +47,33 @@ 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()); - } + super.tearDown(); } @Test @@ -120,7 +82,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, "interpreter/test"); testNoteDir.mkdir(); FileUtils.writeStringToFile(new File(testNoteDir, "note.json"), ""); @@ -132,7 +94,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..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 @@ -17,35 +17,27 @@ package org.apache.zeppelin.resource; import com.google.gson.Gson; -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 java.io.File; import java.util.HashMap; import java.util.LinkedList; -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 { - 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 +47,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", @@ -109,6 +61,7 @@ public void setUp() throws Exception { new AuthenticationInfo(), new HashMap(), new GUI(), + new GUI(), null, null, new LinkedList(), @@ -117,30 +70,17 @@ 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 - public void testRemoteDistributedResourcePool() { + public void testRemoteDistributedResourcePool() throws InterpreterException { Gson gson = new Gson(); InterpreterResult ret; intp1.interpret("put key1 value1", context); @@ -223,7 +163,7 @@ public Resource invokeMethod(ResourceId id, String methodName, Class[] paramType } @Test - public void testResourcePoolUtils() { + public void testResourcePoolUtils() throws InterpreterException { Gson gson = new Gson(); InterpreterResult ret; @@ -235,13 +175,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 +191,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)); @@ -262,7 +202,7 @@ public void testResourcePoolUtils() { } @Test - public void testResourceInvokeMethod() { + public void testResourceInvokeMethod() throws InterpreterException { Gson gson = new Gson(); InterpreterResult ret; intp1.interpret("put key1 hey", context); 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 ebb51004285..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 @@ -17,83 +17,84 @@ 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.InterpreterException; +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(); + 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(), - 10); + Scheduler scheduler = intpA.getScheduler(); Job job = new Job("jobId", "jobName", null, 200) { Object results; + @Override public Object getReturn() { return results; @@ -120,7 +121,8 @@ protected Object jobRun() throws Throwable { new AuthenticationInfo(), new HashMap(), new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), + new GUI(), + null, new LocalResourcePool("pool1"), new LinkedList(), null)); return "1000"; @@ -145,7 +147,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 +167,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 = intpA.getScheduler(); Job job1 = new Job("jobId1", "jobName1", null, 200) { Object results; @@ -205,7 +183,8 @@ public void testAbortOnPending() throws Exception { new AuthenticationInfo(), new HashMap(), new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), + new GUI(), + null, new LocalResourcePool("pool1"), new LinkedList(), null); @@ -233,7 +212,11 @@ protected Object jobRun() throws Throwable { @Override protected boolean jobAbort() { if (isRunning()) { - intpA.cancel(context); + try { + intpA.cancel(context); + } catch (InterpreterException e) { + e.printStackTrace(); + } } return true; } @@ -255,7 +238,8 @@ public void setResult(Object results) { new AuthenticationInfo(), new HashMap(), new GUI(), - new AngularObjectRegistry(intpGroup.getId(), null), + new GUI(), + null, new LocalResourcePool("pool1"), new LinkedList(), null); @@ -283,7 +267,11 @@ protected Object jobRun() throws Throwable { @Override protected boolean jobAbort() { if (isRunning()) { - intpA.cancel(context); + try { + intpA.cancel(context); + } catch (InterpreterException e) { + e.printStackTrace(); + } } return true; } @@ -358,7 +346,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/java/org/apache/zeppelin/user/CredentialsTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/user/CredentialsTest.java similarity index 93% rename from zeppelin-interpreter/src/test/java/org/apache/zeppelin/user/CredentialsTest.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/user/CredentialsTest.java index 259516f9766..84a1244fc67 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/user/CredentialsTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/user/CredentialsTest.java @@ -17,17 +17,17 @@ package org.apache.zeppelin.user; -import static org.junit.Assert.*; - import org.junit.Test; import java.io.IOException; +import static org.junit.Assert.assertEquals; + public class CredentialsTest { @Test public void testDefaultProperty() throws IOException { - Credentials credentials = new Credentials(false, null); + Credentials credentials = new Credentials(false, null, null); UserCredentials userCredentials = new UserCredentials(); UsernamePassword up1 = new UsernamePassword("user2", "password"); userCredentials.putUsernamePassword("hive(vertica)", up1); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/user/EncryptorTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/user/EncryptorTest.java new file mode 100644 index 00000000000..9950be6a632 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/user/EncryptorTest.java @@ -0,0 +1,41 @@ +/* + * 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.user; + +import java.io.IOException; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +public class EncryptorTest { + + @Test + public void testEncryption() throws IOException { + Encryptor encryptor = new Encryptor("foobar1234567890"); + + String input = "test"; + + String encrypted = encryptor.encrypt(input); + assertNotEquals(input, encrypted); + + String decrypted = encryptor.decrypt(encrypted); + assertEquals(input, decrypted); + } +} 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..528921ca5b4 --- /dev/null +++ b/zeppelin-zengine/src/test/resources/conf/interpreter.json @@ -0,0 +1,150 @@ +{ + "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 + } + }, + + "2C4BJDRRZ" : { + "group": "mock1", + "name": "mock1", + "className": "org.apache.zeppelin.interpreter.mock.MockInterpreter1", + "properties": { + }, + "option": { + "remote": true, + "port": -1, + "perNote": "shared", + "perUser": "shared", + "isExistingProcess": false, + "setPermission": false, + "users": [], + "isUserImpersonate": false + } + }, + + "2C3PTPMUH" : { + "group": "mock2", + "name": "mock2", + "className": "org.apache.zeppelin.interpreter.mock.MockInterpreter2", + "properties": { + }, + "option": { + "remote": true, + "port": -1, + "perNote": "shared", + "perUser": "isolated", + "isExistingProcess": false, + "setPermission": false, + "users": [], + "isUserImpersonate": false + } + }, + + "2C5DCRVGM" : { + "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 + } + } + }, + "interpreterBindings": { + "2C6793KRV": [ + "2C3RWCVAG", + "2CKWE7B19" + ] + }, + "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-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..2f628dab8e9 --- /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": true, + "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..fae03f65004 --- /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": true, + "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/interpreter/test/interpreter-setting.json b/zeppelin-zengine/src/test/resources/interpreter/test/interpreter-setting.json new file mode 100644 index 00000000000..99e980bf650 --- /dev/null +++ b/zeppelin-zengine/src/test/resources/interpreter/test/interpreter-setting.json @@ -0,0 +1,64 @@ +[ + { + "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" + } + }, + "editor": { + "language": "java", + "editOnDblClick": false + } + }, + + { + "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" + } + }, + "editor": { + "language": "java", + "editOnDblClick": false + } + }, + + { + "group": "test", + "name": "sleep", + "defaultInterpreter": false, + "className": "org.apache.zeppelin.interpreter.SleepInterpreter", + "properties": { + + }, + "editor": { + "language": "java", + "editOnDblClick": false + } + } +] diff --git a/zeppelin-zengine/src/test/resources/log4j.properties b/zeppelin-zengine/src/test/resources/log4j.properties index 001a222535d..e778199f5c3 100644 --- a/zeppelin-zengine/src/test/resources/log4j.properties +++ b/zeppelin-zengine/src/test/resources/log4j.properties @@ -28,14 +28,11 @@ log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c:%L - %m%n # Root logger option log4j.rootLogger=INFO, stdout -log4j.logger.org.apache.zeppelin.notebook.repo=DEBUG - #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 @@ -45,4 +42,7 @@ log4j.logger.DataNucleus.Datastore=ERROR # Log all JDBC parameters log4j.logger.org.hibernate.type=ALL +log4j.logger.org.apache.hadoop=WARN +log4j.logger.org.apache.zeppelin.interpreter=DEBUG +log4j.logger.org.apache.zeppelin.python=DEBUG