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

hee9841 / excel-module / #14

27 Mar 2025 12:44PM UTC coverage: 83.096% (-0.3%) from 83.415%
#14

Pull #35

github

web-flow
Merge c460834c6 into 00304f2fa
Pull Request #35: Feat: Excel, ExcelColumn 어노테이션 정합성 예외 처리

22 of 34 new or added lines in 3 files covered. (64.71%)

526 of 633 relevant lines covered (83.1%)

0.83 hits per line

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

89.42
/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/import operations. It handles column indexing, cell type determination,
33
 * and style application based on the annotation configurations.
34
 *
35
 * @see ColumnInfo
36
 * @see CellType
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 field '%s' 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 CellType} is not compatible with the field type
259
     */
260
    private ColumnInfo getColumnInfo(ExcelColumn excelColumn, Class<?> fieldType,
261
        String fieldName) {
262
        //Get cell type
263
        CellType cellType = getCellType(excelColumn.columnCellType(), fieldType, fieldName);
1✔
264

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

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

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

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

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

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

324
        // When the specified column cell type is not compatible with the field type, throw an exception.
325
        if (!columnCellType.equals(CellType.findMatchingCellType(fieldType, columnCellType))) {
1✔
326
            throw new ExcelException(String.format(
1✔
327
                "Invalid cell type : The cell type of field '%s' is not compatible with the specified cell type(%s).",
328
                fieldName,
329
                columnCellType.name()
1✔
330
            ), type.getName());
1✔
331
        }
332

333
        return columnCellType;
1✔
334
    }
335

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

355
        CellStyle cellStyle = wb.createCellStyle();
1✔
356

357
        // When cell style is default value of @ExcelColumn,
358
        // return the default cell style specified by @Excel.
359
        if (style.cellStyleClass().getName().equals(STANDARD_STYLE)) {
1✔
360
            cellStyle.cloneStyleFrom(defaultStyle);
1✔
361
            return cellStyle;
1✔
362
        }
363

364
        // When cell style is not default value of @ExcelColumn,
365
        // apply and return the cell style specified by @ExcelColumn.
366
        getExcelCellStyle(style).apply(cellStyle);
1✔
367
        return cellStyle;
1✔
368
    }
369

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

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

© 2026 Coveralls, Inc