пятница, 11 ноября 2011 г.

Чтение и запись Tab Separated Values файлов

Что же представляют из себя файлы в формате TSV? Это текстовые файлы, представляющие собой набор строк. В каждой строке одинаковое количество "полей" определенного формата, разделенные символом табуляции ("\t"). Похоже на некое подобие таблицы.

Итак, приступим к написанию reader-а:
/**
 * @author vie
 *
 * TSVReader.java
 * Class for read tab separated values files
 */
public class TSVReader<T> {

    private final String filePath;

    public TSVReader(String filePath) {
        this.filePath = filePath;
    }

    /**
     * This method reads tab separated values file
     * into object of the arbitrary class
     *
     * @param clazz type of transmitted class
     * @return List
     * @throws com.site.tsv.exceptions.TSVException
     */
    public List<T> read(Class<T> clazz) throws TSVException {
        List<T> result = new LinkedList<T>();

        try {
            BufferedReader bReader = new BufferedReader(new FileReader(filePath));
            String line;

            //recieving all lines
            int i = 1;
            while ((line = bReader.readLine()) != null) {
                //recieving all line members
                String datavalue[] = line.split("\t");

                //create instance of line entity
                Constructor constructor = clazz.getConstructor(null);
                Object somethingInstance = constructor.newInstance(null);

                //parsing line entity members
                Field[] fields = clazz.getDeclaredFields();

                //recieve map of line entity member types
                Map<Integer, Class<?>> mapFields = defineFields(clazz);

                //for each line entity field
                if (fields != null && datavalue != null && fields.length == datavalue.length) {
                    for (int j = 0; j < fields.length; j++) {
                        //for each line entity field
                        String s = fields[j].getName();
                        s = s.substring(0, 1).toUpperCase() + s.substring(1, s.length());

                        //exec line entity setter methods
                        Method method = clazz.getMethod("set" + s, mapFields.get(j));
                        method.invoke(somethingInstance, getDeterminatedValue(datavalue[j], fields[j].getType()));
                    }
                } else if (fields != null && datavalue != null && fields.length != datavalue.length) {
                    throw new TSVInvalidFileException("Count of formatted file fields on line " + i + " not equal count of entity fields");
                }

                i++;
                result.add((T) somethingInstance);
            }
            bReader.close();
        } catch (FileNotFoundException e) {
            throw new TSVException(e);
        } catch (IOException e) {
            throw new TSVException(e);
        } catch (ParseException e) {
            throw new TSVInvalidFileException(e);
        } catch (NumberFormatException e) {
            throw new TSVInvalidFileException(e);
        } catch (NoSuchMethodException e) { //reflect
            throw new TSVException(e);
        } catch (InstantiationException e) {
            throw new TSVException(e);
        } catch (IllegalAccessException e) {
            throw new TSVException(e);
        } catch (InvocationTargetException e) {
            throw new TSVException(e);
        }
        return result;
    }

    /**
     * Returns field names and types
     *
     * @param clazz type of transmitted class
     * @return Map<string,integer>.
     * @throws com.site.tsv.exceptions.TSVIncorrectAnnotatedFieldsException
     */
    private Map<Integer, Class<?>> defineFields(Class<?> clazz) throws TSVIncorrectAnnotatedFieldsException {
        Map<Integer, Class<?>> map = new HashMap<Integer, Class<?>>();

        Field[] fields = clazz.getDeclaredFields();
        int i = 0;
        for (Field field : fields) {
            if (field.isAnnotationPresent(TSVInteger.class)) {
                map.put(i++, Integer.class);
            } else if (field.isAnnotationPresent(TSVDouble.class)) {
                map.put(i++, Double.class);
            } else if (field.isAnnotationPresent(TSVString.class)) {
                map.put(i++, String.class);
            } else if (field.isAnnotationPresent(TSVDate.class)) {
                map.put(i++, Date.class);
            }
        }

        if (fields.length == 0 || fields.length != map.size()) {
            throw new TSVIncorrectAnnotatedFieldsException("Some field not annotated or count of fields = 0!");
        }

        return map;
    }

    /**
     * Returns converted to the native type field
     *
     * @param original value of obtained field
     * @param needType type of transmitted class
     * @return Object
     * @throws java.text.ParseException
     */
    private Object getDeterminatedValue(String original, Class<?> needType) throws ParseException {
        if (needType.getName().equals("java.lang.Integer")) {
            return Integer.parseInt(original);
        } else if (needType.getName().equals("java.lang.Double")) {
            return Double.parseDouble(original);
        } else if (needType.getName().equals("java.lang.String")) {
            return original;
        } else if (needType.getName().equals("java.util.Date")) {
            SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.yyyy");
            return formatter.parse(original);
        } else {
            return null;
        }
    }
}
В классе я использовал generics, чтобы проверять заданного типы entity на этапе компиляции. В преобразовании строк с различными типами данных в entity объект мне помог reflection. Для разбора с его помощью entity, написаны приведенные ниже аннотации. Также я создал несколько исключений, чтобы в при выпадении ошибок можно было быстро понять что свалилось, будь то reflection, IO или преобразование чисел и дат.


