All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.cdap.wrangler.Wrangler Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2016-2019 Cask Data, Inc.
 *
 * Licensed 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 io.cdap.wrangler;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import io.cdap.cdap.api.annotation.Description;
import io.cdap.cdap.api.annotation.Macro;
import io.cdap.cdap.api.annotation.Name;
import io.cdap.cdap.api.annotation.Plugin;
import io.cdap.cdap.api.data.format.StructuredRecord;
import io.cdap.cdap.api.data.schema.Schema;
import io.cdap.cdap.api.metrics.Metrics;
import io.cdap.cdap.api.plugin.PluginConfig;
import io.cdap.cdap.api.plugin.PluginProperties;
import io.cdap.cdap.etl.api.Emitter;
import io.cdap.cdap.etl.api.FailureCollector;
import io.cdap.cdap.etl.api.InvalidEntry;
import io.cdap.cdap.etl.api.PipelineConfigurer;
import io.cdap.cdap.etl.api.StageContext;
import io.cdap.cdap.etl.api.StageSubmitterContext;
import io.cdap.cdap.etl.api.Transform;
import io.cdap.cdap.etl.api.TransformContext;
import io.cdap.cdap.etl.api.relational.Expression;
import io.cdap.cdap.etl.api.relational.ExpressionFactory;
import io.cdap.cdap.etl.api.relational.InvalidRelation;
import io.cdap.cdap.etl.api.relational.LinearRelationalTransform;
import io.cdap.cdap.etl.api.relational.Relation;
import io.cdap.cdap.etl.api.relational.RelationalTranformContext;
import io.cdap.cdap.etl.api.relational.StringExpressionFactoryType;
import io.cdap.cdap.features.Feature;
import io.cdap.directives.aggregates.DefaultTransientStore;
import io.cdap.wrangler.api.CompileException;
import io.cdap.wrangler.api.CompileStatus;
import io.cdap.wrangler.api.Compiler;
import io.cdap.wrangler.api.Directive;
import io.cdap.wrangler.api.DirectiveLoadException;
import io.cdap.wrangler.api.DirectiveParseException;
import io.cdap.wrangler.api.EntityCountMetric;
import io.cdap.wrangler.api.ErrorRecord;
import io.cdap.wrangler.api.ExecutorContext;
import io.cdap.wrangler.api.RecipeParser;
import io.cdap.wrangler.api.RecipePipeline;
import io.cdap.wrangler.api.RecipeSymbol;
import io.cdap.wrangler.api.Row;
import io.cdap.wrangler.api.TokenGroup;
import io.cdap.wrangler.api.TransientStore;
import io.cdap.wrangler.api.TransientVariableScope;
import io.cdap.wrangler.executor.RecipePipelineExecutor;
import io.cdap.wrangler.lineage.LineageOperations;
import io.cdap.wrangler.parser.GrammarBasedParser;
import io.cdap.wrangler.parser.MigrateToV2;
import io.cdap.wrangler.parser.NoOpDirectiveContext;
import io.cdap.wrangler.parser.RecipeCompiler;
import io.cdap.wrangler.proto.Contexts;
import io.cdap.wrangler.registry.CompositeDirectiveRegistry;
import io.cdap.wrangler.registry.DirectiveInfo;
import io.cdap.wrangler.registry.DirectiveRegistry;
import io.cdap.wrangler.registry.SystemDirectiveRegistry;
import io.cdap.wrangler.registry.UserDirectiveRegistry;
import io.cdap.wrangler.utils.StructuredToRowTransformer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import javax.annotation.Nullable;

import static io.cdap.cdap.features.Feature.WRANGLER_FAIL_PIPELINE_FOR_ERROR;
import static io.cdap.wrangler.metrics.Constants.Tags.APP_ENTITY_TYPE;
import static io.cdap.wrangler.metrics.Constants.Tags.APP_ENTITY_TYPE_NAME;

/**
 * Wrangler - A interactive tool for data cleansing and transformation.
 *
 * This plugin is an implementation of the transformation that are performed in the
 * backend for operationalizing all the interactive wrangling that is being performed
 * by the user.
 */
