package eu.dnetlib.iis.common.java.jsonworkflownodes; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import org.apache.avro.specific.SpecificRecord; import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.lang.StringUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import com.google.common.base.Preconditions; import eu.dnetlib.iis.common.OrderedProperties; import eu.dnetlib.iis.common.java.PortBindings; import eu.dnetlib.iis.common.java.Process; import eu.dnetlib.iis.common.java.io.CloseableIterator; import eu.dnetlib.iis.common.java.io.DataStore; import eu.dnetlib.iis.common.java.io.FileSystemPath; import eu.dnetlib.iis.common.java.io.JsonUtils; import eu.dnetlib.iis.common.java.porttype.AnyPortType; import eu.dnetlib.iis.common.java.porttype.PortType; import eu.dnetlib.iis.common.report.test.ValueSpecMatcher; /** * Avro datastores selective fields testing consumer. * * Requirements are provided as comma separated list of properties files holding field requirements for each individual input object. * Properties keys are paths to the fields we want to test in given object, properties values are expected values (with special markup for null: $NULL$). * * Number of input avro records should be equal to the number of provided properties files. * * @author mhorst */ public class SelectiveTestingConsumer implements Process { private static final String EXPECTATION_FILE_PATHS = "expectation_file_paths"; private static final String PORT_INPUT = "datastore"; private final Map<String, PortType> inputPorts = new HashMap<String, PortType>(); private final ValueSpecMatcher valueMatcher = new ValueSpecMatcher(); //------------------------ CONSTRUCTORS -------------------------- public SelectiveTestingConsumer() { inputPorts.put(PORT_INPUT, new AnyPortType()); } //------------------------ LOGIC --------------------------------- @Override public Map<String, PortType> getInputPorts() { return inputPorts; } @Override public Map<String, PortType> getOutputPorts() { return Collections.emptyMap(); } @Override public void run(PortBindings portBindings, Configuration configuration, Map<String, String> parameters) throws Exception { Path inputRecordsPath = portBindings.getInput().get(PORT_INPUT); String expectationsPathsCSV = parameters.get(EXPECTATION_FILE_PATHS); Preconditions.checkArgument(StringUtils.isNotBlank(expectationsPathsCSV), "no '%s' property value provided, field requirements were not specified!", EXPECTATION_FILE_PATHS); FileSystem fs = FileSystem.get(configuration); if (!fs.exists(inputRecordsPath)) { throw new RuntimeException(inputRecordsPath + " hdfs location does not exist!"); } try (CloseableIterator<SpecificRecord> recordIterator = DataStore.getReader(new FileSystemPath(fs, inputRecordsPath))) { String[] recordExpectationsLocations = StringUtils.split(expectationsPathsCSV, ','); int recordCount = 0; while (recordIterator.hasNext()) { SpecificRecord record = recordIterator.next(); recordCount++; if (recordCount > recordExpectationsLocations.length) { throw new RuntimeException("got more records than expected: " + "unable to verify record no " + recordCount + ", no field specification provided! Record contents: " + JsonUtils.toPrettyJSON(record.toString())); } else { validateRecord(record, readExpectedValues(recordExpectationsLocations[recordCount - 1])); } } if (recordCount < recordExpectationsLocations.length) { throw new RuntimeException( "records count mismatch: " + "got: " + recordCount + " expected: " + recordExpectationsLocations.length); } } } //------------------------ PRIVATE --------------------------------- /** * Reads expected values from given location. */ private Properties readExpectedValues(String location) throws IOException { Properties properties = new OrderedProperties(); properties.load(TestingConsumer.class.getResourceAsStream(location.trim())); return properties; } /** * Validates record fields against specified expectations. RuntimeException is thrown when invalid. * * @param record avro record to be validated * @param recordFieldExpectations set of field expectations defined as properties where key is field location and value is expected value */ private void validateRecord(SpecificRecord record, Properties recordFieldExpectations) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { for (Entry<Object, Object> fieldExpectation : recordFieldExpectations.entrySet()) { String currentValue = PropertyUtils.getNestedProperty(record, (String)fieldExpectation.getKey()).toString(); String expectedValue = fieldExpectation.getValue().toString(); if (!valueMatcher.matches(currentValue, expectedValue)) { throw new RuntimeException( "invalid field value for path: " + fieldExpectation.getKey() + ", expected: '" + fieldExpectation.getValue() + "', " + "got: '" + currentValue + "' Full object content: " + JsonUtils.toPrettyJSON(record.toString())); } } } }