alfio.manager.system.ConfigurationManager.java Source code

Java tutorial

Introduction

Here is the source code for alfio.manager.system.ConfigurationManager.java

Source

/**
 * This file is part of alf.io.
 *
 * alf.io is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * alf.io is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with alf.io.  If not, see <http://www.gnu.org/licenses/>.
 */
package alfio.manager.system;

import alfio.manager.user.UserManager;
import alfio.model.Event;
import alfio.model.modification.ConfigurationModification;
import alfio.model.system.Configuration;
import alfio.model.system.Configuration.*;
import alfio.model.system.ConfigurationKeys;
import alfio.model.system.ConfigurationPathLevel;
import alfio.model.transaction.PaymentProxy;
import alfio.model.user.User;
import alfio.repository.EventRepository;
import alfio.repository.system.ConfigurationRepository;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.CompareToBuilder;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Collectors;

import static alfio.model.system.ConfigurationKeys.*;
import static alfio.model.system.ConfigurationPathLevel.*;
import static alfio.util.OptionalWrapper.optionally;

@Component
@Transactional
@Log4j2
@AllArgsConstructor
public class ConfigurationManager {

    private static final Map<ConfigurationKeys.SettingCategory, List<Configuration>> ORGANIZATION_CONFIGURATION = collectConfigurationKeysByCategory(
            ORGANIZATION);
    private static final Map<ConfigurationKeys.SettingCategory, List<Configuration>> EVENT_CONFIGURATION = collectConfigurationKeysByCategory(
            ConfigurationPathLevel.EVENT);
    private static final Map<ConfigurationKeys.SettingCategory, List<Configuration>> CATEGORY_CONFIGURATION = collectConfigurationKeysByCategory(
            ConfigurationPathLevel.TICKET_CATEGORY);

    private static final Predicate<ConfigurationModification> TO_BE_SAVED = c -> Optional.ofNullable(c.getId())
            .orElse(-1) > -1 || !StringUtils.isBlank(c.getValue());

    private final ConfigurationRepository configurationRepository;
    private final UserManager userManager;
    private final EventRepository eventRepository;

    //TODO: refactor, not the most beautiful code, find a better solution...
    private Configuration findByConfigurationPathAndKey(ConfigurationPath path, ConfigurationKeys key) {
        switch (path.pathLevel()) {
        case SYSTEM:
            return configurationRepository.findByKey(key.getValue());
        case ORGANIZATION: {
            OrganizationConfigurationPath o = from(path);
            return selectPath(configurationRepository.findByOrganizationAndKey(o.getId(), key.getValue()));
        }
        case EVENT: {
            EventConfigurationPath o = from(path);
            return selectPath(
                    configurationRepository.findByEventAndKey(o.getOrganizationId(), o.getId(), key.getValue()));
        }
        case TICKET_CATEGORY: {
            TicketCategoryConfigurationPath o = from(path);
            return selectPath(configurationRepository.findByTicketCategoryAndKey(o.getOrganizationId(),
                    o.getEventId(), o.getId(), key.getValue()));
        }
        }
        throw new IllegalStateException("Can't reach here");
    }

    /**
     * Select the most "precise" configuration in the given list.
     *
     * @param conf
     * @return
     */
    private Configuration selectPath(List<Configuration> conf) {
        return conf.size() == 1 ? conf.get(0)
                : conf.stream().sorted(Comparator.comparing(Configuration::getConfigurationPathLevel).reversed())
                        .findFirst().orElse(null);
    }

    //meh
    @SuppressWarnings("unchecked")
    private static <T> T from(ConfigurationPath c) {
        return (T) c;
    }

    public int getIntConfigValue(ConfigurationPathKey pathKey, int defaultValue) {
        try {
            return Optional.ofNullable(findByConfigurationPathAndKey(pathKey.getPath(), pathKey.getKey()))
                    .map(Configuration::getValue).map(Integer::parseInt).orElse(defaultValue);
        } catch (NumberFormatException | EmptyResultDataAccessException e) {
            return defaultValue;
        }
    }

    public boolean getBooleanConfigValue(ConfigurationPathKey pathKey, boolean defaultValue) {
        return getStringConfigValue(pathKey).map(Boolean::parseBoolean).orElse(defaultValue);
    }

    public String getStringConfigValue(ConfigurationPathKey pathKey, String defaultValue) {
        return getStringConfigValue(pathKey).orElse(defaultValue);
    }