@Plugin(type = "transform")
@Name("Wrangler")
@Description("Wrangler - A interactive tool for data cleansing and transformation.")
public class Wrangler extends Transform implements LinearRelationalTransform {
  private static final Logger LOG = LoggerFactory.getLogger(Wrangler.class);

  // Configuration specifying the dataprep application and service name.
  private static final String APPLICATION_NAME = "dataprep";
  private static final String SERVICE_NAME = "service";
  private static final String CONFIG_METHOD = "config";
  private static final String ON_ERROR_DEFAULT = "fail-pipeline";
  private static final String ON_ERROR_FAIL_PIPELINE = "fail-pipeline";
  private static final String ON_ERROR_PROCEED = "send-to-error-port";
  private static final String ERROR_STRATEGY_DEFAULT = "wrangler.error.strategy.default";

  // Directive usage metric
  public static final String DIRECTIVE_METRIC_NAME = "wrangler.directive.count";
  public static final int DIRECTIVE_METRIC_COUNT = 1;
  public static final String DIRECTIVE_ENTITY_TYPE = "directive";

  // Precondition languages
  private static final String PRECONDITION_LANGUAGE_JEXL = "jexl";
  private static final String PRECONDITION_LANGUAGE_SQL = "sql";

  // Plugin configuration.
  private final Config config;

  // Wrangle Execution RecipePipeline
  private RecipePipeline pipeline;

  // Output Schema associated with readable output.
  private Schema oSchema = null;

  // Error counter.
  private long errorCounter;

  // Precondition application
  private Precondition condition = null;

  // Transient Store
  private TransientStore store;

  // Directive registry.
  private DirectiveRegistry registry;

  // on error strategy
  private String onErrorStrategy;

  // This is used only for tests, otherwise this is being injected by the ingestion framework.
  public Wrangler(Config config) {
    this.config = config;
  }

