org.mayocat.configuration.internal.DefaultConfigurationService.java Source code

Java tutorial

Introduction

Here is the source code for org.mayocat.configuration.internal.DefaultConfigurationService.java

Source

/*
 * Copyright (c) 2012, Mayocat <hello@mayocat.org>
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package org.mayocat.configuration.internal;

import java.io.IOException;
import java.io.Serializable;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

import javax.inject.Inject;
import javax.inject.Provider;

import org.mayocat.accounts.model.Tenant;
import org.mayocat.accounts.model.TenantConfiguration;
import org.mayocat.accounts.store.TenantStore;
import org.mayocat.configuration.ConfigurationService;
import org.mayocat.configuration.ExposedSettings;
import org.mayocat.configuration.GestaltConfigurationSource;
import org.mayocat.configuration.NoSuchModuleException;
import org.mayocat.configuration.jackson.GestaltConfigurationModule;
import org.mayocat.configuration.jackson.TimeZoneModule;
import org.mayocat.context.WebContext;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Maps;

import io.dropwizard.jackson.GuavaExtrasModule;

/**
 * @version $Id: f45dc71f51963b3075763ea25c278ebaea6d57c2 $
 */
@Component
public class DefaultConfigurationService implements ConfigurationService {
    @Inject
    private Provider<TenantStore> tenantStore;

    @Inject
    private Map<String, ExposedSettings> exposedSettings;

    @Inject
    private Map<String, GestaltConfigurationSource> gestaltConfigurationSources;

    @Inject
    private WebContext context;

    @Inject
    private Logger logger;

    /**
     * Configurations cache.
     *
     * Keys are tenant ids, and values their configuration as JSON (here a map).
     *
     * An event listener flushed their entry when a tenant entity event is received.
     */
    private Cache<UUID, Map<String, Serializable>> configurations = CacheBuilder.newBuilder().maximumSize(1000)
            .build();

    private Object lock = new Object();

    /**
     * Exposed settings, as provided by the platform
     */
    private Map<String, Serializable> exposedPlatformSettingsAsJson;

    public Map<Class, Serializable> getSettings() {
        return this.getSettings(this.context.getTenant());
    }

    public Map<Class, Serializable> getSettings(Tenant tenant) {
        ObjectMapper mapper = getObjectMapper();
        Map<Class, Serializable> configurations = Maps.newHashMap();
        if (tenant != null) {
            // For a tenant : merge global configuration with tenant own overrides
            Map<String, Serializable> mergedConfiguration = getSettingsAsJson(tenant);

            for (String source : exposedSettings.keySet()) {
                Map<String, Serializable> merged = (Map<String, Serializable>) mergedConfiguration.get(source);
                Class c = exposedSettings.get(source).getClass();
                try {
                    String json = mapper.writeValueAsString(merged);
                    Serializable result = (Serializable) mapper.readValue(json, c);
                    configurations.put(c, result);
                } catch (IOException e) {
                    this.logger.error("Error while converting configuration to JSON string", e);
                }
            }
        } else {
            // Not for a tenant : just return the global configuration

            for (ExposedSettings settings : exposedSettings.values()) {
                configurations.put(settings.getClass(), settings);
            }
        }

        return configurations;
    }

    public <T extends ExposedSettings> T getSettings(Class<T> c, Tenant tenant) {
        if (tenant != null) {
            return (T) this.getSettings(tenant).get(c);
        } else {
            for (ExposedSettings settings : exposedSettings.values()) {
                if (c.isAssignableFrom(settings.getClass())) {
                    return (T) settings;
                }
            }
        }
        return null;
    }

    public <T extends ExposedSettings> T getSettings(Class<T> c) {
        if (this.context.getTenant() != null) {
            return (T) this.getSettings().get(c);
        } else {
            for (ExposedSettings settings : exposedSettings.values()) {
                if (c.isAssignableFrom(settings.getClass())) {
                    return (T) settings;
                }
            }
        }
        return null;
    }

