diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index 73e878a58f5..425c0bef0d2 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -335,10 +335,26 @@ 2.15.2 + compile + + compile + + compile + + + test-compile testCompile + test-compile + + process-resources + + compile + + + diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/ExporterRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/ExporterRestApi.java new file mode 100644 index 00000000000..3f95125553d --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/ExporterRestApi.java @@ -0,0 +1,113 @@ +/* + * 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.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.notebook.Note; +import org.apache.zeppelin.notebook.Notebook; +import org.apache.zeppelin.notebook.Paragraph; +import org.apache.zeppelin.server.JsonResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.io.IOException; + +/** + * Rest api endpoint for the noteBook. + */ +@Path("/export") +@Produces("application/text") +public class ExporterRestApi { + private static final Logger LOG = LoggerFactory.getLogger(ExporterRestApi.class); + private Notebook notebook; + + public ExporterRestApi() {} + + public ExporterRestApi(Notebook notebook) { + this.notebook = notebook; + } + + /** + * Run paragraph job and return the results as a CSV file + * + * @return Text with status code + * @throws IOException, IllegalArgumentException + */ + @GET + @Produces("text/tab-separated-values") + @Path("job/runThenExportCSV/{notebookId}/paragraph/{paragraphId}-export.csv") + public Response runThenExportTSV(@PathParam("notebookId") String notebookId, + @PathParam("paragraphId") String paragraphId) throws + IOException, IllegalArgumentException { + LOG.info("running CSV export of {} {}", notebookId, paragraphId); + + Note note = notebook.getNote(notebookId); + if (note == null) { + return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); + } + + Paragraph paragraph = note.getParagraph(paragraphId); + if (paragraph == null) { + return new JsonResponse<>(Status.NOT_FOUND, "paragraph not found.").build(); + } + + LOG.info("running job."); + InterpreterResult result = note.runSynchronously(paragraph.getId()); + LOG.info("Length of result returned by query: {}", + result.message() == null ? "null result" : result.message().length()); + + return Response.ok(TsvToCSV.toCSV(result.message())).build(); + } + + /** + * Run paragraph job and return the results as a Tableau WDC document + * + * @return Text with status code + * @throws IOException, IllegalArgumentException + */ + @GET + @Produces("text/html") + @Path("job/runThenExportWDC/{notebookId}/paragraph/{paragraphId}-export.html") + public Response runThenExportWDC(@PathParam("notebookId") String notebookId, + @PathParam("paragraphId") String paragraphId) throws + IOException, IllegalArgumentException { + LOG.info("running WDC export of {} {}", notebookId, paragraphId); + + Note note = notebook.getNote(notebookId); + if (note == null) { + return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); + } + + Paragraph paragraph = note.getParagraph(paragraphId); + if (paragraph == null) { + return new JsonResponse<>(Status.NOT_FOUND, "paragraph not found.").build(); + } + + LOG.info("running job."); + InterpreterResult result = note.runSynchronously(paragraph.getId()); + LOG.info("Length of result returned by query: {}", + result.message() == null ? "null result" : result.message().length()); + + String exportName = paragraph.getTitle() != null ? paragraph.getTitle() : paragraph.getId(); + String wdcHtml = Tableau.buildWDCResult(result.message(), exportName); + return Response.ok(wdcHtml).build(); + } +} diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java index 40e4d14d1df..125490469b1 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 @@ -280,6 +280,9 @@ public Set getSingletons() { NotebookRestApi notebookApi = new NotebookRestApi(notebook, notebookWsServer, notebookIndex); singletons.add(notebookApi); + ExporterRestApi exporterApi = new ExporterRestApi(notebook); + singletons.add(exporterApi); + InterpreterRestApi interpreterApi = new InterpreterRestApi(replFactory); singletons.add(interpreterApi); diff --git a/zeppelin-server/src/main/scala/org/apache/zeppelin/rest/Tableau.scala b/zeppelin-server/src/main/scala/org/apache/zeppelin/rest/Tableau.scala new file mode 100644 index 00000000000..55dc6169784 --- /dev/null +++ b/zeppelin-server/src/main/scala/org/apache/zeppelin/rest/Tableau.scala @@ -0,0 +1,93 @@ +package org.apache.zeppelin.rest + +import org.apache.commons.lang3.StringEscapeUtils + +object Tableau { + /** + * Turns TSV data into a + * + * @param data + * @return + */ + def buildWDCResult(data: String, exportName: String): String ={ + val lines = data.split("\n") + val header = lines.head.split("\t") + + val (columnHeaderJS, columnTypesJS) = makeColumnHeaderJavascript(header) + val tableDataJS: String = makeDataTableJavascript(lines.drop(1), header) + + pageTemplate.replace("HEADER_NAMES", columnHeaderJS) + .replace("HEADER_TYPES", columnTypesJS) + .replace("DATA_RESULTS", tableDataJS) + .replace("EXPORT_NAME", exportName) + } + + private def makeColumnHeaderJavascript(header: Array[String]): (String, String) = { + val headerNames = "[" + header.map(header => s"'$header'").mkString(",") + "];\n" + + // For simplicity just assume everything is of type string; + // this can be overriden in the Tableau UI + val headerTypes = "[" + header.map(header => "'string'").mkString(",") + "];\n" + + (headerNames, headerTypes) + } + + /** + * Take a TSV / newline delimited string and return a bunch of: + * + * @param dataTable + * @param header + * @return + */ + private def makeDataTableJavascript(dataTable: Array[String], header: Array[String]): String = { + dataTable.map { row => + val json = + row.split("\t").zipWithIndex.map { case (columnValue, columnIndex) => + val headerName = header(columnIndex) + s"'$headerName': '${StringEscapeUtils.escapeEcmaScript(columnValue)}'" + }.mkString(",") + + s"dtr.push({$json});" + }.mkString("\n") + } + + private val pageTemplate: String = """ + + + + Stock Quote Connector-Tutorial + + + + + + + + """ +} diff --git a/zeppelin-server/src/main/scala/org/apache/zeppelin/rest/TsvToCSV.scala b/zeppelin-server/src/main/scala/org/apache/zeppelin/rest/TsvToCSV.scala new file mode 100644 index 00000000000..0290efd4c00 --- /dev/null +++ b/zeppelin-server/src/main/scala/org/apache/zeppelin/rest/TsvToCSV.scala @@ -0,0 +1,17 @@ +package org.apache.zeppelin.rest + +import org.apache.commons.lang3.StringEscapeUtils +import org.slf4j.{LoggerFactory, Logger} + +object TsvToCSV { + def toCSV(tsvData: String): String = { + val lines: Array[String] = tsvData.split("\n") + + lines.map { row => + row.split("\t").map { + StringEscapeUtils.escapeCsv + }.mkString(",") // Flatten column to comma separated string + + }.mkString("\n") // Flatten rows to newline separated string + } +} diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html b/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html index 2ab26b74d59..81f474d5e14 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html @@ -78,6 +78,12 @@
  • Link this paragraph
  • +
  • + CSV export +
  • +
  • + Tableau WDC export +
  • Clear output
  • diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index aeb942f5400..82eb342391a 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -2115,4 +2115,15 @@ angular.module('zeppelinWebApp') $scope.keepScrollDown = false; }; + $scope.goToCSVExportParagraph = function () { + var noteId = $route.current.pathParams.noteId; + var redirectToUrl = location.protocol + '//' + location.host + location.pathname + 'api/export/job/runThenExportCSV/' + noteId + '/paragraph/' + $scope.paragraph.id + '-export.csv'; + $window.open(redirectToUrl); + }; + + $scope.goToTableauWDCExportParagraph = function () { + var noteId = $route.current.pathParams.noteId; + var redirectToUrl = location.protocol + '//' + location.host + location.pathname + 'api/export/job/runThenExportWDC/' + noteId + '/paragraph/' + $scope.paragraph.id + '-export.html'; + $window.open(redirectToUrl); + }; }); 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 52e7ea3482d..6a85ad13efb 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 @@ -375,6 +375,30 @@ public void run(String paragraphId) { } } + /** + * Run a single paragraph and block for the results + * + * @param paragraphId + */ + public InterpreterResult runSynchronously(String paragraphId) { + Paragraph p = getParagraph(paragraphId); +// p.setNoteReplLoader(replLoader); +// p.setListener(jobListenerFactory.getParagraphJobListener(this)); + Interpreter intp = replLoader.get(p.getRequiredReplName()); + if (intp == null) { + throw new InterpreterException("Interpreter " + p.getRequiredReplName() + " not found"); + } + if (p.getConfig().get("enabled") == null || (Boolean) p.getConfig().get("enabled")) { + p.getConfig().put("OVERRIDE_MAX_RESULTS", "100000"); + logger.info("Config after adding OVERRIDE_MAX_RESULTS: ", p.getConfig()); + p.run(); + p.getConfig().remove("OVERRIDE_MAX_RESULTS"); + return p.getResult(); + } else { + return null; + } + } + public List completion(String paragraphId, String buffer, int cursor) { Paragraph p = getParagraph(paragraphId); p.setNoteReplLoader(replLoader);