    public Optional<String> getStringConfigValue(ConfigurationPathKey pathKey) {
        return optionally(() -> findByConfigurationPathAndKey(pathKey.getPath(), pathKey.getKey()))
                .map(Configuration::getValue);
    }

    public Map<ConfigurationKeys, Optional<String>> getStringConfigValueFrom(ConfigurationPathKey... keys) {
        Map<ConfigurationKeys, Optional<String>> res = new HashMap<>();
        for (ConfigurationPathKey key : keys) {
            res.put(key.getKey(), getStringConfigValue(key));
        }
        return res;
    }

    public String getRequiredValue(ConfigurationPathKey pathKey) {
        return getStringConfigValue(pathKey).orElseThrow(() -> new IllegalArgumentException(
                "Mandatory configuration key " + pathKey.getKey() + " not present"));
    }

    // begin SYSTEM related configuration methods

    public void saveConfig(ConfigurationPathKey pathKey, String value) {
        ConfigurationPath path = pathKey.getPath();
        switch (path.pathLevel()) {
        case SYSTEM:
            saveSystemConfiguration(pathKey.getKey(), value);
            break;
        case ORGANIZATION:
            OrganizationConfigurationPath orgPath = (OrganizationConfigurationPath) path;
            saveOrganizationConfiguration(orgPath.getId(), pathKey.getKey().name(), value);
            break;
        case EVENT:
            EventConfigurationPath eventPath = (EventConfigurationPath) path;
            saveEventConfiguration(eventPath.getId(), eventPath.getOrganizationId(), pathKey.getKey().name(),
                    value);
            break;
        }
    }

    public void saveAllSystemConfiguration(List<ConfigurationModification> list) {
        list.forEach(c -> saveSystemConfiguration(ConfigurationKeys.fromString(c.getKey()), c.getValue()));
    }

    private void saveOrganizationConfiguration(int organizationId, String key, String optionValue) {
        Optional<String> value = evaluateValue(key, optionValue);
        Optional<Configuration> existing = configurationRepository.findByKeyAtOrganizationLevel(organizationId,
                key);
        if (!value.isPresent()) {
            configurationRepository.deleteOrganizationLevelByKey(key, organizationId);
        } else if (existing.isPresent()) {
            configurationRepository.updateOrganizationLevel(organizationId, key, value.get());
        } else {
            configurationRepository.insertOrganizationLevel(organizationId, key, value.get(),
                    ConfigurationKeys.fromString(key).getDescription());
        }
    }

    public void saveAllOrganizationConfiguration(int organizationId, List<ConfigurationModification> list,
            String username) {
        Validate.isTrue(userManager.isOwnerOfOrganization(userManager.findUserByUsername(username), organizationId),
                "Cannot update settings, user is not owner");
        list.stream().filter(TO_BE_SAVED)
                .forEach(c -> saveOrganizationConfiguration(organizationId, c.getKey(), c.getValue()));
    }

    private void saveEventConfiguration(int eventId, int organizationId, String key, String optionValue) {
        Optional<Configuration> existing = configurationRepository.findByKeyAtEventLevel(eventId, organizationId,
                key);
        Optional<String> value = evaluateValue(key, optionValue);
        if (!value.isPresent()) {
            configurationRepository.deleteEventLevelByKey(key, eventId);
        } else if (existing.isPresent()) {
            configurationRepository.updateEventLevel(eventId, organizationId, key, value.get());
        } else {
            configurationRepository.insertEventLevel(organizationId, eventId, key, value.get(),
                    ConfigurationKeys.fromString(key).getDescription());
        }
    }

    public void saveAllEventConfiguration(int eventId, int organizationId, List<ConfigurationModification> list,
            String username) {
        User user = userManager.findUserByUsername(username);
        Validate.isTrue(userManager.isOwnerOfOrganization(user, organizationId),
                "Cannot update settings, user is not owner");
        Event event = eventRepository.findById(eventId);
        Validate.notNull(event, "event does not exist");
        if (organizationId != event.getOrganizationId()) {
            Validate.isTrue(userManager.isOwnerOfOrganization(user, event.getOrganizationId()),
                    "Cannot update settings, user is not owner of event");
        }
        list.stream().filter(TO_BE_SAVED)
                .forEach(c -> saveEventConfiguration(eventId, organizationId, c.getKey(), c.getValue()));
    }

    public void saveCategoryConfiguration(int categoryId, int eventId, List<ConfigurationModification> list,
            String username) {
        User user = userManager.findUserByUsername(username);
        Event event = eventRepository.findById(eventId);
        Validate.notNull(event, "event does not exist");
        Validate.isTrue(userManager.isOwnerOfOrganization(user, event.getOrganizationId()),
                "Cannot update settings, user is not owner of event");
        list.stream().filter(TO_BE_SAVED).forEach(c -> {
            Optional<Configuration> existing = configurationRepository.findByKeyAtCategoryLevel(eventId,
                    event.getOrganizationId(), categoryId, c.getKey());
            Optional<String> value = evaluateValue(c.getKey(), c.getValue());
            if (!value.isPresent()) {
                configurationRepository.deleteCategoryLevelByKey(c.getKey(), eventId, categoryId);
            } else if (existing.isPresent()) {
                configurationRepository.updateCategoryLevel(eventId, event.getOrganizationId(), categoryId,
                        c.getKey(), value.get());
            } else {
                configurationRepository.insertTicketCategoryLevel(event.getOrganizationId(), eventId, categoryId,
                        c.getKey(), value.get(), ConfigurationKeys.fromString(c.getKey()).getDescription());
            }
        });
    }

    private Optional<String> evaluateValue(String key, String value) {
        if (ConfigurationKeys.fromString(key).isBooleanComponentType()) {
            return Optional.ofNullable(StringUtils.trimToNull(value));
        }
        return Optional.of(Objects.requireNonNull(value));
    }

    private Optional<Boolean> getThreeStateValue(String value) {
        return Optional.ofNullable(StringUtils.trimToNull(value)).map(Boolean::parseBoolean);
    }

    public void saveSystemConfiguration(ConfigurationKeys key, String value) {
        Optional<Configuration> conf = optionally(() -> findByConfigurationPathAndKey(Configuration.system(), key));
        if (key.isBooleanComponentType()) {
            Optional<Boolean> state = getThreeStateValue(value);
            if (conf.isPresent()) {
                if (state.isPresent()) {
                    configurationRepository.update(key.getValue(), value);
                } else {
                    configurationRepository.deleteByKey(key.getValue());
                }
            } else {
                state.ifPresent(
                        v -> configurationRepository.insert(key.getValue(), v.toString(), key.getDescription()));
            }
        } else {
            Optional<String> valueOpt = Optional.ofNullable(value);
            if (!conf.isPresent()) {
                valueOpt.ifPresent(v -> configurationRepository.insert(key.getValue(), v, key.getDescription()));
            } else {
                configurationRepository.update(key.getValue(), value);
            }
        }
    }

    /**
     * Checks if the basic options have been already configured:
     * <ul>
     *     <li>Google maps' api keys</li>
     *     <li>Base application URL</li>
     *     <li>E-Mail</li>
     * </ul>
     * @return {@code true} if there are missing options, {@code true} otherwise
     */
    public boolean isBasicConfigurationNeeded() {
        return ConfigurationKeys.basic().stream().anyMatch(key -> {
            boolean absent = !configurationRepository.findOptionalByKey(key.getValue()).isPresent();
            if (absent) {
                log.warn("cannot find a value for " + key.getValue());
            }
            return absent;
        });
    }

    private Predicate<Configuration> checkActualConfigurationLevel(boolean isAdmin, ConfigurationPathLevel level) {
        return conf -> isAdmin || conf.getConfigurationKey().supports(level);
    }

    public Map<ConfigurationKeys.SettingCategory, List<Configuration>> loadOrganizationConfig(int organizationId,
            String username) {
        User user = userManager.findUserByUsername(username);
        if (!userManager.isOwnerOfOrganization(user, organizationId)) {
            return Collections.emptyMap();
        }
        boolean isAdmin = userManager.isAdmin(user);
        Map<ConfigurationKeys.SettingCategory, List<Configuration>> existing = configurationRepository
                .findOrganizationConfiguration(organizationId).stream()
                .filter(checkActualConfigurationLevel(isAdmin, ORGANIZATION)).sorted().collect(groupByCategory());
        String paymentMethodsBlacklist = getStringConfigValue(
                Configuration.from(organizationId, ConfigurationKeys.PAYMENT_METHODS_BLACKLIST), "");
        Map<SettingCategory, List<Configuration>> result = groupByCategory(
                isAdmin ? union(SYSTEM, ORGANIZATION) : ORGANIZATION_CONFIGURATION, existing);
        List<SettingCategory> toBeRemoved = PaymentProxy.availableProxies().stream()
                .filter(pp -> paymentMethodsBlacklist.contains(pp.getKey()))
                .flatMap(pp -> pp.getSettingCategories().stream()).collect(Collectors.toList());

        if (toBeRemoved.isEmpty()) {
            return result;
        } else {
            return result.entrySet().stream().filter(entry -> !toBeRemoved.contains(entry.getKey()))
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        }
    }

