Simple XML Framework — пишем API для работы с диаграммами DIA

в 8:18, , рубрики: Dia Diagram Editor, DiaXML API, java, Simple XML Framework, XML, Программирование, метки: , ,

Simple Xml Framework Dia Diagram Editor   Dia Diagram Editor

Фреймфорк Simple XML — известен многим, при своей простоте, он способен потягаться возможностями с большим «интерпрайзным» JAXB, и при этом совместим с Андроид.

Статей по его использованию не «навалом», но хватает. Фреймфорк упоминался на Хабре, есть статья на ibm developerworks, в конце концов, на официальном сайте есть хорошие примеры
и руководство.

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

В данной статье будет рассказано о том, как научить Simple Framework работать в такой ситуации. Мы создадим собственную «стратегию» для Simple Framework; мы отнаследуемся от класса TreeStrategy и опишем «хитрую логику» того, как надо сопоставлять элементы xml-файла DIA к Java классам.

И да, я предполагаю, что читатель знаком с основами использования Simple XML Framework.

Пару слов про DIA и начало истории

Думаю, редактор диаграмм DIA известен практически всем. В своем роде, это «классика жанра».

Создан он достаточно давно, но более-менее внятного и полного описания формата файла нет. Известно, что файл .dia — это zip-архив xml-файла с расширением .dia (и он умеет работать с несжатыми файлами). А дальше… дальше, мол, «разберётесь сами, там не сложно».

В почтовой рассылке пару лет назад упоминалось ссылки на некие более-менее связанные описания, но ныне они мертвы.

Про наличие какого либо программного интерфейса для генерации или редактирования диаграмм, речи тоже нет. Есть список редакторов диаграмм, способных экспортировать в формат Dia, но ничего пригодного для себя я не нашел.

«Программы — это хорошо», подумал я, «но мне нужен API. Готовый к использованию. Желательно на Java».

В итоге, я решился делать свой велосипед. Удобный мне, формой под моё седалище,
с рулем под форму моих рук, с колесами по форме выбоин тропинок, которыми хожу я.

Формат хранения диаграмм в Dia и его особенности

Не будем сейчас разбирать полностью всю структуру, а перейдем сразу к «проблемным особенностям».

В .dia, структура хранения любого элемента диаграммы унифицирована: это тег object, который «обрамляет» перечень тегов attribute, в которых описываются все характеристики объекта.

Например, вот так описывается элемент 'линия' из базовой палитры DIA
    <dia:object type="Standard - Line" version="0" id="O10">
      <dia:attribute name="obj_pos">
        <dia:point val="2.99224,32.237"/>
      </dia:attribute>
      <dia:attribute name="obj_bb">
        <dia:rectangle val="2.92606,32.1708;7.37558,34.382"/>
      </dia:attribute>
      <dia:attribute name="conn_endpoints">
        <dia:point val="2.99224,32.237"/>
        <dia:point val="7.3094,34.1931"/>
      </dia:attribute>
      <dia:attribute name="numcp">
        <dia:int val="1"/>
      </dia:attribute>
      <dia:attribute name="end_arrow">
        <dia:enum val="5"/>
      </dia:attribute>
      <dia:attribute name="end_arrow_length">
        <dia:real val="0.5"/>
      </dia:attribute>
      <dia:attribute name="end_arrow_width">
        <dia:real val="0.5"/>
      </dia:attribute>
    </dia:object>

Основная проблема тут в том, что все типы элементов описываются одним тегом.
В итоге, не понятно как сказать Simple Framework, что:

<dia:object type="Standard - Line" version="0" id="O10">

Надо разбирать в объект класса Line, а:

<dia:object type="Standard - Box" version="0" id="O0">

В объект класса Box. В штатном наборе инструментария Simple Framework я не нашел как решать эту задачу. Именно поэтому была описана «стратегия» DiaTreeStrategy, история создания которой приведена ниже.

К слову, раз уж мы говорим про минимализм Simple Framework применимо к разбору формата .dia, стоит упомянуть про аннотацию @Xpath и её ограничения.

Проблема кроется в том, что состав атрибутов объекта диаграммы, которые Dia сохраняет в xml — не постоянен и зависит от того, какие свойства вы меняли у данного объекта.

Из-за этого, при разборе xml, нельзя использовать аннотацию @Xpath с указанием индекса для того, что бы сопоставить свойство класса и описываемый в xml атрибут объекта. Если бы в @Xpath можно было указать имя и значения атрибута, как мы это можем сделать в xpath-запросе — то это сделало бы задачу проще, но увы — в @Xpath можно указать только путь и индекс. Эти обстоятельства привели к появлению у меня в коде не самого удобного механизма заполнения свойств класса из массива атрибутов, но об этом в другой раз.

«Cтратегия» в Simple Framework, начинаем писать DiaTreeStrategy

Класс реализующий «стратегию» (реализующий интерфейс Strategy) в Simple Framework занимается тем, что определяет сопоставление между узлами XML и классами в Java.

У этого интерфейса всего 2 метода — read() и write(). Первый занимается тем, что по переданному xml-узлу пытается понять, какой класс мы сейчас будем заполнять, а второй создает xml-узел, в который мы будем заполнять свойства объекта.

Писать стратегию с нуля — «дело не барское», тем более, что обычный TreeStrategy во всем остальном (кроме незнания как правильно сопоставлять классы и узлы типа object) — вполне хорошо работает. Потому мы его просто подправим. Отнаследуем и подправим.

public class DiaTreeStrategy extends TreeStrategy

Читатель наверняка уже догадался, что «всё», что нам надо — это научить «стратегию» читать свойство type у тега object, и откуда-то понимать что «UML — Class» — соответствует классу diaXML.shapes.uml.UmlClass, а «Standard — Box» — классу diaXML.shapes.standart.StdBox.

Информацию о маппинге (type + version => имя класса) я решил хранить в обычном ArrayList:

ArrayList<DiaObjToClassMapRecord> diaObj2ClassMap;

Инициализируем маппинг прямо под объявлением

public ArrayList<DiaObjToClassMapRecord> diaObj2ClassMap = new ArrayList<DiaObjToClassMapRecord>();
  {	//default base mapping
		
    diaObj2ClassMap.add(new DiaObjToClassMapRecord("UML - Class",			"0",	"diaXML.shapes.uml.UmlClass" ));
    diaObj2ClassMap.add(new DiaObjToClassMapRecord("UML - Association",		"2",	"diaXML.shapes.uml.UmlAssociation" ));
		
    diaObj2ClassMap.add(new DiaObjToClassMapRecord("Standard - Box",		"0",	"diaXML.shapes.standart.StdBox" ));
    diaObj2ClassMap.add(new DiaObjToClassMapRecord("Standard - Text",		"1",	"diaXML.shapes.standart.StdText" ));
    diaObj2ClassMap.add(new DiaObjToClassMapRecord("Standard - ZigZagLine",	"1",	"diaXML.shapes.standart.StdZigZagLine" ));
		
    diaObj2ClassMap.add(new DiaObjToClassMapRecord("Standard - BezierLine",	"*",	"diaXML.shapes.standart.StdBezierLine" ));
		
    //universal
    diaObj2ClassMap.add(new DiaObjToClassMapRecord("*",	"*",	"diaXML.shapes.UncknownShapeObject" ));
		
   }

Если кто напишет свой класс для ещё одного элемента диаграммы, то его можно добавить в маппинг после создания экземпляра DiaTreeStrategy. Пока описано 5 классов (POJO) которые умеют инициализировать свои свойства из атрибутов объекта диаграммы, и один класс универсальный — UncknownShapeObject — в него попадают всё неизвестные нам объекты; он ничего не инициализирует, и хранит массив атрибутов в неизменном виде.

Механизм поиска в этом массиве сопоставления я вынес в отдельную функцию

   private Class readValueAdv(Type type, NodeMap node) throws Exception 
   {      
	  // we need here catch only <object> tag
	  if( !"object".equals(node.getName()) ) 
			  return null;
	  
      Node entry_type = node.get("type");
      Node entry_version = node.get("version");
      if( entry_type == null || entry_version==null) 
	      {  return null;
	      };
      
      String name_type = entry_type.getValue();
      String name_version = entry_version.getValue();
      String className=null;
      Class expect=null;
      
      for (DiaObjToClassMapRecord crec: diaObj2ClassMap)
	      {
		      if (	  ( 	crec.diaType!=null && (crec.diaType.equals(name_type) ||  !crec.diaType.isEmpty() && crec.diaType.equals("*") )
		    		  )
		    	  && 
		    		  ( 	crec.diaVersion!=null && ( crec.diaVersion.equals(name_version) ||  !crec.diaVersion.isEmpty() && crec.diaVersion.equals("*")  ) 
		    		  )
		    	 )
		      {		className = crec.javaClassName;
		      		break;
			  }
		  }
      
      if (className !=null) 
    	  { expect = loader.load(className);
    	  	Node entry = node.remove(label);
    	  }
      
      return expect;
   }  
   