  /**
   * Configures the plugin during deployment of the pipeline that uses the plugin.
   *
   * 

*

    *
  • Parses the directives configured. If there are any issues they will highlighted during deployment
  • *
  • Input schema is validated.
  • *
  • Compiles pre-condition expression.
  • *
*

*/ @Override public void configurePipeline(PipelineConfigurer configurer) { super.configurePipeline(configurer); FailureCollector collector = configurer.getStageConfigurer().getFailureCollector(); try { Schema iSchema = configurer.getStageConfigurer().getInputSchema(); if (!config.containsMacro(Config.NAME_FIELD) && !(config.getField().equals("*") || config.getField().equals("#"))) { validateInputSchema(iSchema, collector); } String directives = config.getDirectives(); if (config.getUDDs() != null && !config.getUDDs().trim().isEmpty()) { if (config.containsMacro("directives")) { directives = String.format("#pragma load-directives %s;", config.getUDDs()); } else { directives = String.format("#pragma load-directives %s;%s", config.getUDDs(), config.getDirectives()); } } if (!config.containsMacro(Config.NAME_PRECONDITION_LANGUAGE)) { if (PRECONDITION_LANGUAGE_SQL.equalsIgnoreCase(config.getPreconditionLanguage())) { if (!config.containsMacro(Config.NAME_PRECONDITION_SQL)) { validatePrecondition(config.getPreconditionSQL(), true, collector); } validateSQLModeDirectives(collector); } else { if (!config.containsMacro(Config.NAME_PRECONDITION)) { validatePrecondition(config.getPreconditionJEXL(), false, collector); } } } // Validate the DSL by compiling the DSL. In case of macros being // specified, the compilation will them at this phase. Compiler compiler = new RecipeCompiler(); try { // Compile the directive extracting the loadable plugins (a.k.a // Directives in this context). CompileStatus status = compiler.compile(new MigrateToV2(directives).migrate()); RecipeSymbol symbols = status.getSymbols(); if (symbols != null) { Set dynamicDirectives = symbols.getLoadableDirectives(); for (String directive : dynamicDirectives) { Object directivePlugin = configurer.usePlugin(Directive.TYPE, directive, directive, PluginProperties.builder().build()); if (directivePlugin == null) { collector.addFailure( String.format("User Defined Directive '%s' is not deployed or is not available.", directive), "Ensure the directive is deployed.") .withPluginNotFound(directive, directive, Directive.TYPE) .withConfigElement(Config.NAME_UDD, directive); } } // If the 'directives' contains macro, then we would not attempt to compile // it. if (!config.containsMacro(Config.NAME_DIRECTIVES)) { // Create the registry that only interacts with system directives. registry = SystemDirectiveRegistry.INSTANCE; Iterator iterator = symbols.iterator(); while (iterator.hasNext()) { TokenGroup group = iterator.next(); if (group != null) { String directive = (String) group.get(0).value(); DirectiveInfo directiveInfo = registry.get("", directive); if (directiveInfo == null && !dynamicDirectives.contains(directive)) { collector.addFailure( String.format("Wrangler plugin has a directive '%s' that does not exist in system or " + "user space.", directive), "Ensure the directive is loaded or the directive name is correct.") .withConfigProperty(Config.NAME_DIRECTIVES); } } } } } } catch (CompileException e) { collector.addFailure("Compilation error occurred : " + e.getMessage(), null); } catch (DirectiveParseException e) { collector.addFailure(e.getMessage(), null); } // Based on the configuration create output schema. try { if (!config.containsMacro(Config.NAME_SCHEMA)) { oSchema = Schema.parseJson(config.schema); } } catch (IOException e) { collector.addFailure("Invalid output schema.", null) .withConfigProperty(Config.NAME_SCHEMA).withStacktrace(e.getStackTrace()); } // Check if jexl pre-condition is not null or empty and if so compile expression. if (!config.containsMacro(Config.NAME_PRECONDITION) && !config.containsMacro(Config.NAME_PRECONDITION_LANGUAGE)) { if (PRECONDITION_LANGUAGE_JEXL.equalsIgnoreCase(config.getPreconditionLanguage()) && checkPreconditionNotEmpty(false)) { try { new Precondition(config.getPreconditionJEXL()); } catch (PreconditionException e) { collector.addFailure(e.getMessage(), null).withConfigProperty(Config.NAME_PRECONDITION); } } } // Set the output schema. if (oSchema != null) { configurer.getStageConfigurer().setOutputSchema(oSchema); } } catch (Exception e) { LOG.error(e.getMessage()); collector.addFailure("Error occurred : " + e.getMessage(), null).withStacktrace(e.getStackTrace()); } } /** * {@code prepareRun} is invoked by the client once before the job is submitted, but after the resolution * of macros if there are any defined. * * @param context a instance {@link StageSubmitterContext} * @throws Exception thrown if there any issue with prepareRun. */ @Override public void prepareRun(StageSubmitterContext context) throws Exception { super.prepareRun(context); // Validate input schema. If there is no input schema available then there // is no transformations that can be applied to just return. Schema inputSchema = context.getInputSchema(); if (inputSchema == null || inputSchema.getFields() == null || inputSchema.getFields().isEmpty()) { return; } // After input and output schema are validated, it's time to extract // all the fields from input and output schema. Set input = inputSchema.getFields().stream() .map(Schema.Field::getName).collect(Collectors.toSet()); // If there is input schema, but if there is no output schema, there is nothing to apply // transformations on. So, there is no point in generating field level lineage. Schema outputSchema = context.getOutputSchema(); if (outputSchema == null || outputSchema.getFields() == null || outputSchema.getFields().isEmpty()) { return; } // After input and output schema are validated, it's time to extract // all the fields from input and output schema. Set output = outputSchema.getFields().stream() .map(Schema.Field::getName).collect(Collectors.toSet()); // Parse the recipe and extract all the instances of directives // to be processed for extracting lineage. RecipeParser recipe = getRecipeParser(context); List directives = recipe.parse(); emitDirectiveMetrics(directives, context.getMetrics()); LineageOperations lineageOperations = new LineageOperations(input, output, directives); context.record(lineageOperations.generate()); } /** * Initialize the wrangler by parsing the directives and creating the runtime context. * * @param context framework context being passed. */ @Override public void initialize(TransformContext context) throws Exception { super.initialize(context); // Parse DSL and initialize the wrangle pipeline. store = new DefaultTransientStore(); RecipeParser recipe = getRecipeParser(context); ExecutorContext ctx = new WranglerPipelineContext(ExecutorContext.Environment.TRANSFORM, context, store); // Based on the configuration create output schema. try { oSchema = Schema.parseJson(config.schema); } catch (IOException e) { throw new IllegalArgumentException( String.format("Stage:%s - Format of output schema specified is invalid. Please check the format.", context.getStageName()), e ); } // Check if jexl pre-condition is not null or empty and if so compile expression. if (!config.containsMacro(Config.NAME_PRECONDITION_LANGUAGE)) { if (PRECONDITION_LANGUAGE_JEXL.equalsIgnoreCase(config.getPreconditionLanguage()) && checkPreconditionNotEmpty(false)) { try { condition = new Precondition(config.getPreconditionJEXL()); } catch (PreconditionException e) { throw new IllegalArgumentException(e.getMessage(), e); } } } try { // Create the pipeline executor with context being set. pipeline = new RecipePipelineExecutor(recipe, ctx); } catch (Exception e) { throw new Exception(String.format("Stage:%s - %s", getContext().getStageName(), e.getMessage()), e); } String defaultStrategy = context.getArguments().get(ERROR_STRATEGY_DEFAULT); onErrorStrategy = (defaultStrategy != null && config.onError == null) ? defaultStrategy : config.getOnError(); // Initialize the error counter. errorCounter = 0; } @Override public void destroy() { super.destroy(); pipeline.close(); try { registry.close(); } catch (IOException e) { LOG.warn("Unable to close the directive registry. You might see increasing number of open file handle.", e); } } /** * Transforms the input record by applying directives on the record being passed. * * @param input record to be transformed. * @param emitter to collect all the output of the transformation. * @throws Exception thrown if there are any issue with the transformation. */ @Override public void transform(StructuredRecord input, Emitter emitter) throws Exception { long start = 0; List records; try { // Creates a row as starting point for input to the pipeline. Row row = new Row(); if ("*".equalsIgnoreCase(config.getField())) { row = StructuredToRowTransformer.transform(input); } else if ("#".equalsIgnoreCase(config.getField())) { row.add(input.getSchema().getRecordName(), input); } else { row.add(config.getField(), StructuredToRowTransformer.getValue(input, config.getField())); } // If pre-condition is set, then evaluate the precondition if (PRECONDITION_LANGUAGE_JEXL.equalsIgnoreCase(config.getPreconditionLanguage()) && checkPreconditionNotEmpty(false)) { boolean skip = condition.apply(row); if (skip) { getContext().getMetrics().count("precondition.filtered", 1); return; // Expression evaluated to true, so we skip the record. } } // Reset record aggregation store. store.reset(TransientVariableScope.GLOBAL); store.reset(TransientVariableScope.LOCAL); start = System.nanoTime(); records = pipeline.execute(Collections.singletonList(row), oSchema); // We now extract errors from the execution and pass it on to the error emitter. List errors = pipeline.errors(); if (errors.size() > 0) { StringJoiner errorMessages = new StringJoiner(","); getContext().getMetrics().count("errors", errors.size()); for (ErrorRecord error : errors) { emitter.emitError(new InvalidEntry<>(error.getCode(), error.getMessage(), input)); errorMessages.add(error.getMessage()); } if (WRANGLER_FAIL_PIPELINE_FOR_ERROR.isEnabled(getContext()) && onErrorStrategy.equalsIgnoreCase(ON_ERROR_FAIL_PIPELINE)) { throw new Exception( String.format("Errors in Wrangler Transformation - %s", errorMessages)); } } } catch (Exception e) { getContext().getMetrics().count("failure", 1); if (onErrorStrategy.equalsIgnoreCase(ON_ERROR_PROCEED)) { // Emit error record, if the Error flattener or error handlers are not connected, then // the record is automatically omitted. emitter.emitError(new InvalidEntry<>(0, e.getMessage(), input)); return; } if (onErrorStrategy.equalsIgnoreCase(ON_ERROR_FAIL_PIPELINE)) { emitter.emitAlert(ImmutableMap.of( "stage", getContext().getStageName(), "code", String.valueOf(1), "message", String.format("Stopping pipeline stage %s on error %s", getContext().getStageName(), e.getMessage()), "value", String.valueOf(errorCounter) )); throw new Exception(String.format("Stage:%s - Failing pipeline due to error : %s", getContext().getStageName(), e.getMessage()), e); } // If it's 'skip-on-error' we continue processing and don't emit any error records. return; } finally { getContext().getMetrics().gauge("process.time", System.nanoTime() - start); } for (StructuredRecord record : records) { StructuredRecord.Builder builder = StructuredRecord.builder(oSchema); // Iterate through output schema, if the 'record' doesn't have it, then // attempt to take if from 'input'. for (Schema.Field field : oSchema.getFields()) { Object wObject = record.get(field.getName()); // wrangled records if (wObject == null) { builder.set(field.getName(), null); } else { if (wObject instanceof String) { builder.convertAndSet(field.getName(), (String) wObject); } else { // No need to use specific methods for fields of logical type - timestamp, date and time. This is because // the wObject should already have correct values for corresponding primitive types. builder.set(field.getName(), wObject); } } } emitter.emit(builder.build()); } } /** * Validates input schema. * * @param inputSchema configured for the plugin * @param collector failure collector */ private void validateInputSchema(@Nullable Schema inputSchema, FailureCollector collector) { if (inputSchema != null) { // Check the existence of field in input schema Schema.Field inputSchemaField = inputSchema.getField(config.getField()); if (inputSchemaField == null) { collector.addFailure(String.format("Field '%s' must be present in input schema.", config.getField()), null) .withConfigProperty(Config.NAME_FIELD); } } } private void validatePrecondition(String precondition, Boolean isConditionSQL, FailureCollector collector) { String field = Config.NAME_PRECONDITION; String language = "Precondition (JEXL)"; if (isConditionSQL == true) { field = Config.NAME_PRECONDITION_SQL; language = "Precondition (SQL)"; } if (Strings.isNullOrEmpty(precondition)) { collector.addFailure(String.format("%s must be present.", language), null) .withConfigProperty(field); } } private void validateSQLModeDirectives(FailureCollector collector) { if (!Strings.isNullOrEmpty(config.getDirectives())) { collector.addFailure("Directives are not supported for precondition of type SQL", null) .withConfigProperty(Config.NAME_DIRECTIVES); } if (!Strings.isNullOrEmpty(config.getUDDs())) { collector.addFailure("UDDs are not supported for precondition of type SQL", null) .withConfigProperty(Config.NAME_UDD); } } private boolean checkPreconditionNotEmpty(Boolean isConditionSQL) { if (!isConditionSQL && !Strings.isNullOrEmpty(config.getPreconditionJEXL()) && !config.getPreconditionJEXL().trim().isEmpty()) { return true; } if (isConditionSQL && !Strings.isNullOrEmpty(config.getPreconditionSQL()) && !config.getPreconditionSQL().trim().isEmpty()) { return true; } return false; } /** * This method creates a {@link CompositeDirectiveRegistry} and initializes the {@link RecipeParser} * with {@link NoOpDirectiveContext} * * @param context * @return * @throws DirectiveLoadException * @throws DirectiveParseException */ private RecipeParser getRecipeParser(StageContext context) throws DirectiveLoadException, DirectiveParseException { registry = new CompositeDirectiveRegistry(SystemDirectiveRegistry.INSTANCE, new UserDirectiveRegistry(context)); registry.reload(context.getNamespace()); String directives = config.getDirectives(); if (config.getUDDs() != null && !config.getUDDs().trim().isEmpty()) { directives = String.format("#pragma load-directives %s;%s", config.getUDDs(), config.getDirectives()); } return new GrammarBasedParser(context.getNamespace(), new MigrateToV2(directives).migrate(), registry); } @Override public Relation transform(RelationalTranformContext relationalTranformContext, Relation relation) { if (PRECONDITION_LANGUAGE_SQL.equalsIgnoreCase(config.getPreconditionLanguage()) && checkPreconditionNotEmpty(true)) { if (!Feature.WRANGLER_PRECONDITION_SQL.isEnabled(relationalTranformContext)) { throw new RuntimeException("SQL Precondition feature is not available"); } Optional> expressionFactory = getExpressionFactory(relationalTranformContext); if (!expressionFactory.isPresent()) { return new InvalidRelation("Cannot find an Expression Factory"); } Expression filterExpression = expressionFactory.get().compile(config.getPreconditionSQL()); return relation.filter(filterExpression); } return new InvalidRelation("Plugin is not configured for relational transformation"); } private Optional> getExpressionFactory(RelationalTranformContext ctx) { return ctx.getEngine().getExpressionFactory(StringExpressionFactoryType.SQL); } /** * This method emits all metrics for the given list of directives * * @param directives a list of Wrangler directives * @param metrics CDAP {@link Metrics} object using which metrics can be emitted */ private void emitDirectiveMetrics(List directives, Metrics metrics) throws DirectiveLoadException { for (Directive directive : directives) { // skip emitting metrics if the directive is not system directive if (registry.get(Contexts.SYSTEM, directive.define().getDirectiveName()) == null) { continue; } List countMetrics = new ArrayList<>(); // add usage metric countMetrics.add(getDirectiveUsageMetric(directive.define().getDirectiveName())); // add custom directive metrics if (directive.getCountMetrics() != null) { countMetrics.addAll(directive.getCountMetrics()); } for (EntityCountMetric countMetric : countMetrics) { Metrics child = metrics.child(getEntityMetricTags(countMetric)); child.countLong(countMetric.getName(), countMetric.getCount()); } } } private EntityCountMetric getDirectiveUsageMetric(String directiveName) { return new EntityCountMetric( DIRECTIVE_METRIC_NAME, DIRECTIVE_ENTITY_TYPE, directiveName, DIRECTIVE_METRIC_COUNT); } private Map getEntityMetricTags(EntityCountMetric metricDef) { Map tags = new HashMap<>(); tags.put(APP_ENTITY_TYPE, metricDef.getAppEntityType()); tags.put(APP_ENTITY_TYPE_NAME, metricDef.getAppEntityTypeName()); return tags; } /** * Config for the plugin. */ public static class Config extends PluginConfig { static final String NAME_PRECONDITION = "precondition"; static final String NAME_PRECONDITION_SQL = "preconditionSQL"; static final String NAME_PRECONDITION_LANGUAGE = "expressionLanguage"; static final String NAME_FIELD = "field"; static final String NAME_DIRECTIVES = "directives"; static final String NAME_UDD = "udd"; static final String NAME_SCHEMA = "schema"; static final String NAME_ON_ERROR = "on-error"; @Name(NAME_PRECONDITION_LANGUAGE) @Description("Toggle to configure precondition language between JEXL and SQL") @Macro @Nullable private String preconditionLanguage; @Name(NAME_PRECONDITION) @Description("JEXL Precondition expression specifying filtering before applying directives (true to filter)") @Macro @Nullable private String precondition; @Name(NAME_PRECONDITION_SQL) @Description("SQL Precondition expression specifying filtering before applying directives (false to filter)") @Macro @Nullable private String preconditionSQL; @Name(NAME_DIRECTIVES) @Description("Recipe for wrangling the input records") @Macro @Nullable private String directives; @Name(NAME_UDD) @Description("List of User Defined Directives (UDD) that have to be loaded.") @Nullable private String udds; @Name(NAME_FIELD) @Description("Name of the input field to be wrangled or '*' to wrangle all the fields.") @Macro private final String field; @Name(NAME_SCHEMA) @Description("Specifies the schema that has to be output.") @Macro private final String schema; @Name(NAME_ON_ERROR) @Description("How to handle error in record processing") @Macro @Nullable private final String onError; public Config(String preconditionLanguage, String precondition, String directives, String udds, String field, String schema, String onError) { this.preconditionLanguage = preconditionLanguage; this.precondition = precondition; this.directives = directives; this.udds = udds; this.preconditionSQL = precondition; this.field = field; this.schema = schema; this.onError = onError; } /** * @return if on-error is not specified returns default, else value. */ public String getOnError() { return onError == null ? ON_ERROR_DEFAULT : onError; } public String getPreconditionLanguage() { if (Strings.isNullOrEmpty(preconditionLanguage)) { // due to backward compatibility... return PRECONDITION_LANGUAGE_JEXL; } return preconditionLanguage; } public String getPreconditionJEXL() { return precondition; } public String getPreconditionSQL() { return preconditionSQL; } public String getField() { return field; } public String getDirectives() { return directives; } public String getUDDs() { return udds; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy