Java edit bar chart in ppt by using poi - java

I am a newbie to using POI at work.Now i'm going to use POI in java to read a bar chart in the PPT.I've added several series x to it in advance,which are the column headers of the excel to which the bar graph belongs.
But i can only read the first three columns by default with the POI.In addition,once I modify the column header of the bar chart,or want to add a fourth column(thar is,add a color)to a bar chart with only three columns, the bar chart cannot be edited when I open the PPT,indicating that the node of the bar chart is damaged.
So is there a master who can help talking how to use POI to prroperly add a color to the bar chart(add a series)?
Eg: when I debug to the "
long ptCatCnt = catDataSource.getStrRef().getStrCache().getPtCount().getVal();
It show nullpointerexecption, I don't know how structure in ppt the bar-chart is.So I want know how to update the bar-chart。
The code is :
public class PPTDemo {
public void run() {
try {
SlideShow slideShow = SlideShowFactory.create(new File("./res/1.pptx"));
for (Object o : slideShow.getSlides()) {
XSLFSlide slider = (XSLFSlide) o;
// 第一页
if (slider.getSlideNumber() == 1) {
for (POIXMLDocumentPart.RelationPart part : slider.getRelationParts()) {
POIXMLDocumentPart documentPart = part.getDocumentPart();
// 是图表
if (documentPart instanceof XSLFChart) {
XSLFChart chart = (XSLFChart) documentPart;
// 查看里面的图表数据,才能知道是什么图表
CTPlotArea plot = chart.getCTChart().getPlotArea();
// 测试数据
List<SeriesData> seriesDatas = Arrays.asList(
new SeriesData("", Arrays.asList(
new NameDouble("行1", Math.random() * 100),
new NameDouble("行2", Math.random() * 100),
new NameDouble("行3", Math.random() * 100),
new NameDouble("行4", Math.random() * 100),
new NameDouble("行5", Math.random() * 100)
)),
new SeriesData("", Arrays.asList(
new NameDouble("行1", Math.random() * 100),
new NameDouble("行2", Math.random() * 100),
new NameDouble("行3", Math.random() * 100),
new NameDouble("行4", Math.random() * 100),
new NameDouble("行5", Math.random() * 100)
))
);
XSSFWorkbook workbook = chart.getWorkbook();
XSSFSheet sheet = workbook.getSheetAt(0);
// 柱状图
if (!plot.getBarChartList().isEmpty()) {
CTBarChart barChart = plot.getBarChartArray(0);
updateChartExcelV(seriesDatas, workbook, sheet);
workbook.write(chart.getPackagePart().getOutputStream());
int i = 0;
for (CTBarSer ser : barChart.getSerList()) {
updateChartCatAndNum(seriesDatas.get(i), ser.getTx(), ser.getCat(), ser.getVal());
++i;
}
}
// 饼图
else if (!plot.getPieChartList().isEmpty()) {
// 示例饼图只有一列数据
updateChartExcelV(Arrays.asList(seriesDatas.get(0)), workbook, sheet);
workbook.write(chart.getPackagePart().getOutputStream());
CTPieChart pieChart = plot.getPieChartArray(0);
int i = 0;
for (CTPieSer ser : pieChart.getSerList()) {
updateChartCatAndNum(seriesDatas.get(i), ser.getTx(), ser.getCat(), ser.getVal());
++i;
}
}
}
}
}
}
try {
try (FileOutputStream out = new FileOutputStream("./res/o1.pptx")) {
slideShow.write(out);
}
} catch (FileNotFoundException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
} catch (InvalidFormatException e) {
e.printStackTrace();
}
}
/**
* 更新图表的关联 excel, 值是纵向的
*
* #param param
* #param workbook
* #param sheet
*/
protected void updateChartExcelV(List<SeriesData> seriesDatas, XSSFWorkbook workbook, XSSFSheet sheet) {
XSSFRow title = sheet.getRow(0);
for (int i = 0; i < seriesDatas.size(); i++) {
SeriesData data = seriesDatas.get(i);
if (data.name != null && !data.name.isEmpty()) {
// 系列名称,不能修改,修改后无法打开 excel
// title.getCell(i + 1).setCellValue(data.name);
}
int size = data.value.size();
for (int j = 0; j < size; j++) {
XSSFRow row = sheet.getRow(j + 1);
if (row == null) {
row = sheet.createRow(j + 1);
}
NameDouble cellValu = data.value.get(j);
XSSFCell cell = row.getCell(0);
if (cell == null) {
cell = row.createCell(0);
}
cell.setCellValue(cellValu.name);
cell = row.getCell(i + 1);
if (cell == null) {
cell = row.createCell(i + 1);
}
cell.setCellValue(cellValu.value);
}
int lastRowNum = sheet.getLastRowNum();
if (lastRowNum > size) {
for (int idx = lastRowNum; idx > size; idx--) {
sheet.removeRow(sheet.getRow(idx));
}
}
}
}
/**
* 更新 chart 的缓存数据
*
* #param data 数据
* #param serTitle 系列的标题缓存
* #param catDataSource 条目的数据缓存
* #param numDataSource 数据的缓存
*/
protected void updateChartCatAndNum(SeriesData data, CTSerTx serTitle, CTAxDataSource catDataSource,
CTNumDataSource numDataSource) {
// 更新系列标题
// serTitle.getStrRef().setF(serTitle.getStrRef().getF()); //
// serTitle.getStrRef().getStrCache().getPtArray(0).setV(data.name);
// TODO cat 也可能是 numRef
long ptCatCnt = catDataSource.getStrRef().getStrCache().getPtCount().getVal();
long ptNumCnt = numDataSource.getNumRef().getNumCache().getPtCount().getVal();
int dataSize = data.value.size();
for (int i = 0; i < dataSize; i++) {
NameDouble cellValu = data.value.get(i);
CTStrVal cat = ptCatCnt > i ? catDataSource.getStrRef().getStrCache().getPtArray(i)
: catDataSource.getStrRef().getStrCache().addNewPt();
cat.setIdx(i);
cat.setV(cellValu.name);
CTNumVal val = ptNumCnt > i ? numDataSource.getNumRef().getNumCache().getPtArray(i)
: numDataSource.getNumRef().getNumCache().addNewPt();
val.setIdx(i);
val.setV(String.format("%.2f", cellValu.value));
}
// 更新对应 excel 的range
catDataSource.getStrRef().setF(
replaceRowEnd(catDataSource.getStrRef().getF(),
ptCatCnt,
dataSize));
numDataSource.getNumRef().setF(
replaceRowEnd(numDataSource.getNumRef().getF(),
ptNumCnt,
dataSize));
// 删除多的
if (ptNumCnt > dataSize) {
for (int idx = dataSize; idx < ptNumCnt; idx++) {
catDataSource.getStrRef().getStrCache().removePt(dataSize);
numDataSource.getNumRef().getNumCache().removePt(dataSize);
}
}
// 更新个数
catDataSource.getStrRef().getStrCache().getPtCount().setVal(dataSize);
numDataSource.getNumRef().getNumCache().getPtCount().setVal(dataSize);
}
/**
* 替换 形如: Sheet1!$A$2:$A$4 的字符
*
* #param range
* #return
*/
public static String replaceRowEnd(String range, long oldSize, long newSize) {
Pattern pattern = Pattern.compile("(:\\$[A-Z]+\\$)(\\d+)");
Matcher matcher = pattern.matcher(range);
if (matcher.find()) {
long old = Long.parseLong(matcher.group(2));
return range.replaceAll("(:\\$[A-Z]+\\$)(\\d+)", "$1" + Long.toString(old - oldSize + newSize));
}
return range;
}
/**
* 一个系列的数据
*/
public static class SeriesData {
/**
* value 系列的名字
*/
public String name;
public List<NameDouble> value;
public SeriesData(java.util.List<NameDouble> value) {
this.value = value;
}
public SeriesData(String name, List<NameDouble> value) {
this.name = name;
this.value = value;
}
public SeriesData() {
}
}
/**
*
*/
public class NameDouble {
public String name;
/**
*/
public double value;
public NameDouble(String name, double value) {
this.name = name;
this.value = value;
}
#SuppressWarnings("unused")
public NameDouble() {
}
}
}

Using current apache poi 5.0.0 updating a chart in PowerPoint is possible using the new XDDF classes. That avoids using the ooxml-schemas classes (CT... classes) directly. Using CT classes directly is error prone and needs very good knowlegde about the internally XML structure of Office Open XML.
What one needs to know is that chart data is stored in an embedded Excel workbook. So while updating the data the need is always updating the data in that workbook and updating the data in the chart.
The following example is a minimal reproducible example for how to do this.
The template BarChartSample.pptx contained a bar chart having only one series an one category. It defines the chart format. It looks like so:
The code is like this:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import org.apache.poi.xslf.usermodel.*;
import org.apache.poi.xddf.usermodel.chart.*;
import org.apache.poi.xssf.usermodel.*;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.AreaReference;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumns;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumn;
public class PowerPointChangeChartData {
//patched version of XSSFTable.updateHeaders, see https://stackoverflow.com/questions/55532006/renaming-headers-of-xssftable-with-apache-poi-leads-to-corrupt-xlsx-file/55539181#55539181
static void updateHeaders(XSSFTable table) {
XSSFSheet sheet = (XSSFSheet)table.getParent();
CellReference ref = table.getStartCellReference();
if (ref == null) return;
int headerRow = ref.getRow();
int firstHeaderColumn = ref.getCol();
XSSFRow row = sheet.getRow(headerRow);
DataFormatter formatter = new DataFormatter();
if (row != null /*&& row.getCTRow().validate()*/) {
int cellnum = firstHeaderColumn;
CTTableColumns ctTableColumns = table.getCTTable().getTableColumns();
if(ctTableColumns != null) {
for (CTTableColumn col : ctTableColumns.getTableColumnList()) {
XSSFCell cell = row.getCell(cellnum);
if (cell != null) {
col.setName(formatter.formatCellValue(cell));
}
cellnum++;
}
}
}
}
static void updateChart(XSLFChart chart, Object[][] data) throws Exception {
// get chart's data source which is a Excel sheet
XSSFWorkbook chartDataWorkbook = chart.getWorkbook();
String sheetName = chartDataWorkbook.getSheetName(0);
XSSFSheet chartDataSheet = chartDataWorkbook.getSheet(sheetName);
// current Office uses a table as data source
// so get that table if present
XSSFTable chartDataTable = null;
if (chartDataSheet.getTables().size() > 0) {
chartDataTable = chartDataSheet.getTables().get(0);
}
if (chart.getChartSeries().size() == 1) { // we will process only one chart data
XDDFChartData chartData = chart.getChartSeries().get(0);
if (chartData.getSeriesCount() == 1) { // we will process only templates having one series
int rMin = 1; // first row (0) is headers row
int rMax = data.length - 1;
// set new category data
XDDFCategoryDataSource category = null;
int c = 0;
for (int r = rMin; r <= rMax; r++) {
XSSFRow row = chartDataSheet.getRow(r); if (row == null) row = chartDataSheet.createRow(r);
XSSFCell cell = row.getCell(c); if (cell == null) cell = row.createCell(c);
cell.setCellValue((String)data[r][c]); // in sheet
}
category = XDDFDataSourcesFactory.fromStringCellRange(chartDataSheet, new CellRangeAddress(rMin,rMax,c,c)); // in chart
// series 1, is present already
c = 1;
// set new values in sheet and in chart
XDDFNumericalDataSource<Double> values = null;
for (int r = rMin; r < rMax+1; r++) {
XSSFRow row = chartDataSheet.getRow(r); if (row == null) row = chartDataSheet.createRow(r);
XSSFCell cell = row.getCell(c); if (cell == null) cell = row.createCell(c);
cell.setCellValue((Double)data[r][c]); // in sheet
}
values = XDDFDataSourcesFactory.fromNumericCellRange(chartDataSheet, new CellRangeAddress(rMin,rMax,c,c));
XDDFChartData.Series series1 = chartData.getSeries(0);
series1.replaceData(category, values); // in chart
// set new title in sheet and in chart
String series1Title = (String)data[0][c];
chartDataSheet.getRow(0).getCell(c).setCellValue(series1Title); // in sheet
series1.setTitle(series1Title, new CellReference(sheetName, 0, c, true, true)); // in chart
series1.plot();
//further series, all new created
int seriesCount = data[0].length - 1;
for (int s = 2; s <= seriesCount; s++) {
c++;
// set new values
for (int r = rMin; r < rMax+1; r++) {
XSSFRow row = chartDataSheet.getRow(r); if (row == null) row = chartDataSheet.createRow(r);
XSSFCell cell = row.getCell(c); if (cell == null) cell = row.createCell(c);
cell.setCellValue((Double)data[r][c]); // in sheet
}
values = XDDFDataSourcesFactory.fromNumericCellRange(chartDataSheet, new CellRangeAddress(rMin,rMax,c,c));
XDDFChartData.Series series = chartData.addSeries(category, values); // in chart
// set new title
String seriesTitle = (String)data[0][c];
XSSFCell cell = chartDataSheet.getRow(0).getCell(c); if (cell == null) cell = chartDataSheet.getRow(0).createCell(c);
cell.setCellValue(seriesTitle); // in sheet
series.setTitle(seriesTitle, new CellReference(sheetName, 0, c, true, true)); // in chart
series.plot();
}
// update the table if present
if (chartDataTable != null) {
CellReference topLeft = new CellReference(chartDataSheet.getRow(0).getCell(0));
CellReference bottomRight = new CellReference(chartDataSheet.getRow(rMax).getCell(c));
AreaReference tableArea = chartDataWorkbook.getCreationHelper().createAreaReference(topLeft, bottomRight);
chartDataTable.setArea(tableArea);
updateHeaders(chartDataTable);
}
}
}
}
public static void main(String[] args) throws Exception {
String filePath = "BarChartSample.pptx"; // has template bar chart
String filePathNew = "BarChartSample_New.pptx";
Object[][] data = new Object[][] { // new data 3 series, 5 categories
{"", "Amount", "Values", "Others"}, // series title
{"Jan", 321d, 456d, 222d}, // category 1
{"Feb", 543d, 567d, 111d}, // category 2
{"Mar", 432d, 123d, 333d}, // category 3
{"Apr", 210d, 234d, 444d}, // category 4
{"May", 198d, 345d, 444d} // category 5
};
XMLSlideShow slideShow = new XMLSlideShow(new FileInputStream(filePath));
XSLFChart chart = slideShow.getCharts().get(0);
updateChart(chart, data);
FileOutputStream out = new FileOutputStream(filePathNew);
slideShow.write(out);
out.close();
slideShow.close();
}
}
The result looks like so:
Hint: The code uses a patched version of XSSFTable.updateHeaders as the current version fails updating the table headers. See Renaming headers of XSSFTable with Apache Poi leads to corrupt XLSX-file.

Related

XSSFWorkbook cloneSheet corrupts workbook if sheet contains a table

I want to clone an Excel sheet and all of its contents. I tried the XSSFWorkbook cloneSheet method, but it seems the workbook is corrupted if my sheet contains an Excel table. See the examle workbook below with a simple table:
When I try to open the output workbook, I get a prompt telling me that the file is broken and needs to be repaired. If I recover the workbook, it is clear the table has not been copied correctly; the original totals row is now a data row.
try (InputStream is = Table.class.getResourceAsStream("table.xlsx")) {
XSSFWorkbook workbook = new XSSFWorkbook(is);
workbook.cloneSheet(0, "Test");
try (OutputStream fileOut = new FileOutputStream("table-2.xlsx")) {
workbook.write(fileOut);
}
} catch (IOException e) {
e.printStackTrace();
}
How I would go about copying this sheet? Any help is appreciated!
XSSFWorkbook.cloneSheet clones a sheet. But it does not considering the possible defined tables in it. It simply clones the table references. But two table-ranges in sheets cannot refer to the same table reference. The tables itself needs to be cloned. That's why the corrupted workbook as result.
I've tried to solve this by programming a method cloneTables(XSSFSheet sheet) which simply creates clones of each table in a sheet which then refer to their own table reference each. I consider table styles, auto-filter, a totals-row and calculated column formulas. I hope I have not overlooked something, but I doubt that.
The code its tested and works using current apache poi 5.2.2.
It contains fixes for following bugs too:
XSSFTable.updateHeaders fails in Excel workbooks created using current Excel versions. This is because of the test row.getCTRow().validate() which always will be false because of the usage of new name spaces. See Renaming headers of XSSFTable with Apache Poi leads to corrupt XLSX-file.
XSSFSheet.removeTable does not remove the links to the table part reference from the sheet.
Complete example to test:
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.*;
import org.apache.poi.ss.util.*;
import org.apache.poi.ss.SpreadsheetVersion;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumn;
class ExcelCloneSheetHavingTable {
static void updateHeaders(XSSFTable table) {
XSSFSheet sheet = (XSSFSheet)table.getParent();
CellReference ref = table.getStartCellReference();
if (ref == null) return;
int headerRow = ref.getRow();
int firstHeaderColumn = ref.getCol();
XSSFRow row = sheet.getRow(headerRow);
DataFormatter formatter = new DataFormatter();
if (row != null /*&& row.getCTRow().validate()*/) { // see bug: https://stackoverflow.com/questions/55532006/renaming-headers-of-xssftable-with-apache-poi-leads-to-corrupt-xlsx-file/55539181#55539181
int cellnum = firstHeaderColumn;
org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumns ctTableColumns = table.getCTTable().getTableColumns();
if(ctTableColumns != null) {
for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumn col : ctTableColumns.getTableColumnList()) {
XSSFCell cell = row.getCell(cellnum);
if (cell != null) {
String colName = formatter.formatCellValue(cell);
colName = colName.replace("\n", "_x000a_");
colName = colName.replace("\r", "_x000d_");
col.setName(colName);
}
cellnum++;
}
}
}
//tableColumns = null;
//columnMap = null;
//xmlColumnPrs = null;
//commonXPath = null;
try {
java.lang.reflect.Field tableColumns = XSSFTable.class.getDeclaredField("tableColumns");
tableColumns.setAccessible(true);
tableColumns.set(table, null);
java.lang.reflect.Field columnMap = XSSFTable.class.getDeclaredField("columnMap");
columnMap.setAccessible(true);
columnMap.set(table, null);
java.lang.reflect.Field xmlColumnPrs = XSSFTable.class.getDeclaredField("xmlColumnPrs");
xmlColumnPrs.setAccessible(true);
xmlColumnPrs.set(table, null);
java.lang.reflect.Field commonXPath = XSSFTable.class.getDeclaredField("commonXPath");
commonXPath.setAccessible(true);
commonXPath.set(table, null);
} catch (Exception ex) {
ex.printStackTrace();
}
}
static String getSubtotalFormulaStartFromTotalsRowFunction(int intTotalsRowFunction) {
final int INT_NONE = 1;
final int INT_SUM = 2;
final int INT_MIN = 3;
final int INT_MAX = 4;
final int INT_AVERAGE = 5;
final int INT_COUNT = 6;
final int INT_COUNT_NUMS = 7;
final int INT_STD_DEV = 8;
final int INT_VAR = 9;
final int INT_CUSTOM = 10;
String subtotalFormulaStart = null;
switch (intTotalsRowFunction) {
case INT_NONE:
subtotalFormulaStart = null;
break;
case INT_SUM:
subtotalFormulaStart = "SUBTOTAL(109";
break;
case INT_MIN:
subtotalFormulaStart = "SUBTOTAL(105";
break;
case INT_MAX:
subtotalFormulaStart = "SUBTOTAL(104";
break;
case INT_AVERAGE:
subtotalFormulaStart = "SUBTOTAL(101";
break;
case INT_COUNT:
subtotalFormulaStart = "SUBTOTAL(103";
break;
case INT_COUNT_NUMS:
subtotalFormulaStart = "SUBTOTAL(102";
break;
case INT_STD_DEV:
subtotalFormulaStart = "SUBTOTAL(107";
break;
case INT_VAR:
subtotalFormulaStart = "SUBTOTAL(110";
break;
case INT_CUSTOM:
subtotalFormulaStart = null;
break;
default:
subtotalFormulaStart = null;
}
return subtotalFormulaStart;
}
static void cloneTables(XSSFSheet sheet) {
for (XSSFTable table : sheet.getTables()) {
// clone table; XSSFTable.setArea fails and throws exception for too small tables
XSSFTable clonedTable = null;
int rowCount = (table.getArea().getLastCell().getRow() - table.getArea().getFirstCell().getRow()) + 1;
int headerRowCount = table.getHeaderRowCount(); if (headerRowCount == 0) headerRowCount = 1;
int minimumRowCount = 1 + headerRowCount + table.getTotalsRowCount();
if (rowCount >= minimumRowCount) {
clonedTable = sheet.createTable(table.getArea());
}
if (clonedTable != null) {
//clonedTable.updateHeaders(); // don't work, see bug: https://stackoverflow.com/questions/55532006/renaming-headers-of-xssftable-with-apache-poi-leads-to-corrupt-xlsx-file/55539181#55539181
updateHeaders(clonedTable);
// clone style
clonedTable.setStyleName(table.getStyleName());
XSSFTableStyleInfo style = (XSSFTableStyleInfo)table.getStyle();
XSSFTableStyleInfo clonedStyle = (XSSFTableStyleInfo)clonedTable.getStyle();
if (style != null && clonedStyle != null) {
clonedStyle.setShowColumnStripes(style.isShowColumnStripes());
clonedStyle.setShowRowStripes(style.isShowRowStripes());
clonedStyle.setFirstColumn(style.isShowFirstColumn());
clonedStyle.setLastColumn(style.isShowLastColumn());
}
//clone autofilter
clonedTable.getCTTable().setAutoFilter(table.getCTTable().getAutoFilter());
//clone totalsrow
int totalsRowCount = table.getTotalsRowCount();
if (totalsRowCount == 1) { // never seen more than one totals row
XSSFRow totalsRow = sheet.getRow(clonedTable.getEndCellReference().getRow());
if (clonedTable.getCTTable().getTableColumns().getTableColumnList().size() > 0) {
clonedTable.getCTTable().setTotalsRowCount(totalsRowCount);
for (int i = 0; i < clonedTable.getCTTable().getTableColumns().getTableColumnList().size(); i++) {
org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumn tableCol = table.getCTTable().getTableColumns().getTableColumnList().get(i);
org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumn clonedTableCol = clonedTable.getCTTable().getTableColumns().getTableColumnList().get(i);
clonedTableCol.setTotalsRowFunction(tableCol.getTotalsRowFunction());
int intTotalsRowFunction = clonedTableCol.getTotalsRowFunction().intValue();
sheet.getWorkbook().setCellFormulaValidation(false);
if (intTotalsRowFunction == 10) { //custom
org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableFormula totalsRowFormula = tableCol.getTotalsRowFormula();
clonedTableCol.setTotalsRowFormula(totalsRowFormula);
totalsRow.getCell(clonedTable.getStartCellReference().getCol()+i).setCellFormula(totalsRowFormula.getStringValue());
} else if (intTotalsRowFunction == 1) { //none
//totalsRow.getCell(clonedTable.getStartCellReference().getCol()+i).setBlank();
} else {
String subtotalFormulaStart = getSubtotalFormulaStartFromTotalsRowFunction(intTotalsRowFunction);
if (subtotalFormulaStart != null)
totalsRow.getCell(clonedTable.getStartCellReference().getCol()+i).setCellFormula(subtotalFormulaStart + "," + clonedTable.getName() +"[" + clonedTableCol.getName()+ "])");
}
}
}
}
// clone calculated column formulas
if (clonedTable.getCTTable().getTableColumns().getTableColumnList().size() > 0) {
clonedTable.getCTTable().setTotalsRowCount(totalsRowCount);
for (int i = 0; i < clonedTable.getCTTable().getTableColumns().getTableColumnList().size(); i++) {
org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumn tableCol = table.getCTTable().getTableColumns().getTableColumnList().get(i);
org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumn clonedTableCol = clonedTable.getCTTable().getTableColumns().getTableColumnList().get(i);
if (tableCol.getCalculatedColumnFormula() != null) {
clonedTableCol.setCalculatedColumnFormula(tableCol.getCalculatedColumnFormula());
org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableFormula calculatedColumnFormula = clonedTableCol.getCalculatedColumnFormula();
String formula = tableCol.getCalculatedColumnFormula().getStringValue();
String clonedFormula = formula.replace(table.getName(), clonedTable.getName());
calculatedColumnFormula.setStringValue(clonedFormula);
int rFirst = clonedTable.getStartCellReference().getRow() + clonedTable.getHeaderRowCount();
int rLast = clonedTable.getEndCellReference().getRow() - clonedTable.getTotalsRowCount();
int c = clonedTable.getStartCellReference().getCol() + i;
sheet.getWorkbook().setCellFormulaValidation(false);
for (int r = rFirst; r <= rLast; r++) {
XSSFRow row = sheet.getRow(r); if (row == null) row = sheet.createRow(r);
XSSFCell cell = row.getCell(c); if (cell == null) cell = row.createCell(c);
cell.setCellFormula(clonedFormula);
}
}
}
}
}
// remove old table; do that even if XSSFsheet.createTable failed, because a one-cell-table doesn't make any sense
String rId = sheet.getRelationId(table);
sheet.removeTable(table);
// remove links to the table part reference
org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableParts tblParts = sheet.getCTWorksheet().getTableParts();
if (tblParts != null && tblParts.getTablePartList().size() > 0) {
for (int i = 0; i < tblParts.getTablePartList().size(); i++) {
org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTablePart tblPart = tblParts.getTablePartArray​(i);
if(tblPart.getId().equals(rId)) {
tblParts.removeTablePart​(i);
}
}
}
}
}
public static void main(String[] args) throws Exception {
try (Workbook workbook = WorkbookFactory.create(new FileInputStream("SAMPLE.xlsx"));
FileOutputStream out = new FileOutputStream("SAMPLE_NEW.xlsx")) {
XSSFSheet sheet = ((XSSFWorkbook)workbook).cloneSheet(0, "Test");
cloneTables(sheet);
workbook.write(out);
}
}
}
This code needs the full jar of all of the schemas, which is poi-ooxml-full-5.2.2.jar for apache poi 5.2.2, as mentioned in FAQ. Note, since apache poi 5.* the formerly used ooxml-schemas-*.jar cannot be used anymore. There must not be any ooxml-schemas-*.jar in class path when using apache poi 5.*.
Note, you need this additional. The file names poi-ooxml-5.2.2.jar and poi-ooxml-lite-5.2.2.jar respective poi-ooxml-full-5.2.2.jar are misleading. The *-lite*.jar respective *-full*.jar do not replace, but complement the poi-ooxml-5.2.2.jar.

