Przeglądaj źródła

add excel import api

zhouhao 7 lat temu
rodzic
commit
01a99064d0

+ 27 - 0
hsweb-boost/hsweb-boost-excel/pom.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>hsweb-boost</artifactId>
+        <groupId>org.hswebframework.web</groupId>
+        <version>3.0.0-RC-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>hsweb-boost-excel</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.hswebframework</groupId>
+            <artifactId>hsweb-expands-office</artifactId>
+            <version>${hsweb.expands.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-commons-entity</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+
+</project>

+ 264 - 0
hsweb-boost/hsweb-boost-excel/src/main/java/org/hswebframework/web/excel/DefaultExcelImporter.java

@@ -0,0 +1,264 @@
+package org.hswebframework.web.excel;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.SneakyThrows;
+import org.hswebframework.expands.office.excel.ExcelIO;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ReflectionUtils;
+
+import java.io.InputStream;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * @author zhouhao
+ * @since 3.0.0-RC
+ */
+public class DefaultExcelImporter implements ExcelImporter {
+
+    protected static Map<Class, Map<Class, HeaderMapper>> headerMappings = new ConcurrentHashMap<>();
+
+    @SuppressWarnings("all")
+    protected Map<Class, HeaderMapper> createHeaderMapping(Class type) {
+        //一些基本类型不做处理
+        if (type == String.class
+                || Number.class.isAssignableFrom(type)
+                || ClassUtils.isPrimitiveWrapper(type)
+                || type.isPrimitive()
+                || type.isEnum()
+                || type.isArray()
+                || Date.class.isAssignableFrom(type)) {
+            return Collections.emptyMap();
+        }
+        AtomicInteger index = new AtomicInteger(0);
+        Map<Class, DefaultHeaderMapper> headerMapperMap = new HashMap<>();
+        ReflectionUtils.doWithFields(type, field -> {
+            Excel excel = field.getAnnotation(Excel.class);
+            ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class);
+            if ((excel == null && apiModelProperty == null) || (excel != null && excel.ignore())) {
+                return;
+            }
+            String header = excel == null ? apiModelProperty.value() : excel.value();
+            HeaderMapping mapping = new HeaderMapping();
+            mapping.header = header;
+            mapping.field = field.getName();
+            mapping.index = excel == null || excel.sheetIndex() == -1 ? index.getAndAdd(1) : excel.sheetIndex();
+            mapping.converter = createConvert(field.getType());
+            if (null != excel) {
+                mapping.enableImport = excel.enableImport();
+                mapping.enableExport = excel.enableExport();
+                mapping.children = () -> getHeaderMapper(field.getType(), excel.group());
+                for (Class group : excel.group()) {
+                    headerMapperMap.computeIfAbsent(group, DefaultHeaderMapper::new)
+                            .mappings
+                            .add(mapping);
+                }
+            } else {
+                mapping.children = () -> getHeaderMapper(field.getType());
+                headerMapperMap.computeIfAbsent(Void.class, DefaultHeaderMapper::new)
+                        .mappings
+                        .add(mapping);
+
+            }
+        });
+
+        return (Map) headerMapperMap;
+    }
+
+    protected <T> ExcelCellConverter<T> createConvert(Class<T> type) {
+        // TODO: 18-6-12
+        return new ExcelCellConverter<T>() {
+            @Override
+            public T convertFromCell(Object from) {
+                return type.cast(from);
+            }
+
+            @Override
+            public Object convertToCell(T from) {
+                return from;
+            }
+        };
+    }
+
+    @Getter
+    class HeaderMapping implements Comparable<HeaderMapping> {
+        private String field;
+
+        private String header;
+
+        private int index;
+
+        private boolean enableExport = true;
+
+        private boolean enableImport = true;
+
+        private Supplier<HeaderMapper> children;
+
+        private ExcelCellConverter converter;
+
+        public HeaderMapping copy() {
+            HeaderMapping mapping = new HeaderMapping();
+            mapping.children = children;
+            mapping.field = field;
+            mapping.header = header;
+            mapping.index = index;
+            mapping.enableImport = enableImport;
+            mapping.enableExport = enableExport;
+            mapping.children = children;
+            mapping.converter = converter;
+            return mapping;
+        }
+
+        @Override
+        public int compareTo(HeaderMapping o) {
+            return Integer.compare(index, o.index);
+        }
+    }
+
+
+    class DefaultHeaderMapper implements HeaderMapper {
+
+        @Getter
+        private Class group;
+
+        public DefaultHeaderMapper(Class group) {
+            this.group = group;
+        }
+
+        private Map<String, HeaderMapping> fastMapping = new HashMap<>();
+
+        private final List<HeaderMapping> mappings = new ArrayList<HeaderMapping>() {
+            private static final long serialVersionUID = 5995980497414973311L;
+
+            @Override
+            public boolean add(HeaderMapping o) {
+                fastMapping.put(o.header, o);
+                fastMapping.put(o.field, o);
+                return super.add(o);
+            }
+        };
+
+
+        @Override
+        public Optional<HeaderMapping> getMapping(String key) {
+            return Optional.ofNullable(fastMapping.computeIfAbsent(key, k -> {
+                for (HeaderMapping mapping : mappings) {
+                    String newKey = key;
+                    if (newKey.startsWith(mapping.field)) {
+                        newKey = newKey.substring(mapping.field.length());
+                    } else if (newKey.startsWith(mapping.header)) {
+                        newKey = newKey.substring(mapping.header.length());
+                    } else {
+                        continue;
+                    }
+                    HeaderMapper mapper = mapping.children.get();
+                    if (null != mapper) {
+                        HeaderMapping map = mapper.getMapping(newKey).orElse(null);
+                        if (map != null) {
+                            map = map.copy();
+                            map.field = mapping.field.concat(".").concat(map.field);
+                            return map;
+                        }
+                    }
+                }
+                return null;
+            }));
+        }
+    }
+
+    interface HeaderMapper {
+        Optional<HeaderMapping> getMapping(String key);
+    }
+
+    protected HeaderMapper getHeaderMapper(Class type, Class... group) {
+        Map<Class, HeaderMapper> mapperMap = headerMappings.computeIfAbsent(type, this::createHeaderMapping);
+
+        if (group != null && group.length > 0) {
+            return Arrays.stream(group)
+                    .map(mapperMap::get)
+                    .filter(Objects::nonNull)
+                    .findFirst()
+                    .orElse(null);
+        } else {
+            return mapperMap.get(Void.class);
+        }
+    }
+
+    @Override
+    @SneakyThrows
+    public <T> Result<T> doImport(InputStream inputStream, Class<T> type, Function<T, Error> validator, Class... group) {
+        AtomicInteger counter = new AtomicInteger(0);
+        AtomicInteger errorCounter = new AtomicInteger(0);
+        List<T> data = new ArrayList<>();
+        List<Error> errors = new ArrayList<>();
+        HeaderMapper headerMapper = getHeaderMapper(type, group);
+        if (headerMapper == null) {
+            throw new UnsupportedOperationException("不支持导入此类型");
+        }
+        ExcelIO.read(inputStream, row -> {
+            counter.getAndAdd(1);
+            Map<String, Object> mapValue = row.getResult();
+
+            Map<String, Object> newValue = new HashMap<>();
+
+            for (Map.Entry<String, Object> entry : mapValue.entrySet()) {
+                String key = entry.getKey();
+                Object value = entry.getValue();
+
+                HeaderMapping mapping = headerMapper.getMapping(key).orElse(null);
+
+                if (mapping == null) {
+                    continue;
+                }
+
+                String field = mapping.field;
+                //嵌套的字段
+                if (field.contains(".")) {
+                    String tmpField = field;
+                    Map<String, Object> nestMapValue = newValue;
+                    while (tmpField.contains(".")) {
+                        String[] nestFields = tmpField.split("[.]", 2);
+                        tmpField = nestFields[1];
+                        Object nestValue = nestMapValue.get(nestFields[0]);
+                        if (nestValue == null) {
+                            nestMapValue.put(nestFields[0], nestMapValue = new HashMap<>());
+                        } else {
+                            if (nestValue instanceof Map) {
+                                nestMapValue = ((Map) nestValue);
+                            }
+                        }
+                    }
+                    nestMapValue.put(tmpField, value);
+                } else {
+                    newValue.put(field, value);
+                }
+            }
+            //创建实例并将map复制到实例中
+            T instance = FastBeanCopier.getBeanFactory().newInstance(type);
+
+            FastBeanCopier.copy(newValue, instance);
+
+            data.add(instance);
+
+            Error error = validator.apply(instance);
+            if (null != error) {
+                errorCounter.getAndAdd(1);
+                error.setRowIndex(counter.get());
+                error.setSheetIndex(row.getSheet());
+                errors.add(error);
+            }
+        });
+        return Result.<T>builder()
+                .data(data)
+                .errors(errors)
+                .success(counter.get() - errorCounter.get())
+                .total(counter.get())
+                .error(errorCounter.get())
+                .build();
+    }
+}

