DefaultAtlasValidationService.java
/*
* Copyright (C) 2017 Red Hat, 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.atlasmap.core;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.atlasmap.api.AtlasValidationService;
import io.atlasmap.spi.AtlasValidator;
import io.atlasmap.v2.AtlasMapping;
import io.atlasmap.v2.BaseMapping;
import io.atlasmap.v2.DataSource;
import io.atlasmap.v2.Field;
import io.atlasmap.v2.FieldGroup;
import io.atlasmap.v2.LookupTable;
import io.atlasmap.v2.LookupTables;
import io.atlasmap.v2.Mapping;
import io.atlasmap.v2.MappingType;
import io.atlasmap.v2.Mappings;
import io.atlasmap.v2.Validation;
import io.atlasmap.v2.ValidationScope;
import io.atlasmap.v2.ValidationStatus;
import io.atlasmap.validators.CompositeValidator;
import io.atlasmap.validators.LookupTableNameValidator;
import io.atlasmap.validators.NonNullValidator;
import io.atlasmap.validators.NotEmptyValidator;
import io.atlasmap.validators.PositiveIntegerValidator;
import io.atlasmap.validators.StringPatternValidator;
public class DefaultAtlasValidationService implements AtlasValidationService {
enum Validators {
MAPPING_NAME (() -> {
StringPatternValidator namePattern = new StringPatternValidator(
ValidationScope.ALL,
"Mapping name must not contain spaces nor special characters other than period (.) and underscore (_), but was '%s'",
"[^A-Za-z0-9_.]");
NonNullValidator nameNotNull = new NonNullValidator(
ValidationScope.ALL, "Mapping name must not be null nor empty");
return new CompositeValidator(namePattern, nameNotNull);
}),
DATASOURCE_TARGET_URI (() ->
new NonNullValidator(ValidationScope.DATA_SOURCE, "DataSource target uri must not be null nor empty")
),
DATASOURCE_SOURCE_URI (() ->
new NonNullValidator(ValidationScope.DATA_SOURCE, "DataSource source uri must not be null nor empty")
),
MAPPINGS_NOT_NULL (() ->
new NonNullValidator(ValidationScope.MAPPING, "Field mappings must not be null")
),
COMBINE_INPUT_NOT_NULL (() ->
new NonNullValidator(ValidationScope.MAPPING, "Source field should not be null")
),
COMBINE_INPUT_FIELD_NOT_EMPTY (() ->
new NotEmptyValidator(ValidationScope.MAPPING, "Source field should not be empty")
),
COMBINE_OUTPUT_NOT_NULL (() ->
new NonNullValidator(ValidationScope.MAPPING, "Target element must not be null")
),
COMBINE_OUTPUT_FIELD_NOT_EMPTY (() ->
new NotEmptyValidator(ValidationScope.MAPPING, "Target field must not be empty")
),
COMBINE_INPUT_FIELD_NOT_NULL (() ->
new NonNullValidator(
ValidationScope.MAPPING, "Source fields should not be null")
),
COMBINE_INPUT_FIELD_FIELD_ACTION_INDEX_POSITIVE (() ->
new PositiveIntegerValidator(
ValidationScope.MAPPING, "MapAction index must exists and be greater than or equal to zero (0), but was '%s'")
),
MAP_INPUT_NOT_NULL (() ->
new NonNullValidator(ValidationScope.MAPPING, "Source field must not be null")
),
MAP_INPUT_FIELD_NOT_EMPTY (() ->
new NotEmptyValidator(ValidationScope.MAPPING, "Source field must not be empty")
),
MAP_OUTPUT_NOT_NULL (() ->
new NonNullValidator(ValidationScope.MAPPING, "Target field should not be null")
),
MAP_OUTPUT_FIELD_NOT_EMPTY (() ->
new NotEmptyValidator(ValidationScope.MAPPING, "Target field should not be empty")
),
SEPARATE_INPUT_NOT_NULL (() ->
new NonNullValidator(ValidationScope.MAPPING, "Source field must not be null")
),
SEPARATE_INPUT_FIELD_NOT_NULL (() ->
new NonNullValidator(ValidationScope.MAPPING, "Source field must not be null")
),
SEPARATE_INPUT_FIELD_NOT_EMPTY (() ->
new NotEmptyValidator(ValidationScope.MAPPING, "Source field must not be empty")
),
SEPARATE_OUTPUT_NOT_NULL (() ->
new NonNullValidator(ValidationScope.MAPPING, "Target field should not be null")
),
SEPARATE_OUTPUT_FIELD_NOT_NULL (() ->
new NonNullValidator(ValidationScope.MAPPING, "Target fields should not be null")
),
SEPARATE_OUTPUT_FIELD_NOT_EMPTY (() ->
new NotEmptyValidator(ValidationScope.MAPPING, "Target fields should not be empty")
),
SEPARATE_OUTPUT_FIELD_FIELD_ACTION_NOT_EMPTY (() ->
new NotEmptyValidator(ValidationScope.MAPPING, "Field actions cannot be null or empty")
),
SEPARATE_OUTPUT_FIELD_FIELD_ACTION_INDEX_POSITIVE (() ->
new PositiveIntegerValidator(ValidationScope.MAPPING, "MapAction index must exists and be greater than or equal to zero (0), but was '%s'")
),
LOOKUPTABLE_NAME_CHECK_FOR_DUPLICATE (() ->
new LookupTableNameValidator("LookupTables contain duplicated LookupTable names '%s'.")
);
private final AtlasValidator validator;
private Validators(Supplier<AtlasValidator> s) {
validator = s.get();
}
public AtlasValidator get() {
return validator;
}
}
@Override
public List<Validation> validateMapping(AtlasMapping mapping) {
if (mapping == null) {
throw new IllegalArgumentException("Mapping definition must not be null");
}
List<Validation> validations = new ArrayList<>();
Validators.MAPPING_NAME.get().validate(mapping.getName(), validations, null);
List<DataSource> dataSources = mapping.getDataSource();
for (DataSource ds : dataSources) {
switch (ds.getDataSourceType()) {
case SOURCE:
Validators.DATASOURCE_SOURCE_URI.get().validate(ds.getUri(), validations, ds.getId());
break;
case TARGET:
Validators.DATASOURCE_TARGET_URI.get().validate(ds.getUri(), validations, ds.getId());
break;
default:
throw new IllegalArgumentException(String.format("Unknown DataSource type '%s'", ds.getDataSourceType()));
}
}
validateFieldMappings(mapping.getMappings(), mapping.getLookupTables(), validations);
return validations;
}
private void validateFieldMappings(Mappings mappings, LookupTables lookupTables, List<Validation> validations) {
Validators.MAPPINGS_NOT_NULL.get().validate(mappings, validations, null);
if (mappings != null) {
List<BaseMapping> fieldMappings = mappings.getMapping();
if (fieldMappings != null && !fieldMappings.isEmpty()) {
List<Mapping> mapFieldMappings = fieldMappings.stream()
.filter(p -> p.getMappingType() == MappingType.MAP).map(p -> (Mapping) p)
.collect(Collectors.toList());
List<Mapping> combineFieldMappings = fieldMappings.stream()
.filter(p -> p.getMappingType() == MappingType.COMBINE).map(p -> (Mapping) p)
.collect(Collectors.toList());
List<Mapping> separateFieldMappings = fieldMappings.stream()
.filter(p -> p.getMappingType() == MappingType.SEPARATE).map(p -> (Mapping) p)
.collect(Collectors.toList());
List<Mapping> lookupFieldMappings = fieldMappings.stream()
.filter(p -> p.getMappingType() == MappingType.LOOKUP).map(p -> (Mapping) p)
.collect(Collectors.toList());
Set<String> usedIds = new HashSet<>();
validateMapMapping(mapFieldMappings, validations, usedIds);
validateCombineMapping(combineFieldMappings, validations, usedIds);
validateSeparateMapping(separateFieldMappings, validations, usedIds);
validateLookupTables(lookupFieldMappings, lookupTables, validations, usedIds);
}
}
}
private void validateLookupTables(List<Mapping> lookupFieldMappings, LookupTables lookupTables,
List<Validation> validations, Set<String> usedIds) {
if (lookupTables != null && lookupTables.getLookupTable() != null && !lookupTables.getLookupTable().isEmpty()) {
// check for duplicate names
Validators.LOOKUPTABLE_NAME_CHECK_FOR_DUPLICATE.get().validate(lookupTables, validations, null);
if (lookupFieldMappings.isEmpty()) {
Validation validation = new Validation();
validation.setScope(ValidationScope.LOOKUP_TABLE);
validation.setMessage("LookupTables are defined but no LookupFields are utilized.");
validation.setStatus(ValidationStatus.WARN);
validations.add(validation);
} else {
validateLookupFieldMapping(lookupFieldMappings, lookupTables, validations, usedIds);
}
}
}
// mapping field validations
private void validateLookupFieldMapping(List<Mapping> fieldMappings, LookupTables lookupTables,
List<Validation> validations, Set<String> usedIds) {
Set<String> lookupFieldMappingTableNameRefs = fieldMappings.stream().map(Mapping::getLookupTableName)
.collect(Collectors.toSet());
Set<String> tableNames = lookupTables.getLookupTable().stream().map(LookupTable::getName)
.collect(Collectors.toSet());
if (!lookupFieldMappingTableNameRefs.isEmpty() && !tableNames.isEmpty()) {
Set<String> disjoint = Stream.concat(lookupFieldMappingTableNameRefs.stream(), tableNames.stream())
.collect(Collectors.toMap(Function.identity(), t -> true, (a, b) -> null)).keySet();
if (!disjoint.isEmpty()) {
boolean isInFieldList = !lookupFieldMappingTableNameRefs.stream().filter(disjoint::contains)
.collect(Collectors.toList()).isEmpty();
boolean isInTableNameList = !tableNames.stream().filter(disjoint::contains).collect(Collectors.toList())
.isEmpty();
// which list has the disjoin.... if its the lookup fields then ERROR
if (isInFieldList) {
Validation validation = new Validation();
validation.setScope(ValidationScope.LOOKUP_TABLE);
validation.setMessage(
"One ore more LookupFieldMapping references a non existent LookupTable name in the mapping: " + disjoint.toString());
validation.setStatus(ValidationStatus.ERROR);
validations.add(validation);
}
// check that if a name exists in table names that at least one field mapping
// uses it, else WARN
if (isInTableNameList) {
Validation validation = new Validation();
validation.setScope(ValidationScope.LOOKUP_TABLE);
validation.setMessage("A LookupTable is defined but not used by any LookupField: " + disjoint.toString());
validation.setStatus(ValidationStatus.WARN);
validations.add(validation);
}
}
}
for (Mapping fieldMapping : fieldMappings) {
String mappingId = fieldMapping.getId();
validateMappingId(mappingId, usedIds, validations);
if (fieldMapping.getInputField() != null) {
Validators.MAP_INPUT_FIELD_NOT_EMPTY.get().validate(fieldMapping.getInputField(), validations, mappingId);
}
Validators.MAP_OUTPUT_NOT_NULL.get().validate(fieldMapping.getOutputField(), validations,
mappingId, ValidationStatus.WARN);
if (fieldMapping.getOutputField() != null) {
Validators.MAP_OUTPUT_FIELD_NOT_EMPTY.get().validate(fieldMapping.getOutputField(), validations,
mappingId, ValidationStatus.WARN);
}
}
}
private void validateMapMapping(List<Mapping> fieldMappings, List<Validation> validations, Set<String> usedIds) {
for (Mapping fieldMapping : fieldMappings) {
String mappingId = fieldMapping.getId();
FieldGroup sourceFieldGroup = fieldMapping.getInputFieldGroup();
List<Field> sourceFields = sourceFieldGroup != null ? sourceFieldGroup.getField() : fieldMapping.getInputField();
validateMappingId(mappingId, usedIds, validations);
Validators.MAP_INPUT_NOT_NULL.get().validate(sourceFields, validations, mappingId);
if (fieldMapping.getInputField() != null) {
Validators.MAP_INPUT_FIELD_NOT_EMPTY.get().validate(sourceFields, validations, mappingId);
}
Validators.MAP_OUTPUT_NOT_NULL.get().validate(fieldMapping.getOutputField(), validations,
mappingId, ValidationStatus.WARN);
if (fieldMapping.getOutputField() != null) {
Validators.MAP_OUTPUT_FIELD_NOT_EMPTY.get().validate(fieldMapping.getOutputField(), validations,
mappingId, ValidationStatus.WARN);
}
}
}
private void validateSeparateMapping(List<Mapping> fieldMappings, List<Validation> validations, Set<String> usedIds) {
for (Mapping fieldMapping : fieldMappings) {
String mappingId = fieldMapping.getId();
validateMappingId(mappingId, usedIds, validations);
Validators.SEPARATE_INPUT_NOT_NULL.get().validate(fieldMapping.getInputField(), validations, mappingId);
if (fieldMapping.getInputField() != null) {
Validators.SEPARATE_INPUT_FIELD_NOT_EMPTY.get().validate(fieldMapping.getInputField(), validations, mappingId);
// source must be a String type
}
Validators.SEPARATE_OUTPUT_NOT_NULL.get().validate(fieldMapping.getOutputField(), validations,
mappingId, ValidationStatus.WARN);
Validators.SEPARATE_OUTPUT_FIELD_NOT_EMPTY.get().validate(fieldMapping.getOutputField(), validations,
mappingId, ValidationStatus.WARN);
if (fieldMapping.getOutputField() != null) {
for (Field field : fieldMapping.getOutputField()) {
Validators.SEPARATE_OUTPUT_FIELD_NOT_NULL.get().validate(field, validations, mappingId);
if (field.getIndex() == null || field.getIndex() < 0) {
Validators.SEPARATE_OUTPUT_FIELD_FIELD_ACTION_INDEX_POSITIVE.get().validate(field.getIndex(),
validations, mappingId);
}
}
}
}
}
private void validateCombineMapping(List<Mapping> fieldMappings, List<Validation> validations, Set<String> usedIds) {
for (Mapping fieldMapping : fieldMappings) {
String mappingId = fieldMapping.getId();
validateMappingId(mappingId, usedIds, validations);
Validators.COMBINE_OUTPUT_NOT_NULL.get().validate(fieldMapping.getOutputField(), validations, mappingId);
if (fieldMapping.getOutputField() != null) {
Validators.COMBINE_OUTPUT_FIELD_NOT_EMPTY.get().validate(fieldMapping.getOutputField(), validations, mappingId);
// source must be a String type
}
Validators.COMBINE_INPUT_NOT_NULL.get().validate(fieldMapping.getInputField(), validations,
mappingId, ValidationStatus.WARN);
Validators.COMBINE_INPUT_FIELD_NOT_EMPTY.get().validate(fieldMapping.getInputField(), validations,
mappingId, ValidationStatus.WARN);
if (fieldMapping.getInputField() != null) {
for (Field field : fieldMapping.getInputField()) {
Validators.COMBINE_INPUT_FIELD_NOT_NULL.get().validate(field, validations, mappingId);
if (field.getIndex() == null || field.getIndex() < 0) {
Validators.COMBINE_INPUT_FIELD_FIELD_ACTION_INDEX_POSITIVE.get().validate(field.getIndex(),
validations, mappingId);
}
}
}
}
}
private void validateMappingId(String id, Set<String> usedIds, List<Validation> validations) {
if (id == null) {
return;
}
if (usedIds.contains(id)) {
Validation validation = new Validation();
validation.setScope(ValidationScope.MAPPING);
validation.setMessage(String.format("Duplicated mapping ID '%s' is found", id));
validation.setStatus(ValidationStatus.WARN);
validations.add(validation);
} else {
usedIds.add(id);
}
}
}