com.denkbares.semanticcore.utils.ResultTableModel.java Source code

Java tutorial

Introduction

Here is the source code for com.denkbares.semanticcore.utils.ResultTableModel.java

Source

/*
 * Copyright (C) 2013 Chair of Artificial Intelligence and Applied Informatics
 * Computer Science VI, University of Wuerzburg
 *
 * This is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 3 of the License, or (at your option) any
 * later version.
 *
 * This software is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this software; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
 * site: http://www.fsf.org.
 */
package com.denkbares.semanticcore.utils;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.csv.CSVRecord;
import org.eclipse.rdf4j.model.BNode;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.impl.LiteralImpl;
import org.eclipse.rdf4j.query.BindingSet;
import org.jetbrains.annotations.NotNull;

import com.denkbares.collections.DefaultMultiMap;
import com.denkbares.collections.MultiMap;
import com.denkbares.collections.MultiMaps;
import com.denkbares.collections.SubSpanIterator;
import com.denkbares.semanticcore.CachedTupleQueryResult;
import com.denkbares.semanticcore.TupleQueryResult;
import com.denkbares.utils.Pair;
import de.d3web.testing.Message;
import de.d3web.testing.Message.Type;

public class ResultTableModel implements Iterable<TableRow> {

    public static final int MAX_DISPLAYED_FAILRUES = 20;

    public Map<Value, Set<TableRow>> getData() {
        if (groupedRows == null) {
            Map<Value, Set<TableRow>> data = new LinkedHashMap<>();
            for (TableRow row : rows) {
                Value firstNode = row.getValue(variables.get(0));
                Set<TableRow> nodeRows = data.computeIfAbsent(firstNode, k -> new HashSet<>());
                nodeRows.add(row);
            }
            groupedRows = data;
        }
        return groupedRows;
    }

    private final Collection<TableRow> rows = new LinkedHashSet<>();

    private final List<String> variables;

    private Map<Value, Set<TableRow>> groupedRows = null;

    public int getSize() {
        return rows.size();
    }

    private final List<Comparator<TableRow>> comparators = new LinkedList<>();

    public ResultTableModel(CachedTupleQueryResult result) {
        this.variables = result.getBindingNames();
        populateTable(result);
        result.close();
    }

    public ResultTableModel(List<TableRow> rows, List<String> variables) {
        this.variables = variables;
        rows.forEach(this::importRow);
    }

    public ResultTableModel(List<String> variables) {
        this.variables = variables;
    }

    public boolean contains(TableRow row) {
        return rows.contains(row);
    }

    @NotNull
    @Override
    public Iterator<TableRow> iterator() {
        if (comparators.isEmpty()) {
            return rows.iterator();
        } else {
            ArrayList<TableRow> tableRows = new ArrayList<>(rows);
            Collections.reverse(comparators);
            for (Comparator<TableRow> comparator : comparators) {
                tableRows.sort(comparator);
            }
            return tableRows.iterator();
        }
    }

    public void sortRows(List<Pair<String, Boolean>> sorting) {
        for (Pair<String, Boolean> stringBooleanPair : sorting) {
            final int factor = stringBooleanPair.getB() ? 1 : -1;
            Comparator<TableRow> comparator = (o1, o2) -> {
                Value v1 = o1.getValue(stringBooleanPair.getA());
                Value v2 = o2.getValue(stringBooleanPair.getA());
                int result = (v1 == v2) ? 0
                        : (v1 == null) ? -1 : (v2 == null) ? 1 : v1.stringValue().compareTo(v2.stringValue());
                return factor * result;
            };
            comparators.add(comparator);
        }
    }

    /**
     * Returns an iterator for a subset of the rows, starting from row 'start' inclusively (where 0
     * is the first row) and end before row "end" (exclusively). If "start" is below 0, it will be
     * assumed as 0. If "end" is above the current number of rows or end is below 0, it will be
     * assumed to be the number of rows.
     *
     * @param start the first row to iterate
     * @param end   the row to stop iteration before
     * @return an iterator for the sub-span of rows
     */
    public Iterator<TableRow> iterator(int start, int end) {
        return new SubSpanIterator<>(iterator(), start, end);
    }

    private void populateTable(TupleQueryResult result) {
        for (BindingSet queryRow : result.getBindingSets()) {
            importRow(queryRow);
        }
    }

    private void importRow(TableRow row) {
        rows.add(row);
        groupedRows = null;
    }

    @Override
    public String toString() {
        StringBuilder buffy = new StringBuilder();
        buffy.append("Variables: ").append(variables).append("\n");
        for (TableRow tableRow : rows) {
            buffy.append(tableRow).append("\n");
        }
        return buffy.toString();
    }

    private void importRow(BindingSet queryRow) {
        rows.add(new QueryRowTableRow(queryRow, variables));
    }

    public List<String> getVariables() {
        return variables;
    }

    @NotNull
    public Collection<TableRow> findRowFor(Value ascendorParent) {
        return getData().getOrDefault(ascendorParent, Collections.emptySet());
    }

    public void addTableRow(TableRow artificialTopLevelRow) {
        importRow(artificialTopLevelRow);
    }

