gt:         if (columnNames[i] == null) {
context: /*
   * Copyright (c) 2002 Cunningham & Cunningham, Inc.
   * Copyright (c) 2009-2015 by Jochen Wierum & Cologne Intelligence
   *
   * This file is part of FitGoodies.
   *
   * FitGoodies is free software: you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   *
   * FitGoodies 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 General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with FitGoodies.  If not, see <http://www.gnu.org/licenses/>.
   */
  
  package de.cologneintelligence.fitgoodies;
  
  import de.cologneintelligence.fitgoodies.htmlparser.FitCell;
  import de.cologneintelligence.fitgoodies.htmlparser.FitRow;
  import de.cologneintelligence.fitgoodies.htmlparser.FitTable;
  import de.cologneintelligence.fitgoodies.typehandler.TypeHandler;
  import de.cologneintelligence.fitgoodies.util.FitUtils;
  import de.cologneintelligence.fitgoodies.valuereceivers.ValueReceiver;
  import de.cologneintelligence.fitgoodies.valuereceivers.ValueReceiverFactory;
  
  import java.lang.reflect.Field;
  import java.text.ParseException;
  import java.util.*;
  import java.util.regex.Matcher;
  
  abstract public class RowFixture extends Fixture {
  
      protected List<FitRow> missing = new LinkedList<>();
      protected List<Object> surplus = new LinkedList<>();
      private String[] columnParameters;
      private String[] columnNames;
      private FitTable table;
  
      /**
       * get rows to be compared
       * @return Actual values to be compared with the HTML content.
       * @throws Exception when querying for object fails.
       */
      abstract protected Object[] query() throws Exception;
  
      /**
       * get expected type of row
       *
       * @return Class which is processed by this RowFixture.
       */
      abstract protected Class getTargetClass();
  
      @Override
      public void doTable(FitTable table) {
          this.table = table;
          super.doTable(table);
      }
  
      protected void doRows(List<FitRow> rows) throws Exception {
          FitRow header = rows.get(0);
          columnParameters = extractColumnParameters(header);
          columnNames = findColumnNames(header);
  
          List<FitRow> expected = new LinkedList<>(rows.subList(1, rows.size()));
          List<Object> computed = new LinkedList<>(Arrays.asList(query()));
          multiLevelMatch(expected, computed, 0);
  
          appendSurplusRows();
          markMissingRows();
      }
  
      protected String[] findColumnNames(FitRow heads) {
          String[] names = new String[heads.size()];
          int i = 0;
  
          for (FitCell fitCell : heads.cells()) {
              names[i++] = validator.preProcess(fitCell);
          }
  
          return names;
      }
  
      /**
       * Sorts and compare two lists.
       * It is assumed that all items in {@code expected} and {@code computed}
       * are equal on the {@code col - 1} columns.
       */
      private void multiLevelMatch(List<FitRow> expected, List<Object> computed, int col) {
          boolean cantGoDeeper = col >= columnNames.length;
          if (cantGoDeeper) {
              check(expected, computed);
          } else {
              boolean isComment = isComment(col);
              if (isComment) {
                  multiLevelMatch(expected, computed, col + 1);
              } else {
                  groupAndMatch(expected, computed, col);
              }
          }
      }
  
      private boolean isComment(int col) {
          return columnNames[col] == null || columnNames[col].isEmpty();
      }
  
      /**
       * Groups both lists by column {@code col}. For every resulting group check if
       * there is a 1:1 or a 1:0 match. Otherwise, group further.
       */
      private void groupAndMatch(List<FitRow> expected, List<Object> computed, int col) {
          Map<Object, List<FitRow>> expectedMap = groupExpectedByColumn(expected, col);
          Map<Object, List<Object>> computedMap = groupComputedByColumn(computed, col);
          Set keys = union(expectedMap.keySet(), computedMap.keySet());
  
          for (Object key : keys) {
              List<FitRow> expectedList = expectedMap.get(key);
              List<Object> computedList = computedMap.get(key);
  
              boolean isAmbiguous = hasMultipleEntries(expectedList) && hasMultipleEntries(computedList);
  
              if (isAmbiguous) {
                  multiLevelMatch(expectedList, computedList, col + 1);
              } else {
                  check(expectedList, computedList);
              }
          }
      }
  
      private boolean hasMultipleEntries(List<?> expectedList) {
          return expectedList != null && expectedList.size() > 1;
      }
  
      private Map<Object, List<FitRow>> groupExpectedByColumn(List<FitRow> list, int col) {
          Map<Object, List<FitRow>> result = new HashMap<>(list.size());
  
          for (FitRow row : list) {
              List<FitCell> cells = row.cells();
  
              FitCell keyCell = cells.get(col);
              try {
                  Object key = parseCell(col, keyCell);
                  addToMap(result, key, row);
              } catch (Exception e) {
                  keyCell.exception(e);
                  for (int i = col + 1; i < cells.size(); i++) {
                      cells.get(i).ignore();
                  }
              }
          }
          return result;
      }
  
      private Object parseCell(int col, FitCell cell) throws NoSuchMethodException, NoSuchFieldException, ParseException {
          String preprocessedText = validator.preProcess(cell);
          String parameter = FitUtils.saveGet(col, columnParameters);
          Class<?> columnType = getColumnType(columnNames[col]);
          TypeHandler typeHandler = typeHandlerFactory.getHandler(columnType, parameter);
          return typeHandler.parse(preprocessedText);
      }
  
      private Map<Object, List<Object>> groupComputedByColumn(List<Object> list, int col) {
          Map<Object, List<Object>> result = new HashMap<>(list.size());
  
          for (Object row : list) {
              try {
                  Object key = createReceiver(row, columnNames[col]).get();
                  addToMap(result, key, row);
              } catch (Exception e) {
                  // surplus anything with bad keys, including null
                  surplus.add(row);
              }
          }
          return result;
      }
  
      private <T> void addToMap(Map<Object, List<T>> map, Object key, T row) {
          if (key.getClass().isArray()) {
              key = Arrays.asList((Object[]) key);
          }
  
          if (map.containsKey(key)) {
              map.get(key).add(row);
          } else {
              List<T> list = new LinkedList<>();
              list.add(row);
              map.put(key, list);
          }
      }
  
      private <T> Set<T> union(Set<T> a, Set<T> b) {
          Set<T> result = new HashSet<>(a);
          result.addAll(b);
          return result;
      }
  
      /**
       * Compares two lists.
       * <ul>
       * <li>if {@code expectedList} is empty, all {@code computedList} items are surplus</li>
       * <li>if {@code computedList} is empty, all {@code expectedList} items are missing</li>
       * <li>otherwise, match the first rows and compare the rest recursively</li>
       * </ul>
       *
       * @param computedList Objects from {@link #query()}.
       * @param expectedList Objects from HTML.
       */
      protected void check(List<FitRow> expectedList, List<Object> computedList) {
          if (expectedList == null || expectedList.size() == 0) {
              surplus.addAll(computedList);
          } else if (computedList == null || computedList.size() == 0) {
              missing.addAll(expectedList);
          } else {
              Object computedRow = computedList.remove(0);
              List<FitCell> expectedRow = expectedList.remove(0).cells();
              compareRow(computedRow, expectedRow);
  
              check(expectedList, computedList);
          }
      }
  
      /**
       * Compares two rows item by item using {@link Fixture#check(FitCell, ValueReceiver, String)}.
       * Each cell will be marked as right, wrong, ignored or exception
       */
      private void compareRow(Object computedRow, List<FitCell> expectedCells) {
          for (int i = 0; i < columnNames.length && expectedCells != null; i++) {
              try {
                  ValueReceiver valueReceiver;
                  if (isComment(i)) {
                      valueReceiver = null;
                  } else {
                      valueReceiver = createReceiver(computedRow, columnNames[i]);
                  }
  
                  String columnParameter = FitUtils.saveGet(i, columnParameters);
                  check(expectedCells.get(i), valueReceiver, columnParameter);
              } catch (NoSuchMethodException | NoSuchFieldException e) {
                  expectedCells.indexOf(e);
              }
          }
      }
  
      private Class<?> getColumnType(String name) throws NoSuchMethodException, NoSuchFieldException {
          Matcher matcher = ValueReceiverFactory.METHOD_PATTERN.matcher(name);
  
          if (matcher.find()) {
              final String methodName = FitUtils.camel(matcher.group(1));
              return getTargetClass().getMethod(methodName).getReturnType();
          } else {
              final Field field = getTargetClass().getField(FitUtils.camel(name));
              return field.getType();
          }
      }
  
      private void appendSurplusRows() {
          for (Object row : surplus) {
              FitRow fitRow = table.appendRow();
              buildRow(fitRow, row, "surplus");
          }
      }
  
      private void buildRow(FitRow fitRow, Object row, String info) {
          if (row == null) {
              fitRow.insert(0).blank("null", columnNames.length);
              fitRow.wrong(info);
          } else {
              fitRow.wrong(info);
              for (int i = 0; i < columnNames.length; i++) {
                  FitCell cell = fitRow.insert(i);
                  buildCell(cell, row, i);
              }
          }
      }
  
      private void buildCell(FitCell cell, Object row, int i) {
  