blob: 6ae5b3cc1d700bf49072dda8294d072332297d6b [file] [log] [blame]
/*
* 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.seatunnel.datasource.configuration.util;
import org.apache.seatunnel.datasource.configuration.Option;
import lombok.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Validation rule for {@link Option}.
* <p>
* The option rule is typically built in one of the following pattern:
*
* <pre>{@code
* // simple rule
* OptionRule simpleRule = OptionRule.builder()
* .optional(POLL_TIMEOUT, POLL_INTERVAL)
* .required(CLIENT_SERVICE_URL)
* .build();
*
* // basic full rule
* OptionRule fullRule = OptionRule.builder()
* .optional(POLL_TIMEOUT, POLL_INTERVAL, CURSOR_STARTUP_MODE)
* .required(CLIENT_SERVICE_URL, ADMIN_SERVICE_URL)
* .exclusive(TOPIC_PATTERN, TOPIC)
* .conditional(CURSOR_STARTUP_MODE, StartMode.TIMESTAMP, CURSOR_STARTUP_TIMESTAMP)
* .build();
*
* // complex conditional rule
* // moot expression
* Expression expression = Expression.of(TOPIC_DISCOVERY_INTERVAL, 200)
* .and(Expression.of(Condition.of(CURSOR_STARTUP_MODE, StartMode.EARLIEST)
* .or(CURSOR_STARTUP_MODE, StartMode.LATEST)))
* .or(Expression.of(Condition.of(TOPIC_DISCOVERY_INTERVAL, 100)))
*
* OptionRule complexRule = OptionRule.builder()
* .optional(POLL_TIMEOUT, POLL_INTERVAL, CURSOR_STARTUP_MODE)
* .required(CLIENT_SERVICE_URL, ADMIN_SERVICE_URL)
* .exclusive(TOPIC_PATTERN, TOPIC)
* .conditional(expression, CURSOR_RESET_MODE)
* .build();
* }</pre>
*/
public class OptionRule {
/**
* Optional options with default value.
*
* <p> This options will not be validated.
* <p> This is used by the web-UI to show what options are available.
*/
private final List<Option<?>> optionalOptions;
/**
* Required options with no default value.
*
* <p> Verify that the option is valid through the defined rules.
*/
private final List<RequiredOption> requiredOptions;
OptionRule(List<Option<?>> optionalOptions, List<RequiredOption> requiredOptions) {
this.optionalOptions = optionalOptions;
this.requiredOptions = requiredOptions;
}
public List<Option<?>> getOptionalOptions() {
return optionalOptions;
}
public List<RequiredOption> getRequiredOptions() {
return requiredOptions;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof OptionRule)) {
return false;
}
OptionRule that = (OptionRule) o;
return Objects.equals(optionalOptions, that.optionalOptions)
&& Objects.equals(requiredOptions, that.requiredOptions);
}
@Override
public int hashCode() {
return Objects.hash(optionalOptions, requiredOptions);
}
public static Builder builder() {
return new Builder();
}
/**
* Builder for {@link OptionRule}.
*/
public static class Builder {
private final List<Option<?>> optionalOptions = new ArrayList<>();
private final List<RequiredOption> requiredOptions = new ArrayList<>();
private Builder() {
}
/**
* Optional options
*
* <p> This options will not be validated.
* <p> This is used by the web-UI to show what options are available.
*/
public Builder optional(@NonNull Option<?>... options) {
for (Option<?> option : options) {
verifyDuplicate(option, "OptionsOption");
}
this.optionalOptions.addAll(Arrays.asList(options));
return this;
}
/**
* Absolutely required options without any constraints.
*/
public Builder required(@NonNull Option<?>... options) {
for (Option<?> option : options) {
verifyDuplicate(option, "RequiredOption");
verifyRequiredOptionDefaultValue(option);
}
this.requiredOptions.add(RequiredOption.AbsolutelyRequiredOptions.of(options));
return this;
}
/**
* Exclusive options, only one of the options needs to be configured.
*/
public Builder exclusive(@NonNull Option<?>... options) {
if (options.length <= 1) {
throw new OptionValidationException("The number of exclusive options must be greater than 1.");
}
for (Option<?> option : options) {
verifyDuplicate(option, "ExclusiveOption");
verifyRequiredOptionDefaultValue(option);
}
this.requiredOptions.add(RequiredOption.ExclusiveRequiredOptions.of(options));
return this;
}
public <T> Builder conditional(@NonNull Option<T> conditionalOption, @NonNull List<T> expectValues, @NonNull Option<?>... requiredOptions) {
for (Option<?> o : requiredOptions) {
verifyDuplicate(o, "ConditionalOption");
verifyRequiredOptionDefaultValue(o);
}
verifyConditionalExists(conditionalOption);
if (expectValues.size() == 0) {
throw new OptionValidationException(
String.format("conditional option '%s' must have expect values .", conditionalOption.key()));
}
/**
* Each parameter can only be controlled by one other parameter
*/
Expression expression = Expression.of(Condition.of(conditionalOption, expectValues.get(0)));
for (int i = 0; i < expectValues.size(); i++) {
if (i != 0) {
expression = expression.or(Expression.of(Condition.of(conditionalOption, expectValues.get(i))));
}
}
this.requiredOptions.add(RequiredOption.ConditionalRequiredOptions.of(expression,
new ArrayList<>(Arrays.asList(requiredOptions))));
return this;
}
public <T> Builder conditional(@NonNull Option<T> conditionalOption, @NonNull T expectValue, @NonNull Option<?>... requiredOptions) {
for (Option<?> o : requiredOptions) {
verifyDuplicate(o, "ConditionalOption");
verifyRequiredOptionDefaultValue(o);
}
verifyConditionalExists(conditionalOption);
/**
* Each parameter can only be controlled by one other parameter
*/
Expression expression = Expression.of(Condition.of(conditionalOption, expectValue));
this.requiredOptions.add(RequiredOption.ConditionalRequiredOptions.of(expression,
new ArrayList<>(Arrays.asList(requiredOptions))));
return this;
}
/**
* Bundled options, must be present or absent together.
*/
public Builder bundled(@NonNull Option<?>... requiredOptions) {
for (Option<?> option : requiredOptions) {
verifyDuplicate(option, "BundledOption");
}
this.requiredOptions.add(RequiredOption.BundledRequiredOptions.of(requiredOptions));
return this;
}
public OptionRule build() {
return new OptionRule(optionalOptions, requiredOptions);
}
private void verifyRequiredOptionDefaultValue(@NonNull Option<?> option) {
if (option.defaultValue() != null) {
throw new OptionValidationException(
String.format("Required option '%s' should have no default value.", option.key()));
}
}
private void verifyDuplicate(@NonNull Option<?> option, @NonNull String currentOptionType) {
if (optionalOptions.contains(option)) {
throw new OptionValidationException(
String.format("%s '%s' duplicate in option options.", currentOptionType, option.key()));
}
requiredOptions.forEach(requiredOption -> {
if (requiredOption.getOptions().contains(option)) {
throw new OptionValidationException(
String.format("%s '%s' duplicate in '%s'.", currentOptionType, option.key(), requiredOption.getClass().getName()));
}
});
}
private void verifyConditionalExists(@NonNull Option<?> option) {
boolean inOptions = optionalOptions.contains(option);
AtomicBoolean inRequired = new AtomicBoolean(false);
requiredOptions.forEach(requiredOption -> {
if (requiredOption.getOptions().contains(option)) {
inRequired.set(true);
}
});
if (!inOptions && !inRequired.get()) {
throw new OptionValidationException(
String.format("Conditional '%s' not found in options.", option.key()));
}
}
}
}