    /**
     * Compares the two sparql result data tables with each other. Equality is checked if the
     * atLeast-flag is set false. If the atLeast-flag is set to true, only the subset relation of
     * expected to actual data is checked.
     * <p/>
     * CAUTION: result rows with blank nodes are ignored from consideration (graph isomorphism
     * problem)
     *
     * @param expectedResultTable the expected data
     * @param actualResultTable   the actual data
     * @param atLeast             false: equality of data is required; true: expectedData SUBSET-OF actualData
     *                            is required
     * @return if the expected data is equal to (or a subset of) the actual data
     * @created 20.01.2014
     */
    public static MultiMap<String, Message> checkEquality(ResultTableModel expectedResultTable,
            ResultTableModel actualResultTable, boolean atLeast) {
        MultiMap<String, Message> errorMessages = new DefaultMultiMap<>(MultiMaps.linkedFactory(),
                MultiMaps.linkedFactory());

        /*
         * 2. Compare all result rows (except for those with blank nodes)
         */
        for (TableRow expectedTableRow : expectedResultTable) {
            // found no easy way to retrieve and match statements with
            // blanknodes, so for now we skip them from the check...
            boolean containsBlankNode = false;
            for (String var : expectedResultTable.getVariables()) {
                if (expectedTableRow.getValue(var) instanceof BNode) {
                    containsBlankNode = true;
                    break;
                }
            }
            if (containsBlankNode) {
                continue;
            }

            boolean contained = actualResultTable.contains(expectedTableRow);
            if (!contained) {
                if (!isOutdatedExpectedTableWithOneEmptyCell(expectedResultTable, expectedTableRow)) {
                    errorMessages.put("expected rows missing",
                            new Message(Type.ERROR, expectedTableRow.toString()));
                }
            }
        }

        if (atLeast) {
            /*
             * if the atLeast-flag is set we stop at this point as we asserted
             * that the actual data contains at least the rows from the expected
             * data
             */
            return errorMessages;
        }

        /*
         * 3. Compare numbers for each subject node (except for blank nodes)
         */

        Map<Value, Set<TableRow>> expectedData = expectedResultTable.getData();
        Map<Value, Set<TableRow>> actualData = actualResultTable.getData();
        Set<Value> keySet = actualData.keySet();
        for (Value node : keySet) {
            if (node != null && !(node instanceof BNode)) {
                if (!expectedData.keySet().contains(node)) {
                    errorMessages.put("expected nodes missing", new Message(Type.ERROR, node.toString()));
                    continue;
                }

                if (!(expectedData.get(node).size() == (actualData.get(node).size()))) {
                    errorMessages.put("cases where number of result columns does not match",
                            new Message(Type.ERROR, node.toString()));
                }
            }
        }
        return errorMessages;
    }

    /**
     * Originally, to check an empty actual result, we had to define an expected result table with one empty cell, like
     * <pre>
     * %%ExpectedSparqlResult
     * |
     * %
     * </pre>
     * Since we now also check empty cells in the result, this is no longer a "clean" empty table. It is now possible
     * to just have an %%ExpectedSparqlResult without a table, but to give backwards compatibility, we still handle
     * expected tables with one empty cell as effectively an empty expected table.
     */
    private static boolean isOutdatedExpectedTableWithOneEmptyCell(ResultTableModel expectedResultTable,
            TableRow expectedTableRow) {
        return expectedResultTable.getSize() == 1 && expectedTableRow.getVariables().size() == 1
                && expectedTableRow.getValue(expectedTableRow.getVariables().get(0)) == null;
    }

    public String toCSV() throws IOException {
        StringWriter out = new StringWriter();
        CSVPrinter printer = CSVFormat.DEFAULT.withHeader(variables.toArray(new String[variables.size()]))
                .print(out);
        for (TableRow row : rows) {
            List<Object> values = new ArrayList<>(variables.size());
            for (String variable : variables) {
                Value value = row.getValue(variable);
                values.add(value == null ? null : value.stringValue());
            }
            printer.printRecord(values);
        }
        return out.toString();
    }

    public static ResultTableModel fromCSV(String csv) throws IOException {
        try (CSVParser parser = CSVFormat.DEFAULT.withHeader().parse(new StringReader(csv))) {

            // read the header
            Map<String, Integer> headerMap = parser.getHeaderMap();
            List<String> variables = headerMap.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getValue))
                    .map(Map.Entry::getKey).collect(Collectors.toList());

            // read the rows
            List<TableRow> rows = new LinkedList<>();
            for (final CSVRecord record : parser) {
                SimpleTableRow row = new SimpleTableRow();
                for (String variable : variables) {
                    String value = record.get(variable);
                    if (value != null) {
                        row.addValue(variable, new LiteralImpl(value));
                    }
                }
                rows.add(row);
            }

            //  and return the parsed table
            return new ResultTableModel(rows, variables);
        }
    }

    public static String generateErrorsText(MultiMap<String, Message> failures) {
        return generateErrorsText(failures, true);
    }

    public static String generateErrorsText(MultiMap<String, Message> failures, boolean full) {
        StringBuilder buffy = new StringBuilder();
        buffy.append("The following test failures occurred:\n");
        for (String type : failures.keySet()) {
            Set<Message> failuresOfType = failures.getValues(type);
            buffy.append(failuresOfType.size()).append(" ").append(type).append(":\n");
            int i = 0;
            for (Message message : failuresOfType) {
                if (!full && ++i > MAX_DISPLAYED_FAILRUES) {
                    buffy.append("* ... see expected and actual result linked below\n");
                    break;
                }
                buffy.append("* ").append(message.getText()).append("\n");
            }
        }
        return buffy.toString();
    }
}