From 9169da5fda20cd0a63733b284493820e1cacef7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E6=A2=A6=E6=99=96?= <942874769@qq.com> Date: Fri, 26 Feb 2021 16:28:48 +0800 Subject: [PATCH] # WARNING: head commit changed in the meantime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit map简单支持 --- pom.xml | 376 ++++---- .../github/chimmhuang/excel/ExcelHelper.java | 838 +++++++++--------- .../chimmhuang/excel/parser/MapParser.java | 74 ++ .../github/chimmhuang/excel/demo/Demo2.java | 85 ++ src/test/resources/demo_map.xlsx | Bin 0 -> 11084 bytes 5 files changed, 775 insertions(+), 598 deletions(-) create mode 100644 src/main/java/com/github/chimmhuang/excel/parser/MapParser.java create mode 100644 src/test/java/com/github/chimmhuang/excel/demo/Demo2.java create mode 100644 src/test/resources/demo_map.xlsx diff --git a/pom.xml b/pom.xml index b3d823a..337079c 100644 --- a/pom.xml +++ b/pom.xml @@ -1,189 +1,195 @@ - 4.0.0 - - com.github.chimmhuang - chimm.excel - 1.3.0 - jar - chimm.excel - chimm.excel 是一款能够简单操作 excel 的程序,该程序提供了如:填充excel模板数据、动态更改表格样式、导出excel等功能,降低了我们对excel的操作难度 - https://github.com/chimmhuang/chimm.excel - - - - Chimm Huang - chimmhuang@163.com - - - - - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - - - - - - scm:git:git://github.com/chimmhuang/OSSRH-58168.git - scm:git:ssh://github.com/chimmhuang/OSSRH-58168.git - https://github.com/chimmhuang/OSSRH-58168/tree/master - - - - - 1.8 - UTF-8 - 1.8 - 1.8 - 1.8 - UTF-8 - - - 4.1.2 - 4.8 - - - 1.3.2 - 3.11 - - - 4.13 - - - - - - org.apache.poi - poi-ooxml - ${poi.version} - - - - - junit - junit - ${junit.version} - test - - - - - org.apache.commons - commons-io - ${commons-io.version} - test - - - org.apache.commons - commons-lang3 - ${commons-lang3.version} - - - - - org.antlr - antlr4-runtime - ${antlr4-runtime.version} - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - - - org.apache.maven.plugins - maven-jar-plugin - - - false - - - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.10.3 - - - attach-javadocs - - jar - - - -Xdoclint:none - - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 - true - - sonatype-nexus-staging - https://oss.sonatype.org/ - true - - - - - - - - sonatype-nexus-snapshots - oss Snapshots Repository - https://oss.sonatype.org/content/repositories/snapshots - - - sonatype-nexus-staging - oss Staging Repository - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + com.github.chimmhuang + chimm.excel + 1.3.0 + jar + chimm.excel + chimm.excel 是一款能够简单操作 excel 的程序,该程序提供了如:填充excel模板数据、动态更改表格样式、导出excel等功能,降低了我们对excel的操作难度 + https://github.com/chimmhuang/chimm.excel + + + + Chimm Huang + chimmhuang@163.com + + + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + scm:git:git://github.com/chimmhuang/OSSRH-58168.git + scm:git:ssh://github.com/chimmhuang/OSSRH-58168.git + https://github.com/chimmhuang/OSSRH-58168/tree/master + + + + + 1.18.16 + 1.8 + UTF-8 + 1.8 + 1.8 + 1.8 + UTF-8 + + + 4.1.2 + 4.8 + + + 1.3.2 + 3.11 + + + 4.13 + + + + + org.projectlombok + lombok + ${lombok.version} + + + + org.apache.poi + poi-ooxml + ${poi.version} + + + + + junit + junit + ${junit.version} + test + + + + + org.apache.commons + commons-io + ${commons-io.version} + test + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + + org.antlr + antlr4-runtime + ${antlr4-runtime.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-jar-plugin + + + false + + + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.3 + + + attach-javadocs + + jar + + + -Xdoclint:none + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.7 + true + + sonatype-nexus-staging + https://oss.sonatype.org/ + true + + + + + + + + sonatype-nexus-snapshots + oss Snapshots Repository + https://oss.sonatype.org/content/repositories/snapshots + + + sonatype-nexus-staging + oss Staging Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + \ No newline at end of file diff --git a/src/main/java/com/github/chimmhuang/excel/ExcelHelper.java b/src/main/java/com/github/chimmhuang/excel/ExcelHelper.java index 38487a3..ed4ecff 100644 --- a/src/main/java/com/github/chimmhuang/excel/ExcelHelper.java +++ b/src/main/java/com/github/chimmhuang/excel/ExcelHelper.java @@ -1,34 +1,5 @@ package com.github.chimmhuang.excel; -import com.github.chimmhuang.excel.parser.DataVariableParserVisitor; -import com.github.chimmhuang.excel.parser.VariableParserLexer; -import com.github.chimmhuang.excel.parser.VariableParserParser; -import com.github.chimmhuang.excel.tablemodel.Cell; -import com.github.chimmhuang.excel.tablemodel.CellStyle; -import com.github.chimmhuang.excel.tablemodel.Font; -import com.github.chimmhuang.excel.tablemodel.SheetTable; -import com.github.chimmhuang.excel.parser.VariableParserParser.ArrayIdxContext; -import com.github.chimmhuang.excel.parser.VariableParserParser.VariableContext; -import com.github.chimmhuang.excel.exception.ConvertException; -import com.github.chimmhuang.excel.exception.InvokeMethodException; -import com.github.chimmhuang.excel.exception.ReflectionException; -import com.github.chimmhuang.excel.tablemodel.ExcelWorkbook; -import com.github.chimmhuang.excel.tablemodel.MergedRegion; -import com.github.chimmhuang.excel.tablemodel.Row; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; -import org.apache.poi.common.usermodel.HyperlinkType; -import org.apache.poi.ss.usermodel.CellType; -import org.apache.poi.ss.usermodel.RichTextString; -import org.apache.poi.ss.util.CellRangeAddress; -import org.apache.poi.xssf.usermodel.XSSFCellStyle; -import org.apache.poi.xssf.usermodel.XSSFCreationHelper; -import org.apache.poi.xssf.usermodel.XSSFFont; -import org.apache.poi.xssf.usermodel.XSSFHyperlink; -import org.apache.poi.xssf.usermodel.XSSFRow; -import org.apache.poi.xssf.usermodel.XSSFSheet; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; - import java.beans.PropertyDescriptor; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -44,393 +15,434 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.apache.poi.common.usermodel.HyperlinkType; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.RichTextString; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.apache.poi.xssf.usermodel.XSSFCreationHelper; +import org.apache.poi.xssf.usermodel.XSSFFont; +import org.apache.poi.xssf.usermodel.XSSFHyperlink; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import com.github.chimmhuang.excel.exception.ConvertException; +import com.github.chimmhuang.excel.exception.InvokeMethodException; +import com.github.chimmhuang.excel.exception.ReflectionException; +import com.github.chimmhuang.excel.parser.DataVariableParserVisitor; +import com.github.chimmhuang.excel.parser.MapParser; +import com.github.chimmhuang.excel.parser.VariableParserLexer; +import com.github.chimmhuang.excel.parser.VariableParserParser; +import com.github.chimmhuang.excel.parser.VariableParserParser.ArrayIdxContext; +import com.github.chimmhuang.excel.parser.VariableParserParser.VariableContext; +import com.github.chimmhuang.excel.tablemodel.Cell; +import com.github.chimmhuang.excel.tablemodel.CellStyle; +import com.github.chimmhuang.excel.tablemodel.ExcelWorkbook; +import com.github.chimmhuang.excel.tablemodel.Font; +import com.github.chimmhuang.excel.tablemodel.MergedRegion; +import com.github.chimmhuang.excel.tablemodel.Row; +import com.github.chimmhuang.excel.tablemodel.SheetTable; + /** * @author Chimm Huang */ public class ExcelHelper { - private ExcelHelper() { } - - private static final char[] COL_SET = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', - 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; - - public static String getColName(Integer index) { - StringBuilder sb = new StringBuilder(); - int length = COL_SET.length; - int idx = index; - while (idx > length - 1) { - idx = idx / length; - sb.append(COL_SET[idx - 1]); - } - sb.append(COL_SET[index % length]); - return sb.toString(); - } - - public static Integer getColIndex(String name) { - String colSet = new String(COL_SET); - char[] chars = name.toCharArray(); - int result = 0; - for (int index = chars.length - 1; index >= 0; index--) { - char ch = chars[index]; - int i = colSet.indexOf(ch); - result += (i + 1) * Math.pow(COL_SET.length, chars.length - index - 1d); - } - return result - 1; - } - - /** - * 创建 excel 工作簿 - * create excel workbook - * - * @param bytes excel 的二进制文件 - * excel binary file - */ - public static ExcelWorkbook createWorkbook(byte[] bytes) throws IOException { - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); - XSSFWorkbook xssfWorkbook = new XSSFWorkbook(byteArrayInputStream); - return new ExcelWorkbook(xssfWorkbook); - } - - /** - * 获取 sheet 页的表格信息 - * get the sheet table with index 0 - * - * @param bytes excel binary file - */ - public static SheetTable getSheetTable(byte[] bytes) throws IOException { - return getSheetTable(bytes, 0); - } - - /** - * 获取 sheet 页的表格信息 - * get sheet table - * - * @param bytes excel binary file - * @param sheetIndex sheet index, start from 0 - */ - public static SheetTable getSheetTable(byte[] bytes, int sheetIndex) throws IOException { - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); - XSSFWorkbook xssfWorkbook = new XSSFWorkbook(byteArrayInputStream); - return new SheetTable(xssfWorkbook.getSheetAt(sheetIndex)); - } - - /** - * 获取 sheet 页的表格信息 - * get sheet table - * - * @param bytes excel binary file - * @param sheetName sheet name - */ - public static SheetTable getSheetTable(byte[] bytes, String sheetName) throws IOException { - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); - XSSFWorkbook xssfWorkbook = new XSSFWorkbook(byteArrayInputStream); - return new SheetTable(xssfWorkbook.getSheet(sheetName)); - } - - /** - * 将变量填充入表格 - * fill variables into excel table - * - * @param table excel sheet table - * @param data table data - */ - public static void fillInData(SheetTable table, Object data) { - - DataVariableParserVisitor visitor = new DataVariableParserVisitor(data); - - for (Cell cell : table) { - Object value = cell.getValue(); - CellType cellType = cell.getCellType(); - - if (cellType.equals(CellType.FORMULA) && value != null) { - // insert formula - VariableParserLexer lexer = new VariableParserLexer(CharStreams.fromString(value.toString())); - CommonTokenStream tokens = new CommonTokenStream(lexer); - - // syntax analysis - VariableParserParser parser = new VariableParserParser(tokens); - - String newFormula = (String) parser.expr().accept(visitor); - - cell.setFormula(newFormula); - } else if (value instanceof String && ((String) value).startsWith("$")) { - // insert value - Object propValue = parseCellVariable(data, (String) value); - cell.setValue(propValue); - } - } - } - - /** - * 将 sheet 对象转换为 excel 二进制文件 - * convert excel table{@link SheetTable} to byte - * - * @param table sheet table - * @return excel binary file - */ - public static byte[] convert2Byte(SheetTable table) { - - XSSFWorkbook xssfWorkbook = new XSSFWorkbook(); - XSSFSheet xssfSheet = xssfWorkbook.createSheet(table.getSheetName()); - XSSFCreationHelper xssfCreationHelper = new XSSFCreationHelper(xssfWorkbook); - - // set sheet style - Map colWidthMap = table.getColWidthMap(); - colWidthMap.forEach(xssfSheet::setColumnWidth); - - Iterator rowIterator = table.rowIterator(); - while (rowIterator.hasNext()) { - Row row = rowIterator.next(); - org.apache.poi.ss.usermodel.Row xssfRow = getOrCreateRow(row.getRowNum(), xssfSheet); - - // set row style - xssfRow.setHeight(row.getHeight()); - xssfRow.setHeightInPoints(row.getHeightInPoints()); - xssfRow.setZeroHeight(row.getZeroHeight()); - - for (Cell cell : row) { - CellType cellType = cell.getCellType(); - org.apache.poi.ss.usermodel.Cell xssfCell = xssfRow.createCell(getColIndex(cell.getCol()), cellType); - - Object value = cell.getValue(); - - // set cell value - switch (cellType) { - case BLANK: - xssfCell.setBlank(); - break; - case FORMULA: - if (value != null) { - String formula = value.toString().replace("=", ""); - xssfCell.setCellFormula(formula); - } - break; - default: - if (value != null) { - switch (value.getClass().getName()) { - case "java.lang.Integer": - xssfCell.setCellValue((Integer) value); - break; - case "java.lang.Double": - xssfCell.setCellValue((Double) value); - break; - case "java.math.BigDecimal": - xssfCell.setCellValue(((BigDecimal) value).doubleValue()); - break; - case "java.lang.String": - xssfCell.setCellValue(value.toString()); - break; - case "java.lang.Boolean": - xssfCell.setCellValue((Boolean) value); - break; - case "java.util.Date": - xssfCell.setCellValue((Date) value); - break; - case "java.util.Calendar": - xssfCell.setCellValue((Calendar) value); - break; - case "java.time.LocalDate": - xssfCell.setCellValue((LocalDate) value); - break; - case "java.time.LocalDateTime": - xssfCell.setCellValue((LocalDateTime) value); - break; - case "org.apache.poi.ss.usermodel.RichTextString": - xssfCell.setCellValue((RichTextString) value); - break; - default: - break; - } - } - break; - } - - // set cell style - org.apache.poi.ss.usermodel.CellStyle xssfCellStyle = toExcelCellStyle(xssfWorkbook, cell.getCellStyle()); - xssfCell.setCellStyle(xssfCellStyle); - - // set merged region - MergedRegion mergedRegion = cell.getMergedRegion(); - if (mergedRegion != null) { - CellRangeAddress cellRangeAddress = new CellRangeAddress(mergedRegion.getFirstRowNum() - 1, mergedRegion.getLastRowNum() - 1, getColIndex(mergedRegion.getFirstColName()), getColIndex(mergedRegion.getLastColName())); - xssfSheet.addMergedRegion(cellRangeAddress); - } - - // set hyper link - String hyperlink = cell.getHyperlink(); - HyperlinkType hyperlinkType = cell.getHyperlinkType(); - if (hyperlink != null && !"".equals(hyperlink) && hyperlinkType != null) { - XSSFHyperlink link = xssfCreationHelper.createHyperlink(hyperlinkType); - link.setAddress(hyperlink); - xssfCell.setHyperlink(link); - } - } - } - - try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { - // convert xssfWorkbook to binary file - xssfWorkbook.setForceFormulaRecalculation(true); - xssfWorkbook.write(bos); - return bos.toByteArray(); - } catch (IOException e) { - throw new ConvertException(e); - } - } - - /** - * 转换 cellStyle 对象为 Apache poi 的对象 - * convert {@link CellStyle} to {@link org.apache.poi.ss.usermodel.CellStyle} - */ - private static org.apache.poi.ss.usermodel.CellStyle toExcelCellStyle(XSSFWorkbook xssfWorkbook, CellStyle cellStyle) { - XSSFCellStyle xssfCellStyle = xssfWorkbook.createCellStyle(); - - xssfCellStyle.setBorderTop(cellStyle.getBorderTopEnum()); - xssfCellStyle.setBorderBottom(cellStyle.getBorderBottomEnum()); - xssfCellStyle.setBorderLeft(cellStyle.getBorderLeftEnum()); - xssfCellStyle.setBorderRight(cellStyle.getBorderRightEnum()); - - xssfCellStyle.setTopBorderColor(cellStyle.getTopBorderColor()); - xssfCellStyle.setBottomBorderColor(cellStyle.getBottomBorderColor()); - xssfCellStyle.setLeftBorderColor(cellStyle.getLeftBorderColor()); - xssfCellStyle.setRightBorderColor(cellStyle.getRightBorderColor()); - - xssfCellStyle.setFillBackgroundColor(cellStyle.getFillBackgroundXSSFColor()); - xssfCellStyle.setFillForegroundColor(cellStyle.getFillForegroundXSSFColor()); - xssfCellStyle.setFillPattern(cellStyle.getFillPattern()); - - xssfCellStyle.setDataFormat(cellStyle.getDataFormat()); - xssfCellStyle.setHidden(cellStyle.getHidden()); - xssfCellStyle.setLocked(cellStyle.getLocked()); - - xssfCellStyle.setIndention(cellStyle.getIndention()); - xssfCellStyle.setWrapText(cellStyle.getWrapText()); - xssfCellStyle.setShrinkToFit(cellStyle.getShrinkToFit()); - xssfCellStyle.setReadingOrder(cellStyle.getReadingOrder()); - xssfCellStyle.setQuotePrefixed(cellStyle.getQuotePrefixed()); - xssfCellStyle.setRotation(cellStyle.getRotation()); - - xssfCellStyle.setAlignment(cellStyle.getAlignmentEnum()); - xssfCellStyle.setVerticalAlignment(cellStyle.getVerticalAlignmentEnum()); - - Font font = cellStyle.getFont(); - if (font != null) { - XSSFFont xssfFont = xssfWorkbook.createFont(); - - xssfFont.setBold(font.getBold()); - xssfFont.setCharSet(font.getCharSet()); - xssfFont.setColor(font.getColor()); - xssfFont.setFamily(font.getFamily()); - xssfFont.setFontHeight(font.getFontHeight()); - xssfFont.setFontHeightInPoints(font.getFontHeightInPoints()); - xssfFont.setFontName(font.getFontName()); - xssfFont.setItalic(font.getItalic()); - xssfFont.setScheme(font.getScheme()); - xssfFont.setStrikeout(font.getStrikeout()); - xssfFont.setThemeColor(font.getThemeColor()); - xssfFont.setTypeOffset(font.getTypeOffset()); - xssfFont.setUnderline(font.getUnderline()); - - xssfCellStyle.setFont(xssfFont); - } - - return xssfCellStyle; - } - - /** - * 获取 XSSFRow 对象,若不存在,则创建一个 - * get XSSFRow {@link XSSFRow}. if it's not exist, then create - * - * @param rowNum row-num, start from 1 - * @param xssfSheet {@link XSSFSheet} - * @return {@link XSSFRow} - */ - private static org.apache.poi.ss.usermodel.Row getOrCreateRow(int rowNum, XSSFSheet xssfSheet) { - int rowIndex = rowNum - 1; - XSSFRow row = xssfSheet.getRow(rowIndex); - if (row == null) { - row = xssfSheet.createRow(rowIndex); - } - return row; - } - - /** - * 将 excel 里的变量转换为对应的值 - * parse the variable name filled in the cell - * - * @param data table data - * @param cellVariableName the variable name filled in the cell - * @return the value corresponding to the table data - */ - public static Object parseCellVariable(Object data, String cellVariableName) { - // lexical analysis - VariableParserLexer lexer = new VariableParserLexer(CharStreams.fromString(cellVariableName)); - CommonTokenStream tokens = new CommonTokenStream(lexer); - - // syntax analysis - VariableParserParser parser = new VariableParserParser(tokens); - - Object propValue = data; - for (VariableContext variableContext : parser.variableExpr().variable()) { - String variableName = variableContext.IDENTIFIER().getText(); - propValue = getPropValue(propValue, variableName); - if (propValue == null) { - return null; - } - - // if the prop instanceof List, then get the value under a specific index. - List arrayIdxContexts = variableContext.arrayIdx(); - for (ArrayIdxContext arrayIdxContext : arrayIdxContexts) { - int index = Integer.parseInt(arrayIdxContext.NUMBER().getSymbol().getText()); - if (propValue instanceof List) { - List propValueList = (List) propValue; - propValue = propValueList.get(index); - } else { - throw new IllegalArgumentException(propValue.getClass().getName() + "must be List."); - } - } - } - return propValue; - } - - /** - * 获取表格数据对应变量的值 - * get the value of the specified attribute of the incoming data - * - * @param obj 表格数据对象 - * object - * @param propName 表格数据对象的属性名称 - * object attribute name - * @return 对应属性的值。the value of the specified attribute of the incoming data - */ - private static Object getPropValue(Object obj, String propName) { - - if (obj == null || propName == null) { - return null; - } - - if (obj instanceof Map) { - Map mapObj = (Map) obj; - return mapObj.get(propName); - } - - Field declaredField = null; - try { - declaredField = obj.getClass().getDeclaredField(propName); - } catch (NoSuchFieldException e) { - // try to get the parent class - try { - declaredField = obj.getClass().getSuperclass().getDeclaredField(propName); - } catch (NoSuchFieldException noSuchFieldException) { - throw new ReflectionException(noSuchFieldException); - } - } - - try { - PropertyDescriptor pd = new PropertyDescriptor(declaredField.getName(), obj.getClass()); - Method method = pd.getReadMethod(); - return method.invoke(obj); - } catch (Exception e) { - throw new InvokeMethodException(e); - } - } + private ExcelHelper() { + } + + private static final char[] COL_SET = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' }; + + public static String getColName(Integer index) { + StringBuilder sb = new StringBuilder(); + int length = COL_SET.length; + int idx = index; + while (idx > length - 1) { + idx = idx / length; + sb.append(COL_SET[idx - 1]); + } + sb.append(COL_SET[index % length]); + return sb.toString(); + } + + public static Integer getColIndex(String name) { + String colSet = new String(COL_SET); + char[] chars = name.toCharArray(); + int result = 0; + for (int index = chars.length - 1; index >= 0; index--) { + char ch = chars[index]; + int i = colSet.indexOf(ch); + result += (i + 1) * Math.pow(COL_SET.length, chars.length - index - 1d); + } + return result - 1; + } + + /** + * 创建 excel 工作簿 create excel workbook + * + * @param bytes excel 的二进制文件 excel binary file + */ + public static ExcelWorkbook createWorkbook(byte[] bytes) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + XSSFWorkbook xssfWorkbook = new XSSFWorkbook(byteArrayInputStream); + return new ExcelWorkbook(xssfWorkbook); + } + + /** + * 获取 sheet 页的表格信息 get the sheet table with index 0 + * + * @param bytes excel binary file + */ + public static SheetTable getSheetTable(byte[] bytes) throws IOException { + return getSheetTable(bytes, 0); + } + + /** + * 获取 sheet 页的表格信息 get sheet table + * + * @param bytes excel binary file + * @param sheetIndex sheet index, start from 0 + */ + public static SheetTable getSheetTable(byte[] bytes, int sheetIndex) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + XSSFWorkbook xssfWorkbook = new XSSFWorkbook(byteArrayInputStream); + return new SheetTable(xssfWorkbook.getSheetAt(sheetIndex)); + } + + /** + * 获取 sheet 页的表格信息 get sheet table + * + * @param bytes excel binary file + * @param sheetName sheet name + */ + public static SheetTable getSheetTable(byte[] bytes, String sheetName) throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + XSSFWorkbook xssfWorkbook = new XSSFWorkbook(byteArrayInputStream); + return new SheetTable(xssfWorkbook.getSheet(sheetName)); + } + + /** + * 将变量填充入表格 fill variables into excel table + * + * @param table excel sheet table + * @param data table data + */ + public static void fillInData(SheetTable table, Object data) { + + DataVariableParserVisitor visitor = new DataVariableParserVisitor(data); + + for (Cell cell : table) { + Object value = cell.getValue(); + CellType cellType = cell.getCellType(); + + if (cellType.equals(CellType.FORMULA) && value != null) { + // insert formula + VariableParserLexer lexer = new VariableParserLexer(CharStreams.fromString(value.toString())); + CommonTokenStream tokens = new CommonTokenStream(lexer); + + // syntax analysis + VariableParserParser parser = new VariableParserParser(tokens); + + String newFormula = (String) parser.expr().accept(visitor); + + cell.setFormula(newFormula); + } else if (value instanceof String && ((String) value).startsWith("$")) { + // insert value + Object propValue = parseCellVariable(data, (String) value); + cell.setValue(propValue); + } + } + } + + /** + * 将变量填充入表格 fill variables into excel table + * + * @param table excel sheet table + * @param data table data + */ + public static void fillInMapData(SheetTable table, MapParser mapData) { + for (Cell cell : table) { + Object value = cell.getValue(); + if (value instanceof String && ((String) value).startsWith("$")) { + Object val = mapData.getObject(value.toString()); + cell.setValue(val); + } + } + } + + /** + * 将 sheet 对象转换为 excel 二进制文件 convert excel table{@link SheetTable} to byte + * + * @param table sheet table + * @return excel binary file + */ + public static byte[] convert2Byte(SheetTable table) { + + XSSFWorkbook xssfWorkbook = new XSSFWorkbook(); + XSSFSheet xssfSheet = xssfWorkbook.createSheet(table.getSheetName()); + XSSFCreationHelper xssfCreationHelper = new XSSFCreationHelper(xssfWorkbook); + + // set sheet style + Map colWidthMap = table.getColWidthMap(); + colWidthMap.forEach(xssfSheet::setColumnWidth); + + Iterator rowIterator = table.rowIterator(); + while (rowIterator.hasNext()) { + Row row = rowIterator.next(); + org.apache.poi.ss.usermodel.Row xssfRow = getOrCreateRow(row.getRowNum(), xssfSheet); + + // set row style + xssfRow.setHeight(row.getHeight()); + xssfRow.setHeightInPoints(row.getHeightInPoints()); + xssfRow.setZeroHeight(row.getZeroHeight()); + + for (Cell cell : row) { + CellType cellType = cell.getCellType(); + org.apache.poi.ss.usermodel.Cell xssfCell = xssfRow.createCell(getColIndex(cell.getCol()), cellType); + + Object value = cell.getValue(); + + // set cell value + switch (cellType) { + case BLANK: + xssfCell.setBlank(); + break; + case FORMULA: + if (value != null) { + String formula = value.toString().replace("=", ""); + xssfCell.setCellFormula(formula); + } + break; + default: + if (value != null) { + switch (value.getClass().getName()) { + case "java.lang.Integer": + xssfCell.setCellValue((Integer) value); + break; + case "java.lang.Double": + xssfCell.setCellValue((Double) value); + break; + case "java.math.BigDecimal": + xssfCell.setCellValue(((BigDecimal) value).doubleValue()); + break; + case "java.lang.String": + xssfCell.setCellValue(value.toString()); + break; + case "java.lang.Boolean": + xssfCell.setCellValue((Boolean) value); + break; + case "java.util.Date": + xssfCell.setCellValue((Date) value); + break; + case "java.util.Calendar": + xssfCell.setCellValue((Calendar) value); + break; + case "java.time.LocalDate": + xssfCell.setCellValue((LocalDate) value); + break; + case "java.time.LocalDateTime": + xssfCell.setCellValue((LocalDateTime) value); + break; + case "org.apache.poi.ss.usermodel.RichTextString": + xssfCell.setCellValue((RichTextString) value); + break; + default: + break; + } + } + break; + } + + // set cell style + org.apache.poi.ss.usermodel.CellStyle xssfCellStyle = toExcelCellStyle(xssfWorkbook, + cell.getCellStyle()); + xssfCell.setCellStyle(xssfCellStyle); + + // set merged region + MergedRegion mergedRegion = cell.getMergedRegion(); + if (mergedRegion != null) { + CellRangeAddress cellRangeAddress = new CellRangeAddress(mergedRegion.getFirstRowNum() - 1, + mergedRegion.getLastRowNum() - 1, getColIndex(mergedRegion.getFirstColName()), + getColIndex(mergedRegion.getLastColName())); + xssfSheet.addMergedRegion(cellRangeAddress); + } + + // set hyper link + String hyperlink = cell.getHyperlink(); + HyperlinkType hyperlinkType = cell.getHyperlinkType(); + if (hyperlink != null && !"".equals(hyperlink) && hyperlinkType != null) { + XSSFHyperlink link = xssfCreationHelper.createHyperlink(hyperlinkType); + link.setAddress(hyperlink); + xssfCell.setHyperlink(link); + } + } + } + + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + // convert xssfWorkbook to binary file + xssfWorkbook.setForceFormulaRecalculation(true); + xssfWorkbook.write(bos); + return bos.toByteArray(); + } catch (IOException e) { + throw new ConvertException(e); + } + } + + /** + * 转换 cellStyle 对象为 Apache poi 的对象 convert {@link CellStyle} to + * {@link org.apache.poi.ss.usermodel.CellStyle} + */ + private static org.apache.poi.ss.usermodel.CellStyle toExcelCellStyle(XSSFWorkbook xssfWorkbook, + CellStyle cellStyle) { + XSSFCellStyle xssfCellStyle = xssfWorkbook.createCellStyle(); + + xssfCellStyle.setBorderTop(cellStyle.getBorderTopEnum()); + xssfCellStyle.setBorderBottom(cellStyle.getBorderBottomEnum()); + xssfCellStyle.setBorderLeft(cellStyle.getBorderLeftEnum()); + xssfCellStyle.setBorderRight(cellStyle.getBorderRightEnum()); + + xssfCellStyle.setTopBorderColor(cellStyle.getTopBorderColor()); + xssfCellStyle.setBottomBorderColor(cellStyle.getBottomBorderColor()); + xssfCellStyle.setLeftBorderColor(cellStyle.getLeftBorderColor()); + xssfCellStyle.setRightBorderColor(cellStyle.getRightBorderColor()); + + xssfCellStyle.setFillBackgroundColor(cellStyle.getFillBackgroundXSSFColor()); + xssfCellStyle.setFillForegroundColor(cellStyle.getFillForegroundXSSFColor()); + xssfCellStyle.setFillPattern(cellStyle.getFillPattern()); + + xssfCellStyle.setDataFormat(cellStyle.getDataFormat()); + xssfCellStyle.setHidden(cellStyle.getHidden()); + xssfCellStyle.setLocked(cellStyle.getLocked()); + + xssfCellStyle.setIndention(cellStyle.getIndention()); + xssfCellStyle.setWrapText(cellStyle.getWrapText()); + xssfCellStyle.setShrinkToFit(cellStyle.getShrinkToFit()); + xssfCellStyle.setReadingOrder(cellStyle.getReadingOrder()); + xssfCellStyle.setQuotePrefixed(cellStyle.getQuotePrefixed()); + xssfCellStyle.setRotation(cellStyle.getRotation()); + + xssfCellStyle.setAlignment(cellStyle.getAlignmentEnum()); + xssfCellStyle.setVerticalAlignment(cellStyle.getVerticalAlignmentEnum()); + + Font font = cellStyle.getFont(); + if (font != null) { + XSSFFont xssfFont = xssfWorkbook.createFont(); + + xssfFont.setBold(font.getBold()); + xssfFont.setCharSet(font.getCharSet()); + xssfFont.setColor(font.getColor()); + xssfFont.setFamily(font.getFamily()); + xssfFont.setFontHeight(font.getFontHeight()); + xssfFont.setFontHeightInPoints(font.getFontHeightInPoints()); + xssfFont.setFontName(font.getFontName()); + xssfFont.setItalic(font.getItalic()); + xssfFont.setScheme(font.getScheme()); + xssfFont.setStrikeout(font.getStrikeout()); + xssfFont.setThemeColor(font.getThemeColor()); + xssfFont.setTypeOffset(font.getTypeOffset()); + xssfFont.setUnderline(font.getUnderline()); + + xssfCellStyle.setFont(xssfFont); + } + + return xssfCellStyle; + } + + /** + * 获取 XSSFRow 对象,若不存在,则创建一个 get XSSFRow {@link XSSFRow}. if it's not exist, then + * create + * + * @param rowNum row-num, start from 1 + * @param xssfSheet {@link XSSFSheet} + * @return {@link XSSFRow} + */ + private static org.apache.poi.ss.usermodel.Row getOrCreateRow(int rowNum, XSSFSheet xssfSheet) { + int rowIndex = rowNum - 1; + XSSFRow row = xssfSheet.getRow(rowIndex); + if (row == null) { + row = xssfSheet.createRow(rowIndex); + } + return row; + } + + /** + * 将 excel 里的变量转换为对应的值 parse the variable name filled in the cell + * + * @param data table data + * @param cellVariableName the variable name filled in the cell + * @return the value corresponding to the table data + */ + public static Object parseCellVariable(Object data, String cellVariableName) { + // lexical analysis + VariableParserLexer lexer = new VariableParserLexer(CharStreams.fromString(cellVariableName)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + + // syntax analysis + VariableParserParser parser = new VariableParserParser(tokens); + + Object propValue = data; + for (VariableContext variableContext : parser.variableExpr().variable()) { + String variableName = variableContext.IDENTIFIER().getText(); + propValue = getPropValue(propValue, variableName); + if (propValue == null) { + return null; + } + + // if the prop instanceof List, then get the value under a specific index. + List arrayIdxContexts = variableContext.arrayIdx(); + for (ArrayIdxContext arrayIdxContext : arrayIdxContexts) { + int index = Integer.parseInt(arrayIdxContext.NUMBER().getSymbol().getText()); + if (propValue instanceof List) { + List propValueList = (List) propValue; + propValue = propValueList.get(index); + } else { + throw new IllegalArgumentException(propValue.getClass().getName() + "must be List."); + } + } + } + return propValue; + } + + /** + * 获取表格数据对应变量的值 get the value of the specified attribute of the incoming data + * + * @param obj 表格数据对象 object + * @param propName 表格数据对象的属性名称 object attribute name + * @return 对应属性的值。the value of the specified attribute of the incoming data + */ + private static Object getPropValue(Object obj, String propName) { + + if (obj == null || propName == null) { + return null; + } + + if (obj instanceof Map) { + Map mapObj = (Map) obj; + return mapObj.get(propName); + } + + Field declaredField = null; + try { + declaredField = obj.getClass().getDeclaredField(propName); + } catch (NoSuchFieldException e) { + // try to get the parent class + try { + declaredField = obj.getClass().getSuperclass().getDeclaredField(propName); + } catch (NoSuchFieldException noSuchFieldException) { + throw new ReflectionException(noSuchFieldException); + } + } + + try { + PropertyDescriptor pd = new PropertyDescriptor(declaredField.getName(), obj.getClass()); + Method method = pd.getReadMethod(); + return method.invoke(obj); + } catch (Exception e) { + throw new InvokeMethodException(e); + } + } } diff --git a/src/main/java/com/github/chimmhuang/excel/parser/MapParser.java b/src/main/java/com/github/chimmhuang/excel/parser/MapParser.java new file mode 100644 index 0000000..5ca9f65 --- /dev/null +++ b/src/main/java/com/github/chimmhuang/excel/parser/MapParser.java @@ -0,0 +1,74 @@ +package com.github.chimmhuang.excel.parser; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import lombok.Data; + +public class MapParser extends HashMap { + @Data + class ParseKey { + String key; + Boolean isList; + Integer index; + ParseKey sub; + } + + public ParseKey parseKey(String key) { + ParseKey parse = new MapParser.ParseKey(); + int point = key.indexOf('.'); + String preKey; + if (point == -1) { + preKey = key; + } else { + preKey = key.substring(0, point); + String newKey = key.substring(point + 1, key.length()); + parse.setSub(parseKey(newKey)); + } + parse.setIsList(false); + if (preKey.endsWith("]")) { + parse.setIsList(true); + int prePoint = preKey.lastIndexOf("["); + if (prePoint + 1 != preKey.length() - 1) { + String index = preKey.substring(prePoint + 1, preKey.length() - 1); + parse.setIndex(Integer.parseInt(index)); + } + preKey = preKey.substring(0, prePoint); + } + parse.setKey(preKey); + return parse; + } + + public Object getObject(String key) { + if (key.startsWith("${") && key.endsWith("}")) { + key = key.substring(2, key.length() - 1); + ParseKey parse = this.parseKey(key); + Map subMap = this; + while (parse != null) { + Object obj = subMap.get(parse.getKey()); + if (obj == null) { + return null; + } + if (parse.getIsList() && parse.getIndex() != null && obj instanceof List) { + List list = (List) obj; + if (list.size() > parse.getIndex()) { + Object listVal = list.get(parse.getIndex()); + obj = listVal; + } + } + if (parse.getSub() == null) { + return obj; + } + + if (!(obj instanceof Map)) { + return null; + } + subMap = (Map) obj; + parse = parse.getSub(); + } + } + return null; + } + +} diff --git a/src/test/java/com/github/chimmhuang/excel/demo/Demo2.java b/src/test/java/com/github/chimmhuang/excel/demo/Demo2.java new file mode 100644 index 0000000..6e75426 --- /dev/null +++ b/src/test/java/com/github/chimmhuang/excel/demo/Demo2.java @@ -0,0 +1,85 @@ +package com.github.chimmhuang.excel.demo; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.filechooser.FileSystemView; + +import org.apache.commons.io.FileUtils; + +import com.github.chimmhuang.excel.ExcelHelper; +import com.github.chimmhuang.excel.parser.MapParser; +import com.github.chimmhuang.excel.tablemodel.ExcelWorkbook; +import com.github.chimmhuang.excel.tablemodel.SheetTable; + +/** + * @author Chimm Huang + */ +public class Demo2 { + public static void main(String[] args) { + try { + String xlsFile = "src/test/resources/demo_map.xlsx"; + // 获取桌面路径 + + FileSystemView fsv = FileSystemView.getFileSystemView(); + String desktop = fsv.getHomeDirectory().getPath(); + String outFile = desktop + "/template_map.xlsx"; + testFillInTable(xlsFile, outFile); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void testFillInTable(String xlsFile, String outFile) throws Exception { + // 获取 excel 二进制文件 + File file = new File(xlsFile); + byte[] bytes = FileUtils.readFileToByteArray(file); + // 创建 table 对象 + ExcelWorkbook excelWorkbook = ExcelHelper.createWorkbook(bytes); + SheetTable table = excelWorkbook.getSheet(0); + // 设置 自定义 sheet 页名称 + table.setSheetName("成绩表"); + + MapParser map = new MapParser(); + List> score = new ArrayList>() { + { + add(new HashMap() { + { + put("chinese", 100); + put("english", 100); + put("math", 100); + put("class", "三年2班"); + put("name", "陈小小"); + } + }); + add(new HashMap() { + { + put("chinese", 100); + put("english", 99); + put("math", 100); + put("class", "三年1班"); + put("name", "陈小二"); + } + }); + } + }; + + map.put("score", score); + map.put("title", "测试"); + // 将变量的值填充进表格 + ExcelHelper.fillInMapData(table, map); + + // 获取转换后的二进制 + byte[] bytes1 = ExcelHelper.convert2Byte(table); + + // 获取桌面路径 + File targetFile = new File(outFile); + FileOutputStream fos = new FileOutputStream(targetFile); + fos.write(bytes1); + fos.close(); + } +} \ No newline at end of file diff --git a/src/test/resources/demo_map.xlsx b/src/test/resources/demo_map.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..bafeabb832cb4e6dc268ae4c0ff2f9ceca45266a GIT binary patch literal 11084 zcma)i1yo(h(lr*`-QC@TyE_DTcXxM};10oq2Pe3@ySoN=5Aeg}n>RCgvi@ER&Ruua z?mA~zb)D{8TTT)f1QOtVgvf01yZEPLrVb&`1hb+Jh49DbsvKsDs#D|558WFe~h=RvNE3&;M=wwE8K=K(XFG3T;2KU?8U;d z4K2PXFgqqN=Hlx~5FK5uWQz-g!G}sjz1Ko`=|CW-GC__lx+qfMm?Mw8T#4|FIw}~d z5D&AG*f@T$*q6qSCL8_13tu+uD(MAFoo)4EbJcyHsB&biwIWIx`Fnz7UVxwqq6VcE zHGA2@uCiceSPo4y=xnB5ffQeA9kUI8JQVb(0as6CjYrS~%z#^|zkJeG_E=I{+c$27 zm@FGQyfayy@ru+?clV>=94FOuB=!@SDmG?kz%U}nkU$GTmJR7f1-}=7FR*?d!GYQx*ksQ`sJ!iT?STKJ`9WWrP4kUr(>IzC{!X)jjlI!3&t7rTGTrpZfp=mn z1eb^ub@91VBI1P;UzuxyuP^E zb7>8)cmP=mL=UNZ z7w&xZ=VepzA(YZtIG}{V7~xmHC4s_1O-Pc+*&B*J?t(RanW)U>AUcrq8tKQ2pN}_} z&esnwxZCR`-K;d~thVF3;Y+BTyUwoOv7>UBaWVHhJ#Zly7{Z-wggb5Co;Gb`i`oNL zfv;hHvI4x=Mm?LPlNb4-D}Ym^cQ5N=zlS1Sa-H<`Ed;)A;XwLFD4ZM|ZLI!C`j|oM z0D5HMyTnJhq`N*r39J|b${5Qg(}Mg(A_GKXt^wBCqQmxid5+^|&n8*XeeE_k6{3`a ztANQ5`4E15=6X&1S&;Yc3pp$xJhe?0qIpJuXa}2LTUs~i##9fw@VP_5PoP|3JNM8% z?2<}u#3P-g+_x36whm+#uAlJ@4SMqPVN($7qkDG(}B9PPyEK+*#8A^kfs0A&_tgE*g#`GG zFIUSyO2~gUfc<6QVq9xJj} z#muX(t2rJk$Ur{`i5iTKd4YA^q2sbiFdKw=2Sv=Fl#f?5FTF0h*_79U6m5V1-4_qb zV@uw4)W!>e5{|sxv04*{X?!CII5oGW)b*E0UhWi;RpMQOK{IjZJ*?u<@h3c=g*DTg zRvx4&bY1eJOgHPa2op6YcT!=6@gG46uJ-v#vCQ}CnPra(u{#}LD2=-#`&6Z3)l0_mFxw}t$13I1uujy79-@ng_D$P zzxEp^O?4p_N0qLqrt@bCa}cJ%mx`5~%@32o>&UG*B$Qy`iQt zLgn^5t`kn%Q2$s99#6_}r?KW&kyy8m%UCp^Zlk!F3FR^S`oekSPMbGEF`nRU;@Gagkx|fLw$LNKWJpJh&%VLK zw9Lze^4b}Jr%IY6QSj=Lt~_#bQ^yFqlcIS=n*Ff8n8R*x_+UBv^qo~>0Pp%1YEWh$ z6y0NwS~@^Gor`MFW+!L|e<4+p)lR};#Q!yl&_J@AIeaJ4XKgJ#$9gRV^|M*N<{2xB zI9cd<0@7v;iTYu&cj!Q(Q0eD^2OXgak%W4CJXD-TF}_3;i-?$zk0FcZ?d3m4JK_Rl zq}0*WAP^E3ew>i#aR!}~>-r_+-~oz)6zEDt{TTLctJ6kr7rLkMuM32MxaKPYBjkp^ z146x29Lxn<0i*8}jr)K-C_tE4R!%|%-^I(NhTw~C9?FD`2-%b75eO4@%|{KS&vIQ8 zVt!``$5y>w6y}Lgxlf5W0KTJe(4^jk=%0aX$Pg%m6tfGDC<)GkJS@QvO5?wDhYZ2y zpxQ2KAw2TDEeY=aZOwBKnYk zRm)NJU2?D(re8U&fVO|gq<6J!Gv9CG76QVC;bagdvVA^f4m+@6e7gjhxjfq49yKFEIfDw%~MeeK$86O+h3!v03^e6$y zP?~g!m@bE|LX&`op1hG{a>%AHLCX_k4)0;?TDJ(GJa20(g$_0uMy+;aH;))h#y61* zt!MU!&8Nf&$+aW57DVdp=)Jq{K_x>d)QK#mk!}CLFQ8=iookms48HL>anC%vlB7Bj zFz?-V+sI&>#z)0zD}T%R>LC(n)q%ZHN-GewZ)VPC0rrTF&gz56Cv>Y1eKeqF$|OeL zL4(_RG+j`jxJt`7k00ux%M;f4vD95eA(ne-hP}>O7hpH~xoK6IVf9*4GP3r9jk0H& zhv54^_jZUqCXJ4LDsq}+VKo&r{i-mBQz!rV$ho5kXFg~lF}~)`nal172`$23k<6RF zEGj`;awPcM>37PN=4Qs#I@b+XHooZXm!<9}eZnxbpX|(6>;P(%f#2T-cWkT?&Q`Q`&Jz4{?fxWo1zr z*g5;2^?e$-h{^gF1vzNmjP74yyxHJGh<~~Y+VZE41Mny;)#}i9rt(I(#f%nu<1D>` z{r8IgzBPQS=#HjFRz|;V4ewvD;iEtS08+4iy(s@>_J{EwW%@*I-3E&V+edWEr}EUx zVv1Dj6HR6svzp$KGRxuwZf1nTeyf6VDPq?OqVCq4L+6#m2>o@vsORmbp%NwLl9Te`x>djILxkyB_Xr1~ zw1n%Lk)H!jL@i9&7gRSP>tTA3_aVfz@?gqmW$*L8Twj)owy$^r_^gTa<#qeYm*wN0 zr~WX5?NKwW?6y-P+EJ`e<$FqD;nw}Q1LkEf>K3Y3tebnFG}5oJ)X)$(KP@apqpG1g z&ywY+)uhX&&B`TAKai^2T-O&=p3w$K&k}v)$Z;aWQfLCOaL50NN;1~Cyjj`Gh#3n1 ziA}ayt60UHwvErnNM=u@bhhN9f*Q3vaCLMY`%iVFy86cB4VZzgBZqr+i<(t!r*lxl zi&BUC)1W@{(OxXM0aLCof)*jp%@jA-j9Zq?$ZeqKbrO(SIrx~DKc=WfxRfLGX}Z2Q z{s7_w#!zM!F;9y@xFtwgX2S#zX8~@JvDNE?eXzF{yAqf}W0sdb>jn|jANkSheS30b zB0F(p5)F68&E0a>z4`nUa@Y2~HX~@64K^G~$Lo0zPiIBAo%i8#wtIZ&qB`{TrWtJ9 z=cN`K9D_OjNAG^LkH_PEZ(g*H_f;iG8^w7x_}O%*m}3-nHaD)Hbszn=9YcJX@40yT zhh-fE6SM@Y@;SgQti5H>^Mv0ntRcy9jXH+omp_-a%1I&9Y^z8xKh#ca=s0 zr}d6K1veAVmf4d=#kimFR|f75ZZl12jC=A#4&umE4#i!pC6t&;;R=v?@mDIH$&;-e9~Nt~t#%3R?|N z_X4&7pr6HR04AA`013532)2k6kzYg(1kZ$hj63kt&^K_c#gzTaL?`1*Q?K{tZPI= zsv!aSYt?*?1LfxhQPvKcZa0S6#~B%rTV8!6 zcZj&?Qfm?mb`?{=Gi%-VQe=@C(l?SpC=Y=F^>XIwGyh->BYjKLkl7|FQZnDMMN zD!Cf`iyK4{j1?b-Q z=L2vyeIv<3xKNs03_`Htd`e4F;E@-)?%D*ztPES|-tfjL$sUi9XtRcX3ye8p?{L?4^InD$cz_ zYVcRZFo(Oh6{n`3N}alHkRu48ObDDggEYmKI`K92qtZ?Z&h{@gzoHpvd#PtPF-xfp zj?Z7p14Kl=B|?D|aL%Dl{lOWm%Cg!_EuMtPh@b?oLaKZWxJXu3VX;xNw#%fqzJ{(G6->E+&hdp(1*^-XzcNYXC4T(9lQ5Dv%K zWUr@pi;F1?$F|ec0A?56)2-=*-G;4W3?5!onT(4RB+TQ3LXO-p97n63wSxg0MhdWr z2K8G>^K@B1A!?P)qs+J2A4K7)u70fWiAZR9fuu-v z@iKKVSjTn$rdZ-n7t556we1p_p2_3t7OUBAa(5ED>09GZH_6mJ)}gN=CF_0ja}n5m zR?mbP6wlz7ll$RK-U3zVsMf%&^iW!&37b_(%&5k#rk|*@5Hvrz zF;~od-$o(52nA_^kr}xl62JP0!62Egi^t#8h`_fs%N%wTNE(_P4Pg7K-;#9n!9wNZ zAeqdGzz<7+%qUf)>>V2Krvfx+a-$@J0>#2j&DB=9{e0;z^rqxiZ=Fc3A)~3yz&Dme zar&!71d(8&@1E*3L?>3UyHvwn<-Ta4+23>)Ih2AwFARL>b8ep}A%|UR*j^aHkcoz_ zfIQkYu2HK{#+ucyQKXn!TlnT-XR}cEnFifv*3{%%fn0x3@jSZx~(#;+Z?Sj z9q;cy88y+8NHx{WO0DM~G*$4@6jH74Hfb$0OiLt^drsX@B_FwSs1Y83DiH(6Mot;U zi<%7v%5o&+ims%;ZuimzZ7v!?%F17m6dq@pO_d+#oU>cP3xxJRG<- zdtSbcP!s#(PvM3=`Z`0J5_!|!k#zqI@j1~5csH4%iNqOFKp!X)$`uZV;HCi@Y-QiR znhsZla*uq(li^7|*cgB*ZCat)nOu;*QFs9TIs2D6!8uW?+gI&$0K{hqDalXOa}XU3 zDPBV0ooKiGRU@M1;;o>Y1^<=vq0sC}RcX+#Zpj3zpiI>g5KT z%%Iu2a%s$l-JpsYfoAOBrz(J3<2DTf|k2T^l}oh}cbY8}i&H1hiWroi`A=XpQvk z>ZPsgmP?ZHAJxg1hzV>r0efgu%fP{757e-Q$OFWJ813pe5dxnmU^aXh@&QrWh_y6% ztp$tKtsZI(8fCVcu=kL{XBBpS|>BX(~>~>KA zFd^+2$Rx-&&9x{#kppWI+hqV&%QyJB;fr`4P-WZBWQTARe_p8oD!FbD0}=xP;1O`e zC{L$Al=St#vhORaHiXyo^ws{ddEZNc)Yt2LGHAVyc=ho#pHdBRe9mP{En~pw=TcC( ze1ouQYHtP+wZclHa(xnG3Ajv@p>VR4&2v@wNDQJkr644EC^Q8^$Y%|bOM;qnc_$%~ z+=Ea^p&5k4&=MkYzR60iEeDi3$u!ck`>&fAoWVQ5GugLT^OkLz)G zk00Ju#F<^@$GBORexcvTMHz8|>N45z%th zEp`(@Bf#M(6bb`heQFbQ0w16hV8G+^pzb_os$+C@2)t=jm)>K7D$8 zmBDS;PEI4hnYp9HB&4PNR73h_6!)R1?=p4M-fcb>)aSW@k*$ z*J~76BLBG?djM*==| z(HjOb-QKu7YKydn+bS^j8ifgu)v%553STm{bv%4DbMY4RgNrc|JgLH=q;YX~1YaDM zUuyIlBmj#cb#X6vB#9yf_FCTlYJ57r4#agSiw21A>eZIX*f{qK{`q=mR#f!%voIP^ z!?=O*C)*8bOeYvxeinUyeMjuxuaG?mINKL_G%n{O(UYt2O{^k>Of+(S`0!U7*^{oZ z7%?UNP@@F`el-+S zAq8f6exPpKrRHA{OHaMWw`Fwo4@nC#;|0Fo>Ih0hmB*%&7X-$Ho8~N% z7Bu_vIw0#a4ZUVd+&EGf)KK?Sv5ivm``^C31=%nXk-SP17QHxcYMbqD>FM*)`=V0W zN=LT77H^WWv(;f_VxT=zjI{$3RK9Y3FkRDsmRnoJ zd2q8?z-IXk0m!hvW3e4qHtXd&sw=$(y7pTUv=>>wWvap^R$m&!^VU^O`#SscYU@FK z$UdHr>+P^COsqkUA77&Hu2NjPCw=QvXhcwM5lG~L5(&AoHc%W9B|n}dBb1~2>)4ga zU19tE!vokE5ek8DPTaMTbqxts4yC{tUt+?dhwnoGOaluK=cXxv=HzbWpkNMO@bEDI zM7I6)O>&K@MUHA`v^+^AFh9Z1<8_yDJWsbr3q6D;y`?MM0vMg2o07VB*EVMk4UXcO zDxqyv`lx5uHL~L}Z5%$XmsA}Q= z@YQqn7q?}7)25fBO?n2c6_p-iOEZ3G$EW)ZSn19t3uJ4EO^BWLZ3;+;&Z`R!eANKt z($~}ard^-xWVmj(&c-8HqFhf;snvWq7z~30xo8s8dg?W1C74|8Aq>!3GBB8>oxz1H zyh9D*GLI}Q!`tZf*e^Q$)SkK4>hL&;mRokr4WKHooL(So6JpJBC8v@Yn!GcW$b~Z2Uly+l zWrYwHgCvH^3XM>uhAx3Htg^{{pY0$AA&rAxaq(2s}E{Fem`noTxu#imx}4dRy+coUP}bl>pJ%SGc#jOgo#RlxN&Mvs?yK ze@Eie?t`ud&4ciwW5URqxMP|od+Owz#@Z2l=qhx9&e+Mqqg>t0#>ysrRG@g`1!BQ*0`D^&E*e$&O3pU6 z*2sRpN61_@llEI*L*#a0&1LTHn15|;{p>{T;bL;&(6RsRJO=%K%9g?;b~X>YEWfoKTR{K- z-dpf*zoe(9^esZ)ovo`tTXd0wAY`)BgybAUR*S%py%_|@to=**=74&UN>4(4o zk_BIyJj?c!J|yjEx40O#dWNGJl9*=rq4O*v)*};+xb`j@BcWPKtR;=pDfiZj!#IxD zRqMXL3%PCzBCzr>3OIa~8F2hpK%KJ|O|1}Ly9j3LWJJRbxX@1NtHtm!bg($d6hNG@ zXSB3FOE5Q7s!wFm@tmU4o09aS8ycP|X~z_q54C&Z&vZi`oosne!fRe$+i@nJ zSUC^FHsS1SZF#xJY&no7xfLf3u|<+aDKT$+!p=p6VPbjjD}`8P#-h%i6@$%K1{u z&?w?*BohZ835_Nnk~3O*v$_{&$uZ8zc+aLsF_ZKE>_E%;`=!z$#^h?GiB|6n9@g)Jq$W4m1uaEW^=PRoOJk){iXa4DR=P0{k->;A>Mj7~YcM=u^SE zbC%u|DD6OHCW|l)h+@FcBqAu_VI1>lc!Zm8QDF*Dwaz#Uz_SkGd(aG*O7B4l$1 zkb*SluEuS7=*<8ap5(ZE$XiXN5=(s~AN!lB1898G2^TSQoy^^N1GJuyD*GF zX78TGX`n^ao@ndu+qhGTlhx6$79!{9$?d42+OU~ckVi|0HY^gF`{Av{%Y$A$uuUG| z>H8#!of1v9?(>XqKTi*`2AF+MV6elAy$d|52`@E25`#3O&yxDuTbtGJ^63HizxO!a zdKdsY_C}V!H={k+oLkG~$4B@1RH=8_Al;g|dWDzc8=QSV-U~RRQ zvmUw>>C3{EljiDOqYU+E2DM`gYI(4+DLES3;7-jG9A~21QH;%WvW!8A4DifbXotTM z1_fmww|C+hK68->DJwUoff3dWO8FGNva8u{N1fWQDT5bIl0EvsH$A=I)fqdfQ9CTg z5lE7I^h(Uw-?NkC#>1i`W`9Xj>hj;Y{J(zDf1-2uJQw}sjmD-oIx*j_Z`JLW*}EFx z&$RxNUGulx?mG);4Vd(@n~Gu($<*%4o-g+RFre}=)d!Q*NdKNSkfWCEn5+q%71(}8 zHoUyrcs2t^YHmO;XUI+tP5a0?1KI|A21l12YIR;80SFT*R!MbsR*$!!mLxe-3rt}L zVVJFlt6fJruWLgQ0StPe$Z(S+{8b%p4#z6WJ$`?jhjtC9t-ZbdKl+QExmm_u*v6hk?Xy~o?cDuhd)rv0E9ZcRGw^2-y3Wq=rvgYQV&y7M)dKH2I;>gjG86%%g(&)jw}UnhO>KfnD# zp$o+C7nv^3aq`gq`m+^UgvuhEL}+GhwoKGS z*j$~8)KZjRRIWX?iT(MNxvWv^%hJN&Ba;YYKeag?`YKpMpeXIJEd22OXc%yj7wEj_ z(ougJl^dqc&w$F%3Mt9jaA}Y2#U~S7(ysJ*;d;6_i4PM>5> zp(Ng>{XK17{iFUCQ2h5~COFI_wLPi(AzMo!kee@rnPSIM{qX$cmH=@qvtMpci?AbF zZ(`>_Usm~0x%p)ri+%TP+8qGY_CU^u`yUGLuEr|6YbkDqAWz0N_Gtu`yh;w%ZrWn& z+o@ZwI0 zihftI{e}MgmeT)n`lqJtclhthtH0nFZ^EL#;Q!KL{jZ09mm>Wd?+-2hwxfEhH~$Op zPnIN!|5l)WztP_-*}I=#EZP4Z_HT9V_j!Kb@BKAT0^Wb#{rx_}?_G?)W_XtozV9FZ z)zkR@e|D_5oPC4-+4KKrhWy_3|I4Gm-(i1g0Diya-}g0tO^`$Ihu^=`_P=iWT}$?B yyhqslSA^fe|5BFy^Ui