/* * 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.flink.api.java.utils; import org.apache.flink.annotation.PublicEvolving; import java.util.HashMap; import java.util.List; import java.util.LinkedList; import java.util.Map; import java.util.Objects; /** * Facility to manage required parameters in user defined functions. */ @PublicEvolving public class RequiredParameters { private static final String HELP_TEXT_PARAM_DELIMITER = "\t"; private static final String HELP_TEXT_LINE_DELIMITER = "\n"; private static final int HELP_TEXT_LENGTH_PER_PARAM = 100; private HashMap<String, Option> data; public RequiredParameters() { this.data = new HashMap<>(); } /** * Add a parameter based on its name. * * @param name - the name of the parameter * @return - an {@link Option} object representing the parameter * @throws RequiredParametersException if an option with the same name is already defined */ public Option add(String name) throws RequiredParametersException { if (!this.data.containsKey(name)) { Option option = new Option(name); this.data.put(name, option); return option; } else { throw new RequiredParametersException("Option with key " + name + " already exists."); } } /** * Add a parameter encapsulated in an {@link Option} object. * * @param option - the parameter * @throws RequiredParametersException if an option with the same name is already defined */ public void add(Option option) throws RequiredParametersException { if (!this.data.containsKey(option.getName())) { this.data.put(option.getName(), option); } else { throw new RequiredParametersException("Option with key " + option.getName() + " already exists."); } } /** * Check for all required parameters defined: * - has a value been passed * - if not, does the parameter have an associated default value * - does the type of the parameter match the one defined in RequiredParameters * - does the value provided in the parameterTool adhere to the choices defined in the option * * If any check fails, a RequiredParametersException is thrown * * @param parameterTool - parameters supplied by the user. * @throws RequiredParametersException if any of the specified checks fail */ public void applyTo(ParameterTool parameterTool) throws RequiredParametersException { List<String> missingArguments = new LinkedList<>(); for (Option o : data.values()) { if (parameterTool.data.containsKey(o.getName())) { if (Objects.equals(parameterTool.data.get(o.getName()), ParameterTool.NO_VALUE_KEY)) { // the parameter has been passed, but no value, check if there is a default value checkAndApplyDefaultValue(o, parameterTool.data); } else { // a value has been passed in the parameterTool, now check if it adheres to all constraints checkAmbiguousValues(o, parameterTool.data); checkIsCastableToDefinedType(o, parameterTool.data); checkChoices(o, parameterTool.data); } } else { // check if there is a default name or a value passed for a possibly defined alternative name. if (hasNoDefaultValueAndNoValuePassedOnAlternativeName(o, parameterTool.data)) { missingArguments.add(o.getName()); } } } if (!missingArguments.isEmpty()) { throw new RequiredParametersException(this.missingArgumentsText(missingArguments), missingArguments); } } // check if the given parameter has a default value and add it to the passed map if that is the case // else throw an exception private void checkAndApplyDefaultValue(Option o, Map<String, String> data) throws RequiredParametersException { if (hasNoDefaultValueAndNoValuePassedOnAlternativeName(o, data)) { throw new RequiredParametersException("No default value for undefined parameter " + o.getName()); } } // check if the value in the given map which corresponds to the name of the given option // is castable to the type of the option (if any is defined) private void checkIsCastableToDefinedType(Option o, Map<String, String> data) throws RequiredParametersException { if (o.hasType() && !o.isCastableToDefinedType(data.get(o.getName()))) { throw new RequiredParametersException("Value for parameter " + o.getName() + " cannot be cast to type " + o.getType()); } } // check if the value in the given map which corresponds to the name of the given option // adheres to the list of given choices for the param in the options (if any are defined) private void checkChoices(Option o, Map<String, String> data) throws RequiredParametersException { if (o.getChoices().size() > 0 && !o.getChoices().contains(data.get(o.getName()))) { throw new RequiredParametersException("Value " + data.get(o.getName()) + " is not in the list of valid choices for key " + o.getName()); } } // move value passed on alternative name to standard name or apply default value if any defined // else return true to indicate parameter is 'really' missing private boolean hasNoDefaultValueAndNoValuePassedOnAlternativeName(Option o, Map<String, String> data) throws RequiredParametersException { if (o.hasAlt() && data.containsKey(o.getAlt())) { data.put(o.getName(), data.get(o.getAlt())); } else { if (o.hasDefaultValue()) { data.put(o.getName(), o.getDefaultValue()); if (o.hasAlt()) { data.put(o.getAlt(), o.getDefaultValue()); } } else { return true; } } return false; } // given that the map contains a value for the name of the option passed // check if it also contains a value for the shortName in option (if any is defined) private void checkAmbiguousValues(Option o, Map<String, String> data) throws RequiredParametersException{ if (data.containsKey(o.getAlt()) && !Objects.equals(data.get(o.getAlt()), ParameterTool.NO_VALUE_KEY)) { throw new RequiredParametersException("Value passed for parameter " + o.getName() + " is ambiguous. Value passed for short and long name."); } } /** * Build a help text for the defined parameters. * * The format of the help text will be: * Required Parameters: * \t -:shortName:, --:name: \t :helpText: \t default: :defaultValue: \t choices: :choices: \n * * @return a formatted help String. */ public String getHelp() { StringBuilder sb = new StringBuilder(data.size() * HELP_TEXT_LENGTH_PER_PARAM); sb.append("Required Parameters:"); sb.append(HELP_TEXT_LINE_DELIMITER); for (Option o : data.values()) { sb.append(this.helpText(o)); } sb.append(HELP_TEXT_LINE_DELIMITER); return sb.toString(); } /** * Build a help text for the defined parameters and list the missing arguments at the end of the text. * * The format of the help text will be: * Required Parameters: * \t -:shortName:, --:name: \t :helpText: \t default: :defaultValue: \t choices: :choices: \n * * Missing parameters: * \t param1 param2 ... paramN * * @param missingArguments - a list of missing parameters * @return a formatted help String. */ public String getHelp(List<String> missingArguments) { return this.getHelp() + this.missingArgumentsText(missingArguments); } /** * for the given option create a line for the help text which looks like: * \t -:shortName:, --:name: \t :helpText: \t default: :defaultValue: \t choices: :choices: */ private String helpText(Option option) { StringBuilder sb = new StringBuilder(HELP_TEXT_LENGTH_PER_PARAM); sb.append(HELP_TEXT_PARAM_DELIMITER); // if there is a short name, add it. if (option.hasAlt()) { sb.append("-"); sb.append(option.getAlt()); sb.append(", "); } // add the name sb.append("--"); sb.append(option.getName()); sb.append(HELP_TEXT_PARAM_DELIMITER); // if there is a help text, add it if (option.getHelpText() != null) { sb.append(option.getHelpText()); sb.append(HELP_TEXT_PARAM_DELIMITER); } // if there is a default value, add it. if (option.hasDefaultValue()) { sb.append("default: "); sb.append(option.getDefaultValue()); sb.append(HELP_TEXT_PARAM_DELIMITER); } // if there is a list of choices add it. if (!option.getChoices().isEmpty()) { sb.append("choices: "); for (String choice : option.getChoices()) { sb.append(choice); sb.append(" "); } } sb.append(HELP_TEXT_LINE_DELIMITER); return sb.toString(); } private String missingArgumentsText(List<String> missingArguments) { StringBuilder sb = new StringBuilder(missingArguments.size() * 10); sb.append("Missing arguments for:"); sb.append(HELP_TEXT_LINE_DELIMITER); for (String arg : missingArguments) { sb.append(arg); sb.append(" "); } return sb.toString(); } }