Repository: zeppelin Updated Branches: refs/heads/master 4b1b521fc -> 7b585c739
ZEPPELIN-2395. Refactor Input.java to make dynamic forms extensible ### What is this PR for? Currently, zeppelin only support 3 kinds of dynamic form controls: TextBox, Select, CheckBox. All the things are in `Input.java`, this is hard to add new controls, this PR is for refactoring Input to make dynamic forms extensible. Main Changes: * Make `Input` as the base class of dynamic forms also use it as the factory class * All the concret dynamic forms extend `Input` * Add method `toJson` and `fromJson` for `GUI` for `GUI`'s serialization/deserialization. I plan to do it for other classes as well, so that we can remove duplicated serde code and also make it easy to test serialization/deserialization * Change `z.input` to `z.textbox` as I think z.input is a little misleading. But I still keep `z.input` and make `z.input` as deprecated. * Ideally the new input forms' json should be the same as the old input form json. But there's one bug in the old input form, `type` is missing if the input forms are created in frontend for textbox and select. So I keep the old input forms for compatibility. I will load the old input forms json and convert it into new input forms, and after saving, `note.json` would have the new input forms json. After this PR, user needs to do 3 things to add new ui controls * Implement its UI control classes, (refer TextBox/CheckBox/Select), and specify it in `TypeAdapterFactory` of `Input` for serde. * Add parsing logic in `Input.getInputForm` if you want to support this control in frontend. * Add display logic in `paragraph-parameterizedQueryForm.html` ### What type of PR is it? [ Improvement | Refactoring] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-2395 ### How should this be tested? Test is added ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? Yes * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang <zjf...@apache.org> Closes #2245 from zjffdu/ZEPPELIN-2395 and squashes the following commits: 16d42a8 [Jeff Zhang] ZEPPELIN-2395. Refactor Input.java to make dynamic forms extensible Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/7b585c73 Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/7b585c73 Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/7b585c73 Branch: refs/heads/master Commit: 7b585c7399f29492c308f83e342cac29b0c7ca07 Parents: 4b1b521 Author: Jeff Zhang <zjf...@apache.org> Authored: Wed Apr 12 10:50:16 2017 +0800 Committer: Jeff Zhang <zjf...@apache.org> Committed: Thu Apr 20 10:56:54 2017 +0800 ---------------------------------------------------------------------- LICENSE | 1 + .../zeppelin/cassandra/InterpreterLogic.scala | 2 +- .../cassandra/InterpreterLogicTest.java | 2 +- .../org/apache/zeppelin/groovy/GObject.java | 2 +- pom.xml | 7 + .../main/resources/python/zeppelin_python.py | 2 +- .../apache/zeppelin/spark/ZeppelinContext.java | 25 ++- zeppelin-interpreter/pom.xml | 5 + .../java/org/apache/zeppelin/display/GUI.java | 69 ++++++- .../java/org/apache/zeppelin/display/Input.java | 178 +++++++------------ .../org/apache/zeppelin/display/OldInput.java | 87 +++++++++ .../display/RuntimeTypeAdapterFactory.java | 149 ++++++++++++++++ .../apache/zeppelin/display/ui/CheckBox.java | 45 +++++ .../apache/zeppelin/display/ui/OptionInput.java | 85 +++++++++ .../org/apache/zeppelin/display/ui/Select.java | 36 ++++ .../org/apache/zeppelin/display/ui/TextBox.java | 38 ++++ .../interpreter/remote/RemoteInterpreter.java | 4 +- .../remote/RemoteInterpreterServer.java | 4 +- .../org/apache/zeppelin/display/GUITest.java | 120 +++++++++++++ .../org/apache/zeppelin/display/InputTest.java | 35 ++-- .../apache/zeppelin/socket/NotebookServer.java | 5 +- .../integration/ParagraphActionsIT.java | 2 +- .../zeppelin/rest/ZeppelinSparkClusterTest.java | 2 +- .../paragraph-parameterizedQueryForm.html | 20 +-- .../websocketEvents/websocketEvents.factory.js | 1 + .../java/org/apache/zeppelin/notebook/Note.java | 19 ++ .../notebook/repo/AzureNotebookRepo.java | 2 +- .../zeppelin/notebook/repo/S3NotebookRepo.java | 2 +- .../zeppelin/notebook/repo/VFSNotebookRepo.java | 2 +- 29 files changed, 793 insertions(+), 158 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/LICENSE ---------------------------------------------------------------------- diff --git a/LICENSE b/LICENSE index 83a9131..e206a6c 100644 --- a/LICENSE +++ b/LICENSE @@ -255,6 +255,7 @@ The text of each license is also included at licenses/LICENSE-[project]-[version (Apache 2.0) Bootstrap v3.0.2 (http://getbootstrap.com/) - https://github.com/twbs/bootstrap/blob/v3.0.2/LICENSE (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) ======================================================================== BSD 3-Clause licenses http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/cassandra/src/main/scala/org/apache/zeppelin/cassandra/InterpreterLogic.scala ---------------------------------------------------------------------- diff --git a/cassandra/src/main/scala/org/apache/zeppelin/cassandra/InterpreterLogic.scala b/cassandra/src/main/scala/org/apache/zeppelin/cassandra/InterpreterLogic.scala index 363da7b..c83a186 100644 --- a/cassandra/src/main/scala/org/apache/zeppelin/cassandra/InterpreterLogic.scala +++ b/cassandra/src/main/scala/org/apache/zeppelin/cassandra/InterpreterLogic.scala @@ -30,7 +30,7 @@ import com.datastax.driver.core.exceptions.DriverException import com.datastax.driver.core.policies.{LoggingRetryPolicy, FallthroughRetryPolicy, DowngradingConsistencyRetryPolicy, Policies} import org.apache.zeppelin.cassandra.TextBlockHierarchy._ import org.apache.zeppelin.display.AngularObjectRegistry -import org.apache.zeppelin.display.Input.ParamOption +import org.apache.zeppelin.display.ui.OptionInput.ParamOption import org.apache.zeppelin.interpreter.InterpreterResult.Code import org.apache.zeppelin.interpreter.{InterpreterException, InterpreterResult, InterpreterContext} import org.slf4j.LoggerFactory http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/cassandra/src/test/java/org/apache/zeppelin/cassandra/InterpreterLogicTest.java ---------------------------------------------------------------------- diff --git a/cassandra/src/test/java/org/apache/zeppelin/cassandra/InterpreterLogicTest.java b/cassandra/src/test/java/org/apache/zeppelin/cassandra/InterpreterLogicTest.java index 698397a..f3848fd 100644 --- a/cassandra/src/test/java/org/apache/zeppelin/cassandra/InterpreterLogicTest.java +++ b/cassandra/src/test/java/org/apache/zeppelin/cassandra/InterpreterLogicTest.java @@ -34,7 +34,7 @@ import com.datastax.driver.core.Statement; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.GUI; -import org.apache.zeppelin.display.Input.ParamOption; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterException; import org.junit.Rule; http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/groovy/src/main/java/org/apache/zeppelin/groovy/GObject.java ---------------------------------------------------------------------- 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 e460651..7f6809a 100644 --- a/groovy/src/main/java/org/apache/zeppelin/groovy/GObject.java +++ b/groovy/src/main/java/org/apache/zeppelin/groovy/GObject.java @@ -36,7 +36,7 @@ import org.apache.zeppelin.interpreter.InterpreterContextRunner; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.AngularObject; import org.apache.zeppelin.display.GUI; -import org.apache.zeppelin.display.Input.ParamOption; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; import org.apache.zeppelin.annotation.ZeppelinApi; import org.apache.zeppelin.interpreter.RemoteWorksController; import org.apache.zeppelin.interpreter.InterpreterException; http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/pom.xml ---------------------------------------------------------------------- diff --git a/pom.xml b/pom.xml index e1a2094..3bede62 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,7 @@ <log4j.version>1.2.17</log4j.version> <libthrift.version>0.9.2</libthrift.version> <gson.version>2.2</gson.version> + <gson-extras.version>0.2.1</gson-extras.version> <guava.version>15.0</guava.version> <jetty.version>9.2.15.v20160210</jetty.version> <httpcomponents.core.version>4.3.3</httpcomponents.core.version> @@ -193,6 +194,12 @@ </dependency> <dependency> + <groupId>org.danilopianini</groupId> + <artifactId>gson-extras</artifactId> + <version>${gson-extras.version}</version> + </dependency> + + <dependency> <groupId>commons-configuration</groupId> <artifactId>commons-configuration</artifactId> <version>${commons.configuration.version}</version> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/python/src/main/resources/python/zeppelin_python.py ---------------------------------------------------------------------- diff --git a/python/src/main/resources/python/zeppelin_python.py b/python/src/main/resources/python/zeppelin_python.py index 31b993d..eff8824 100644 --- a/python/src/main/resources/python/zeppelin_python.py +++ b/python/src/main/resources/python/zeppelin_python.py @@ -53,7 +53,7 @@ class PyZeppelinContext(object): def __init__(self, z): self.z = z - self.paramOption = gateway.jvm.org.apache.zeppelin.display.Input.ParamOption + self.paramOption = gateway.jvm.org.apache.zeppelin.display.ui.OptionInput.ParamOption self.javaList = gateway.jvm.java.util.ArrayList self.max_result = 1000 self._displayhook = lambda *args: None http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java ---------------------------------------------------------------------- diff --git a/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java b/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java index 7e1ab70..b78410f 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java +++ b/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java @@ -40,7 +40,7 @@ import org.apache.zeppelin.display.AngularObject; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.AngularObjectWatcher; import org.apache.zeppelin.display.GUI; -import org.apache.zeppelin.display.Input.ParamOption; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterContextRunner; import org.apache.zeppelin.interpreter.InterpreterException; @@ -114,14 +114,33 @@ public class ZeppelinContext { public SQLContext sqlContext; private GUI gui; + /** + * @deprecated use z.textbox instead + * + */ + @Deprecated @ZeppelinApi public Object input(String name) { - return input(name, ""); + return textbox(name); } + /** + * @deprecated use z.textbox instead + */ + @Deprecated @ZeppelinApi public Object input(String name, Object defaultValue) { - return gui.input(name, defaultValue); + return textbox(name, defaultValue.toString()); + } + + @ZeppelinApi + public Object textbox(String name) { + return textbox(name, ""); + } + + @ZeppelinApi + public Object textbox(String name, String defaultValue) { + return gui.textbox(name, defaultValue); } @ZeppelinApi http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-interpreter/pom.xml ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index e55144c..109099c 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -62,6 +62,11 @@ </dependency> <dependency> + <groupId>org.danilopianini</groupId> + <artifactId>gson-extras</artifactId> + </dependency> + + <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-exec</artifactId> <version>${commons.exec.version}</version> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java index 40ce8ca..66b21c6 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java @@ -17,17 +17,27 @@ package org.apache.zeppelin.display; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.zeppelin.display.ui.CheckBox; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; +import org.apache.zeppelin.display.ui.Select; +import org.apache.zeppelin.display.ui.TextBox; + import java.io.Serializable; import java.util.*; -import org.apache.zeppelin.display.Input.ParamOption; /** * Settings of a form. */ public class GUI implements Serializable { + private static Gson gson = new GsonBuilder() + .registerTypeAdapterFactory(Input.TypeAdapterFactory) + .create(); + Map<String, Object> params = new HashMap<>(); // form parameters from client LinkedHashMap<String, Input> forms = new LinkedHashMap<>(); // form configuration @@ -51,19 +61,29 @@ public class GUI implements Serializable { this.forms = forms; } + @Deprecated + public Object input(String id) { + return textbox(id, ""); + } + + @Deprecated public Object input(String id, Object defaultValue) { + return textbox(id, defaultValue.toString()); + } + + public Object textbox(String id, String defaultValue) { // first find values from client and then use default Object value = params.get(id); if (value == null) { value = defaultValue; } - forms.put(id, new Input(id, defaultValue, "input")); + forms.put(id, new TextBox(id, defaultValue)); return value; } - public Object input(String id) { - return input(id, ""); + public Object textbox(String id) { + return textbox(id, ""); } public Object select(String id, Object defaultValue, ParamOption[] options) { @@ -71,7 +91,7 @@ public class GUI implements Serializable { if (value == null) { value = defaultValue; } - forms.put(id, new Input(id, defaultValue, "select", options)); + forms.put(id, new Select(id, defaultValue, options)); return value; } @@ -81,7 +101,7 @@ public class GUI implements Serializable { if (checked == null) { checked = defaultChecked; } - forms.put(id, new Input(id, defaultChecked, "checkbox", options)); + forms.put(id, new CheckBox(id, defaultChecked, options)); List<Object> filtered = new LinkedList<>(); for (Object o : checked) { if (isValidOption(o, options)) { @@ -103,4 +123,41 @@ public class GUI implements Serializable { public void clear() { this.forms = new LinkedHashMap<>(); } + + public String toJson() { + return gson.toJson(this); + } + + public void convertOldInput() { + for (Map.Entry<String, Input> entry : forms.entrySet()) { + if (entry.getValue() instanceof OldInput) { + Input convertedInput = convertFromOldInput((OldInput) entry.getValue()); + forms.put(entry.getKey(), convertedInput); + } + } + } + + public static GUI fromJson(String json) { + GUI gui = gson.fromJson(json, GUI.class); + gui.convertOldInput(); + return gui; + } + + private Input convertFromOldInput(OldInput oldInput) { + Input convertedInput = null; + + if (oldInput.options == null || oldInput instanceof OldInput.OldTextBox) { + convertedInput = new TextBox(oldInput.name, oldInput.defaultValue.toString()); + } else if (oldInput instanceof OldInput.OldCheckBox) { + convertedInput = new CheckBox(oldInput.name, (List) oldInput.defaultValue, oldInput.options); + } else if (oldInput instanceof OldInput && oldInput.options != null) { + convertedInput = new Select(oldInput.name, oldInput.defaultValue, oldInput.options); + } else { + throw new RuntimeException("Can not convert this OldInput."); + } + convertedInput.setDisplayName(oldInput.getDisplayName()); + convertedInput.setHidden(oldInput.isHidden()); + convertedInput.setArgument(oldInput.getArgument()); + return convertedInput; + } } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/Input.java ---------------------------------------------------------------------- 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 4924b2b..12fa782 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 @@ -18,6 +18,8 @@ package org.apache.zeppelin.display; import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.display.ui.*; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; import java.io.Serializable; import java.util.*; @@ -25,105 +27,43 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * Input type. + * Base class for dynamic forms. Also used as factory class of dynamic forms. + * + * @param <T> */ -public class Input implements Serializable { - /** - * Parameters option. - */ - public static class ParamOption { - Object value; - String displayName; - - public ParamOption(Object value, String displayName) { - super(); - this.value = value; - this.displayName = displayName; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - ParamOption that = (ParamOption) o; - - if (value != null ? !value.equals(that.value) : that.value != null) return false; - return displayName != null ? displayName.equals(that.displayName) : that.displayName == null; - - } - - @Override - public int hashCode() { - int result = value != null ? value.hashCode() : 0; - result = 31 * result + (displayName != null ? displayName.hashCode() : 0); - return result; - } - - public Object getValue() { - return value; - } - - public void setValue(Object value) { - this.value = value; - } - - public String getDisplayName() { - return displayName; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - } - - String name; - String displayName; - String type; - String argument; - Object defaultValue; - ParamOption[] options; - boolean hidden; - - public Input(String name, Object defaultValue, String type) { - this.name = name; - this.displayName = name; - this.defaultValue = defaultValue; - this.type = type; +public class Input<T> implements Serializable { + + // @TODO(zjffdu). Use gson's RuntimeTypeAdapterFactory and remove the old input form support + // in future. + public static final RuntimeTypeAdapterFactory TypeAdapterFactory = + RuntimeTypeAdapterFactory.of(Input.class, "type") + .registerSubtype(TextBox.class, "TextBox") + .registerSubtype(Select.class, "Select") + .registerSubtype(CheckBox.class, "CheckBox") + .registerSubtype(OldInput.OldTextBox.class, "input") + .registerSubtype(OldInput.OldSelect.class, "select") + .registerSubtype(OldInput.OldCheckBox.class, "checkbox") + .registerSubtype(OldInput.class, null); + + protected String name; + protected String displayName; + protected T defaultValue; + protected boolean hidden; + protected String argument; + + public Input() { } - public Input(String name, Object defaultValue, String type, ParamOption[] options) { - this.name = name; - this.displayName = name; - this.defaultValue = defaultValue; - this.type = type; - this.options = options; - } - - public Input(String name, String displayName, String type, String argument, Object defaultValue, - ParamOption[] options, boolean hidden) { - super(); - this.name = name; - this.displayName = displayName; - this.argument = argument; - this.type = type; - this.defaultValue = defaultValue; - this.options = options; - this.hidden = hidden; - } - - @Override - public boolean equals(Object o) { - return name.equals(((Input) o).getName()); + public boolean isHidden() { + return hidden; } public String getName() { - return name; + return this.name; } - public void setName(String name) { - this.name = name; + public T getDefaultValue() { + return defaultValue; } public String getDisplayName() { @@ -134,41 +74,37 @@ public class Input implements Serializable { this.displayName = displayName; } - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; + public void setArgument(String argument) { + this.argument = argument; } - public Object getDefaultValue() { - return defaultValue; + public void setHidden(boolean hidden) { + this.hidden = hidden; } - public void setDefaultValue(Object defaultValue) { - this.defaultValue = defaultValue; + public String getArgument() { + return argument; } - public ParamOption[] getOptions() { - return options; + public static TextBox textbox(String name, String defaultValue) { + return new TextBox(name, defaultValue); } - public void setOptions(ParamOption[] options) { - this.options = options; + public static Select select(String name, Object defaultValue, ParamOption[] options) { + return new Select(name, defaultValue, options); } - public boolean isHidden() { - return hidden; + public static CheckBox checkbox(String name, Object[] defaultChecked, ParamOption[] options) { + return new CheckBox(name, defaultChecked, options); } // Syntax of variables: ${TYPE:NAME=DEFAULT_VALUE1|DEFAULT_VALUE2|...,VALUE1|VALUE2|...} // Type is optional. Type may contain an optional argument with syntax: TYPE(ARG) // NAME and VALUEs may contain an optional display name with syntax: NAME(DISPLAY_NAME) // DEFAULT_VALUEs may not contain display name - // Examples: ${age} input form without default value - // ${age=3} input form with default value - // ${age(Age)=3} input form with display name and default value + // Examples: ${age} textbox form without default value + // ${age=3} textbox form with default value + // ${age(Age)=3} textbox form with display name and default value // ${country=US(United States)|UK|JP} select form with // ${checkbox( or ):country(Country)=US|JP,US(United States)|UK|JP} // checkbox form with " or " as delimiter: will be @@ -282,7 +218,22 @@ public class Input implements Serializable { } - return new Input(varName, displayName, type, arg, defaultValue, paramOptions, hidden); + Input input = null; + if (type == null) { + if (paramOptions == null) { + input = new TextBox(varName, (String) defaultValue); + } else { + input = new Select(varName, defaultValue, paramOptions); + } + } else if (type.equals("checkbox")) { + input = new CheckBox(varName, (Object[]) defaultValue, paramOptions); + } else { + throw new RuntimeException("Could not recognize dynamic form with type: " + type); + } + input.setArgument(arg); + input.setDisplayName(displayName); + input.setHidden(hidden); + return input; } public static LinkedHashMap<String, Input> extractSimpleQueryForm(String script) { @@ -314,11 +265,12 @@ public class Input implements Serializable { if (params.containsKey(input.name)) { value = params.get(input.name); } else { - value = input.defaultValue; + value = input.getDefaultValue(); } String expanded; if (value instanceof Object[] || value instanceof Collection) { // multi-selection + OptionInput optionInput = (OptionInput) input; String delimiter = input.argument; if (delimiter == null) { delimiter = DEFAULT_DELIMITER; @@ -327,7 +279,7 @@ public class Input implements Serializable { : Arrays.asList((Object[]) value); List<Object> validChecked = new LinkedList<>(); for (Object o : checked) { // filter out obsolete checked values - for (ParamOption option : input.getOptions()) { + for (ParamOption option : optionInput.getOptions()) { if (option.getValue().equals(o)) { validChecked.add(o); break; http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/OldInput.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/OldInput.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/OldInput.java new file mode 100644 index 0000000..7c67dad --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/OldInput.java @@ -0,0 +1,87 @@ +/* + * 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.display; + +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; + +/** + * Old Input type. + * The reason I still keep Old Input is for compatibility. There's one bug in the old input forms. + * There's 2 ways to create input forms: frontend & backend. + * The bug is in frontend. The type would not be set correctly when input form + * is created in frontend (Input.getInputForm). + */ +public class OldInput extends Input<Object> { + + ParamOption[] options; + + public OldInput() {} + + public OldInput(String name, Object defaultValue) { + this.name = name; + this.displayName = name; + this.defaultValue = defaultValue; + } + + public OldInput(String name, Object defaultValue, ParamOption[] options) { + this.name = name; + this.displayName = name; + this.defaultValue = defaultValue; + this.options = options; + } + + @Override + public boolean equals(Object o) { + return name.equals(((OldInput) o).getName()); + } + + public ParamOption[] getOptions() { + return options; + } + + public void setOptions(ParamOption[] options) { + this.options = options; + } + + /** + * + */ + public static class OldTextBox extends OldInput { + public OldTextBox(String name, Object defaultValue) { + super(name, defaultValue); + } + } + + /** + * + */ + public static class OldSelect extends OldInput { + public OldSelect(String name, Object defaultValue, ParamOption[] options) { + super(name, defaultValue, options); + } + } + + /** + * + */ + public static class OldCheckBox extends OldInput { + public OldCheckBox(String name, Object defaultValue, ParamOption[] options) { + super(name, defaultValue, options); + } + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/RuntimeTypeAdapterFactory.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/RuntimeTypeAdapterFactory.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/RuntimeTypeAdapterFactory.java new file mode 100644 index 0000000..da05caa --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/RuntimeTypeAdapterFactory.java @@ -0,0 +1,149 @@ +/* + * 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.display; + +import com.google.gson.*; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Copied from gson with minor changes to support old input forms + * + * @param <T> + */ +public class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory { + private final Class<?> baseType; + private final String typeFieldName; + private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>(); + private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>(); + + private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + */ + public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + */ + public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) { + return new RuntimeTypeAdapterFactory<T>(baseType, "type"); + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) { + if (type == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) { + return registerSubtype(type, type.getSimpleName()); + } + + public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) { + if (type.getRawType() != baseType) { + return null; + } + + final Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<String, TypeAdapter<?>>(); + final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = + new LinkedHashMap<Class<?>, TypeAdapter<?>>(); + for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) { + TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter<R>() { + @Override public R read(JsonReader in) throws IOException { + JsonElement jsonElement = Streams.parse(in); + JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + String label = (labelJsonElement == null ? null : labelJsonElement.getAsString()); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + + label + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override public void write(JsonWriter out, R value) throws IOException { + Class<?> srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + if (jsonObject.has(typeFieldName) && !srcType.getSimpleName().equals("OldInput")) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } + JsonObject clone = new JsonObject(); + if (!srcType.getSimpleName().equals("OldInput")) { + clone.add(typeFieldName, new JsonPrimitive(label)); + } + for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + Streams.write(clone, out); + } + }.nullSafe(); + } +} + http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/CheckBox.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/CheckBox.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/CheckBox.java new file mode 100644 index 0000000..f9b4650 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/CheckBox.java @@ -0,0 +1,45 @@ +/* + * 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.display.ui; + +import java.awt.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +/** + * Html Checkbox + */ +public class CheckBox extends OptionInput<Object[]> { + + public CheckBox() { + } + + public CheckBox(String name, Object[] defaultValue, ParamOption[] options) { + this.name = name; + this.displayName = name; + this.defaultValue = defaultValue; + this.options = options; + } + + public CheckBox(String name, Collection<Object> defaultValue, ParamOption[] options) { + this(name, defaultValue.toArray(), options); + } + +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/OptionInput.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/OptionInput.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/OptionInput.java new file mode 100644 index 0000000..d5a1c0d --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/OptionInput.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.display.ui; + +import org.apache.zeppelin.display.Input; + +/** + * Base class for Input with options + * + * @param <T> + */ +public abstract class OptionInput<T> extends Input<T> { + + /** + * Parameters option. + */ + public static class ParamOption { + Object value; + String displayName; + + public ParamOption(Object value, String displayName) { + super(); + this.value = value; + this.displayName = displayName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ParamOption that = (ParamOption) o; + + if (value != null ? !value.equals(that.value) : that.value != null) return false; + return displayName != null ? displayName.equals(that.displayName) : that.displayName == null; + + } + + @Override + public int hashCode() { + int result = value != null ? value.hashCode() : 0; + result = 31 * result + (displayName != null ? displayName.hashCode() : 0); + return result; + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + } + + protected ParamOption[] options; + + public ParamOption[] getOptions() { + return options; + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/Select.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/Select.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/Select.java new file mode 100644 index 0000000..212d3d7 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/Select.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zeppelin.display.ui; + +/** + * Html Dropdown list + */ +public class Select extends OptionInput<Object> { + + public Select() { + + } + + public Select(String name, Object defaultValue, ParamOption[] options) { + this.name = name; + this.displayName = name; + this.defaultValue = defaultValue; + this.options = options; + } + +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/TextBox.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/TextBox.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/TextBox.java new file mode 100644 index 0000000..b9f9946 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/TextBox.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.display.ui; + +import org.apache.zeppelin.display.Input; + +/** + * Html TextBox control + */ +public class TextBox extends Input<String> { + + public TextBox() { + + } + + public TextBox(String name, String defaultValue) { + this.name = name; + this.displayName = name; + this.defaultValue = defaultValue; + } + +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java index 2f9d2bb..123ad75 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java @@ -381,14 +381,14 @@ public class RemoteInterpreter extends Interpreter { context.getConfig().putAll(remoteConfig); if (form == FormType.NATIVE) { - GUI remoteGui = gson.fromJson(remoteResult.getGui(), GUI.class); + GUI remoteGui = GUI.fromJson(remoteResult.getGui()); currentGUI.clear(); currentGUI.setParams(remoteGui.getParams()); currentGUI.setForms(remoteGui.getForms()); } else if (form == FormType.SIMPLE) { final Map<String, Input> currentForms = currentGUI.getForms(); final Map<String, Object> currentParams = currentGUI.getParams(); - final GUI remoteGUI = gson.fromJson(remoteResult.getGui(), GUI.class); + final GUI remoteGUI = GUI.fromJson(remoteResult.getGui()); final Map<String, Input> remoteForms = remoteGUI.getForms(); final Map<String, Object> remoteParams = remoteGUI.getParams(); currentForms.putAll(remoteForms); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java index 3b7ec5c..6c43813 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 @@ -592,7 +592,7 @@ public class RemoteInterpreterServer gson.fromJson(ric.getAuthenticationInfo(), AuthenticationInfo.class), (Map<String, Object>) gson.fromJson(ric.getConfig(), new TypeToken<Map<String, Object>>() {}.getType()), - gson.fromJson(ric.getGui(), GUI.class), + GUI.fromJson(ric.getGui()), interpreterGroup.getAngularObjectRegistry(), interpreterGroup.getResourcePool(), contextRunners, output, remoteWorksController, eventClient); @@ -737,7 +737,7 @@ public class RemoteInterpreterServer result.code().name(), msg, gson.toJson(config), - gson.toJson(gui)); + gui.toJson()); } @Override http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/GUITest.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/GUITest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/GUITest.java new file mode 100644 index 0000000..6def2e7 --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/GUITest.java @@ -0,0 +1,120 @@ +/* + * 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.display; + +import org.apache.commons.io.IOUtils; +import org.apache.zeppelin.display.ui.CheckBox; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; +import org.apache.zeppelin.display.ui.Select; +import org.apache.zeppelin.display.ui.TextBox; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +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.assertTrue; + +public class GUITest { + + private ParamOption[] options = new ParamOption[]{ + new ParamOption("1", "value_1"), + new ParamOption("2", "value_2") + }; + + private List<Object> checkedItems; + + @Before + public void setUp() { + checkedItems = new ArrayList<>(); + checkedItems.add("1"); + } + + @Test + public void testGson() { + GUI gui = new GUI(); + gui.textbox("textbox_1", "default_text_1"); + gui.select("select_1", "1", options); + List<Object> list = new ArrayList(); + list.add("1"); + gui.checkbox("checkbox_1", list, options); + + String json = gui.toJson(); + System.out.println(json); + GUI gui2 = GUI.fromJson(json); + assertEquals(gui2.toJson(), json); + assertEquals(gui2.forms, gui2.forms); + assertEquals(gui2.params, gui2.params); + } + + // Case 1. Old input forms are created in backend, in this case type is always set + @Test + public void testOldGson_1() throws IOException { + + GUI gui = new GUI(); + gui.forms.put("textbox_1", new OldInput.OldTextBox("textbox_1", "default_text_1")); + gui.forms.put("select_1", new OldInput.OldSelect("select_1", "1", options)); + gui.forms.put("checkbox_1", + new OldInput.OldCheckBox("checkbox_1", checkedItems, options)); + + // convert to old json format. + String json = gui.toJson(); + + // convert to new input forms + GUI gui2 = GUI.fromJson(json); + assertTrue(3 == gui2.forms.size()); + assertTrue(gui2.forms.get("textbox_1") instanceof TextBox); + assertEquals("default_text_1", gui2.forms.get("textbox_1").getDefaultValue()); + assertTrue(gui2.forms.get("select_1") instanceof Select); + assertEquals(options, ((Select) gui2.forms.get("select_1")).getOptions()); + assertTrue(gui2.forms.get("checkbox_1") instanceof CheckBox); + assertEquals(options, ((CheckBox) gui2.forms.get("checkbox_1")).getOptions()); + } + + // Case 2. Old input forms are created in frontend, in this case type is only set for checkbox + // Actually this is a bug due to method Input#getInputForm + @Test + public void testOldGson_2() throws IOException { + + GUI gui = new GUI(); + gui.forms.put("textbox_1", new OldInput("textbox_1", "default_text_1")); + gui.forms.put("select_1", new OldInput("select_1", "1", options)); + gui.forms.put("checkbox_1", + new OldInput.OldCheckBox("checkbox_1", checkedItems, options)); + + // convert to old json format. + String json = gui.toJson(); + + // convert to new input forms + GUI gui2 = GUI.fromJson(json); + assertTrue(3 == gui2.forms.size()); + assertTrue(gui2.forms.get("textbox_1") instanceof TextBox); + assertEquals("default_text_1", gui2.forms.get("textbox_1").getDefaultValue()); + assertTrue(gui2.forms.get("select_1") instanceof Select); + assertEquals(options, ((Select) gui2.forms.get("select_1")).getOptions()); + assertTrue(gui2.forms.get("checkbox_1") instanceof CheckBox); + assertEquals(options, ((CheckBox) gui2.forms.get("checkbox_1")).getOptions()); + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/InputTest.java ---------------------------------------------------------------------- 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 b6f1e3e..d15fab4 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 @@ -20,16 +20,19 @@ package org.apache.zeppelin.display; import java.util.HashMap; import java.util.Map; +import org.apache.zeppelin.display.ui.CheckBox; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; +import org.apache.zeppelin.display.ui.Select; +import org.apache.zeppelin.display.ui.TextBox; import org.junit.After; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertNull; -import org.apache.zeppelin.display.Input.ParamOption; - public class InputTest { @Before @@ -42,7 +45,7 @@ public class InputTest { @Test public void testFormExtraction() { - // input form + // textbox form String script = "${input_form=}"; Map<String, Input> forms = Input.extractSimpleQueryForm(script); assertEquals(1, forms.size()); @@ -50,50 +53,57 @@ public class InputTest { assertEquals("input_form", form.name); assertNull(form.displayName); assertEquals("", form.defaultValue); - assertNull(form.options); + assertTrue(form instanceof TextBox); - // input form with display name & default value + // textbox form with display name & default value script = "${input_form(Input Form)=xxx}"; forms = Input.extractSimpleQueryForm(script); 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"); assertEquals("select_form", form.name); assertEquals("op1", form.defaultValue); + assertTrue(form instanceof Select); assertArrayEquals(new ParamOption[]{new ParamOption("op1", null), - new ParamOption("op2", "Option 2"), new ParamOption("op3", null)}, form.options); + new ParamOption("op2", "Option 2"), new ParamOption("op3", null)}, + ((Select) form).getOptions()); // checkbox form script = "${checkbox:checkbox_form=op1,op1|op2|op3}"; form = Input.extractSimpleQueryForm(script).get("checkbox_form"); assertEquals("checkbox_form", form.name); - assertEquals("checkbox", form.type); + assertTrue(form instanceof CheckBox); + assertArrayEquals(new Object[]{"op1"}, (Object[]) form.defaultValue); assertArrayEquals(new ParamOption[]{new ParamOption("op1", null), - new ParamOption("op2", null), new ParamOption("op3", null)}, form.options); + new ParamOption("op2", null), new ParamOption("op3", null)}, + ((CheckBox) form).getOptions()); // 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"); assertEquals("checkbox_form", form.name); assertEquals("Checkbox Form", form.displayName); - assertEquals("checkbox", form.type); + assertTrue(form instanceof CheckBox); assertArrayEquals(new Object[]{"op1", "op3"}, (Object[]) form.defaultValue); assertArrayEquals(new ParamOption[]{new ParamOption("op1", "Option 1"), - new ParamOption("op2", null), new ParamOption("op3", null)}, form.options); + new ParamOption("op2", null), new ParamOption("op3", null)}, + ((CheckBox) form).getOptions()); // 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"); assertEquals("checkbox_form", form.name); assertEquals("Checkbox Form", form.displayName); - assertEquals("checkbox", form.type); + assertTrue(form instanceof CheckBox); assertArrayEquals(new Object[]{}, (Object[]) form.defaultValue); assertArrayEquals(new ParamOption[]{new ParamOption("op1", "Option 1"), - new ParamOption("op2", "Option 2"), new ParamOption("op3", "Option 3")}, form.options); + new ParamOption("op2", "Option 2"), new ParamOption("op3", "Option 3")}, + ((CheckBox) form).getOptions()); } @@ -125,4 +135,5 @@ public class InputTest { assertEquals("INPUT=some_inputSELECTED=s_op2\nCHECKED=c_op1\n" + "NEW_CHECKED=nc_a and nc_c", replaced); } + } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java index 1aa4f28..10ed00e 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 @@ -45,6 +45,7 @@ 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.helium.ApplicationEventListener; import org.apache.zeppelin.helium.HeliumPackage; import org.apache.zeppelin.interpreter.Interpreter; @@ -134,7 +135,9 @@ public class NotebookServer extends WebSocketServlet } } } - }).setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").create(); + }).setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .registerTypeAdapterFactory(Input.TypeAdapterFactory).create(); + final Map<String, List<NotebookSocket>> noteSocketMap = new HashMap<>(); final Queue<NotebookSocket> connectedSockets = new ConcurrentLinkedQueue<>(); final Map<String, Queue<NotebookSocket>> userConnectedSockets = new ConcurrentHashMap<>(); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java index 8e09f00..add23ac 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java @@ -548,7 +548,7 @@ public class ParagraphActionsIT extends AbstractZeppelinIT { try { createNewNote(); - setTextOfParagraph(1, "%spark println(\"Hello \"+z.input(\"name\", \"world\")) "); + setTextOfParagraph(1, "%spark println(\"Hello \"+z.textbox(\"name\", \"world\")) "); runParagraph(1); waitForParagraph(1, "FINISHED"); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java index eb8186e..77e0844 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 @@ -490,7 +490,7 @@ public class ZeppelinSparkClusterTest extends AbstractTestRestApi { Map config = p.getConfig(); config.put("enabled", true); p.setConfig(config); - String code = "%spark.spark println(z.input(\"my_input\", \"default_name\"))\n" + + 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\"), " + http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html b/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html index 64da3cf..249e7c1 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html @@ -20,7 +20,7 @@ limitations under the License. <label class="control-label input-sm" ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }">{{formulaire.name}}</label> <div> <input class="form-control input-sm" - ng-if="!paragraph.settings.forms[formulaire.name].options" + ng-if="paragraph.settings.forms[formulaire.name].type == 'TextBox'" ng-enter="runParagraphFromButton(getEditorValue())" ng-model="paragraph.settings.params[formulaire.name]" ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }" @@ -28,7 +28,7 @@ limitations under the License. </div> <div ng-if="paragraph.config.runOnSelectionChange == true"> <select class="form-control input-sm" - ng-if="paragraph.settings.forms[formulaire.name].options && paragraph.settings.forms[formulaire.name].type != 'checkbox'" + ng-if="paragraph.settings.forms[formulaire.name].type == 'Select'" ng-change="runParagraphFromButton(getEditorValue())" ng-model="paragraph.settings.params[formulaire.name]" ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }" @@ -38,16 +38,16 @@ limitations under the License. </div> <div ng-if="paragraph.config.runOnSelectionChange == false"> <select class="form-control input-sm" - ng-if="paragraph.settings.forms[formulaire.name].options && paragraph.settings.forms[formulaire.name].type != 'checkbox'" - ng-enter="runParagraphFromButton(getEditorValue())" - ng-model="paragraph.settings.params[formulaire.name]" - ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }" - name="{{formulaire.name}}" - ng-options="option.value as (option.displayName||option.value) for option in paragraph.settings.forms[formulaire.name].options"> + ng-if="paragraph.settings.forms[formulaire.name].type == 'Select'" + ng-enter="runParagraphFromButton(getEditorValue())" + ng-model="paragraph.settings.params[formulaire.name]" + ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }" + name="{{formulaire.name}}" + ng-options="option.value as (option.displayName||option.value) for option in paragraph.settings.forms[formulaire.name].options"> </select> </div> <div ng-if="paragraph.config.runOnSelectionChange == true && - paragraph.settings.forms[formulaire.name].type == 'checkbox'"> + paragraph.settings.forms[formulaire.name].type == 'CheckBox'"> <label ng-repeat="option in paragraph.settings.forms[formulaire.name].options" class="checkbox-item input-sm"> <input type="checkbox" @@ -57,7 +57,7 @@ limitations under the License. </label> </div> <div ng-if="paragraph.config.runOnSelectionChange == false && - paragraph.settings.forms[formulaire.name].type == 'checkbox'"> + paragraph.settings.forms[formulaire.name].type == 'CheckBox'"> <label ng-repeat="option in paragraph.settings.forms[formulaire.name].options" class="checkbox-item input-sm"> <input type="checkbox" http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js index 75e1b2e..186de88 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js +++ b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js @@ -54,6 +54,7 @@ function websocketEvents($rootScope, $websocket, $location, baseUrlSrv) { if (event.data) { payload = angular.fromJson(event.data); } + console.log('Receive Json << %o', event.data) console.log('Receive << %o, %o', payload.op, payload); var op = payload.op; var data = payload.data; http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java ---------------------------------------------------------------------- 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 3e6ab23..dfe39e9 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 @@ -26,6 +26,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import com.google.gson.GsonBuilder; import org.apache.commons.lang.StringUtils; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.display.AngularObject; @@ -54,6 +55,9 @@ import com.google.gson.Gson; public class Note implements Serializable, ParagraphJobListener { private static final Logger logger = LoggerFactory.getLogger(Note.class); private static final long serialVersionUID = 7920699076577612429L; + private static final Gson gson = new GsonBuilder() + .registerTypeAdapterFactory(Input.TypeAdapterFactory) + .create(); // threadpool for delayed persist of note private static final ScheduledThreadPoolExecutor delayedPersistThreadPool = @@ -882,4 +886,19 @@ public class Note implements Serializable, ParagraphJobListener { this.noteEventListener = noteEventListener; } + public String toJson() { + return gson.toJson(this); + } + + public static Note fromJson(String json) { + Note note = gson.fromJson(json, Note.class); + convertOldInput(note); + return note; + } + + private static void convertOldInput(Note note) { + for (Paragraph p : note.paragraphs) { + p.settings.convertOldInput(); + } + } } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/AzureNotebookRepo.java ---------------------------------------------------------------------- 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 79f5dd6..5ef3c16 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 @@ -138,7 +138,7 @@ public class AzureNotebookRepo implements NotebookRepo { Gson gson = gsonBuilder.registerTypeAdapter(Date.class, new NotebookImportDeserializer()) .create(); - Note note = gson.fromJson(json, Note.class); + Note note = Note.fromJson(json); for (Paragraph p : note.getParagraphs()) { if (p.getStatus() == Job.Status.PENDING || p.getStatus() == Job.Status.RUNNING) { http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java ---------------------------------------------------------------------- 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 bd7fe1a..71fa19f 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 @@ -202,7 +202,7 @@ public class S3NotebookRepo implements NotebookRepo { Note note; try (InputStream ins = s3object.getObjectContent()) { String json = IOUtils.toString(ins, conf.getString(ConfVars.ZEPPELIN_ENCODING)); - note = gson.fromJson(json, Note.class); + note = Note.fromJson(json); } for (Paragraph p : note.getParagraphs()) { http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7b585c73/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.java ---------------------------------------------------------------------- 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 04a7075..0251569 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 @@ -175,7 +175,7 @@ public class VFSNotebookRepo implements NotebookRepo { String json = IOUtils.toString(ins, conf.getString(ConfVars.ZEPPELIN_ENCODING)); ins.close(); - Note note = gson.fromJson(json, Note.class); + Note note = Note.fromJson(json); // note.setReplLoader(replLoader); // note.jobListenerFactory = jobListenerFactory;