Apache POI - When editing chart data in a Word file, it returns to the data defined in the form

I have developed a Word function that includes a Chart.
When editing chart data in a Word file, it returns to the data defined in the form.
Here are the steps:
I edit word(docx) xml data and workbook.
I open microsoft office - the data shown is normal.
I click Chart data edit function - it returns the original data.
library - ooxml-schemas-1.3, poi-4.0.0-SNAPSHOT
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
String inFilePath = "../file/temp/TEMP_Chart_Simple.docx";
String outFilePath = "../file/out/NEW_Chart_" + System.currentTimeMillis() + ".docx";
Map<String, Map<String, String>> CHART_MAP_DATA = new LinkedHashMap<>();
Map<String, String> inData = new LinkedHashMap<>();
inData.put("1", "8.3");
inData.put("2", "7.3");
CHART_MAP_DATA.put("temp", inData);
Path path = Paths.get(inFilePath);
byte[] byteData = Files.readAllBytes(path);
// read as XWPFDocument from byte[]
XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(byteData));
XWPFChart xChart = null;
CTChart ctChart = null;
XSSFWorkbook wb = null;
for (POIXMLDocumentPart part : document.getRelations()) {
if (part instanceof XWPFChart) {
xChart = (XWPFChart) part;
wb = xChart.getWorkbook();
ctChart = xChart.getCTChart();
if(getTitle(ctChart).equals("FIELD_CHART")) {
break;
}
}
}
CTPlotArea plotArea = ctChart.getPlotArea();
List<CTBarChart> arBarChart = plotArea.getBarChartList();
List<CTBarSer> arBarSer = arBarChart.get(0).getSerList();
if(CHART_MAP_DATA != null && !CHART_MAP_DATA.isEmpty()) {
Set<String> keys = CHART_MAP_DATA.keySet();
Iterator<String> itKeys = keys.iterator();
while(itKeys.hasNext()) {
String inKey = itKeys.next();
Map<String, String> barData = CHART_MAP_DATA.get(inKey);
setBarChartData(ctChart, serCnt, inKey, barData);
}
}
XSSFSheet sheet = wb.getSheetAt(0);
sheet.getRow(1).getCell(1).setCellValue(8.3);
sheet.getRow(2).getCell(1).setCellValue(7.3);
FileOutputStream fos = new FileOutputStream(new File(outFilePath));
document.write(fos);
fos.close();
document.close();
}
public static void setBarChartData(CTChart ctChart, int serIdx, String series, Map<String, String> data) {
CTPlotArea plotArea = ctChart.getPlotArea();
List<CTBarChart> arBarChart = plotArea.getBarChartList();
if(arBarChart.size() > 0) {
List<CTBarSer> arBarSer = arBarChart.get(0).getSerList();
CTBarSer barSer = arBarSer.get(serIdx);
CTSerTx serTx = barSer.getTx();
CTStrRef strRef = serTx.getStrRef();
CTStrData strData = strRef.getStrCache();
List<CTStrVal> arStrVal = strData.getPtList();
for(int b=0; b<arStrVal.size(); b++) {
arStrVal.get(b).setV(series);
}
CTAxDataSource dataSource = barSer.getCat();
CTStrRef dStrRef = dataSource.getStrRef();
boolean isCatDataTypeStr = true;
List<CTStrVal> arDStrVal = null;
List<CTNumVal> arDNumVal = null;
CTStrData dStrData = null;
CTNumData dNumData = null;
if(dStrRef != null) {
dStrData = dStrRef.getStrCache();
arDStrVal = dStrData.getPtList();
dStrData.getPtCount().setVal(data.size());
if(arDStrVal.size() > data.size()) {
for(int i=arDStrVal.size(); i>data.size(); i--) {
dStrData.removePt(i-1);
}
}
isCatDataTypeStr = true;
} else {
CTNumRef dNumRef = dataSource.getNumRef();
dNumData = dNumRef.getNumCache();
arDNumVal = dNumData.getPtList();
dNumData.getPtCount().setVal(data.size());
if(arDNumVal.size() > data.size()) {
for(int i=arDNumVal.size(); i>data.size(); i--) {
dNumData.removePt(i-1);
}
}
isCatDataTypeStr = false;
}
CTNumDataSource numDataSource = barSer.getVal();
CTNumRef numRef = numDataSource.getNumRef();
CTNumData numData = numRef.getNumCache();
List<CTNumVal> arNumVal = numData.getPtList();
numData.getPtCount().setVal(data.size());
if(arNumVal.size() > data.size()) {
for(int i=arNumVal.size(); i>data.size(); i--) {
numData.removePt(i-1);
}
}
Set<String> keys = data.keySet();
Iterator<String> itKeys = keys.iterator();
int valSize = 0;
if(isCatDataTypeStr) {
valSize = arDStrVal.size();
} else {
valSize = arDNumVal.size();
}
int idx = 0;
while(itKeys.hasNext()) {
String stKey = itKeys.next();
if(valSize > idx) {
if(isCatDataTypeStr) {
arDStrVal.get(idx).setV(stKey);
} else {
arDNumVal.get(idx).setV(stKey);
}
} else {
if(isCatDataTypeStr) {
CTStrVal val = dStrData.addNewPt();
val.setIdx(idx);
val.setV(stKey);
} else {
CTNumVal val = dNumData.addNewPt();
val.setIdx(idx);
val.setV(stKey);
}
}
if(arNumVal.size() > idx) {
arNumVal.get(idx).setV(data.get(stKey));
} else {
CTNumVal val = numData.addNewPt();
val.setIdx(idx);
val.setV(data.get(stKey));
}
idx++;
}
}
}
public static String getTitle(CTChart chart) {
CTTitle title = chart.getTitle();
if (title != null) {
CTTx tx = title.getTx();
CTTextBody tb = tx.getRich();
return tb.getPArray(0).getRArray(0).getT();
}
return "";
}
Using apache poi 4.0.1 changing XDDFChart data needs parallel updating all changes in underlying chart data workbook and the chart itself. The chart holds the cached data while the workbook holds the source data. But both is possible using the high level apache poiclasses. No access to underlying XML beans needed.
Example
Word template which has template chart having 2 series and 3 categories:
Code:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.poi.xddf.usermodel.chart.*;
import org.apache.poi.xssf.usermodel.*;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.ss.util.CellRangeAddress;
public class WordChangeChartData {
public static void main(String[] args) throws Exception {
String filePath = "TEMP_Chart_SimpleBar.docx"; // has template chart having 2 series, 3 categories
String filePathNew = "New_Chart_Simple.docx";
Object[][] data = new Object[][] { // 2 series, 3 categories
{"", "male", "female"}, // series titles
{"health", 123d, 234d}, // category 1
{"amount", 345d, 123d}, // category 2
{"size", 180d, 160d} // category 3
};
XWPFDocument document = new XWPFDocument(new FileInputStream(filePath));
XWPFChart chart = document.getCharts().get(0);
XSSFWorkbook chartDataWorkbook = chart.getWorkbook();
String sheetName = chartDataWorkbook.getSheetName(0);
XSSFSheet chartDataSheet = chartDataWorkbook.getSheet(sheetName);
if (chart.getChartSeries().size() == 1) { // only one chart data
XDDFChartData chartData = chart.getChartSeries().get(0);
if (chartData.getSeries().size() == 2) { // exact two series
int rMin = 1;
int rMax = 3;
// set new category data (both series)
XDDFCategoryDataSource category = null;
int c = 0;
for (int r = rMin; r < rMax+1; r++) {
chartDataSheet.getRow(r).getCell(c).setCellValue((String)data[r][c]); // in sheet
}
category = XDDFDataSourcesFactory.fromStringCellRange(chartDataSheet, new CellRangeAddress(rMin,rMax,c,c)); // in chart
// series 1
XDDFChartData.Series series1 = chartData.getSeries().get(0);
c = 1;
// set new title
String series1Title = (String)data[0][c];
chartDataSheet.getRow(0).getCell(c).setCellValue(series1Title); // in sheet
if (chartDataSheet.getTables().size() > 0) {
if (chartDataSheet.getTables().get(0).getCTTable().getTableColumns().getTableColumnList().size() > c)
chartDataSheet.getTables().get(0).getCTTable().getTableColumns().getTableColumnList().get(c).setName(series1Title);
}
series1.setTitle(series1Title, new CellReference(sheetName, 0, c, true, true)); // in chart
// set new values
XDDFNumericalDataSource<Double> values = null;
for (int r = rMin; r < rMax+1; r++) {
chartDataSheet.getRow(r).getCell(c).setCellValue((Double)data[r][c]); // in sheet
}
values = XDDFDataSourcesFactory.fromNumericCellRange(chartDataSheet, new CellRangeAddress(rMin,rMax,c,c));
series1.replaceData(category, values);
series1.plot(); //in chart
// series 2
XDDFChartData.Series series2 = chartData.getSeries().get(1);
c = 2;
// set new title
String series2Title = (String)data[0][c];
chartDataSheet.getRow(0).getCell(c).setCellValue(series2Title); // in sheet
if (chartDataSheet.getTables().size() > 0) {
if (chartDataSheet.getTables().get(0).getCTTable().getTableColumns().getTableColumnList().size() > c)
chartDataSheet.getTables().get(0).getCTTable().getTableColumns().getTableColumnList().get(c).setName(series2Title);
}
series2.setTitle(series2Title, new CellReference(sheetName, 0, c, true, true)); // in chart
// set new values
for (int r = rMin; r < rMax+1; r++) {
chartDataSheet.getRow(r).getCell(c).setCellValue((Double)data[r][c]); // in sheet
}
values = XDDFDataSourcesFactory.fromNumericCellRange(chartDataSheet, new CellRangeAddress(rMin,rMax,c,c));
series2.replaceData(category, values);
series2.plot(); // in chart
}
}
FileOutputStream out = new FileOutputStream(filePathNew);
document.write(out);
out.close();
document.close();
}
}
Result:

