DomainUserRepositoryMock.java

/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.bremersee.dccon.repository;

import static org.bremersee.dccon.config.DomainControllerProperties.getComplexPasswordRegex;
import static org.bremersee.dccon.repository.DomainUserRepositoryImpl.isQueryResult;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.bremersee.dccon.config.DomainControllerProperties;
import org.bremersee.dccon.model.AvatarDefault;
import org.bremersee.dccon.model.DomainGroup;
import org.bremersee.dccon.model.DomainUser;
import org.bremersee.dccon.model.PasswordInformation;
import org.bremersee.exception.ServiceException;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Profile;
import org.springframework.context.event.EventListener;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * The domain user repository mock.
 *
 * @author Christian Bremer
 */
@Profile("!ldap")
@Component
@Slf4j
public class DomainUserRepositoryMock implements DomainUserRepository, MockRepository {

  private static final String USERS_LOCATION = "classpath:demo/users.json";

  private static final String GROUPS_LOCATION = "classpath:demo/groups.json";

  private final ResourceLoader resourceLoader = new DefaultResourceLoader();

  private final Map<String, DomainUser> repo = new ConcurrentHashMap<>();

  private Pattern passwordPattern = Pattern.compile(getComplexPasswordRegex(7));

  private ObjectMapper objectMapper;

  private DomainRepository domainRepository;

  private DomainGroupRepository groupRepository;

  /**
   * Instantiates a new Domain user repository mock.
   *
   * @param objectMapperBuilder the object mapper builder
   * @param domainRepository the domain repository
   * @param groupRepository the group repository
   */
  public DomainUserRepositoryMock(
      Jackson2ObjectMapperBuilder objectMapperBuilder,
      DomainRepository domainRepository,
      DomainGroupRepository groupRepository) {
    this.objectMapper = objectMapperBuilder.build();
    this.domainRepository = domainRepository;
    this.groupRepository = groupRepository;
  }

  /**
   * Init.
   */
  @EventListener(ApplicationReadyEvent.class)
  public void init() {
    log.warn("\n"
        + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
        + "!! MOCK is running:  DomainUserRepository                                           !!\n"
        + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
    );
    final PasswordInformation info = domainRepository.getPasswordInformation();
    final int minLength = info.getMinimumPasswordLength() != null
        ? info.getMinimumPasswordLength()
        : 7;
    passwordPattern = Pattern.compile(getComplexPasswordRegex(minLength));
    resetData();
  }

  @Override
  public void resetData() {
    DomainUser[] users;
    DomainGroup[] groups;
    try {
      users = objectMapper.readValue(
          resourceLoader.getResource(USERS_LOCATION).getInputStream(), DomainUser[].class);
      groups = objectMapper.readValue(
          resourceLoader.getResource(GROUPS_LOCATION).getInputStream(), DomainGroup[].class);
    } catch (IOException e) {
      throw ServiceException.internalServerError("Loading demo data failed.", e);
    }
    groupRepository.findAll(null).forEach(group -> groupRepository.delete(group.getName()));
    repo.clear();
    for (DomainUser user : users) {
      if (!StringUtils.hasText(user.getPassword())) {
        user.setPassword(domainRepository.createRandomPassword());
      }
      repo.put(user.getUserName().toLowerCase(), user);
    }
    for (DomainGroup group : groups) {
      groupRepository.save(group);
    }
  }

  private List<String> findDomainGroups(final String userName) {
    return groupRepository.findAll(null)
        .filter(domainGroup -> domainGroup.getMembers().contains(userName))
        .map(DomainGroup::getName)
        .collect(Collectors.toList());
  }

  @Override
  public Stream<DomainUser> findAll(final String query) {
    final boolean all = query == null || query.trim().length() == 0;
    return repo.values().stream()
        .filter(domainUser -> all || isQueryResult(domainUser, query.trim().toLowerCase()))
        .map(domainUser -> domainUser.toBuilder()
            .groups(findDomainGroups(domainUser.getUserName()))
            .build());
  }

  @Override
  public Optional<DomainUser> findOne(final String userName) {
    return Optional.ofNullable(repo.get(userName.toLowerCase()))
        .flatMap(domainUser -> Optional.of(domainUser
            .toBuilder()
            .groups(findDomainGroups(domainUser.getUserName()))
            .password(null)
            .build()));
  }

  @Override
  public Optional<byte[]> findAvatar(
      final String userName,
      final AvatarDefault avatarDefault,
      final Integer size) {

    final int avatarSize = size == null || size < 1 || size > 2048 ? 80 : size;
    return findOne(userName)
        .flatMap(domainUser -> Optional.ofNullable(
            DomainUserRepositoryImpl.findAvatar(
                domainUser.getEmail(),
                new DomainControllerProperties().getGravatarUrl(),
                avatarDefault,
                avatarSize)));
  }

  @Override
  public boolean exists(final String userName) {
    return repo.get(userName.toLowerCase()) != null;
  }

  @Override
  public DomainUser save(final DomainUser domainUser, final Boolean updateGroups) {

    if (repo.size() > 100 && repo.get(domainUser.getUserName().toLowerCase()) == null) {
      throw ServiceException.internalServerError(
          "Maximum size of users is exceeded.",
          "org.bremersee:dc-con-app:9272263e-2074-46f7-ba64-23978981a1d3");
    }
    if (!StringUtils.hasText(domainUser.getPassword())) {
      domainUser.setPassword(domainRepository.createRandomPassword());
    }
    if (!passwordPattern.matcher(domainUser.getPassword()).matches()) {
      throw ServiceException.badRequest(
          "msg=[The password does not meet the complexity criteria!] userName=["
              + domainUser.getUserName() + "]",
          "check_password_restrictions");
    }
    final boolean doGroupUpdate = Boolean.TRUE.equals(updateGroups);
    final Set<String> groups = doGroupUpdate
        ? new HashSet<>(domainUser.getGroups())
        : Collections.emptySet();
    domainUser.getGroups().clear();
    repo.put(domainUser.getUserName().toLowerCase(), domainUser);

    if (doGroupUpdate) {
      groups.forEach(groupName -> groupRepository.findOne(groupName).ifPresent(domainGroup -> {
        if (!domainGroup.getMembers().contains(domainUser.getUserName())) {
          domainGroup.getMembers().add(domainUser.getUserName());
          groupRepository.save(domainGroup);
        }
      }));
    }
    return domainUser.toBuilder().groups(findDomainGroups(domainUser.getUserName())).build();
  }

  @Override
  public void savePassword(final String userName, final String newPassword) {
    final DomainUser domainUser = repo.get(userName.toLowerCase());
    if (domainUser == null) {
      throw ServiceException.notFound(DomainUser.class.getSimpleName(), userName);
    }
    if (!passwordPattern.matcher(newPassword).matches()) {
      throw ServiceException.badRequest(
          "msg=[The password does not meet the complexity criteria!] userName=["
              + domainUser.getUserName() + "]",
          "check_password_restrictions");
    }
    domainUser.setPassword(newPassword);
  }

  @Override
  public boolean delete(final String userName) {
    return Optional.ofNullable(repo.remove(userName.toLowerCase()))
        .map(DomainUser::getUserName)
        .map(name -> {
          groupRepository.findAll(null).forEach(domainGroup -> {
            if (domainGroup.getMembers().remove(name)) {
              groupRepository.save(domainGroup);
            }
          });
          return true;
        })
        .orElse(false);
  }
}