+ 32 - 0
hsweb-boost/hsweb-boost-excel/src/main/java/org/hswebframework/web/excel/Excel.java

@@ -0,0 +1,32 @@
+package org.hswebframework.web.excel;
+
+import java.lang.annotation.*;
+
+/**
+ * @author zhouhao
+ * @since 3.0.0-RC
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Excel {
+
+    String value() default "";
+
+    int sheetIndex() default -1;
+
+    boolean ignore() default false;
+
+    boolean enableImport() default true;
+
+    boolean enableExport() default true;
+
+    int exportOrder() default -1;
+
+    Class[] group() default Void.class;
+
+    Class<ExcelCellConverter> converter() default ExcelCellConverter.class;
+
+    Class<ExcelImporter> importer() default ExcelImporter.class;
+
+}

+ 11 - 0
hsweb-boost/hsweb-boost-excel/src/main/java/org/hswebframework/web/excel/ExcelCellConverter.java

@@ -0,0 +1,11 @@
+package org.hswebframework.web.excel;
+
+/**
+ * @author zhouhao
+ * @since 3.0.0-RC
+ */
+public interface ExcelCellConverter<T> {
+    T convertFromCell(Object from);
+
+    Object convertToCell(T from);
+}

+ 52 - 0
hsweb-boost/hsweb-boost-excel/src/main/java/org/hswebframework/web/excel/ExcelImporter.java