Теперь все что осталось — исправить метод read.

public class DiaTreeStrategy extends TreeStrategy
...
   @Override
   public Value read(Type type, NodeMap node, Map map) throws Exception 
   {
      Class actualDeclaredByDia = readValueAdv(type, node);
      if (actualDeclaredByDia==null) 
    	  return super.read(type, node, map);
      return new ObjectValue(actualDeclaredByDia);
   }

Это, почти всё. Вернее, это был «краеугольный камень преткновения», который не позволял с помощью Simple Framework парсить файлы, которые генерирует DIA.

Естественно, помимо того, что описано в статье, было проведено много другой работы, связанной с аннотированием классов, исследованием того, как в Simple XML работают аннотации-перехватчики событий в процессе сериализации и разбора XML, созданием сервисных классов и тд. и тп., но это уже другой разговор.

Ради чего это всё и как этим пользоваться?

В конце хотел бы привести пару примеров того, как использовать «DiaXML API». Без DiaTreeStrategy оно бы не заработало. И не забудьте перед сборкой подключить к проекту diaXmlApi.jar (брать тут).

Как почитать .dia

import java.io.File;

import org.simpleframework.xml.strategy.DiaTreeStrategy;

import diaXML.Diagram;
import diaXML.shapes.standart.StdText;

public void main(String[] args)
	{
		Strategy strategy = new DiaTreeStrategy(); 
		Serializer serializer = new Persister(strategy); 
		File source = new File("path/to/dia/file/to/read.dia");

		Diagram probeDia=null;		
		try {	probeDia = serializer.read(Diagram.class, source);
			} catch (Exception e) {	e.printStackTrace(); return ;}
		
		System.out.println(" File readed. Here is list of objects at layer 0 :");
		
		for (IDiaObject cObj: probeDia.layers.get(0).objects )
			{	System.out.println(" dia type ["+cObj.getObjectType()+"]ver.["+cObj.getObjectTypeVersion()+"] objId:["+cObj.getId()+"] name:["+cObj.getName()+"]");	
				
				if ( StdText.TYPENAME.equals(cObj.getObjectType())  )
					{ System.out.println(" text value is:["+((StdText)cObj).textValue+"]");	
					};
			}
	}
Как создать .dia

import java.io.File;

import org.simpleframework.xml.strategy.DiaTreeStrategy;

import diaXML.Diagram;
import diaXML.shapes.standart.StdText;

public void main(String[] args)
	{
		Strategy strategy = new DiaTreeStrategy(); 
		Serializer serializer = new Persister(strategy); 
		
		StdText cText= new StdText();
		cText.textValue="this is a demo n of creating DIA-file";
		cText.obj_pos.moveTo(15, 5);

		Diagram probeDia=new Diagram().initWithDefaults();
		probeDia.layers.get(0).objects.add(cText);
		
		File resultFile = new File("path/to/dia/file/to/write.dia");
		
		try {	serializer.write(probeDia, resultFile);
			} catch (Exception e) {	e.printStackTrace(); }
			
	}

Ещё пару примеров можно найти в исходниках проекта (тут).

Заключение

В этой статье я хотел рассказать о том, что такое «стратегия» в Simple XML Framework и как её использовать в ситуации, когда штатных средств уже не хватает и о некоторых ограничениях Simple Framework (например, про то, что в @Xpath-аннотации нельзя использовать выражения для имени и значений атрибутов, как это мы можем делать в @Xpath-запросах).

Решение этих вопросов позволило успешно реализовать ключевые классы проекта «DiaXML API» наименьшими силами. Полный текст исходного кода вы найдете в репозитории проекта.

Cсылки

PS: С использованием разработанного API, была разработана утилита, которая отрисовывает в DIA схему базы данных (или обновляет ранее созданную). В качестве источника данных используется схема БД в формате Turbine XML, которую умеет создавать Apache DDL Utils.

Автор: dplsoft

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js