Apache POI - Reading excel file in 2D array - returning null values

I am trying to read Excel -2*2 matrix through Apache POI. But the first value returned by 2D array is [null,null]. Please check my code and advise for suitable corrections.
public String[][] getDataArray(String sheetName)
{
String value ="";
String[][] data = null;
int rowCount = wb.getSheet(sheetName).getLastRowNum();
int colCount = wb.getSheet(sheetName).getRow(1).getLastCellNum()-1;
data = new String[rowCount][colCount];
for(int i=1; i<=rowCount;i++)
{
Row row = wb.getSheet(sheetName).getRow(i);
for(int j=0;j<colCount;j++)
{
Cell cell = row.getCell(j);
if(cell.getCellType()==Cell.CELL_TYPE_NUMERIC)
{
value = ""+cell.getStringCellValue();
}
else
{
value = cell.getStringCellValue();
}
data[i][j] = value;
}
}
return data;
}
The debug view where we can see that the first value stored in the variable data is null, null
The excel which i am trying to read. I need only the userName and password data(2*2) alone. Not the header and Run mode datas.
Of course the value in the index 0 will be null because the i starts from 1 and not 0
for (int i = 1; i <= rowCount; i++) //i starts from one
...
data[i][j] = value;
either initialize the i from 0 or do like this
data[i-1][j] = value;
public static String[][] getSheetData(final String fileName, final String workSheetName)
throws Exception {
Integer lastRow = null;
short lastCol = 0;
String[][] sheetData = null;
FileInputStream file=new FileInputStream(MettlTest.class.getClass().getResource("/" + fileName).getPath());
workbook = new XSSFWorkbook(file);
sheet = workbook.getSheet(workSheetName);
try {
XSSFRow row;
XSSFCell cell;
lastRow = sheet.getPhysicalNumberOfRows();
lastCol = sheet.getRow(1).getLastCellNum();
sheetData = new String[lastRow - 1][lastCol];
for (int r = 1; r < lastRow; r++) {
row = sheet.getRow(r);
if (row != null) {
for (int c = 0; c < lastCol; c++) {
cell = row.getCell(c);
if (cell == null) {
sheetData[r][c] = null;
} else {
sheetData[r-1][c] = new DataFormatter().formatCellValue(cell);
}
}
}
}
return sheetData;
}
catch (final Exception e) {
throw e;
}
finally {
try {
file.close();
} catch (IOException io) {
Reporter.log("Unable to close File : " + fileName);
throw io;
}
}

copy excel sheet(With charts) to another excel sheet

I am using APACHE POI 3.0 to add sheets to existing excel sheet. It works fine.
But as APACHE POI has limitations about making charts, I used a template excel file to create charts, which also worked fine, but this always result in new excel file.
If I have an existing excel sheet and I want to add a sheet, having charts, I am not able to do it. As, when I create charts, I use template file and it always makes a new excel file.
so I was wondering if there is any solution of it of adding sheets to excel, where the sheets have charts
public class TagBrowserSelection
{
private static String[] excelBarPlot_Template = { "","barPlot_1Panel_template.xlsx"};
private static String[] excelPieChart_Template = { "","pieChart_1Panel_template.xlsx"};
private static String[] excelPieAndBarPlot_Template = { "","pieAndBarChart_1Panel_template.xlsx"};
private static String REGEX = "";
static public boolean makeTagBrowserSelection(String strOutputFileName, ArrayList<TagBrowserChildPanel> childList, String sheetName, boolean addSheet, ArrayList<Boolean> chartAttributes)
{
// chart attributes
boolean addBarChart = chartAttributes.get(0);
boolean addPieChart = chartAttributes.get(1);
boolean addNoTag = chartAttributes.get(2);
boolean addZeros = chartAttributes.get(3);
REGEX = "^" + sheetName;
Pattern p = Pattern.compile(REGEX);
String[] templateArray = null;
if (addBarChart && addPieChart)
templateArray = excelPieAndBarPlot_Template;
else if (addBarChart)
templateArray = excelBarPlot_Template;
else if (addPieChart)
templateArray = excelPieChart_Template;
try
{
int number = childList.size();
XSSFWorkbook workbook = null;
XSSFWorkbook wb = null;
XSSFSheet sheet = null;
int col_num = 0;
int row_num = 0;
XSSFRow row = null;
XSSFCell cell = null;
// if adding sheet to existing excel file
if (addSheet)
{
FileInputStream fis = new FileInputStream(new File(strOutputFileName));
workbook = new XSSFWorkbook(fis);
fis.close();
// number of existing sheets in excel file
int numberOfSheets = workbook.getNumberOfSheets();
// check is sheetName exists already
if (isSheetExist(sheetName, workbook))
{
int counter = 1;
for (int ii = 0; ii < numberOfSheets; ii++)
{
Matcher m = p.matcher(workbook.getSheetName(ii));
if (m.find())
counter++;
}
sheetName = sheetName + " (" + counter + ")";
}
}
else
{
workbook = new XSSFWorkbook();
}
======================================================================
// if template file needs to be used(if bar chart/pie chart option is selected)
if (templateArray != null)
{
InputStream is = TagBrowserSelection.class.getClassLoader().getResourceAsStream(templateArray[number]);
wb = new XSSFWorkbook(OPCPackage.open(is));
sheet = wb.getSheetAt(0);
// wb.close();
}
else
{
sheet = workbook.createSheet(sheetName);
}
// Freeze top two row
// sheet.createFreezePane(0, 1, 0, 1);
// Filling up the workbook and performing the row/column formatting
for (TagBrowserChildPanel child : childList)
{
// Check if row is already created before(previous tag category)
row = sheet.getRow(0);
if (row == null)
row = sheet.createRow(0);
// Adding tag category name as header
String tagCategory = child.getSelectedCategory().getName();
cell = row.createCell(col_num);
cell.setCellValue(tagCategory);
row = sheet.getRow(1);
if (row == null)
row = sheet.createRow(1);
// Adding column headers
cell = row.createCell(col_num);
cell.setCellValue("tag");
cell = row.createCell(col_num + 1);
cell.setCellValue("counts");
row_num = 2;
// Adding tag category document summary(name and counts)
ArrayList<TagSummaryItem> tagSummary = child.getTagChartCounts();
for (int i = 0; i < tagSummary.size(); i++)
{
// Check if row is already created before(previous tag category)
row = sheet.getRow(row_num);
if (row == null)
row = sheet.createRow(row_num);
cell = row.createCell(col_num);
if (!addNoTag)
{
if (tagSummary.get(i).m_strTag == "[No Tag]")
continue;
}
if (!addZeros)
{
if (tagSummary.get(i).m_nCount == 0)
continue;
}
cell.setCellValue(tagSummary.get(i).m_strTag);
cell = row.createCell(col_num + 1);
cell.setCellValue(tagSummary.get(i).m_nCount);
row_num++;
}
// auto-size of tag column
sheet.autoSizeColumn(col_num);
col_num = col_num + 3;
}
FileOutputStream out = new FileOutputStream(strOutputFileName);
if (templateArray != null)
{
wb.setSheetName(0, sheetName);
wb.write(out);
wb.close();
}
else
{
workbook.write(out);
workbook.close();
}
out.close();
}
catch (Exception e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
return true;
}
Above is my code, its one code. I split into two sections. Section is the one which uses template to make chart excel sheet.
there's the method cloneSheet() in the HSSFWorkbook class. Try it.

Excel creation with POI

I was using Apache POI 3.7 to create Excel with HSSF. It was working fine. But during load test, I realized that it is very slow. So I Googled and found I can use SXSSF. I changed my existing code to XSSF. The result was awesome.
But I stuck with a situation, autorezisecolum() is not functioning as expected. It displays large contents as #####, because the column width shrinks. I found it is a kind of a bug already raised.
Now my point is, is there any solution so that i can use SXSSF(very important for performance) with a nice output.
Note: I am using Windows 7, JDK 1.7.09 , POI-3.10.beta-2
Please help me.
Here is my code :
Main Function :
sxssfWorkbook = new SXSSFWorkbook(5);
sxssfSheet = (SXSSFSheet) sxssfWorkbook.createSheet(sheetName);
try {
// TO Write Header
//ew.writeHeaderRow(sheet, headerNames);
ew.writeHeaderRow(sxssfSheet, headerNames);
int rowNum = headerRow + 1;
for(Map.Entry<String, List<Object>> columnData : columnDataMap.entrySet()){
ew.writeNonHeaderRow(sxssfSheet, columnData.getValue(), rowNum);
rowNum++;
}
resizeXLSXColumns(sxssfSheet,rowNum-1);
sxssfWorkbook.write(outputStream);
outputStream.close();
public void writeHeaderRow(Sheet sheet, List<String> headerNames ) {
//public void writeHeaderRow(SXSSFSheet sxssfSheet, List<String> headerNames ) {
// LinkedHashMap<String,Object> mp = getFieldNames(obj);
// ArrayList<String> colNames = (ArrayList<String>) getColumnNames();
try {
XSSFCellStyle hCellStyle = getHeaderStyle();
SXSSFRow row = (SXSSFRow) sheet.createRow(headerRow);
for (int hCellInd = 0; hCellInd < headerNames.size(); hCellInd++) {
SXSSFCell cell = (SXSSFCell) row.createCell(hCellInd);
cell.setCellStyle(hCellStyle);
cell.setCellValue(headerNames.get(hCellInd));
//sheet.autoSizeColumn(hCellInd);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void writeNonHeaderRow(SXSSFSheet sxssfSheet, List<Object> rowObj, int rowIndex)
{
CreationHelper createHelper = sxssfWorkbook.getCreationHelper();
try {
SXSSFRow row = (SXSSFRow)sheet.createRow(rowIndex);
XSSFCellStyle normalStyle = getNormalStyle();
int count = 0;
for (int rCellInd = 0; rCellInd < rowObj.size(); rCellInd++) {
//Cell cell = row.createCell(rCellInd);
SXSSFCell cell = (SXSSFCell)row.createCell(rCellInd);
cell.setCellStyle(normalStyle);
Object cellData = rowObj.get(rCellInd);
if (cellData != null) {
if (cellData instanceof Double) {
cell.setCellValue((Double) cellData);
if((Double)cellData < 0){
cell.setCellStyle(getNegativeValueStyle());
}else if((Double)cellData == 0){
normalStyle.setDataFormat((short) SXSSFCell.CELL_TYPE_BLANK) ;
cell.setCellStyle(normalStyle);
}else
cell.setCellStyle(normalStyle);
} else {
//normalStyle.setDataFormat((short) HSSFCell.CELL_TYPE_BLANK);
cell.setCellType(SXSSFCell.CELL_TYPE_BLANK);
}
//sxssfSheet.autoSizeColumn();
//sxssfSheet.setColumnWidth(rCellInd, sxssfSheet.getColumnWidth(rCellInd));
//resizeXLSXColumns(sheet);
}
//autoResizeColumns(sxssfSheet);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void resizeXLSXColumns(Sheet sheet ,int rowNum){
SXSSFRow row = (SXSSFRow)sheet.getRow(rowNum);
Iterator<Cell> itr = row.cellIterator();
int max = 0;
while(itr.hasNext()){
Cell cell = itr.next();
int width = sheet.getColumnWidth(cell.getColumnIndex());
if(width > max){
max = width;
}
//sheet.setColumnWidth(cell.getColumnIndex(),max);
}
while(itr.hasNext()){
Cell cell = itr.next();
sheet.setColumnWidth(cell.getColumnIndex(),max);
}
}

Categories

Resources