    public Map<ConfigurationKeys.SettingCategory, List<Configuration>> loadEventConfig(int eventId,
            String username) {
        User user = userManager.findUserByUsername(username);
        Event event = eventRepository.findById(eventId);
        int organizationId = event.getOrganizationId();
        if (!userManager.isOwnerOfOrganization(user, organizationId)) {
            return Collections.emptyMap();
        }
        boolean isAdmin = userManager.isAdmin(user);
        Map<ConfigurationKeys.SettingCategory, List<Configuration>> existing = configurationRepository
                .findEventConfiguration(organizationId, eventId).stream()
                .filter(checkActualConfigurationLevel(isAdmin, EVENT)).sorted().collect(groupByCategory());
        boolean offlineCheckInEnabled = areBooleanSettingsEnabledForEvent(ALFIO_PI_INTEGRATION_ENABLED,
                OFFLINE_CHECKIN_ENABLED).test(event);
        return removeAlfioPISettingsIfNeeded(offlineCheckInEnabled,
                groupByCategory(isAdmin ? union(SYSTEM, EVENT) : EVENT_CONFIGURATION, existing));
    }

    public Predicate<Event> areBooleanSettingsEnabledForEvent(ConfigurationKeys... keys) {
        return event -> Arrays.stream(keys).allMatch(k -> getBooleanConfigValue(
                Configuration.from(event.getOrganizationId(), event.getId()).apply(k), false));
    }