@@ -0,0 +1,52 @@
+package org.hswebframework.web.excel;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * @author zhouhao
+ * @since 3.0.0-RC
+ */
+public interface ExcelImporter {
+
+    ExcelImporter instance = new DefaultExcelImporter();
+
+    <T> Result<T> doImport(InputStream inputStream, Class<T> type, Function<T, Error> validator, Class... group);
+
+    @Builder
+    @Getter
+    @Setter
+    class Result<T> {
+        int          total;
+        int          success;
+        int          error;
+        List<Header> headers;
+        List<T>      data;
+        List<Error>  errors;
+    }
+
+    @Builder
+    @Getter
+    @Setter
+    class Error {
+        int    sheetIndex;
+        int    rowIndex;
+        int    errorType;
+        Object reason;
+    }
+
+    @Builder
+    @Getter
+    @Setter
+    class Header {
+        int sheetIndex;
+        @SuppressWarnings("all")
+        String header;
+        String field;
+    }
+}

+ 52 - 0
hsweb-boost/hsweb-boost-excel/src/test/java/org/hswebframework/web/excel/DefaultExcelImporterTest.java

@@ -0,0 +1,52 @@
+package org.hswebframework.web.excel;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import org.hswebframework.web.commons.bean.Bean;
+import org.junit.Assert;
+import org.junit.Test;
+
+
+/**
+ * @author zhouhao
+ * @since 3.0.0-RC
+ */
+public class DefaultExcelImporterTest {
+
+
+    @Test
+    public void test() {
+        ExcelImporter.Result<TestBean> result = ExcelImporter
+                .instance
+                .doImport(this.getClass().getResourceAsStream("/test.xls"), TestBean.class, bean -> null);
+
+        Assert.assertEquals(result.success, 1);
+        System.out.println(JSON.toJSONString(result.getData(), SerializerFeature.PrettyFormat));
+        TestBean bean = result.getData().get(0);
+        Assert.assertNotNull(bean.nest);
+        Assert.assertNotNull(bean.nest.nest);
+
+    }
+
+    @Getter
+    @Setter
+    @ToString
+    public static class TestBean implements Bean {
+
+        private static final long serialVersionUID = -5394537136669692305L;
+
+        @Excel("姓名")
+        private String name;
+
+        @Excel("年龄")
+        private int age;
+
+        @Excel("嵌套-")
+        private TestBean nest;
+    }
+
+
+}

BIN
hsweb-boost/hsweb-boost-excel/src/test/resources/test.xls


+ 1 - 0
hsweb-boost/pom.xml

@@ -32,6 +32,7 @@
     <modules>
         <module>hsweb-boost-aop</module>
         <module>hsweb-boost-ftp</module>
+        <module>hsweb-boost-excel</module>
     </modules>