/*
* 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.lang.StringUtils;
import org.apache.zeppelin.display.ui.*;
import org.apache.zeppelin.display.ui.OptionInput.ParamOption;
import java.io.Serializable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Base class for dynamic forms. Also used as factory class of dynamic forms.
*
* @param <T>
*/
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 boolean isHidden() {
return hidden;
}
public String getName() {
return this.name;
}
public T getDefaultValue() {
return defaultValue;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public void setArgument(String argument) {
this.argument = argument;
}
public void setHidden(boolean hidden) {
this.hidden = hidden;
}
public String getArgument() {
return argument;
}
public static TextBox textbox(String name, String defaultValue) {
return new TextBox(name, defaultValue);
}
public static Select select(String name, Object defaultValue, ParamOption[] options) {
return new Select(name, defaultValue, options);
}
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} 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
// expanded to "US or JP"
private static final Pattern VAR_PTN = Pattern.compile("([_])?[$][{]([^=}]*([=][^}]*)?)[}]");
private static String[] getNameAndDisplayName(String str) {
Pattern p = Pattern.compile("([^(]*)\\s*[(]([^)]*)[)]");
Matcher m = p.matcher(str.trim());
if (m == null || m.find() == false) {
return null;
}
String[] ret = new String[2];
ret[0] = m.group(1);
ret[1] = m.group(2);
return ret;
}
private static String[] getType(String str) {
Pattern p = Pattern.compile("([^:()]*)\\s*([(][^()]*[)])?\\s*:(.*)");
Matcher m = p.matcher(str.trim());
if (m == null || m.find() == false) {
return null;
}
String[] ret = new String[3];
ret[0] = m.group(1).trim();
if (m.group(2) != null) {
ret[1] = m.group(2).trim().replaceAll("[()]", "");
}
ret[2] = m.group(3).trim();
return ret;
}
private static Input getInputForm(Matcher match) {
String hiddenPart = match.group(1);
boolean hidden = false;
if ("_".equals(hiddenPart)) {
hidden = true;
}
String m = match.group(2);
String namePart;
String valuePart;
int p = m.indexOf('=');
if (p > 0) {
namePart = m.substring(0, p);
valuePart = m.substring(p + 1);
} else {
namePart = m;
valuePart = null;
}
String varName;
String displayName = null;
String type = null;
String arg = null;
Object defaultValue = "";
ParamOption[] paramOptions = null;
// get var name type
String varNamePart;
String[] typeArray = getType(namePart);
if (typeArray != null) {
type = typeArray[0];
arg = typeArray[1];
varNamePart = typeArray[2];
} else {
varNamePart = namePart;
}
// get var name and displayname
String[] varNameArray = getNameAndDisplayName(varNamePart);
if (varNameArray != null) {
varName = varNameArray[0];
displayName = varNameArray[1];
} else {
varName = varNamePart.trim();
}
// get defaultValue
if (valuePart != null) {
// find default value
int optionP = valuePart.indexOf(",");
if (optionP >= 0) { // option available
defaultValue = valuePart.substring(0, optionP);
if (type != null && type.equals("checkbox")) {
// checkbox may contain multiple default checks
defaultValue = Input.splitPipe((String) defaultValue);
}
String optionPart = valuePart.substring(optionP + 1);
String[] options = Input.splitPipe(optionPart);
paramOptions = new ParamOption[options.length];
for (int i = 0; i < options.length; i++) {
String[] optNameArray = getNameAndDisplayName(options[i]);
if (optNameArray != null) {
paramOptions[i] = new ParamOption(optNameArray[0], optNameArray[1]);
} else {
paramOptions[i] = new ParamOption(options[i], null);
}
}
} else { // no option
defaultValue = valuePart;
}
}
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) {
LinkedHashMap<String, Input> forms = new LinkedHashMap<>();
if (script == null) {
return forms;
}
String replaced = script;
Matcher match = VAR_PTN.matcher(replaced);
while (match.find()) {
Input form = getInputForm(match);
forms.put(form.name, form);
}
forms.remove("pql");
return forms;
}
private static final String DEFAULT_DELIMITER = ",";
public static String getSimpleQuery(Map<String, Object> params, String script) {
String replaced = script;
Matcher match = VAR_PTN.matcher(replaced);
while (match.find()) {
Input input = getInputForm(match);
Object value;
if (params.containsKey(input.name)) {
value = params.get(input.name);
} else {
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;
}
Collection<Object> checked = value instanceof Collection ? (Collection<Object>) value
: Arrays.asList((Object[]) value);
List<Object> validChecked = new LinkedList<>();
for (Object o : checked) { // filter out obsolete checked values
for (ParamOption option : optionInput.getOptions()) {
if (option.getValue().equals(o)) {
validChecked.add(o);
break;
}
}
}
params.put(input.name, validChecked);
expanded = StringUtils.join(validChecked, delimiter);
} else { // single-selection
expanded = value.toString();
}
replaced = match.replaceFirst(expanded);
match = VAR_PTN.matcher(replaced);
}
return replaced;
}
public static String[] split(String str) {
return str.split(";(?=([^\"']*\"[^\"']*\")*[^\"']*$)");
}
/*
* public static String [] splitPipe(String str){ //return
* str.split("\\|(?=([^\"']*\"[^\"']*\")*[^\"']*$)"); return
* str.split("\\|(?=([^\"']*\"[^\"']*\")*[^\"']*$)"); }
*/
public static String[] splitPipe(String str) {
return split(str, '|');
}
public static String[] split(String str, char split) {
return split(str, new String[] {String.valueOf(split)}, false);
}
public static String[] split(String str, String[] splitters, boolean includeSplitter) {
String escapeSeq = "\"',;${}";
char escapeChar = '\\';
String[] blockStart = new String[] {"\"", "'", "${", "N_(", "N_<"};
String[] blockEnd = new String[] {"\"", "'", "}", "N_)", "N_>"};
return split(str, escapeSeq, escapeChar, blockStart, blockEnd, splitters, includeSplitter);
}
public static String[] split(String str, String escapeSeq, char escapeChar, String[] blockStart,
String[] blockEnd, String[] splitters, boolean includeSplitter) {
List<String> splits = new ArrayList<>();
StringBuilder curString = new StringBuilder();
boolean escape = false; // true when escape char is found
int lastEscapeOffset = -1;
int blockStartPos = -1;
List<Integer> blockStack = new LinkedList<>();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
// escape char detected
if (c == escapeChar && escape == false) {
escape = true;
continue;
}
// escaped char comes
if (escape == true) {
if (escapeSeq.indexOf(c) < 0) {
curString.append(escapeChar);
}
curString.append(c);
escape = false;
lastEscapeOffset = curString.length();
continue;
}
if (blockStack.size() > 0) { // inside of block
curString.append(c);
// check multichar block
boolean multicharBlockDetected = false;
for (int b = 0; b < blockStart.length; b++) {
if (blockStartPos >= 0
&& getBlockStr(blockStart[b]).compareTo(str.substring(blockStartPos, i)) == 0) {
blockStack.remove(0);
blockStack.add(0, b);
multicharBlockDetected = true;
break;
}
}
if (multicharBlockDetected == true) {
continue;
}
// check if current block is nestable
if (isNestedBlock(blockStart[blockStack.get(0)]) == true) {
// try to find nested block start
if (curString.substring(lastEscapeOffset + 1).endsWith(
getBlockStr(blockStart[blockStack.get(0)])) == true) {
blockStack.add(0, blockStack.get(0)); // block is started
blockStartPos = i;
continue;
}
}
// check if block is finishing
if (curString.substring(lastEscapeOffset + 1).endsWith(
getBlockStr(blockEnd[blockStack.get(0)]))) {
// the block closer is one of the splitters (and not nested block)
if (isNestedBlock(blockEnd[blockStack.get(0)]) == false) {
for (String splitter : splitters) {
if (splitter.compareTo(getBlockStr(blockEnd[blockStack.get(0)])) == 0) {
splits.add(curString.toString());
if (includeSplitter == true) {
splits.add(splitter);
}
curString.setLength(0);
lastEscapeOffset = -1;
break;
}
}
}
blockStartPos = -1;
blockStack.remove(0);
continue;
}
} else { // not in the block
boolean splitted = false;
for (String splitter : splitters) {
// forward check for splitter
int curentLenght = i + splitter.length();
if (splitter.compareTo(str.substring(i, Math.min(curentLenght, str.length()))) == 0) {
splits.add(curString.toString());
if (includeSplitter == true) {
splits.add(splitter);
}
curString.setLength(0);
lastEscapeOffset = -1;
i += splitter.length() - 1;
splitted = true;
break;
}
}
if (splitted == true) {
continue;
}
// add char to current string
curString.append(c);
// check if block is started
for (int b = 0; b < blockStart.length; b++) {
if (curString.substring(lastEscapeOffset + 1)
.endsWith(getBlockStr(blockStart[b])) == true) {
blockStack.add(0, b); // block is started
blockStartPos = i;
break;
}
}
}
}
if (curString.length() > 0) {
splits.add(curString.toString().trim());
}
return splits.toArray(new String[] {});
}
private static String getBlockStr(String blockDef) {
if (blockDef.startsWith("N_")) {
return blockDef.substring("N_".length());
} else {
return blockDef;
}
}
private static boolean isNestedBlock(String blockDef) {
if (blockDef.startsWith("N_")) {
return true;
} else {
return false;
}
}
}