12

Java 8 here using Apache POI 4.1 to load Excel (XLSX) files into memory, and write lists of Java beans/POJOs back to new Excel files.

To me, an Excel file (at least the ones I'm working with) is really a list of POJOs, with each row being a different instance of the POJO, and each column a different field value for that instance. Observe:

enter image description here

Here I might have a POJO called Car, and the example spreadsheet above is a List<Car>:

@Getter
@Setter
public class Car {

  private String manufacturer;
  private String model;
  private String color;
  private String year;
  private BigDecimal price;

}

So I have functioning code that will read an Excel file ("new-cars.xlsx") into a List<Car>, process that list, and then write the processed list back to an output file, say, "processed-cars.xlsx":

// 1. Load excel file into a List<Car>
InputStream inp = new FileInputStream("new-cars.xlsx");
Workbook workbook = WorkbookFactory.create(inp);
Iterator<Row> iterator = workbook.getSheetAt(0).iterator();

List<Car> carsInventory = new ArrayList<>();
while (iterator.hasNext()) {

    Car car = new Car();

    Row currentRow = iterator.next();

    // don't read the header
    if (currentRow.getRowNum() == 0) {
        continue;
    }

    Iterator<Cell> cellIterator = currentRow.iterator();

    while (cellIterator.hasNext()) {

        Cell currentCell = cellIterator.next();
        CellAddress address = currentCell.getAddress();

        if (0 == address.getColumn()) {
            // 1st col is "Manufacturer"
            car.setManufacturer(currentCell.getStringCellValue());
        } else if (1 == address.getColumn()) {
            // 2nd col is "Model"
            car.setModel(currentCell.getStringCellValue());
        } else if (2 == address.getColumn()) {
            // 3rd col is "Color"
            car.setColor(currentCell.getStringCellValue());
        } else if (3 == address.getColumn()) {
            // 4th col is "Year"
            car.setYear(currentCell.getStringCellValue());
        } else if (4 == address.getColumn()) {
            // 5th col is "Price"
            car.setPrice(BigDecimal.valueOf(currentCell.getNumericCellValue()));
        }

    }

    carsInventory.add(car);

}

// 2. Process the list of Cars; doesn't matter what this does
List<Car> processedInventory = processInventory(carsInventory);

// 3. Output to "processed-cars.xlsx"
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("Processed Inventory");
int rowNum = 0;

// create headers
Row headerRow = sheet.createRow(rowNum);
headerRow.createCell(0).setCellValue("Manufacturer");
headerRow.createCell(1).setCellValue("Model");
headerRow.createCell(2).setCellValue("Color");
headerRow.createCell(3).setCellValue("Year");
headerRow.createCell(4).setCellValue("Price");

rowNum++;

// rip through the cars list and convert each one into a subsequent row
for (Car processedCar : processedInventory) {

    Row nextRow = sheet.createRow(rowNum);

    nextRow.createCell(0).setCellValue(processedCar.getManufacturer());
    nextRow.createCell(1).setCellValue(processedCar.getModel());
    nextRow.createCell(2).setCellValue(processedCar.getColor());
    nextRow.createCell(3).setCellValue(processedCar.getYear());
    nextRow.createCell(4).setCellValue(processedCar.getPrice().doubleValue());

    rowNum++;

}

FileOutputStream fos = new FileOutputStream("processed-cars.xlsx");
workbook.write(fos);

workbook.close();

While this works, it looks really ugly/nasty to me. I've used JSON mappers (Jackson, GSON, etc.), XML mappers (XStream) and OR/M tools (Hibernate) for years, and it occurred to me that POI's API (or some other library) might offer a "mapper-esque" solution that would allow me to map/bind the Excel data to/from a list of POJOs with minimal code and maximal elegance. However, I cannot find any such feature anywhere. Maybe this is because it doesn't exist, or maybe I'm just not searching for the right keywords.

Ideally, something along the lines of:

// Annotate the fields with something that POI (or whatever tool) can pick up
@Getter
@Setter
public class Car {

  @ExcelColumn(name = "Manufacturer", col = 0)
  private String manufacturer;

  @ExcelColumn(name = "Model", col = 1)
  private String model;

  @ExcelColumn(name = "Color", col = 2)
  private String color;

  @ExcelColumn(name = "Year", col = 3)
  private String year;

  @ExcelColumn(name = "Price", col = 4)
  private BigDecimal price;

}

// 2. Now load the Excel into a List<Car>
InputStream inp = new FileInputStream("new-cars.xlsx");
List<Car> carsInventory = WorkbookFactory.create(inp).buildList(Car.class);

// 3. Process the list
List<Car> processedInventory = processInventory(carsInventory);

//4. Write to a new file
WorkbookFactory.write(processInventory, "processed-cars.xlsx");

Does anything like this exist in POI-land? Or am I stuck with what I got?

hotmeatballsoup
  • 385
  • 6
  • 58
  • 136
  • Maybe have a look at https://github.com/ozlerhakan/poiji. But behind the scenes of course this also needs get the contents from the sheet using code which looks "ugly/nasty". – Axel Richter Nov 21 '19 at 18:11

5 Answers5

21

As of now Apache POI does not have such a feature. There are external libraries which you can check. I provide a few libraries below.

https://github.com/ozlerhakan/poiji

The library is available in mvnrepository, link is given below. This library provides only one way binding like from excel sheet to java pojo only.

https://mvnrepository.com/artifact/com.github.ozlerhakan/poiji/2.2.0

As per the above, you can do something like this.

public class Employee {

    @ExcelRow                  
    private int rowIndex;

    @ExcelCell(0)                
    private long employeeId;  

    @ExcelCell(1)
    private String name;

    @ExcelCell(2)
    private String surname;

    @ExcelCell(3)
    private int age;
}

To get the information from excel sheet to java object, you have to do in the following manner.

List<Employee> employees = Poiji.fromExcel(new File("employees.xls"), Employee.class);

There is another library which can do both things like excel to java and java to excel. I provide below the link.

https://github.com/millij/poi-object-mapper

As per above library, you can do something like this.

@Sheet
public class Employee {

    @SheetColumn("Age")
    private Integer age;

    @SheetColumn("Name")
    public String getName() {
        return name;
    }

}

To get data from xlsx file, you have to write like this.

final File xlsxFile = new File("<path_to_file>");
final XlsReader reader = new XlsReader();
List<Employee> employees = reader.read(Employee.class, xlsxFile);

To write data to the excel sheet, you have to do like this.

List<Employee> employees = new ArrayList<Employee>();
employees.add(new Employee("1", "foo", 12, "MALE", 1.68));
SpreadsheetWriter writer = new SpreadsheetWriter("<output_file_path>");
writer.addSheet(Employee.class, employees);
writer.write();

You have to evaluate both the libraries for your use cases.

Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
Sambit
  • 7,625
  • 7
  • 34
  • 65
  • Thanks so much @Sambit (+1) -- one thing I didn't think to mention when I wrote this question a few hours ago is that my output file (e.g. `processed-cars.xlsx`) needs to be formatted with certain styles (certain columns center-aligned, other columns with font and background colors applied, etc.). Of course, I could always use one of these libs to write my `List` to an Excel file and then apply the formatting after the fact (with POI API), but if one of these libs allows me to do this formatting inline, that'd be the deciding factor in my mind. Do you know if either of these allows this? – hotmeatballsoup Nov 21 '19 at 18:59
  • I think these do not do formatting, we need to check such library if available. – Sambit Nov 21 '19 at 19:02
  • Important to note that Poiji does _not_ support writing back to Excel, and the POI Mapper project no longer seems to be maintained. – hotmeatballsoup Nov 25 '19 at 17:44
  • 1
    Write to file doesn't work. It is just writing headers but not the data – Naveen Jun 17 '21 at 10:51
6

I would consider writing my own apache poi to/from POJO mapper package instead of simply searching for any available packages. Doing this you are more flexible in extending the functionality then because you then know how it works without the need of dive deep into code others have wrote and which is heavily divided up into classes and methods. Trying to understand such code can be really difficult. No to mention to know where to place your own wanted extensions then.

To have a start, here is a package PoiPOJO which until now only consists of two classes. PoiPOJOUtils which provides two static methods. One sheetToPOJO and one pojoToSheet. And ExcelColumn which is an Annotation interface usable in POJO classes then.

PoiPOJOUtils.java:

package PoiPOJO;

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellUtil;
import java.util.*;
import java.lang.reflect.*;

public class PoiPOJOUtils {

 public static <T> List<T> sheetToPOJO(Sheet sheet, Class<T> beanClass) throws Exception {

  DataFormatter formatter = new DataFormatter(java.util.Locale.US);
  FormulaEvaluator evaluator = sheet.getWorkbook().getCreationHelper().createFormulaEvaluator();

  int headerRowNum = sheet.getFirstRowNum();

  // collecting the column headers as a Map of header names to column indexes
  Map<Integer, String> colHeaders = new HashMap<Integer, String>();
  Row row = sheet.getRow(headerRowNum);
  for (Cell cell : row) {
   int colIdx = cell.getColumnIndex();
   String value = formatter.formatCellValue(cell, evaluator);
   colHeaders.put(colIdx, value);
  }

  // collecting the content rows
  List<T> result = new ArrayList<T>();
  String cellValue = "";
  java.util.Date date = null;
  Double num = null;
  for (int r = headerRowNum + 1; r <= sheet.getLastRowNum(); r++) {
   row = sheet.getRow(r); if (row == null) row = sheet.createRow(r);
   T bean = beanClass.getDeclaredConstructor().newInstance();

   for (Map.Entry<Integer, String> entry : colHeaders.entrySet()) {
    int colIdx = entry.getKey();
    Cell cell = row.getCell(colIdx); if (cell == null) cell = row.createCell(colIdx);
    cellValue = formatter.formatCellValue(cell, evaluator); // string values and formatted numbers
    // make some differences for numeric or formula content
    date = null;
    num = null;
    if (cell.getCellType() == CellType.NUMERIC) {
     if (DateUtil.isCellDateFormatted(cell)) { // date
      date = cell.getDateCellValue();
     } else { // other numbers
      num = cell.getNumericCellValue();
     }
    } else if (cell.getCellType() == CellType.FORMULA) {
     // if formula evaluates to numeric
     if (evaluator.evaluateFormulaCell(cell) == CellType.NUMERIC) {
      if (DateUtil.isCellDateFormatted(cell)) { // date
       date = cell.getDateCellValue();
      } else { // other numbers
       num = cell.getNumericCellValue();
      }
     }
    }

    // fill the bean
    for (Field f : beanClass.getDeclaredFields()) {
     if (!f.isAnnotationPresent(ExcelColumn.class)) {
      continue;
     }
     ExcelColumn ec = f.getAnnotation(ExcelColumn.class);
     if(entry.getValue().equals(ec.name())) {
      f.setAccessible(true);
      if (f.getType() == String.class) {
       f.set(bean, cellValue);
      } else if (f.getType() == Double.class) {
       f.set(bean, num);
      } else if (f.getType() == java.util.Date.class) {
       f.set(bean, date);
      } else { // this is for all other; Integer, Boolean, ...
       if (!"".equals(cellValue)) {
        Method valueOf = f.getType().getDeclaredMethod("valueOf", String.class);
        f.set(bean, valueOf.invoke(f.getType(), cellValue));
       }
      }
     }
    } 
   }
   result.add(bean);
  }

  return result;

 }

 public static <T> void pojoToSheet(Sheet sheet, List<T> rows) throws Exception {
  if (rows.size() > 0) { 
   Row row = null;
   Cell cell = null;
   int r = 0;
   int c = 0;
   int colCount = 0;
   Map<String, Object> properties = null;
   DataFormat dataFormat = sheet.getWorkbook().createDataFormat();

   Class beanClass = rows.get(0).getClass();

   // header row
   row = sheet.createRow(r++);
   for (Field f : beanClass.getDeclaredFields()) {
    if (!f.isAnnotationPresent(ExcelColumn.class)) {
     continue;
    }
    ExcelColumn ec = f.getAnnotation(ExcelColumn.class);
    cell = row.createCell(c++);
    // do formatting the header row
    properties = new HashMap<String, Object>();
    properties.put(CellUtil.FILL_PATTERN, FillPatternType.SOLID_FOREGROUND);
    properties.put(CellUtil.FILL_FOREGROUND_COLOR, IndexedColors.GREY_25_PERCENT.getIndex());
    CellUtil.setCellStyleProperties(cell, properties);
    cell.setCellValue(ec.name());
   }

   colCount = c;

   // contents
   for (T bean : rows) {
    c = 0;
    row = sheet.createRow(r++);
    for (Field f : beanClass.getDeclaredFields()) {
     cell = row.createCell(c++);
     if (!f.isAnnotationPresent(ExcelColumn.class)) {
      continue;
     }
     ExcelColumn ec = f.getAnnotation(ExcelColumn.class);
     // do number formatting the contents
     String numberFormat = ec.numberFormat();
     properties = new HashMap<String, Object>();
     properties.put(CellUtil.DATA_FORMAT, dataFormat.getFormat(numberFormat));
     CellUtil.setCellStyleProperties(cell, properties);

     f.setAccessible(true);
     Object value = f.get(bean);
     if (value != null) {
      if (value instanceof String) {
       cell.setCellValue((String)value);
      } else if (value instanceof Double) {
       cell.setCellValue((Double)value);
      } else if (value instanceof Integer) {
       cell.setCellValue((Integer)value);
      } else if (value instanceof java.util.Date) {
       cell.setCellValue((java.util.Date)value);
      } else if (value instanceof Boolean) {
       cell.setCellValue((Boolean)value);
      }
     }
    }
   }

   // auto size columns
   for (int col = 0; col < colCount; col++) {
    sheet.autoSizeColumn(col);
   }
  }
 }

}

and

ExcelColumn.java:

package PoiPOJO;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumn {
 String name();
 String numberFormat() default "General";
} 

This can be used then having ...

Car.java:

import PoiPOJO.ExcelColumn;

public class Car {

 @ExcelColumn(name = "Manufacturer")
 public String manufacturer;

 @ExcelColumn(name = "Model")
 public String model;

 @ExcelColumn(name = "Color")
 public String color;

 @ExcelColumn(name = "Year", numberFormat = "0")
 public Integer year;

 @ExcelColumn(name = "Price", numberFormat = "$#,##0.00")
 public Double price;

 @ExcelColumn(name = "Date", numberFormat = "YYYY-MM-DD")
 public java.util.Date date;

 @ExcelColumn(name = "Available")
 public Boolean available;

 public String toString() {
  String result = ""
   +"Manufacturer=" + this.manufacturer
   +" Model=" + this.model
   +" Color=" + this.color
   +" Year=" + this.year
   +" Price=" + this.price
   +" Date=" + this.date
   +" Available=" + this.available
   +"";
  return result;
 }
}

and

TestPoiPOJO.java:

import PoiPOJO.PoiPOJOUtils;

import org.apache.poi.ss.usermodel.*;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.List;

public class TestPoiPOJO {

 public static void main(String[] args) throws Exception {

  Workbook workbook = WorkbookFactory.create(new FileInputStream("ExcelCars.xlsx"));
  Sheet sheet = workbook.getSheetAt(0);
  List<Car> cars = PoiPOJOUtils.sheetToPOJO(sheet, Car.class);
  System.out.println(cars);

  Car car = new Car();
  car.manufacturer = "Mercedes-Benz";
  car.model = "S 560 4Matic";
  car.color = "Bordeaux";
  car.year = 2019;
  car.price = 78456.78;
  car.date = new java.util.Date();
  car.available = true;

  cars.add(car);

  sheet = workbook.createSheet();
  PoiPOJOUtils.pojoToSheet(sheet, cars);

  FileOutputStream out = new FileOutputStream("ExcelCarsNew.xlsx");
  workbook.write(out);
  out.close();
  workbook.close();
 }
}

The ExcelCars.xlsx must contain your sample cars table in first sheet. The sequence of the columns is flexible. Only the headings must correspond to the names of the ExcelColumn annotations in class Car.

Axel Richter
  • 56,077
  • 6
  • 60
  • 87
  • how would you suggest writing your own custom converter? For instance converting a java object to string. – share75 Apr 03 '20 at 10:26
  • @share75: It is not clear to me what your comment is about. This answer is about getting `Excel` sheet data into a `List` of `POJO`s and vice versa. What own custom converter shall be written? And why converting a java object to string? – Axel Richter Apr 03 '20 at 12:15
  • Lets say in the above example there is one more fields which says; @ExcelColumn(name = "Car") public Car car; I want car.toString() as the value in the column. – share75 Apr 24 '20 at 21:55
6

I would like to recommend to use oCell library for mapping Excel to POJO and POJO to Excel.

https://github.com/rushuat/ocell

<dependency>
  <groupId>io.github.rushuat</groupId>
  <artifactId>ocell</artifactId>
  <version>0.1.7</version>
</dependency>

Plus, this library supports few types of annotations (i.e. oCell, Jackson, JAXB, JPA) and other features for mapping (e.g. data transformation, cell formatting, field ignoring, etc).

Car POJO:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Car {

  @FieldName("Manufacturer")
  private String manufacturer;

  @FieldName("Model")
  private String model;

  @FieldName("Color")
  private String color;

  @FieldAlignment(horizontal = "right")
  @FieldConverter(YearConverter.class)
  @FieldName("Year")
  private String year;

  @FieldAlignment(horizontal = "right")
  @FieldFormat("_($* #,##0.00_);_($* (#,##0.00);_($* \"-\"??_);_(@_)")
  @FieldConverter(PriceConverter.class)
  @FieldName("Price")
  private BigDecimal price;
}

Read/Write Excel:

Car hondaCar = new Car("Honda", "Pilot", "White", "2019", new BigDecimal(39000));
Car chevyCar = new Car("Chevy", "Silverado", "Green", "2018", new BigDecimal(34000));
Car toyotaCar = new Car("Toyota", "Corolla", "Silver", "2002", new BigDecimal(4000));

try (Document document = new DocumentOOXML()) {
  List<Car> cars = Arrays.asList(hondaCar, chevyCar, toyotaCar);
  document.addSheet(cars);
  document.toFile("cars.xlsx");
}

try (Document document = new DocumentOOXML()) {
  document.fromFile("cars.xlsx");
  List<Car> cars = document.getSheet(Car.class);
}

Field Converters:

public class YearConverter implements ValueConverter<String, Integer> {

  @Override
  public String convertInput(Integer value) {
    return value == null ? null : String.valueOf(value);
  }

  @Override
  public Integer convertOutput(String value) {
    return value == null ? null : Integer.valueOf(value);
  }
}

public class PriceConverter implements ValueConverter<BigDecimal, Long> {

  @Override
  public BigDecimal convertInput(Long value) {
    return value == null ? null : new BigDecimal(value);
  }

  @Override
  public Long convertOutput(BigDecimal value) {
    return value == null ? null : value.longValue();
  }
}

@FieldFormat Source:

Basic Excel currency format with Apache POI

RusH UaT
  • 61
  • 1
  • 4
  • I like this library. It supports writing and reading. And the order of the Columns is matching with the order of the pojo field. Love it! – Jocasso Feb 14 '22 at 16:29
  • is there the opportunity to map child-objects to the excel tabel? – Jocasso Feb 14 '22 at 17:31
  • Sorry, not sure if I see your question very clear. So, in the case if you want to map a child object to a Excel cell you can add your own ValueConverter for a child field and use it to map object data to a Excel cell and vice versa. But, if you want to map a child object to a table this way is not supported. Anyway you can add a ticket with your ideas to the project page on GitHub. Thanks. – RusH UaT Feb 16 '22 at 18:44
  • FYI, the latest version of the library now supports enumerations and class hierarchy mapping. Probably, it could help with child-objects mapping as well. Enjoy! – RusH UaT Sep 23 '22 at 20:31
1

A slight variation to @Axel Ritcher's answer, using parallel streams and for Java objects with a Set Field (and no formula evaluation) :

public class ExcelFileUtils {


    @SneakyThrows
    // Call this using ExcelFileUtils.sheetToPOJO(new FileInputStream("yourExcl.xlsx"),YourPojo.class)
    public static <T> List<T> sheetToPOJO(InputStream is, Class<T> beanClass) {
        Workbook workbook = WorkbookFactory.create(is);
        Sheet sheet=workbook.getSheetAt(0);
        Map<Integer, String> colHeadersByColIdx = getColHeadersByCoIndex(sheet);
        Map<String, Field> beanFieldsByExlColName=beanFieldsByExlColName(beanClass);
        return IntStream.range(sheet.getFirstRowNum()+1,sheet.getLastRowNum())
                .parallel()
                .mapToObj(rowNum->{
                    T bean = null;
                    try {
                        bean =beanClass.getDeclaredConstructor().newInstance();
                        Row currentRow=sheet.getRow(rowNum);
                        if(Objects.isNull(currentRow)) currentRow=sheet.createRow(rowNum);
                        Row finalCurrentRow = currentRow;
                        T finalBean = bean;
                        colHeadersByColIdx.keySet().parallelStream()
                                .forEach(colIdx->{
                                    String colName=colHeadersByColIdx.get(colIdx);
                                    Cell cell=finalCurrentRow.getCell(colIdx);
                                    if(Objects.isNull(cell))cell=finalCurrentRow.createCell(colIdx);
                                    String cellValue=cell.getStringCellValue();
                                    Field fieldForColName=beanFieldsByExlColName.get(colName);
                                    fieldForColName.setAccessible(true);
                                    try {
                                        if (fieldForColName.getType() == String.class) {
                                            fieldForColName.set(finalBean, cellValue);
                                        }
                                        if(fieldForColName.getType() == Double.class){
                                            fieldForColName.set(finalBean,cell.getNumericCellValue());
                                        }
                                        if(fieldForColName.getType() == Set.class ){
                                            fieldForColName.set(finalBean, Arrays.stream(cellValue.split(",")).collect(Collectors.toSet()));
                                        }
                                    }catch (IllegalAccessException ex){
                                        throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,ex.getMessage());
                                    }

                                });
                    } catch (InstantiationException | IllegalAccessException | InvocationTargetException |NoSuchMethodException e) {
                        throw new ResponseStatusException(HttpStatus.BAD_REQUEST,e.getMessage());
                    }


                    return bean;
                }).collect(Collectors.toList());


    }

    private static <T> Map<String, Field> beanFieldsByExlColName(Class<T> beanClass){
        Map<String, Field> beanFieldsByExlColName=new HashMap<>();
        Arrays.stream(beanClass.getDeclaredFields())
                .parallel()
                .filter(field -> field.isAnnotationPresent(ExcelColumn.class))
                .forEach(field -> {
                    ExcelColumn ec = field.getAnnotation(ExcelColumn.class);
                    beanFieldsByExlColName.put(ec.name(),field);
                });
        return beanFieldsByExlColName;
    }

    private static  Map<Integer, String> getColHeadersByCoIndex(Sheet sheet){
        Map<Integer, String> colHeadersByColIdx = new HashMap<Integer, String>();
        Row row1 = sheet.getRow(sheet.getFirstRowNum());
        for(Cell cell : row1){
            int colIdx=cell.getColumnIndex();
            colHeadersByColIdx.put(colIdx,cell.getStringCellValue());
        }
        return colHeadersByColIdx;
    }
}


Please note that this example assumes that you have String, Double and Set in your pojo and the excel column that corresponds to the Set has comma separated values.

For example :

POJO :

@Data
public  class TestProduct{
    @ExcelColumn(name = "Product Name")
    private String productName;
    @ExcelColumn(name = "Image Urls")
    private Set<String> mediaUrls;
}

And the Excel sheet : enter image description here

chirag
  • 198
  • 1
  • 3
  • 17
0

I wanted to find a simple way to parse a xls/xlsx file to a list of pojo. After some searching i didn't find anything convenient and preferred to develop it quickly. Now i am able to get pojos by simply calling :

InputStream is = this.getClass().getResourceAsStream("/ExcelUtilsTest.xlsx");
List<Pojo> pojos = ExcelToPojoUtils.toPojo(Pojo.class, is);

If interested take a look on it :

https://github.com/ZPavel/excelToPojo

zpavel
  • 951
  • 5
  • 11