package io.kaif.service.impl; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.*; import java.time.Duration; import java.time.Instant; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import io.kaif.model.account.Account; import io.kaif.model.account.AccountDao; import io.kaif.model.account.Authority; import io.kaif.model.account.Authorization; import io.kaif.model.article.ArticleDao; import io.kaif.model.exception.CreditNotEnoughException; import io.kaif.model.zone.Zone; import io.kaif.model.zone.ZoneDao; import io.kaif.model.zone.ZoneInfo; import io.kaif.service.ZoneService; import io.kaif.util.Try; import io.kaif.web.support.AccessDeniedException; @Service @Transactional public class ZoneServiceImpl implements ZoneService { private static final int HONOR_SCORE_PER_ZONE = 10; private static final int MAX_AVAILABLE_ZONE = 5; @Autowired private ZoneDao zoneDao; @Autowired private ArticleDao articleDao; @Autowired private AccountDao accountDao; @Override public ZoneInfo loadZone(Zone zone) { return zoneDao.loadZoneWithCache(zone); } @Override public ZoneInfo createDefault(String zone, String aliasName) { return zoneDao.create(ZoneInfo.createDefault(zone, aliasName, Instant.now())); } @Override public ZoneInfo createKaif(String zone, String aliasName) { return zoneDao.create(ZoneInfo.createKaif(zone, aliasName, Instant.now())); } @Override public void updateTheme(Zone zone, String theme) { zoneDao.updateTheme(zone, theme); } @Override public Map<String, List<ZoneInfo>> listZoneAtoZ() { Function<ZoneInfo, String> capitalizeFirstChar = zoneInfo -> zoneInfo.getZone() .value() .substring(0, 1) .toUpperCase(); return zoneDao.listOrderByName() .stream() .collect(Collectors.groupingBy(capitalizeFirstChar, LinkedHashMap::new, toList())); } @Override public List<ZoneInfo> listRecommendZones() { int recommendSize = 10; List<ZoneInfo> results = articleDao.listHotZonesWithCache(recommendSize, Instant.now().minus(Duration.ofHours(24))); if (results.size() >= recommendSize) { return results; } // not enough hot zone within 24 hours, fall back to all zones... List<ZoneInfo> all = zoneDao.listOrderByName() .stream() .filter(zoneInfo -> !zoneInfo.isHideFromTop()) .collect(toList()); Collections.shuffle(all); return all.stream().limit(recommendSize).collect(toList()); } @Override public List<ZoneInfo> listCitizenZones() { return zoneDao.listOrderByName() .stream() .filter(zoneInfo -> zoneInfo.getWriteAuthority() == Authority.CITIZEN) .collect(toList()); } @Override public ZoneInfo createByUser(String zone, String aliasName, Authorization creator) throws CreditNotEnoughException { Account account = verifyAuthority(creator).flatMap(this::verifyCredit).get(); ZoneInfo zoneInfo = ZoneInfo.createDefault(zone, aliasName, Instant.now()) .withAdmins(singletonList(account.getAccountId())); zoneDao.create(zoneInfo); return zoneInfo; } @Override public List<ZoneInfo> listAdministerZones(String username) { Account account = accountDao.loadByUsername(username); return zoneDao.listZonesByAdmin(account.getAccountId()); } @Override public boolean isZoneAvailable(String zone) { return Zone.isValid(zone) && ZoneInfo.isValidDefault(zone) && !zoneDao.findZoneWithoutCache(Zone .valueOf(zone)).isPresent(); } @Override public boolean canCreateZone(Authorization authorization) { return verifyAuthority(authorization).flatMap(this::verifyCredit).isSuccess(); } /** * @return username of administrators */ @Override public List<String> listAdministratorsWithCache(Zone zone) { return zoneDao.listAdministratorsWithCache(zone); } private Try<Account> verifyAuthority(Authorization authorization) { // won't compile if put `supplier` inline Supplier<Account> supplier = () -> accountDao.strongVerifyAccount(authorization) .filter(account -> account.containsAuthority(Authority.CITIZEN)) .orElseThrow(() -> new AccessDeniedException("no permission to create zone")); return Try.apply(supplier); } private Try<Account> verifyCredit(Account account) { return Try.apply(() -> { int zones = zoneDao.listZonesByAdmin(account.getAccountId()).size(); if (zones >= MAX_AVAILABLE_ZONE) { throw new CreditNotEnoughException(); } int requireScore = (zones + 1) * HONOR_SCORE_PER_ZONE; if (accountDao.loadStats(account.getUsername()).getHonorScore() < requireScore) { throw new CreditNotEnoughException(); } return account; }); } }