Java - сравнение списков с помощью технологии Castor (Еще раз про XML)

Вывод программы сравнения двух списков

Опубликовано: журнал "Системный администратор" "Задача сравнения списков (Еще раз про XML)" №1-2, 2013г.

Кому из нас в своей работе хоть раз не приходилось сталкиваться с задачей сравнения двух таблиц, списков, файлов и проч. Согласитесь, что эта задача, даже при небольших объемах сравниваемых данных, не из легких. Бухгалтеры обычно называют этот муторный процесс - «крыжить» списки. Однажды автор поставил себе задачу написать достаточно простую программу для сравнения электронных списков, используя бурно развивающиеся в настоящее время технологии XML. Речь пойдет о простом способе преобразования документов XML в объекты Java и обратно с помощью технологии Castor.

Постановка задачи

Конечно, скажете вы, существуют программы типа WinMerge и diff, умеющие находить отличия в файлах на пять с плюсом! Но заставить их выполнять сравнение именно так, как хочется, бывает непросто, да и сделать инструмент самому намного интереснее! Немного смекалки, немного кодинга и знания технологий обработки данных, плюс бесплатные инструменты разработки ― это все что нам потребуется! Проясним ситуацию и поставим задачу более определенно.
Работая на своем предприятии «системным администратором/программистом» я обнаружил несколько похожих по типу задач, которые я назвал условно «Задачи сравнения списков из трех полей (столбцов)». Например, надо сравнить вчерашнюю резервную копию базы данных счетов и сегодняшнюю, и найти, где ошибся оператор. Или сравнить товары, которые выписал клиент в текущий и прошлый год для каких-либо выводов. Размышления подтолкнули к составлению технического задания - схожесть этих и подобных им задач была очевидна. Почему сравнение ТРЕХ полей (столбцов)? А большего и не потребовалось. Просто сравниваемые списки могут быть определенным образом сгруппированы и должным образом структурированы, а отлаженная программа для решения одной-двух задач всегда может пригодиться и для третьей.
После некоторых усилий определилась следующая структура-описание одного из сравниваемых списков:
- первое поле (столбец) «Ключ» - поле, значение которого не должно повторяться в одном списке. Если ключ есть в первом списке, то ключ должен быть и во втором списке;
- второе поле «Параметр1» - первый Параметр ключа;
- третье поле «Параметр2» - второй Параметр ключа.
Для простоты будем сравнивать Параметры на совпадение, т.е. если ключи в обоих списках совпадают, то и оба Параметра ключа должны совпадать. Если взять пример с БД счетов (см. выше), то поле «Ключ» будет номер счета, «Параметр1» - дата счета, «Параметр2» - сумма счета.
Для сравнения двух таких списков будем применять алгоритм, представленный ниже. Он проще стандартного (см. Википедия: Наибольшая общая подпоследовательность) в реализации, и достаточно эффективен для списков интересующего нас размера:
- отсортируем первый список и проверим его на наличие совпадений ключа. При наличии совпадений - сообщение;
- пройдем первый список и проведем проверку на наличие ключей из первого списка во втором списке, а также на равенство Параметра1 и Параметра2 для ключей в обоих списках. При любом несовпадении - сообщение;
- отсортируем второй список и проверим его на наличие совпадений ключа. При наличии совпадений - сообщение;
- пройдем второй список и проведем проверку на наличие ключей из второго списка в первом списке, а также на равенство Параметра1 и Параметра2 для ключей в обоих списках. При любом несовпадении - сообщение;
Кстати, сообщения для удобства можно вывести в отдельный log-файл.
Для хранения списков был выбран формат XML (в дальнейшем я поясню почему), который отражает придуманную нами структуру списка (см. Листинг 1).

Листинг 1. Структура xml-файла для хранения списка

<?xml version="1.0" encoding="UTF-8"?>
<recordslist>
<record>
<key>Ключ</key>
<value1>Параметр1</value1>
<value2>Параметр2</value2>
</record>
<record>
...
</record>
...
...
...
</recordslist>

Замечания по структуре xml-файла: кроме стандартного заголовка файл имеет обязательный корневой элемент
<recordslist>
Элемент
<recordslist>
может иметь сколько угодно элементов
<record>,
которые описывают одну строку (запись) списка. Соответственно, элементы выделенные открывающимися/закрывающимися тегами
<key></key>,
<value1></value1> и
<value2></value2>
- это поля соответствующие Ключу, Параметру1 и Параметру2, которые описаны в нашей структуре.
Подготовить подобные списки можно по-разному. Автор это делал напрямую путем выгрузки из базы с помощью SQL-запроса и несложной программы. Покажем, для примера, как получить такой xml-файл из таблицы Excel. Нам поможет макрос, выгружающий выделенные три столбца таблицы Excel в файл нужной структуры (см. Листинг 2).

Листинг 2. Макрос для выгрузки столбцов Excel в xml-файл

Sub Save2XML()
'
' Save2XML Макрос
'
'открываем файл для записи
Open "c:\data.xml" For Append As #1
Dim cur_range As Range
With ActiveSheet
Set cur_range = Selection
cur_range.Activate
Print #1, "<recordslist>"
For x = 1 To cur_range.Rows.Count
Print #1, "<record>"
'берем значения только первых выделенных 3-х полей
Print #1, "<key>"+cur_range(x, 1).Text+"</key>" + _
"<value1>" + cur_range(x, 2).Text + "</value1>" + _
"<value2>" + cur_range(x, 3).Text + "</value2>"
Print #1, "</record>"
Next x
Print #1, "</recordslist>"
End With
'закрываем файл
Close #1
End Sub

Поговорим о технологиях

Теперь перейдем к самому интересному - собственно программированию нашей задачи. Почему все-таки был выбран формат XML? Хотелось бы познакомить читателя с некоторыми технологиями работы с xml-файлами, показать, что работать с этим форматом достаточно просто, и применить эти технологии в рутинной работе, например, в нашей задаче сравнения списков. Для начала познакомимся с некоторыми понятиями, которые нам понадобятся, а именно с понятиями маршалинга-демаршалинга. Вот какое определение дает Wikipedia.
Маршалинг (от англ. marshal – упорядочивать) – процесс преобразования представления объекта в памяти в формат данных пригодных для хранения или передачи. Противоположный процесс называется демаршалингом или десериализацией. Что же дают эти технологии в нашем случае? Ответ пока будет туманным – простое для использования преобразование xml-файла в объекты в памяти компьютера. Но поверьте, это то что надо!
Еще замечания по программированию. Использовать будем язык программирования Java. Для решения нашей задачи достаточно будет использовать JDK 6.0. На установке JDK подробно останавливаться не будем. А вот о выборе реализации демаршалинга (а именно он пригодится нам при преобразовании xml-файла напрямую в объекты Java) поговорим подробнее. Лидерство в этой области по праву принадлежит технологии Java JAXB (см. обзор технологий в [1]). Однако, как показалось автору, красивое решение для связи данных XML с объектами Java предлагается в проекте с открытым исходным кодом Castor фирмы Exolab (сайт проекта http://castor.codehaus.org/, см. также http://olex.openlogic.com/packages/castor и статью [2]).
Автор использовал версию Castor 1.3. Для установки необходимых компонентов достаточно скачать с сайта и распаковать архив Castor в любую папку на диске. В дальнейшем мы покажем как подключить нужные jar-файлы к нашему проекту. Кстати сказать, данная технология кросс-платформенная и будет работать вместе с Java как в Linux, так и в Windows. Распространяется Castor по лицензии Apache 2.0. На сайте (и в архиве с Castor) также можно найти документацию на английском языке, но лучше почитать статью [2] на русском.

Листинги программ

Итак, создадим подходящий каталог для нашего проекта и в нем – файл Record.java, – класс, описанный в этом файле, будет описывать одну строку (запись) нашего списка (см. Листинг 3).

Листинг 3. Файл Record.java

package si.xml.castor.comparator;

public class Record {
  private String key;
  private String value1;
  private String value2;
  public Record() {};
  public Record(String key, String value1, String value2) {
    this.key = key;
    this.value1 = value1;
    this.value2 = value2;
  }
  public void setKey(String key) {
    this.key = key;
  }
  public String getKey() {
    return key;
  }
  public void setValue1(String value1) {
    this.value1 = value1;
  }
  public String getValue1() {
    return value1;
  }
  public void setValue2(String value2) {
    this.value2 = value2;
  }
  public String getValue2() {
    return value2;
  }
}

В качестве пояснений к этому классу скажем, что в нем определены три строковых поля – key, value1, value2, которые соответствуют структуре нашего списка и, главное, структуре xml-файла, конструктор по умолчанию и конструктор, принимающий три параметра (для удобства). А также три метода get-set с одноименными названиями полей – это уже требование спецификации Castor.
Следующий класс, который нам понадобится – RecordsList (см. Листинг 4).

Листинг 4. Файл RecordsList.java

package si.xml.castor.comparator;
import java.util.ArrayList;

public class RecordsList {
  private ArrayList<Record> records;
  public RecordsList() {};
  public RecordsList(Record record) {
    this.records = new ArrayList<Record>();
    records.add(record);
  }
  public void setRecords(ArrayList<Record> records) {
    this.records = records;
  }
  public ArrayList<Record> getRecords() {
    return records;
  }
  public void addRecord(Record record) {
    records.add(record);
  }
  public void deleteRecord(int index) {
    records.remove(index);
  }
}

RecordList содержит коллекцию объектов Record (ArrayList) и методы добавления/удаления записей.
Далее класс, реализующий демаршалинг – ListMapUnmarshaller (см. Листинг 5):

Листинг 5. файл ListMapUnmarshaller.java

package si.xml.castor.comparator;
import java.io.FileReader;
import java.util.Iterator;
import java.util.ArrayList;
import org.exolab.castor.xml.Unmarshaller;
import org.exolab.castor.mapping.Mapping;

public class ListMapUnmarshaller {
  private RecordsList lst;
  ListMapUnmarshaller() {};
  public RecordsList readFile( String xml_file) {
    Mapping mapping = new Mapping();
    try {
      mapping.loadMapping("list-map.xml");
      FileReader reader = new FileReader(xml_file);
      Unmarshaller unm = new Unmarshaller(RecordsList.class);
      unm.setMapping(mapping);
      lst = (RecordsList)unm.unmarshal(reader);
    }
    catch (Exception e) {
      System.err.println(e.getMessage());
      e.printStackTrace(System.err);
    }
    return lst;
  }
}

Некоторые пояснения для этого класса. Основная функция readFile(). Здесь буквально в трех строках кода:

Unmarshaller unm = new Unmarshaller(RecordsList.class);
unm.setMapping(mapping);
lst = (RecordsList)unm.unmarshal(reader);

сосредоточен весь процесс преобразования xml-файла в наш класс RecordsList, и мы можем уже сравнивать коллекции, состоящие из экземпляров класса Record, сортировать эти коллекции и прочее.
Отдельно поговорим о строке

 
mapping.loadMapping("list-map.xml");

В файле list-map.xml, который, как Вы наверное уже успели заметить, тоже имеет xml-структуру, содержится нужное для Castor описание полей наших классов. Приведем листинг этого файла, который называется mapping-файлом (см. Листинг 6). Отметим важность наличия mapping-файла для данной технологии (см. [2]).

Листинг 6. Файл list-map.xml

<?xml version="1.0"?>
<!DOCTYPE mapping PUBLIC "-//EXOLAB/Castor Mapping DTD Version 1.0//EN" "<a href="http://castor.org/mapping.dtd">
<mapping>
<class">http://castor.org/mapping.dtd">
<mapping>
<class</a> name="si.xml.castor.comparator.RecordsList">
<map-to xml="recordslist" />
<field name="Records"
type="si.xml.castor.comparator.Record"
collection="arraylist">
<bind-xml name="record" />
</field>
</class>
<class name="si.xml.castor.comparator.Record">
<field name="Key" type="java.lang.String">
<bind-xml name="key" node="element" />
</field>
<field name="Value1" type="java.lang.String">
<bind-xml name="value1" node="element" />
</field>
<field name="Value2" type="java.lang.String">
<bind-xml name="value2" node="element" />
</field>
</class>
</mapping>

Нужно просто положить этот файл в каталог проекта.
Настало время привести главный файл проекта с функцией main(). Назовем его с3f.java (от англ. compare three fields). См. Листинг 7.

Листинг 7. Программа сравнения двух списков – файл c3f.java

package si.xml.castor.comparator;
import java.io.*;
import java.util.Iterator;
import java.util.*;

public class c3f {
  private RecordsList rlst;
  private RecordsList rlst2;
  static Comparator<Record> keyorder = new Comparator<Record>() {
    public int compare(Record o1, Record o2) {
      return o1.getKey().compareTo(o2.getKey());
    }
  };

  private ArrayList<Record> sortByKey(ArrayList<Record> lst){
    Collections.sort(lst, keyorder);
    return lst;
  }
  c3f() {};
  c3f(String xml_file_1, String xml_file_2) {
    rlst  = new ListMapUnmarshaller().readFile(xml_file_1);
    rlst2 = new ListMapUnmarshaller().readFile(xml_file_2);
  }
  private int compareTwoLists() {
    //возвращает 1 - если есть ошибки, 0 - если ошибок нет
    ArrayList records = rlst.getRecords();
    ArrayList records2 = rlst2.getRecords();
    TreeMap erTM = new TreeMap();
    //счетчик ошибок
    int ern = 0;
    //сортируем списки
    records = sortByKey(records);
    records2 = sortByKey(records2);
    //проверяем первый список на двойные значения ключа
    String keyPrev = "", keyNext;
    erTM.put( ern, "Проверяем 1-й список" );
    for ( Iterator i = records.iterator(); i.hasNext(); ) {
      Record record = (Record)i.next();
      keyNext = record.getKey();
      //если есть двойные значения ключа в первом списке - ошибка
      if (keyNext.equals(keyPrev)) {
        ern++;
        erTM.put( ern, record.getKey() + " - двойное значение ключа в первом списке" );
      }
      keyPrev = keyNext;
    }
    //первый список проверяем на наличие ключей во втором
    //списке и равенство Параметра1 и Параметра2
    //вспомогательная переменная
    boolean eq1;
    for ( Iterator r = records.iterator(); r.hasNext(); ) {
      eq1 = false;
      Record record = (Record)r.next();
      for ( Iterator k = records2.iterator(); k.hasNext(); ) {
        Record record2 = (Record)k.next();
        if ( record2.getKey().equals( record.getKey() ) ) {
          eq1 = true;
          if ( !record2.getValue1().equals( record.getValue1() ) ) {
            ern++;
            erTM.put ( ern, record.getKey() + " (" + record.getValue1() + ", " + record.getValue2() + ") - для одного ключа отличие первого Параметра во втором списке");
          }
          if ( !record2.getValue2().equals( record.getValue2() ) ) {
            ern++;
            erTM.put ( ern, record.getKey() + " (" + record.getValue1() + ", " + record.getValue2() + ") - для одного ключа отличие второго Параметра во втором списке");
    }
        }
      }
      if ( !eq1 ) {
        ern++;
        erTM.put( ern, record.getKey() + " - ключ есть в первом и нет во втором списке" );
      }
    }
    //проверяем второй список на двойные значения ключа
    //код аналогичен приведенному выше для 1-го списка
    ...
    //второй список проверяем на наличие ключей в первом списке
    //код аналогичен приведенному выше для 1-го списка
    ...
    //если есть ошибки - сохраняем в файл
    if (ern > 1) {
      saveErrors2file( erTM );  
      return 1;
    } else return 0;
  }
  public void saveErrors2file(TreeMap err_map) {
    try {
      PrintWriter out = new PrintWriter(new FileWriter("errors.log"));
      Set <Map.Entry> entries = err_map.entrySet();
      for (Map.Entry entry : entries) {
        out.println( entry.getValue() );
      }
      out.close();
    }
    catch (IOException exc) {}
    }
    public static void main (String[] args) {
      if( args.length > 0 ) {
        if ( args.length == 2 ) {
          c3f lf = new c3f ( args[0], args[1] );
          if ( lf.compareTwoLists() == 1 ) {
            System.out.println("There are errors!See errors.log file");
          }
          else {
            System.out.println("Lists are equals!");
          }
        }
      }
      else {
        System.out.println("must to have two parameters!");
        System.exit(0);
      }
    }
  }

Метод sortByKey нужен для сортировки коллекций. Конструктор с двумя строковыми параметрами преобразует xml-файлы в экземпляр класса RecordsList. Основной метод compareTwoLists() сравнивает две коллекции по описанному выше алгоритму, метод saveErrors2file сохраняет ошибки в файл. В остальном, приведенный код несложен для понимания и обильно снабжен комментариями. Автор использовал простейшие алгоритмы сравнения, желая показать простоту использования XML технологий для решения практических задач IT. Отметим также, что нашу программу можно дополнить, при необходимости, сравнением Параметра1 и Параметра2 не только на равенство, но и на возрастание-убывание (если это цифровые показатели), да и увеличить число Параметров не составит труда. Это зависит от конкретной задачи. Достаточно будет добавить в метод compareTwoLists() соответствующий код.

Компиляция и результаты

Осталось откомпилировать наши java-файлы и посмотреть что получится. Для компиляции проекта использовалась машина с ОС Ubuntu 11.04. Несколько слов о подключении jar-файлов Castor к проекту. Для системы Ubuntu в файл .profile из домашнего каталога пользователя достаточно дописать строки (см. Листинг 8).

Листинг 8. Файл .profile
export CASTOR_HOME=/usr/local/java/castor-1.3
export CASTOR_CLASSES= $CASTOR_HOME/castor-1.3.jar:$CASTOR_HOME/castor-1.3-xml.jar:$CASTOR_HOME/lib/commons-logging-1.1.jar:$CASTOR_HOME/castor-1.3-core.jar
export CLASSPATH=$CASTOR_CLASSES:.

Где CASTOR_HOME – каталог, куда мы развернули архив Castor. Тоже самое (заменив знак «:» на «;» и поменяв пути к файлам на нужные) можно написать в файле autoexec.bat в ОС Windows. После этого компиляция проходит без проблем из каталога проекта с помощью команды
javac –d . *.java
Запуск нашей программы в Ubuntu, где уже установлена переменная CLASSPATH (см. выше содержимое файла .profile) осуществляется командой
java si.xml.castor.comparator.c3f file1.xml file2.xml
где file1.xml, file2.xml – файлы, которые необходимо сравнить.
Можно добавить сюда еще способ запуска уже откомпилированных файлов в ОС Windows. Для этого можно установить только лишь JRE (т.е. виртуальную машину Java), ведь запустить байт код Java можно и без установки JDK. Создадим такой bat-файл:

Листинг 9. Файл start.bat
java -cp .; c:\serg\castor-1.3\castor-1.3.jar;c:\serg\castor-1.3\castor-1.3-xml.jar;c:\serg\castor-1.3\lib\commons-logging-1.1.jar;c:\serg\castor-1.3\castor-1.3-core.jar
si.xml.castor.comparator.c3f file1.xml file2.xml
notepad.exe errors.log

Достаточно положить такой файл вместе с каталогом si, содержащим откомпилированные файлы нашего проекта (с подкаталогами xml\castor\comparator), файлом list-map.xml, файлами для сравнения и запустить bat-файл. Все сообщения сохраняются в файле errors.log. Сообщения Castor и результат запуска нашей программы см. на Рисунке 1.

Итак, в нашей статье мы научились использовать связывание с данными XML с помощью технологии Castor для прикладной задачи сравнения списков. Отметим, что язык XML прочно обосновался в информационных технологиях именно благодаря развитым средствам языков программирования и технологий от сторонних производителей для работы с этим форматом.

Скачать исходный код программы можно здесь

Литература:
1. Хабибуллин И. «Самоучитель JAVA 2» Санкт-Петербург, «БХВ-Петербург» 2005, - С. 692-701
2. Бретт МакЛафлин «Связывание с данными с помощью CASTOR» - http://www.ibm.com/developerworks/ru/library/x-xjavacastor1/
3. Сайт проекта Castor – http://castor.codehaus.org/
4. Сайт поддержки OpenSource проектов – http://olex.openlogic.com/packages/castor