    public Map<String, Serializable> getSettingsAsJson(final Tenant tenant) {
        if (tenant == null) {
            return Collections.emptyMap();
        }
        try {
            return configurations.get(tenant.getId(), new Callable<Map<String, Serializable>>() {
                @Override
                public Map<String, Serializable> call() {
                    synchronized (lock) {
                        logger.debug("loading cache configuration value for tenant {}", tenant.getSlug());
                        Map<String, Serializable> tenantConfiguration = tenant.getConfiguration();
                        Map<String, Serializable> platformConfiguration = getExposedPlatformSettingsAsJson();
                        ConfigurationJsonMerger merger = new ConfigurationJsonMerger(platformConfiguration,
                                tenantConfiguration);
                        return merger.merge();
                    }
                }
            });
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    public Map<String, Serializable> getSettingsAsJson() {
        return this.getSettingsAsJson(context.getTenant());
    }

    public Map<String, Serializable> getSettingsAsJson(String moduleName) throws NoSuchModuleException {
        if (!this.exposedSettings.containsKey(moduleName)) {
            throw new NoSuchModuleException();
        }

        try {
            return (Map<String, Serializable>) getSettingsAsJson().get(moduleName);
        } catch (ClassCastException e) {
            this.logger.warn("Attempt at accessing a configuration that is not an object");
        }
        return Collections.emptyMap();
    }

    public void updateSettings(Map<String, Serializable> data) {
        ValidConfigurationEnforcer enforcer = new ValidConfigurationEnforcer(getExposedPlatformSettingsAsJson(),
                data);
        ValidConfigurationEnforcer.ValidationResult result = enforcer.enforce();

        TenantConfiguration configuration = new TenantConfiguration(TenantConfiguration.CURRENT_VERSION,
                result.getResult());
        this.tenantStore.get().updateConfiguration(configuration);

        // Invalidates the cached configuration for the tenant updating its configuration
        // TODO: do this from the configuration store instead
        this.configurations.invalidate(this.context.getTenant().getId());

        // TODO throw an exception here when there are validation errors, so that it can be acknowledged to the
        // REST accounts consumer ? (meaning the operation has been partially successful only)
    }

    public void updateSettings(String module, Map<String, Serializable> configuration)
            throws NoSuchModuleException {
        if (!this.exposedSettings.containsKey(module)) {
            throw new NoSuchModuleException();
        }

        Tenant tenant = this.context.getTenant();
        TenantConfiguration currentConfiguration = tenant.getConfiguration();
        Map<String, Serializable> data = Maps.newHashMap(currentConfiguration.getData());

        data.put(module, Maps.newHashMap(configuration));
        this.updateSettings(data);
    }

    public Map<String, Serializable> getGestaltConfiguration() {
        ObjectMapper mapper = getObjectMapper();
        mapper.registerModule(new GestaltConfigurationModule());

        Map<String, Object> result = Maps.newHashMap();
        for (String key : this.gestaltConfigurationSources.keySet()) {
            Object value = this.gestaltConfigurationSources.get(key).get();
            if (ExposedSettings.class.isAssignableFrom(value.getClass()) && this.context.getTenant() != null) {
                // If the gestalt source returns an "exposed setting", then we get the version merged with the tenant
                // configuration.
                Class<? extends ExposedSettings> configurationClass = (Class<? extends ExposedSettings>) value
                        .getClass();
                value = this.getSettings(configurationClass);
            }
            result.put(key, value);
        }
        try {
            return mapper.readValue(mapper.writeValueAsString(result), new TypeReference<Map<String, Object>>() {
            });
        } catch (JsonProcessingException e) {
            this.logger.error("Failed to convert gestalt configuration [{}]", e);
            throw new RuntimeException(e);
        } catch (IOException e) {
            this.logger.error("Failed to convert configurations to map", e);
            throw new RuntimeException(e);
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    private Map<String, Serializable> getExposedPlatformSettingsAsJson() {
        if (exposedPlatformSettingsAsJson != null) {
            return exposedPlatformSettingsAsJson;
        }
        Map<String, ExposedSettings> configurationsToSerialize = Maps.newHashMap();
        for (String hint : exposedSettings.keySet()) {
            configurationsToSerialize.put(hint, exposedSettings.get(hint));
        }
        try {
            ObjectMapper mapper = getObjectMapper();
            String json = mapper.writeValueAsString(configurationsToSerialize);
            exposedPlatformSettingsAsJson = mapper.readValue(json, new TypeReference<Map<String, Object>>() {
            });
            return exposedPlatformSettingsAsJson;
        } catch (JsonProcessingException e) {
            this.logger.error("Failed to convert configurations to map", e);
        } catch (IOException e) {
            this.logger.error("Failed to convert configurations to map", e);
        }
        return Collections.emptyMap();
    }

    public ObjectMapper getObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new GuavaModule());
        mapper.registerModule(new JodaModule());
        mapper.registerModule(new GuavaExtrasModule());
        mapper.registerModule(new TimeZoneModule());

        return mapper;
    }
}