• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hee9841 / excel-module / #38

25 Apr 2025 09:07AM UTC coverage: 82.791%. Remained the same
#38

push

github

web-flow
Release 0.0.1 (#58)

* Docs: 초기 배포를 위한 docs 추가 (#21)

* Docs: README 추가

* Chore: gitignore에 maven pushing에 사용할 설정 파일 추가

* Deploy: Coveralls 추가, CI 추가 (#22)

* Release 0.0.1

* Deploy: 배포시 Readme 버전 설정 추가 (#25)

* Deploy: 베포시 readme 버전 설정

* Fix: 테스트로 추가했던 dev 브런치 삭제

* Docs: javadocs 관련 의존성 추가 및 배포된 maven repository url 추가 (#30)

* Docs: javaDocs 만들기 위한 추가 의존성 추가

* Docs: Maven Central url README에 추가

* Fix: slf4j-api 의존성의 implementation으로 변경 (#33)

* Feat: 로그 info 메세지 변경 (#34)

* Feat: 로깅 메세지 변경

* Feat: 로그 info 메세지 변경(dto 클래스명 SimpleName에서 패키지 포함으로 변경)

* Feat: Excel, ExcelColumn 어노테이션 정합성 예외 처리 (#35)

* Feat: SystemValues 클레스에 ExcelColumn에 허용되는 타입들 추가

* Feat: Excel, ExcelColumn 어노테이션 관련 적합성에 대한 예외 처리 추가

* Fix: ExcelColumnAnnotationProcessor에 AutoService import

* Fix: Supported source version에 대한 경고 제거 (#36)

* Fix: AutoService 제거

- 의존성 삭제
- 프로세서에 어노테이션 제거

* Fix: 어노테이션 프로세서 SourceVersion을 latestSupported로 변경

* Fix: poi 의존성을 implementation에서 api로 변경 (#38)

* Fix: 에러 메세지 수정(STY_CU_003_B, STG_CT_001_B,STG_ID_002_B 테스트 사항) (#46)

* Fix: 에러 메세지 수정(STY_CU_003_B, STG_CT_001_B,STG_ID_002_B 테스트 사항)

* Style: comment, 및 포멧 수정

* Fix: compile 옵션 변경, toolchain java 버전 변경 (#47)

* Chore: gradle 버전 변경 gradle-8.5 -> gradle-8.10

* Chore: java compile source, target 8 에서 release 8로 변경

- toolchain을 23으로 변경

* Refactor: ExcelExporter의 validate 메서드 추가(data size, sheet Strategy에 따른) (#48)

- validate 오버라이딩으로 생성자의 setSheetStrategy 메서드호출과 initialize 호출 시점 변경,

* Refactor: VerticalAlignment의 default 값을 CENTER로 설정 (#49)

* Refactor: poi 라이브러리의 클래스명과 겹치는 클래스명 변경 (#50)

* Rename: IndexedColor 관련 클래스명 rename

- IndexedColors.java ->  ColorPalette.java
- IndexedExcelColor.java -> PaletteExcelColor.java

* Rename: CellType.java -> ColumnDataType.java으로 클래스명 변경

* Rename: Alignment 관련 Enum 클래스 'Excel' 접두사 추가

* Rename: BorderStyle.java -> ExcelBorderStyle.java로 변경

* Refactor: Excel, ExcelColumn 어노테이션 프로세서 추가 구현 (#52)

* Refactor: ExcelAnnotationProcessor와 ExcelColumnAn... (continued)

105 of 168 new or added lines in 16 files covered. (62.5%)

29 existing lines in 7 files now uncovered.

534 of 645 relevant lines covered (82.79%)

0.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.62
/src/main/java/io/github/hee9841/excel/meta/ColumnInfoMapper.java
1
package io.github.hee9841.excel.meta;
2

3
import static io.github.hee9841.excel.global.SystemValues.ALLOWED_FIELD_TYPES;
4
import static io.github.hee9841.excel.global.SystemValues.ALLOWED_FIELD_TYPES_STRING;
5

6
import io.github.hee9841.excel.annotation.Excel;
7
import io.github.hee9841.excel.annotation.ExcelColumn;
8
import io.github.hee9841.excel.annotation.ExcelColumnStyle;
9
import io.github.hee9841.excel.exception.ExcelException;
10
import io.github.hee9841.excel.exception.ExcelStyleException;
11
import io.github.hee9841.excel.format.CellFormats;
12
import io.github.hee9841.excel.format.ExcelDataFormater;
13
import io.github.hee9841.excel.strategy.CellTypeStrategy;
14
import io.github.hee9841.excel.strategy.ColumnIndexStrategy;
15
import io.github.hee9841.excel.strategy.DataFormatStrategy;
16
import io.github.hee9841.excel.style.ExcelCellStyle;
17
import io.github.hee9841.excel.style.NoCellStyle;
18
import java.lang.reflect.Field;
19
import java.lang.reflect.InvocationTargetException;
20
import java.lang.reflect.Modifier;
21
import java.util.HashMap;
22
import java.util.Map;
23
import java.util.Optional;
24
import org.apache.commons.lang3.reflect.FieldUtils;
25
import org.apache.poi.ss.usermodel.CellStyle;
26
import org.apache.poi.ss.usermodel.Workbook;
27

28
/**
29
 * Maps Java class fields to Excel columns using reflection and annotations.
30
 * This class processes the {@link io.github.hee9841.excel.annotation.Excel} and
31
 * {@link io.github.hee9841.excel.annotation.ExcelColumn} annotations to generate column information
32
 * for Excel export operations. It handles column indexing, cell data types of column determination,
33
 * and style application based on the annotation configurations.
34
 *
35
 * @see ColumnInfo
36
 * @see ColumnDataType
37
 * @see Excel
38
 * @see ExcelColumn
39
 * @see ExcelColumnStyle
40
 */
41
public class ColumnInfoMapper {
42

43
    /**
44
     * The fully qualified class name of the default "no style" class
45
     */
46
    private static final String STANDARD_STYLE = NoCellStyle.class.getName();
1✔
47

48
    /**
49
     * The Apache POI Workbook to create cell styles for
50
     */
51
    private final Workbook wb;
52
    /**
53
     * The class type being mapped to Excel
54
     */
55
    private final Class<?> type;
56

57
    /**
58
     * The default style to use for header cells
59
     */
60
    private final CellStyle defaultHeaderStyle;
61
    /**
62
     * The default style to use for body cells
63
     */
64
    private final CellStyle defaultBodyStyle;
65

66
    /**
67
     * Strategy for determining column indices
68
     */
69
    private ColumnIndexStrategy columnIndexStrategy;
70
    /**
71
     * Strategy for determining cell types
72
     */
73
    private CellTypeStrategy cellTypeStrategy;
74
    /**
75
     * Strategy for determining data formats
76
     */
77
    private DataFormatStrategy dataFormatStrategy;
78

79

80
    private ColumnInfoMapper(Class<?> type, Workbook wb) {
1✔
81
        this.wb = wb;
1✔
82
        this.type = type;
1✔
83
        this.defaultHeaderStyle = wb.createCellStyle();
1✔
84
        this.defaultBodyStyle = wb.createCellStyle();
1✔
85
    }
1✔
86

87
    /**
88
     * Factory method to create a new {@link ColumnInfoMapper} instance.
89
     *
90
     * @param type     The class type to map
91
     * @param workbook The Apache POI Workbook to create styles for
92
     * @return A new {@link ColumnInfoMapper} instance
93
     */
94
    public static ColumnInfoMapper of(Class<?> type, Workbook workbook) {
95
        return new ColumnInfoMapper(type, workbook);
1✔
96
    }
97

98
    /**
99
     * Maps the class fields to Excel columns and returns a map of column indices to
100
     * {@link ColumnInfo} objects.
101
     * This method processes the {@link io.github.hee9841.excel.annotation.Excel} annotation and
102
     * all {@link io.github.hee9841.excel.annotation.ExcelColumn} annotations in the class.
103
     *
104
     * @return A map of column indices to {@link ColumnInfo} objects
105
     * @throws ExcelException If the class is not properly annotated or has invalid configuration
106
     */
107
    public Map<Integer, ColumnInfo> map() {
108
        parsingExcelAnnotation();
1✔
109
        return parsingExcelColumns().orElseThrow(() -> new ExcelException(
1✔
110
                String.format("No @ExcelColumn annotations found in class '(%s)'."
1✔
111
                    + " At least one field must be annotated with @ExcelColumn", type.getName())
1✔
112
            )
113
        );
114
    }
115

116
    /**
117
     * Parses the {@link Excel} annotation on the class to determine global settings.
118
     * Sets up the column index strategy, cell type strategy, and data format strategy.
119
     * Also applies default styles for headers and bodies.
120
     *
121
     * @throws ExcelException If the {@link Excel} annotation is missing
122
     */
123
    private void parsingExcelAnnotation() {
124
        validateExcelAnnotation(type);
1✔
125

126
        Excel excel = type.getAnnotation(Excel.class);
1✔
127
        columnIndexStrategy = excel.columnIndexStrategy();
1✔
128
        cellTypeStrategy = excel.cellTypeStrategy();
1✔
129
        dataFormatStrategy = excel.dataFormatStrategy();
1✔
130

131
        //set default style
132
        getExcelCellStyle(excel.defaultHeaderStyle()).apply(defaultHeaderStyle);
1✔
133
        getExcelCellStyle(excel.defaultBodyStyle()).apply(defaultBodyStyle);
1✔
134
    }
1✔
135

136
    private void validateExcelAnnotation(Class<?> clazz) {
137
        if (clazz.isInterface()) {
1✔
NEW
138
            throw new ExcelException("The class " + clazz.getName()
×
139
                + " is interface. You can't annotate interface classes with @Excel");
140
        }
141

142
        if (Modifier.isAbstract(clazz.getModifiers())) {
1✔
NEW
143
            throw new ExcelException("The class " + clazz.getName()
×
144
                + " is abstract. You can't annotate abstract classes with @Excel");
145
        }
146

147
        if (!type.isAnnotationPresent(Excel.class)) {
1✔
148
            throw new ExcelException(
1✔
149
                "Missing the @Excel annotation.", type.getName()
1✔
150
            );
151
        }
152
    }
1✔
153

154
    /**
155
     * Parses all fields annotated with {@link io.github.hee9841.excel.annotation.ExcelColumn}
156
     * in the class and builds a map of
157
     * column indices to {@link ColumnInfo} objects.
158
     *
159
     * @return An Optional containing the map of column indices to {@link ColumnInfo} objects,
160
     * or an empty Optional
161
     * if no {@link io.github.hee9841.excel.annotation.ExcelColumn} annotations were found.
162
     * @throws ExcelException If there are duplicate column indices or other validation errors
163
     */
164
    private Optional<Map<Integer, ColumnInfo>> parsingExcelColumns() {
165
        int autoColumnIndexCnt = 0;
1✔
166
        Map<Integer, ColumnInfo> result = new HashMap<>();
1✔
167

168
        for (Field field : FieldUtils.getAllFields(type)) {
1✔
169
            if (!field.isAnnotationPresent(ExcelColumn.class)) {
1✔
170
                continue;
1✔
171
            }
172

173
            validateField(field);
1✔
174

175
            ExcelColumn excelColumn = field.getAnnotation(ExcelColumn.class);
1✔
176
            field.setAccessible(true);
1✔
177

178
            //set column index value
179
            int columnIndex = columnIndexStrategy.isFieldOrder()
1✔
180
                ? autoColumnIndexCnt++
1✔
181
                : excelColumn.columnIndex();
1✔
182
            validateColumnIndex(result, columnIndex, field.getName());
1✔
183

184
            //get column info
185
            result.put(columnIndex,
1✔
186
                getColumnInfo(excelColumn, field.getType(), field.getName()));
1✔
187
        }
188

189
        return result.isEmpty() ? Optional.empty() : Optional.of(result);
1✔
190
    }
191

192
    private void validateField(Field field) {
193

194
        if (field.getType().isArray()) {
1✔
NEW
195
            throw new ExcelException(
×
NEW
196
                String.format("@ExcelColumn cannot be applied to array type: %s",
×
NEW
197
                    field.getName()),
×
NEW
198
                type.getName()
×
199
            );
200
        }
201

202
        if (type.isEnum() || type.isPrimitive()) {
1✔
NEW
203
            return;
×
204
        }
205

206
        Class<?> fieldType = field.getType();
1✔
207
        ALLOWED_FIELD_TYPES.stream()
1✔
208
            .filter(allowedType -> allowedType.isAssignableFrom(fieldType))
1✔
209
            .findFirst()
1✔
210
            .orElseThrow(() -> new ExcelException(
1✔
NEW
211
                String.format(
×
212
                    "%s(%s) Type is %s : @ExcelColumn can only be applied to allowed types(%s).",
NEW
213
                    field.getName(),
×
NEW
214
                    type.getName(),
×
NEW
215
                    fieldType.getSimpleName(),
×
216
                    ALLOWED_FIELD_TYPES_STRING)
217
            ));
218
    }
1✔
219

220
    /**
221
     * Validates a column index to ensure it's not negative and not already in use.
222
     *
223
     * @param columnInfoMap The current map of column indices to {@link ColumnInfo} objects
224
     * @param columnIndex   The column index to validate
225
     * @param fieldName     The name of the field being validated
226
     * @throws ExcelException If the column index is negative or already in use
227
     */
228
    private void validateColumnIndex(Map<Integer, ColumnInfo> columnInfoMap, int columnIndex,
229
        String fieldName) {
230
        //1. Check columnIndex value is negative.
231
        if (columnIndex < 0) {
1✔
232
            throw new ExcelException(String.format(
1✔
233
                "Invalid column index : The column index of '%s' field is negative or "
234
                    + "column index value was not specified when column index strategy is USER_DEFINED.\n"
235
                    + "Please Change index value to non-negative or Use 'FIELD_ORDER' strategy."
236
                , fieldName),
237
                type.getName()
1✔
238
            );
239
        }
240

241
        // 2. Check the columnIndex contains in columnInfoMap
242
        if (columnInfoMap.containsKey(columnIndex)) {
1✔
243
            throw new ExcelException(String.format(
1✔
244
                "Invalid column index : Duplicate value(%d) detected in fields (%s, %s)."
245
                , columnIndex, fieldName, columnInfoMap.get(columnIndex).getFieldName()),
1✔
246
                type.getName()
1✔
247
            );
248
        }
249
    }
1✔
250

251
    /**
252
     * Creates a {@link ColumnInfo} object for a field based on its {@link ExcelColumn} annotation.
253
     *
254
     * @param excelColumn The {@link ExcelColumn} annotation
255
     * @param fieldType   The type of the field
256
     * @param fieldName   The name of the field
257
     * @return A {@link ColumnInfo} object with the appropriate settings
258
     * @throws ExcelException If the {@link ColumnDataType} is not compatible with the field type
259
     */
260
    private ColumnInfo getColumnInfo(ExcelColumn excelColumn, Class<?> fieldType,
261
        String fieldName) {
262
        //Get cell type for column
263
        ColumnDataType columnDataType = getcolumnDataType(excelColumn.columnCellType(), fieldType,
1✔
264
            fieldName);
265

266
        //Set Cell style
267
        CellStyle headerStyle = updateCellStyle(excelColumn.headerStyle(), defaultHeaderStyle);
1✔
268
        CellStyle bodyStyle = updateCellStyle(excelColumn.bodyStyle(), defaultBodyStyle);
1✔
269

270
        //Set colum cell(body) format
271
        ExcelDataFormater dataFormater = getDataFormater(excelColumn.format(), columnDataType);
1✔
272
        dataFormater.apply(bodyStyle);
1✔
273

274
        return ColumnInfo.of(
1✔
275
            fieldName,
276
            excelColumn.headerName(),
1✔
277
            columnDataType,
278
            headerStyle,
279
            bodyStyle
280
        );
281
    }
282

283
    /**
284
     * Creates a {@link ExcelDataFormater} for a cell based on the format pattern and
285
     * {@link ColumnDataType}.
286
     * Applies automatic formatting if the {@link DataFormatStrategy} is
287
     * {@link DataFormatStrategy#AUTO_BY_CELL_TYPE} by {@link ColumnDataType}.
288
     *
289
     * @param pattern        The format pattern specified in the annotation
290
     * @param columnDataType The {@link ColumnDataType}
291
     * @return An {@link ExcelDataFormater} for the cell
292
     */
293
    private ExcelDataFormater getDataFormater(String pattern, ColumnDataType columnDataType) {
294
        // When dataFormatStrategy is "AUTO" and format pattern is "isNone"(empty or null),
295
        // apply auto format pattern.
296
        if ((dataFormatStrategy.isAutoByColumnDataType() && CellFormats.isNone(pattern))) {
1✔
297
            pattern = columnDataType.getDataFormatPattern();
1✔
298
        }
299

300
        // When dataFormatStrategy is "AUTO" and format pattern is not "isNone"
301
        // or dataFormatStrategy is "NONE"(format pattern is "isNone" or any value),
302
        // apply parameter "pattern" value.
303
        return ExcelDataFormater.of(wb.createDataFormat(), pattern);
1✔
304
    }
305

306
    /**
307
     * Determines the appropriate {@link ColumnDataType} for a field based on the column cell type
308
     * and the {@link CellTypeStrategy}.
309
     *
310
     * @param columnColumnDataType The {@link ColumnDataType} specified in the annotation
311
     * @param fieldType            The type of the field
312
     * @param fieldName            The name of the field
313
     * @return The appropriate {@link ColumnDataType}
314
     * @throws ExcelException If the specified {@link ColumnDataType} is not compatible with the
315
     *                        field
316
     *                        type
317
     */
318
    private ColumnDataType getcolumnDataType(ColumnDataType columnColumnDataType,
319
        Class<?> fieldType, String fieldName) {
320
        // When cell type strategy is AUTO and column cell type is not specified
321
        // or when column cell type is AUTO,
322
        // the column's cell type is automatically determined based on the field type.
323
        if ((cellTypeStrategy.isAuto() && columnColumnDataType.isNone())
1✔
324
            || columnColumnDataType.isAuto()) {
1✔
325
            return ColumnDataType.from(fieldType);
1✔
326
        }
327

328
        // When the specified column cell type is not compatible with the field type, throw an exception.
329
        if (!columnColumnDataType.equals(
1✔
330
            ColumnDataType.findMatchingCellType(fieldType, columnColumnDataType))) {
1✔
331
            throw new ExcelException(String.format(
1✔
332
                "Invalid cell type : The cell type of '%s' field is not compatible with the specified cell type(%s).",
333
                fieldName,
334
                columnColumnDataType.name()
1✔
335
            ), type.getName());
1✔
336
        }
337

338
        return columnColumnDataType;
1✔
339
    }
340

341
    /**
342
     * Updates a {@link CellStyle} based on the field's {@link ExcelColumn} annotation.
343
     * If the annotation specifies the default style, the default style is used.
344
     * Otherwise, the specified style is applied.
345
     * <p>
346
     * Note: {@link io.github.hee9841.excel.style.NoCellStyle} has the lowest priority when applying
347
     * styles.
348
     * If you want to give a column the highest priority with no styling, you should create and
349
     * apply
350
     * a custom no-style implementation rather than using the default NoCellStyle.(refer to
351
     * {@link io.github.hee9841.excel.style.NoCellStyle})
352
     *
353
     * @param style        The {@link ExcelColumnStyle} defined in the {@link ExcelColumn}
354
     *                     annotation
355
     * @param defaultStyle The default {@link CellStyle} to use if no style is specified
356
     * @return The updated {@link CellStyle}
357
     */
358
    private CellStyle updateCellStyle(ExcelColumnStyle style, CellStyle defaultStyle) {
359

360
        CellStyle cellStyle = wb.createCellStyle();
1✔
361

362
        // When cell style is default value of @ExcelColumn,
363
        // return the default cell style specified by @Excel.
364
        if (style.cellStyleClass().getName().equals(STANDARD_STYLE)) {
1✔
365
            cellStyle.cloneStyleFrom(defaultStyle);
1✔
366
            return cellStyle;
1✔
367
        }
368

369
        // When cell style is not default value of @ExcelColumn,
370
        // apply and return the cell style specified by @ExcelColumn.
371
        getExcelCellStyle(style).apply(cellStyle);
1✔
372
        return cellStyle;
1✔
373
    }
374

375
    /**
376
     * Gets an {@link ExcelCellStyle} object from an {@link ExcelColumnStyle} annotation.
377
     * Handles both enum and class-based styles.
378
     *
379
     * @param excelColumnStyle The {@link ExcelColumnStyle} annotation
380
     * @return An {@link ExcelCellStyle} object
381
     * @throws ExcelStyleException If the style cannot be instantiated or the enum value is not
382
     *                             found
383
     */
384
    @SuppressWarnings("unchecked")
385
    private <E> ExcelCellStyle getExcelCellStyle(ExcelColumnStyle excelColumnStyle) {
386
        Class<? extends ExcelCellStyle> cellStyleClass = excelColumnStyle.cellStyleClass();
1✔
387
        //1. case of enum
388
        if (cellStyleClass.isEnum()) {
1✔
389
            try {
390
                Class<? extends Enum> enumClass = cellStyleClass.asSubclass(Enum.class);
1✔
391
                return (ExcelCellStyle) Enum.valueOf(enumClass, excelColumnStyle.enumName());
1✔
392
            } catch (IllegalArgumentException e) {
1✔
393
                throw new ExcelStyleException(
1✔
394
                    String.format(
1✔
395
                        "Failed to convert Enum constant to ExcelCellStyle instance : "
396
                            + "Enum value '%s' not found in style class '%s'.",
397
                        excelColumnStyle.enumName(), cellStyleClass.getName()), type.getName(), e);
1✔
398
            }
399
        }
400

401
        //2. case of class
402
        try {
403
            return cellStyleClass.getDeclaredConstructor().newInstance();
1✔
404
        } catch (NoSuchMethodException | IllegalAccessException |
1✔
405
                 InstantiationException | InvocationTargetException e
406
        ) {
407
            throw new ExcelStyleException(
1✔
408
                String.format("Failed to instantiate cellStyle class of '%s'.",
1✔
409
                    cellStyleClass.getName()), type.getName(), e);
1✔
410
        }
411
    }
412
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc