«1. Обзор

В этой статье мы вынесем установочные данные приложения с помощью CSV-файлов вместо их жесткого кодирования.

Этот процесс установки в основном связан с установкой новых данных в новой системе.

2. Библиотека CSV

Давайте начнем с простой библиотеки для работы с CSV — расширения Jackson CSV:

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-csv</artifactId>       
    <version>2.5.3</version>
</dependency>

Конечно, существует множество доступных библиотек для работы с CSV в экосистема Java.

Причина, по которой мы используем здесь Джексона, заключается в следующем: вполне вероятно, что Джексон уже используется в приложении, и обработка, необходимая для чтения данных, довольно проста.

3. Данные настройки

В разных проектах необходимо установить разные данные.

В этом уроке мы собираемся настроить данные пользователя — в основном подготовив систему с несколькими пользователями по умолчанию.

Вот простой CSV-файл, содержащий пользователей:

id,username,password,accessToken
1,john,123,token
2,tom,456,test

Обратите внимание, что первая строка файла является строкой заголовка — в ней перечислены имена полей в каждой строке данных.

3. Загрузчик данных CSV

Давайте начнем с создания простого загрузчика данных для чтения данных из файлов CSV в рабочую память.

3.1. Загрузка списка объектов

Мы реализуем функциональность loadObjectList() для загрузки полностью параметризованного списка определенных объектов из файла:

public <T> List<T> loadObjectList(Class<T> type, String fileName) {
    try {
        CsvSchema bootstrapSchema = CsvSchema.emptySchema().withHeader();
        CsvMapper mapper = new CsvMapper();
        File file = new ClassPathResource(fileName).getFile();
        MappingIterator<T> readValues = 
          mapper.reader(type).with(bootstrapSchema).readValues(file);
        return readValues.readAll();
    } catch (Exception e) {
        logger.error("Error occurred while loading object list from file " + fileName, e);
        return Collections.emptyList();
    }
}

Примечания:

    Мы создали схему CSVS на основе первого — «заголовок» строки. Реализация достаточно универсальна, чтобы обрабатывать объекты любого типа. Если произойдет какая-либо ошибка, будет возвращен пустой список.

3.2. Обработка отношения «многие ко многим»

Вложенные объекты плохо поддерживаются в файле Jackson CSV — нам придется использовать непрямой способ загрузки отношений «многие ко многим».

Мы представим их аналогично простым таблицам соединений, поэтому, естественно, мы будем загружать с диска в виде списка массивов:

public List<String[]> loadManyToManyRelationship(String fileName) {
    try {
        CsvMapper mapper = new CsvMapper();
        CsvSchema bootstrapSchema = CsvSchema.emptySchema().withSkipFirstDataRow(true);
        mapper.enable(CsvParser.Feature.WRAP_AS_ARRAY);
        File file = new ClassPathResource(fileName).getFile();
        MappingIterator<String[]> readValues = 
          mapper.reader(String[].class).with(bootstrapSchema).readValues(file);
        return readValues.readAll();
    } catch (Exception e) {
        logger.error(
          "Error occurred while loading many to many relationship from file = " + fileName, e);
        return Collections.emptyList();
    }
}

Вот как одно из этих отношений – Роли \u003c-\u003e Привилегии – представлен в простом CSV-файле:

role,privilege
ROLE_ADMIN,ADMIN_READ_PRIVILEGE
ROLE_ADMIN,ADMIN_WRITE_PRIVILEGE
ROLE_SUPER_USER,POST_UNLIMITED_PRIVILEGE
ROLE_USER,POST_LIMITED_PRIVILEGE

Обратите внимание, что в этой реализации мы игнорируем заголовок, так как нам на самом деле не нужна эта информация.

4. Данные установки

Теперь мы будем использовать простой компонент установки, чтобы выполнить всю работу по настройке привилегий, ролей и пользователей из CSV-файлов:

@Component
public class Setup {
    ...
    
    @PostConstruct
    private void setupData() {
        setupRolesAndPrivileges();
        setupUsers();
    }
    
    ...
}

4.1. Настройка ролей и привилегий

Во-первых, давайте загрузим роли и привилегии с диска в рабочую память, а затем сохраним их как часть процесса установки:

public List<Privilege> getPrivileges() {
    return csvDataLoader.loadObjectList(Privilege.class, PRIVILEGES_FILE);
}

public List<Role> getRoles() {
    List<Privilege> allPrivileges = getPrivileges();
    List<Role> roles = csvDataLoader.loadObjectList(Role.class, ROLES_FILE);
    List<String[]> rolesPrivileges = csvDataLoader.
      loadManyToManyRelationship(SetupData.ROLES_PRIVILEGES_FILE);

    for (String[] rolePrivilege : rolesPrivileges) {
        Role role = findRoleByName(roles, rolePrivilege[0]);
        Set<Privilege> privileges = role.getPrivileges();
        if (privileges == null) {
            privileges = new HashSet<Privilege>();
        }
        privileges.add(findPrivilegeByName(allPrivileges, rolePrivilege[1]));
        role.setPrivileges(privileges);
    }
    return roles;
}

private Role findRoleByName(List<Role> roles, String roleName) {
    return roles.stream().
      filter(item -> item.getName().equals(roleName)).findFirst().get();
}

private Privilege findPrivilegeByName(List<Privilege> allPrivileges, String privilegeName) {
    return allPrivileges.stream().
      filter(item -> item.getName().equals(privilegeName)).findFirst().get();
}

Затем мы выполним сохранение здесь:

private void setupRolesAndPrivileges() {
    List<Privilege> privileges = setupData.getPrivileges();
    for (Privilege privilege : privileges) {
        setupService.setupPrivilege(privilege);
    }

    List<Role> roles = setupData.getRoles();
    for (Role role : roles) {
        setupService.setupRole(role);
    }
}

А вот и наш SetupService:

public void setupPrivilege(Privilege privilege) {
    if (privilegeRepository.findByName(privilege.getName()) == null) {
        privilegeRepository.save(privilege);
    }
}

public void setupRole(Role role) {
    if (roleRepository.findByName(role.getName()) == null) { 
        Set<Privilege> privileges = role.getPrivileges(); 
        Set<Privilege> persistedPrivileges = new HashSet<Privilege>();
        for (Privilege privilege : privileges) { 
            persistedPrivileges.add(privilegeRepository.findByName(privilege.getName())); 
        } 
        role.setPrivileges(persistedPrivileges); 
        roleRepository.save(role); }
}

Обратите внимание, как после загрузки Ролей и Привилегий в рабочую память мы загружаем их отношения одну за другой.

4.2. Настройка начальных пользователей

Далее — давайте загрузим пользователей в память и сохраним их:

public List<User> getUsers() {
    List<Role> allRoles = getRoles();
    List<User> users = csvDataLoader.loadObjectList(User.class, SetupData.USERS_FILE);
    List<String[]> usersRoles = csvDataLoader.
      loadManyToManyRelationship(SetupData.USERS_ROLES_FILE);

    for (String[] userRole : usersRoles) {
        User user = findByUserByUsername(users, userRole[0]);
        Set<Role> roles = user.getRoles();
        if (roles == null) {
            roles = new HashSet<Role>();
        }
        roles.add(findRoleByName(allRoles, userRole[1]));
        user.setRoles(roles);
    }
    return users;
}

private User findByUserByUsername(List<User> users, String username) {
    return users.stream().
      filter(item -> item.getUsername().equals(username)).findFirst().get();
}

Далее сосредоточимся на сохранении пользователей:

private void setupUsers() {
    List<User> users = setupData.getUsers();
    for (User user : users) {
        setupService.setupUser(user);
    }
}

А вот наш SetupService:

@Transactional
public void setupUser(User user) {
    try {
        setupUserInternal(user);
    } catch (Exception e) {
        logger.error("Error occurred while saving user " + user.toString(), e);
    }
}

private void setupUserInternal(User user) {
    if (userRepository.findByUsername(user.getUsername()) == null) {
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        user.setPreference(createSimplePreference(user));
        Set<Role> roles = user.getRoles(); 
        Set<Role> persistedRoles = new HashSet<Role>(); 
        for (Role role : roles) { 
            persistedRoles.add(roleRepository.findByName(role.getName())); 
        } 
        user.setRoles(persistedRoles);
        userRepository.save(user);
    }
}

А вот метод createSimplePreference():

private Preference createSimplePreference(User user) {
    Preference pref = new Preference();
    pref.setId(user.getId());
    pref.setTimezone(TimeZone.getDefault().getID());
    pref.setEmail(user.getUsername() + "@test.com");
    return preferenceRepository.save(pref);
}

Обратите внимание, что перед сохранением пользователя мы создаем для него простую сущность Preference и сначала сохраняем ее.

5. Протестируйте загрузчик данных CSV

Далее, давайте проведем простой модульный тест на нашем CsvDataLoader:

Мы проверим загрузку списка пользователей, ролей и привилегий:

@Test
public void whenLoadingUsersFromCsvFile_thenLoaded() {
    List<User> users = csvDataLoader.
      loadObjectList(User.class, CsvDataLoader.USERS_FILE);
    assertFalse(users.isEmpty());
}

@Test
public void whenLoadingRolesFromCsvFile_thenLoaded() {
    List<Role> roles = csvDataLoader.
      loadObjectList(Role.class, CsvDataLoader.ROLES_FILE);
    assertFalse(roles.isEmpty());
}

@Test
public void whenLoadingPrivilegesFromCsvFile_thenLoaded() {
    List<Privilege> privileges = csvDataLoader.
      loadObjectList(Privilege.class, CsvDataLoader.PRIVILEGES_FILE);
    assertFalse(privileges.isEmpty());
}

Далее, давайте проверим загрузку некоторые отношения «многие ко многим» через загрузчик данных:

@Test
public void whenLoadingUsersRolesRelationFromCsvFile_thenLoaded() {
    List<String[]> usersRoles = csvDataLoader.
      loadManyToManyRelationship(CsvDataLoader.USERS_ROLES_FILE);
    assertFalse(usersRoles.isEmpty());
}

@Test
public void whenLoadingRolesPrivilegesRelationFromCsvFile_thenLoaded() {
    List<String[]> rolesPrivileges = csvDataLoader.
      loadManyToManyRelationship(CsvDataLoader.ROLES_PRIVILEGES_FILE);
    assertFalse(rolesPrivileges.isEmpty());
}

6. Тестирование данных настройки

Наконец, давайте выполним простой модульный тест нашего bean-компонента SetupData:

@Test
public void whenGettingUsersFromCsvFile_thenCorrect() {
    List<User> users = setupData.getUsers();

    assertFalse(users.isEmpty());
    for (User user : users) {
        assertFalse(user.getRoles().isEmpty());
    }
}

@Test
public void whenGettingRolesFromCsvFile_thenCorrect() {
    List<Role> roles = setupData.getRoles();

    assertFalse(roles.isEmpty());
    for (Role role : roles) {
        assertFalse(role.getPrivileges().isEmpty());
    }
}

@Test
public void whenGettingPrivilegesFromCsvFile_thenCorrect() {
    List<Privilege> privileges = setupData.getPrivileges();
    assertFalse(privileges.isEmpty());
}

7. Заключение

В В этой быстрой статье мы рассмотрели альтернативный метод настройки исходных данных, которые обычно необходимо загружать в систему при запуске. Это, конечно, простое доказательство концепции и хорошая база для дальнейшего развития, а не готовое решение.

Мы также собираемся использовать это решение в веб-приложении Reddit, отслеживаемом в этом продолжающемся тематическом исследовании.