/* * 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 java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import org.apache.commons.lang3.StringUtils; import org.apache.zeppelin.interpreter.InterpreterSetting; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.Notebook; import org.apache.zeppelin.notebook.Paragraph; import org.apache.zeppelin.rest.message.CronRequest; import org.apache.zeppelin.rest.message.InterpreterSettingListForNoteBind; import org.apache.zeppelin.rest.message.NewNotebookRequest; import org.apache.zeppelin.rest.message.NewParagraphRequest; import org.apache.zeppelin.rest.message.RunParagraphWithParametersRequest; import org.apache.zeppelin.search.SearchService; import org.apache.zeppelin.server.JsonResponse; import org.apache.zeppelin.socket.NotebookServer; import org.quartz.CronExpression; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; /** * Rest api endpoint for the noteBook. */ @Path("/notebook") @Produces("application/json") public class NotebookRestApi { private static final Logger LOG = LoggerFactory.getLogger(NotebookRestApi.class); Gson gson = new Gson(); private Notebook notebook; private NotebookServer notebookServer; private SearchService notebookIndex; public NotebookRestApi() {} public NotebookRestApi(Notebook notebook, NotebookServer notebookServer, SearchService search) { this.notebook = notebook; this.notebookServer = notebookServer; this.notebookIndex = search; } /** * bind a setting to note * @throws IOException */ @PUT @Path("interpreter/bind/{noteId}") public Response bind(@PathParam("noteId") String noteId, String req) throws IOException { List<String> settingIdList = gson.fromJson(req, new TypeToken<List<String>>(){}.getType()); notebook.bindInterpretersToNote(noteId, settingIdList); return new JsonResponse<>(Status.OK).build(); } /** * list binded setting */ @GET @Path("interpreter/bind/{noteId}") public Response bind(@PathParam("noteId") String noteId) { List<InterpreterSettingListForNoteBind> settingList = new LinkedList<InterpreterSettingListForNoteBind>(); List<InterpreterSetting> selectedSettings = notebook.getBindedInterpreterSettings(noteId); for (InterpreterSetting setting : selectedSettings) { settingList.add(new InterpreterSettingListForNoteBind( setting.id(), setting.getName(), setting.getGroup(), setting.getInterpreterGroup(), true) ); } List<InterpreterSetting> availableSettings = notebook.getInterpreterFactory().get(); for (InterpreterSetting setting : availableSettings) { boolean selected = false; for (InterpreterSetting selectedSetting : selectedSettings) { if (selectedSetting.id().equals(setting.id())) { selected = true; break; } } if (!selected) { settingList.add(new InterpreterSettingListForNoteBind( setting.id(), setting.getName(), setting.getGroup(), setting.getInterpreterGroup(), false) ); } } return new JsonResponse<>(Status.OK, "", settingList).build(); } @GET @Path("/") public Response getNotebookList() throws IOException { List<Map<String, String>> notesInfo = notebookServer.generateNotebooksInfo(false); return new JsonResponse<>(Status.OK, "", notesInfo ).build(); } @GET @Path("{notebookId}") public Response getNotebook(@PathParam("notebookId") String notebookId) throws IOException { Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } return new JsonResponse<>(Status.OK, "", note).build(); } /** * Create new note REST API * @param message - JSON with new note name * @return JSON with new note ID * @throws IOException */ @POST @Path("/") public Response createNote(String message) throws IOException { LOG.info("Create new notebook by JSON {}" , message); NewNotebookRequest request = gson.fromJson(message, NewNotebookRequest.class); Note note = notebook.createNote(); List<NewParagraphRequest> initialParagraphs = request.getParagraphs(); if (initialParagraphs != null) { for (NewParagraphRequest paragraphRequest : initialParagraphs) { Paragraph p = note.addParagraph(); p.setTitle(paragraphRequest.getTitle()); p.setText(paragraphRequest.getText()); } } note.addParagraph(); // add one paragraph to the last String noteName = request.getName(); if (noteName.isEmpty()) { noteName = "Note " + note.getId(); } note.setName(noteName); note.persist(); notebookServer.broadcastNote(note); notebookServer.broadcastNoteList(); return new JsonResponse<>(Status.CREATED, "", note.getId() ).build(); } /** * Delete note REST API * @param * @return JSON with status.OK * @throws IOException */ @DELETE @Path("{notebookId}") public Response deleteNote(@PathParam("notebookId") String notebookId) throws IOException { LOG.info("Delete notebook {} ", notebookId); if (!(notebookId.isEmpty())) { Note note = notebook.getNote(notebookId); if (note != null) { notebook.removeNote(notebookId); } } notebookServer.broadcastNoteList(); return new JsonResponse<>(Status.OK, "").build(); } /** * Clone note REST API * @param * @return JSON with status.CREATED * @throws IOException, CloneNotSupportedException, IllegalArgumentException */ @POST @Path("{notebookId}") public Response cloneNote(@PathParam("notebookId") String notebookId, String message) throws IOException, CloneNotSupportedException, IllegalArgumentException { LOG.info("clone notebook by JSON {}" , message); NewNotebookRequest request = gson.fromJson(message, NewNotebookRequest.class); String newNoteName = request.getName(); Note newNote = notebook.cloneNote(notebookId, newNoteName); notebookServer.broadcastNote(newNote); notebookServer.broadcastNoteList(); return new JsonResponse<>(Status.CREATED, "", newNote.getId()).build(); } /** * Insert paragraph REST API * @param message - JSON containing paragraph's information * @return JSON with status.OK * @throws IOException */ @POST @Path("{notebookId}/paragraph") public Response insertParagraph(@PathParam("notebookId") String notebookId, String message) throws IOException { LOG.info("insert paragraph {} {}", notebookId, message); Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse(Status.NOT_FOUND, "note not found.").build(); } NewParagraphRequest request = gson.fromJson(message, NewParagraphRequest.class); Paragraph p; Double indexDouble = request.getIndex(); if (indexDouble == null) { p = note.addParagraph(); } else { p = note.insertParagraph(indexDouble.intValue()); } p.setTitle(request.getTitle()); p.setText(request.getText()); note.persist(); notebookServer.broadcastNote(note); return new JsonResponse(Status.CREATED, "", p.getId()).build(); } /** * Get paragraph REST API * @param * @return JSON with information of the paragraph * @throws IOException */ @GET @Path("{notebookId}/paragraph/{paragraphId}") public Response getParagraph(@PathParam("notebookId") String notebookId, @PathParam("paragraphId") String paragraphId) throws IOException { LOG.info("get paragraph {} {}", notebookId, paragraphId); Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse(Status.NOT_FOUND, "note not found.").build(); } Paragraph p = note.getParagraph(paragraphId); if (p == null) { return new JsonResponse(Status.NOT_FOUND, "paragraph not found.").build(); } return new JsonResponse(Status.OK, "", p).build(); } /** * Move paragraph REST API * @param newIndex - new index to move * @return JSON with status.OK * @throws IOException */ @POST @Path("{notebookId}/paragraph/{paragraphId}/move/{newIndex}") public Response moveParagraph(@PathParam("notebookId") String notebookId, @PathParam("paragraphId") String paragraphId, @PathParam("newIndex") String newIndex) throws IOException { LOG.info("move paragraph {} {} {}", notebookId, paragraphId, newIndex); Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse(Status.NOT_FOUND, "note not found.").build(); } Paragraph p = note.getParagraph(paragraphId); if (p == null) { return new JsonResponse(Status.NOT_FOUND, "paragraph not found.").build(); } try { note.moveParagraph(paragraphId, Integer.parseInt(newIndex), true); note.persist(); notebookServer.broadcastNote(note); return new JsonResponse(Status.OK, "").build(); } catch (IndexOutOfBoundsException e) { return new JsonResponse(Status.BAD_REQUEST, "paragraph's new index is out of bound").build(); } } /** * Delete paragraph REST API * @param * @return JSON with status.OK * @throws IOException */ @DELETE @Path("{notebookId}/paragraph/{paragraphId}") public Response deleteParagraph(@PathParam("notebookId") String notebookId, @PathParam("paragraphId") String paragraphId) throws IOException { LOG.info("delete paragraph {} {}", notebookId, paragraphId); Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse(Status.NOT_FOUND, "note not found.").build(); } Paragraph p = note.getParagraph(paragraphId); if (p == null) { return new JsonResponse(Status.NOT_FOUND, "paragraph not found.").build(); } note.removeParagraph(paragraphId); note.persist(); notebookServer.broadcastNote(note); return new JsonResponse(Status.OK, "").build(); } /** * Run notebook jobs REST API * @param * @return JSON with status.OK * @throws IOException, IllegalArgumentException */ @POST @Path("job/{notebookId}") public Response runNoteJobs(@PathParam("notebookId") String notebookId) throws IOException, IllegalArgumentException { LOG.info("run notebook jobs {} ", notebookId); Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } note.runAll(); return new JsonResponse<>(Status.OK).build(); } /** * Stop(delete) notebook jobs REST API * @param * @return JSON with status.OK * @throws IOException, IllegalArgumentException */ @DELETE @Path("job/{notebookId}") public Response stopNoteJobs(@PathParam("notebookId") String notebookId) throws IOException, IllegalArgumentException { LOG.info("stop notebook jobs {} ", notebookId); Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } for (Paragraph p : note.getParagraphs()) { if (!p.isTerminated()) { p.abort(); } } return new JsonResponse<>(Status.OK).build(); } /** * Get notebook job status REST API * @param * @return JSON with status.OK * @throws IOException, IllegalArgumentException */ @GET @Path("job/{notebookId}") public Response getNoteJobStatus(@PathParam("notebookId") String notebookId) throws IOException, IllegalArgumentException { LOG.info("get notebook job status."); Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } return new JsonResponse<>(Status.OK, null, note.generateParagraphsInfo()).build(); } /** * Run paragraph job REST API * * @param message - JSON with params if user wants to update dynamic form's value * null, empty string, empty json if user doesn't want to update * * @return JSON with status.OK * @throws IOException, IllegalArgumentException */ @POST @Path("job/{notebookId}/{paragraphId}") public Response runParagraph(@PathParam("notebookId") String notebookId, @PathParam("paragraphId") String paragraphId, String message) throws IOException, IllegalArgumentException { LOG.info("run paragraph job {} {} {}", notebookId, paragraphId, message); 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(); } // handle params if presented if (!StringUtils.isEmpty(message)) { RunParagraphWithParametersRequest request = gson.fromJson(message, RunParagraphWithParametersRequest.class); Map<String, Object> paramsForUpdating = request.getParams(); if (paramsForUpdating != null) { paragraph.settings.getParams().putAll(paramsForUpdating); note.persist(); } } note.run(paragraph.getId()); return new JsonResponse<>(Status.OK).build(); } /** * Stop(delete) paragraph job REST API * @param * @return JSON with status.OK * @throws IOException, IllegalArgumentException */ @DELETE @Path("job/{notebookId}/{paragraphId}") public Response stopParagraph(@PathParam("notebookId") String notebookId, @PathParam("paragraphId") String paragraphId) throws IOException, IllegalArgumentException { LOG.info("stop paragraph job {} ", notebookId); Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } Paragraph p = note.getParagraph(paragraphId); if (p == null) { return new JsonResponse<>(Status.NOT_FOUND, "paragraph not found.").build(); } p.abort(); return new JsonResponse<>(Status.OK).build(); } /** * Register cron job REST API * @param message - JSON with cron expressions. * @return JSON with status.OK * @throws IOException, IllegalArgumentException */ @POST @Path("cron/{notebookId}") public Response registerCronJob(@PathParam("notebookId") String notebookId, String message) throws IOException, IllegalArgumentException { LOG.info("Register cron job note={} request cron msg={}", notebookId, message); CronRequest request = gson.fromJson(message, CronRequest.class); Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } if (!CronExpression.isValidExpression(request.getCronString())) { return new JsonResponse<>(Status.BAD_REQUEST, "wrong cron expressions.").build(); } Map<String, Object> config = note.getConfig(); config.put("cron", request.getCronString()); note.setConfig(config); notebook.refreshCron(note.id()); return new JsonResponse<>(Status.OK).build(); } /** * Remove cron job REST API * @param * @return JSON with status.OK * @throws IOException, IllegalArgumentException */ @DELETE @Path("cron/{notebookId}") public Response removeCronJob(@PathParam("notebookId") String notebookId) throws IOException, IllegalArgumentException { LOG.info("Remove cron job note {}", notebookId); Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } Map<String, Object> config = note.getConfig(); config.put("cron", null); note.setConfig(config); notebook.refreshCron(note.id()); return new JsonResponse<>(Status.OK).build(); } /** * Get cron job REST API * @param * @return JSON with status.OK * @throws IOException, IllegalArgumentException */ @GET @Path("cron/{notebookId}") public Response getCronJob(@PathParam("notebookId") String notebookId) throws IOException, IllegalArgumentException { LOG.info("Get cron job note {}", notebookId); Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } return new JsonResponse<>(Status.OK, note.getConfig().get("cron")).build(); } /** * Search for a Notes */ @GET @Path("search") public Response search(@QueryParam("q") String queryTerm) { LOG.info("Searching notebooks for: {}", queryTerm); List<Map<String, String>> notebooksFound = notebookIndex.query(queryTerm); LOG.info("{} notbooks found", notebooksFound.size()); return new JsonResponse<>(Status.OK, notebooksFound).build(); } }