Велосипед для генерации Excel документов по шаблону

в 10:36, , рубрики: Excel, java, Песочница, Программирование, метки: ,

Обычно для программного формирования сложных отчётов в xls предлагается использовать «вручную» сформированный документ — шаблон, в нужных местах которого вместо реальных данных подставлены некоторые «теги» н.п. ${userName}, ${userAge}, и в процессе заполнения шаблона находить теги в тексте документа и заменять их соответствующими значениями.

Но как быть, если в итоговом документе должны отсутствовать некоторые страницы из шаблона, и наоборот, другие страницы могут быть несколько раз «клонированы» и заполнены разными данными? И как избавиться от нудного заполнения маппинга тег->значение в коде?

Опишем модель листа(Sheet) документа.

public class SheetModel {

	private String sheetToClone; //Лист,клонируя который получим листы итогового документа

	private String sheetName; //Имя листа итогового документа
	
	private Map<String, Object> mappings; 

	//getters and setters
}

Далее используя Apache POI пробежимся по книге, найдём лист с именем sheetToClone и создадим его копию. Таким образом, можно создать сколько угодно объектов класса SheetModel, пробежаться по ним в цикле и в итоге получить документ, содержащий в себе листы исходного документа и их копии. Далее «исходные» листы удаляются.

private void createNewSheets(List<SheetModel> sheetModelList){
	for (SheetModel sheetModel: sheetModelList){		    
		String sheetName=sheetModel.getSheetName();
		String sheetToClone=sheetModel.getSheetToClone();
		cloneSheet(sheetName, sheetToClone);
	}
}

private void cloneSheet(String sheetName,String sheetToClone ){
	int sheetToCloneIdx=getSheetIndex(sheetToClone);
	cloneSheet(sheetToCloneIdx, sheetName);		
}

private int getSheetIndex(String sheetName) throws SheetNotFoundException{
	for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
		if(workbook.getSheetAt(i).getSheetName().equals(sheetName) ) {
			return i;
		}
	}		
	throw new SheetNotFoundException("Sheet '" + sheetName +"' not found" );
}

public void cloneSheet(int index, String newSheetName) {		
	HSSFSheet newSheet = workbook.cloneSheet(index);
        for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
            if(newSheet.equals(workbook.getSheetAt(i))) {
                workbook.setSheetName(i, newSheetName);
                break;
            }
        }
    }

Но самое интересное — это Apache Commons JEXL library.

JEXL implements an Expression Language based on some extensions to the JSTL Expression Language supporting most of the constructs seen in shell-script or ECMAScript

Вот немного переделанный пример с сайта:

            // Create or retrieve a JexlEngine
            JexlEngine jexl = new JexlEngine();
            // Create an expression object
            String jexlExp = "user.name";
            Expression e = jexl.createExpression( jexlExp );
            // Create a context and add data
            JexlContext jc = new MapContext();
            jc.set("user", new User("Вася") );
            // Now evaluate the expression, getting the result
            Object o = e.evaluate(jc);
            o.toString(); //Вернёт "Вася"

Таким образом отпадает необходимость перечислять в java коде все теги, задавая им соответствие с реальными данными.
Достаточно задать соответствие только для объекта, после этого задача замены «тега» на его значение сводится к поиску «тегов», скармливания их Commons JEXL и запись в ячейку с тегом результата работы библиотеки. Как то сумбурно получилось, попробую объяснить на примере.
Пусть у нас в шаблоне «теги» будут выглядеть например так: ${user.name}, ${user.age}. А в java коде достаточно будет просто поместить в карту объект класса User.
После этого пробежимся в цикле по всем ячейкам документа, найдём строки, ограниченные '${' и '}' и заменим значения в них на результат работы Commons JEXL library

private void fillSheet() {
	User user=new User("Вася",25); //Имя и возраст
	Map<String,Object> mappings=new HashMap<String,Object>();
	mappings.put("user",user);
	JexlEngine engine=new JexlEngine();
	JexlContext context=new MapContext(mappings);
		for(Row row : sheet) {          
			for(Cell cell : row) {    
	        	if(cell.getCellType()==Cell.CELL_TYPE_STRING){
					String exp=findExpression(cell);
					if(exp!=null){
						Expression e=engine.createExpression(exp);
						Object o=e.evaluate(context);
						if(o!=null){
							String result=o.toString();
							cell.setCellValue(result);
						}	
					}
				}
			}
		}
}

Метод findExpression() ищет в строке, содержащейся в ячейке подстроку, заключённую между между '${' и '}'

О Apache Commons JEXL library я узнал, встретившись с проектом JETT. Не хотелось добавлять кучу библиотек в проект (JETT зависит ещё от нескольких библиотек), да и вся функциональность JETT мне не нужна. А вот разобраться, как оно там, внутрях, устроено было интересно :). Рад буду, если хоть кому — то этот пост поможет.

Автор: eof404

Источник

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


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