теперь Writer:
/**
 * @author vie
 *
 * TSVWriter.java
 * Class for write tab separated values files
 */
public class TSVWriter<T> {

    private final static String tab = "\t";

    private final String filePath;

    public TSVWriter(String filePath) {
        this.filePath = filePath;
    }

    /**
     * This method writes tab separated values file
     * from entity object
     *
     * @param list list of line entity objects
     * @param clazz type of transmitted class
     * @throws com.site.tsv.exceptions.TSVException
     */
    public void write(List<T> list, Class<T> clazz) throws TSVException {
        try {
            BufferedWriter bufferWriter = new BufferedWriter(new FileWriter(filePath));
            //getting all line object fields
            Field[] fields = clazz.getDeclaredFields();

            //for each line
            if (fields != null) {
                for (T lineObject: list) {
                    StringBuilder line = new StringBuilder();

                    //for each line entity field
                    for (Field field : fields) {
                        String s = field.getName();
                        s = s.substring(0, 1).toUpperCase() + s.substring(1, s.length());

                        //exec line entity setter methods
                        Method method = clazz.getMethod("get" + s);
                        String val = method.invoke(lineObject) + "";

                        line.append(val).append(tab);
                    }
                    bufferWriter.write(line.substring(0, line.length() - 1));
                    bufferWriter.newLine();
                }
            }
            bufferWriter.close();
        } catch (FileNotFoundException e) {
            throw new TSVException(e);
        } catch (IOException e) {
            throw new TSVException(e);
        } catch (NoSuchMethodException e) {
            throw new TSVException(e);
        } catch (IllegalAccessException e) {
            throw new TSVException(e);
        } catch (InvocationTargetException e) {
            throw new TSVException(e);
        }
    }
}

Аннотации:
/**
 * @author vie
 */
@Target(value = ElementType.FIELD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface TSVInteger {
}

/**
 * @author vie
 */
@Target(value = ElementType.FIELD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface TSVDouble {
}

/**
 * @author vie
 */
@Target(value = ElementType.FIELD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface TSVString {
}

/**
 * @author vie
 */
@Target(value = ElementType.FIELD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface TSVDate {
}

Исключения:
/**
 * @author vie
 */
public class TSVException extends Exception {

    public TSVException() {
       super();
    }

    public TSVException(String message) {
       super(message);
    }

    public TSVException(String message, Throwable cause) {
        super(message, cause);
    }

    public TSVException(Throwable cause) {
        super(cause);
    }
}

/**
 * @author vie
 */
public class TSVInvalidFileException extends TSVException {

    public TSVInvalidFileException() {
       super();
    }

    public TSVInvalidFileException(String message) {
       super(message);
    }

    public TSVInvalidFileException(String message, Throwable cause) {
        super(message, cause);
    }

    public TSVInvalidFileException(Throwable cause) {
        super(cause);
    }
}

/**
 * @author vie
 */
public class TSVIncorrectAnnotatedFieldsException extends TSVException {

    public TSVIncorrectAnnotatedFieldsException() {
       super();
    }

    public TSVIncorrectAnnotatedFieldsException(String message) {
       super(message);
    }

    public TSVIncorrectAnnotatedFieldsException(String message, Throwable cause) {
        super(message, cause);
    }

    public TSVIncorrectAnnotatedFieldsException(Throwable cause) {
        super(cause);
    }
}

Все, теперь можно попробовать загрузить и записать в другой файл. Для этого создадим entity, аналогичный структуре файла:
test1 55 01.01.2010 rw
test2 daswqrg 56 31.01.2011 comment1
test3 dds 57 30.11.2011 comment32

entity:
/**
 * @author vie
 */
public class TestLineEntity {

    @TSVString
    protected String value;

    @TSVInteger
    protected Integer id;

    @TSVDate
    protected Date date;

    @TSVString
    protected String comment;

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
    /*other getters and setters*/
}

сам код считывания и записи данных:
TSVReader<TestLineEntity> tsvReader = new TSVReader<TestLineEntity>("test.tsv");
List<TestLineEntity> test = tsvReader.read(TestLineEntity.class);

TSVWriter<TestLineEntity> tsvWriter = new TSVWriter<TestLineEntity>("new_test.tsv");
tsvWriter.write(forum, TestLineEntity.class);

Комментариев нет:

Отправить комментарий