    private static Map<ConfigurationKeys.SettingCategory, List<Configuration>> removeAlfioPISettingsIfNeeded(
            boolean offlineCheckInEnabled, Map<ConfigurationKeys.SettingCategory, List<Configuration>> settings) {
        if (offlineCheckInEnabled) {
            return settings;
        }
        return settings.entrySet().stream().filter(e -> e.getKey() != ConfigurationKeys.SettingCategory.ALFIO_PI)
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    static Map<ConfigurationKeys.SettingCategory, List<Configuration>> union(ConfigurationPathLevel... levels) {
        List<Configuration> configurations = Arrays.stream(levels)
                .sorted(ConfigurationPathLevel.COMPARATOR.reversed())
                .flatMap(l -> ConfigurationKeys.byPathLevel(l).stream().map(mapEmptyKeys(l)))
                .sorted((c1, c2) -> new CompareToBuilder()
                        .append(c2.getConfigurationPathLevel(), c1.getConfigurationPathLevel())
                        .append(c1.getConfigurationKey(), c2.getConfigurationKey()).toComparison())
                .collect(LinkedList::new, (List<Configuration> list, Configuration conf) -> {
                    int existing = (int) list.stream()
                            .filter(c -> c.getConfigurationKey() == conf.getConfigurationKey()).count();
                    if (existing == 0) {
                        list.add(conf);
                    }
                }, (l1, l2) -> {
                });
        return configurations.stream().collect(groupByCategory());
    }

    public Map<ConfigurationKeys.SettingCategory, List<Configuration>> loadCategoryConfig(int eventId,
            int categoryId, String username) {
        User user = userManager.findUserByUsername(username);
        Event event = eventRepository.findById(eventId);
        int organizationId = event.getOrganizationId();
        if (!userManager.isOwnerOfOrganization(user, organizationId)) {
            return Collections.emptyMap();
        }
        Map<ConfigurationKeys.SettingCategory, List<Configuration>> existing = configurationRepository
                .findCategoryConfiguration(organizationId, eventId, categoryId).stream().sorted()
                .collect(groupByCategory());
        return groupByCategory(CATEGORY_CONFIGURATION, existing);
    }

    private Map<ConfigurationKeys.SettingCategory, List<Configuration>> groupByCategory(
            Map<ConfigurationKeys.SettingCategory, List<Configuration>> all,
            Map<ConfigurationKeys.SettingCategory, List<Configuration>> existing) {
        return all.entrySet().stream().map(e -> {
            Set<Configuration> entries = new TreeSet<>();
            ConfigurationKeys.SettingCategory key = e.getKey();
            entries.addAll(e.getValue());
            if (existing.containsKey(key)) {
                List<Configuration> configurations = existing.get(key);
                entries.removeAll(configurations);
                entries.addAll(configurations);
            }
            return Pair.of(key, new ArrayList<>(entries));
        }).collect(Collectors.toMap(Pair::getKey, Pair::getValue));
    }

    public Map<ConfigurationKeys.SettingCategory, List<Configuration>> loadAllSystemConfigurationIncludingMissing(
            String username) {
        if (!userManager.isAdmin(userManager.findUserByUsername(username))) {
            return Collections.emptyMap();
        }
        final List<Configuration> existing = configurationRepository.findSystemConfiguration().stream()
                .filter(c -> !ConfigurationKeys.fromString(c.getKey()).isInternal()).collect(Collectors.toList());
        final List<Configuration> missing = Arrays.stream(ConfigurationKeys.visible())
                .filter(k -> existing.stream().noneMatch(c -> c.getKey().equals(k.getValue())))
                .map(mapEmptyKeys(ConfigurationPathLevel.SYSTEM)).collect(Collectors.toList());
        List<Configuration> result = new LinkedList<>(existing);
        result.addAll(missing);
        return result.stream().sorted().collect(groupByCategory());
    }

    private static Collector<Configuration, ?, Map<ConfigurationKeys.SettingCategory, List<Configuration>>> groupByCategory() {
        return Collectors.groupingBy(c -> c.getConfigurationKey().getCategory());
    }

    private static Function<ConfigurationKeys, Configuration> mapEmptyKeys(ConfigurationPathLevel level) {
        return k -> new Configuration(-1, k.getValue(), null, k.getDescription(), level);
    }

    public void deleteKey(String key) {
        configurationRepository.deleteByKey(key);
    }

    public void deleteOrganizationLevelByKey(String key, int organizationId, String username) {
        Validate.isTrue(userManager.isOwnerOfOrganization(userManager.findUserByUsername(username), organizationId),
                "User is not owner of the organization. Therefore, delete is not allowed.");
        configurationRepository.deleteOrganizationLevelByKey(key, organizationId);
    }

    public void deleteEventLevelByKey(String key, int eventId, String username) {
        Event event = eventRepository.findById(eventId);
        Validate.notNull(event, "Wrong event id");
        Validate.isTrue(
                userManager.isOwnerOfOrganization(userManager.findUserByUsername(username),
                        event.getOrganizationId()),
                "User is not owner of the organization. Therefore, delete is not allowed.");
        configurationRepository.deleteEventLevelByKey(key, eventId);
    }

    public void deleteCategoryLevelByKey(String key, int eventId, int categoryId, String username) {
        Event event = eventRepository.findById(eventId);
        Validate.notNull(event, "Wrong event id");
        Validate.isTrue(
                userManager.isOwnerOfOrganization(userManager.findUserByUsername(username),
                        event.getOrganizationId()),
                "User is not owner of the organization. Therefore, delete is not allowed.");
        configurationRepository.deleteCategoryLevelByKey(key, eventId, categoryId);
    }

    private static Map<ConfigurationKeys.SettingCategory, List<Configuration>> collectConfigurationKeysByCategory(
            ConfigurationPathLevel pathLevel) {
        return ConfigurationKeys.byPathLevel(pathLevel).stream().map(mapEmptyKeys(pathLevel)).sorted()
                .collect(groupByCategory());
    }

    public String getShortReservationID(Event event, String reservationId) {
        return StringUtils.substring(reservationId, 0, getIntConfigValue(
                Configuration.from(event.getOrganizationId(), event.getId(), PARTIAL_RESERVATION_ID_LENGTH), 8))
                .toUpperCase();
    }

    public boolean hasAllConfigurationsForInvoice(Event event) {
        return getStringConfigValue(
                Configuration.from(event.getOrganizationId(), event.getId(), ConfigurationKeys.INVOICE_ADDRESS))
                        .isPresent()
                && getStringConfigValue(
                        Configuration.from(event.getOrganizationId(), event.getId(), ConfigurationKeys.VAT_NR))
                                .isPresent();
    }

    public boolean isRecaptchaForOfflinePaymentEnabled(Event event) {
        return getBooleanConfigValue(
                Configuration.from(event.getOrganizationId(), event.getId(), ENABLE_CAPTCHA_FOR_OFFLINE_PAYMENTS),
                false)
                && getStringConfigValue(Configuration.getSystemConfiguration(ENABLE_CAPTCHA_FOR_OFFLINE_PAYMENTS),
                        null) != null;
    }
}