org.niord.core.settings.SettingsService.java Source code

Java tutorial

Introduction

Here is the source code for org.niord.core.settings.SettingsService.java

Source

/*
 * Copyright 2016 Danish Maritime Authority.
 *
 * 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.niord.core.settings;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang.StringUtils;
import org.niord.core.cache.CacheElement;
import org.niord.core.service.BaseService;
import org.niord.core.util.JsonUtils;
import org.slf4j.Logger;

import javax.annotation.PostConstruct;
import javax.ejb.Lock;
import javax.ejb.LockType;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static org.keycloak.util.JsonSerialization.mapper;

/**
 * Interface for accessing settings.
 * <p/>
 * This bean can either be injected directly,
 * or the {@code @Setting} annotation can be used.
 */
@Singleton
@Lock(LockType.READ)
@Startup
@SuppressWarnings("unused")
public class SettingsService extends BaseService {

    private final static String SETTINGS_FILE = "/niord.json";

    @Inject
    private Logger log;

    @Inject
    SettingsCache settingsCache;

    /**
     * Called when the system starts up.
     *
     * First, loads all settings from the classpath "/niord.json" file.<br>
     *
     * Then, loads all settings from the "${niord.home}/niord.json" file
     * and add these to the list of loaded settings.<br>
     *
     * Lastly, persists all the loaded settings that do not already exists in the database.
     */
    @PostConstruct
    public void loadSettingsFromPropertiesFile() {
        try {

            // Read the settings from the "/niord.json" classpath file
            Map<String, Setting> settingMap = loadSettingsFromClasspath();

            // Read the settings from the "${niord.home}/niord.json" file
            settingMap = loadSettingsFromNiordHome(settingMap);

            // Determine the keys that are not yet persisted to the database
            em.createNamedQuery("Setting.findSettingsWithKeys", Setting.class)
                    .setParameter("keys", settingMap.keySet()).getResultList().stream().map(Setting::getKey)
                    .forEach(settingMap::remove);

            // Persist all settings not yet persisted to the database
            settingMap.values().forEach(s -> {
                s.updateType();
                em.persist(s);
                log.info(String.format("Loaded setting %s from niord.json", s.getKey()));
            });

        } catch (Exception e) {
            // Stop the application starting up
            throw new RuntimeException("Error loading settings from niord.json", e);
        }
    }

    /** Called upon startup. Read the settings from the "/niord.json" classpath file */
    private Map<String, Setting> loadSettingsFromClasspath() throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        List<Setting> settings = mapper.readValue(getClass().getResource(SETTINGS_FILE),
                new TypeReference<List<Setting>>() {
                });
        return settings.stream().collect(Collectors.toMap(Setting::getKey, Function.identity()));
    }

    /** Called upon startup. Read the settings from the "${niord.home}/niord.json" file and update the settingMap */
    private Map<String, Setting> loadSettingsFromNiordHome(Map<String, Setting> settingMap) throws IOException {
        Object niordHome = peek("niord.home");
        if (niordHome == null && settingMap.containsKey("niord.home")) {
            niordHome = settingMap.get("niord.home").getValue();
        }
        if (niordHome != null && niordHome instanceof String) {
            niordHome = expandSettingValue((String) niordHome);
            Path niordFile = Paths.get(niordHome.toString(), "niord.json");
            if (Files.exists(niordFile) && Files.isRegularFile(niordFile)) {
                List<Setting> settings = mapper.readValue(niordFile.toFile(), new TypeReference<List<Setting>>() {
                });
                // Update (and overwrite) the setting map with these settings
                settings.forEach(s -> settingMap.put(s.getKey(), s));
            }
        }
        return settingMap;
    }

    /**
     * Returns all settings that should be emitted to the web application
     * @return all settings that should be emitted to the web application
     */
    public List<Setting> getAllForWeb() {
        return em.createNamedQuery("Setting.findAllForWeb", Setting.class).getResultList();
    }

    /**
     * Returns all settings that are editable on the Settings admin page
     * @return all settings that are editable on the Settings admin page
     */
    public List<Setting> getAllEditable() {
        return em.createNamedQuery("Setting.findAllEditable", Setting.class).getResultList();
    }

    /**
     * Returns the value associated with the setting.
     * If it does not exist, it is created
     *
     * @param key the setting key
     * @return the associated value
     */
    public Object get(String key) {
        return get(new Setting(key));
    }

    /**
     * Returns the value associated with the setting.
     * If it does not exist, it is created
     *
     * @param setting the source
     * @return the associated value
     */
    public Object get(Setting setting) {
        Objects.requireNonNull(setting, "Must specify valid setting");

        // If a corresponding system property is set, it takes precedence
        if (System.getProperty(setting.getKey()) != null) {
            return System.getProperty(setting.getKey());
        }

        // Look for a cached value
        CacheElement<Object> value = settingsCache.getCache().get(setting.getKey());

        // No cached value
        if (value == null) {
            Setting result = em.find(Setting.class, setting.getKey());
            if (result == null) {
                result = new Setting(setting);
                em.persist(result);
            }
            value = new CacheElement<>(result.getValue());

            // Cache it.
            if (setting.isCached()) {
                settingsCache.getCache().put(setting.getKey(), value);
            }
        }

        // Check if we need to substitute with system properties. Only applies to String-based settings.
        Object result = value.getElement();
        if (result != null && result instanceof String) {
            result = expandSettingValue((String) result);
        }

        return result;
    }

    /**
     * Returns the value associated with the setting.
     * Always use the value from the database, and never creates the setting like "get()".
     *
     * @param key the setting key
     * @return the associated value
     */
    public Object peek(String key) {
        Objects.requireNonNull(key, "Must specify valid setting key");

        // If a corresponding system property is set, it takes precedence
        if (System.getProperty(key) != null) {
            return System.getProperty(key);
        }

        Setting setting = em.find(Setting.class, key);
        if (setting == null) {
            return null;
        }

        // Check if we need to substitute with system properties. Only applies to String-based settings.
        Object result = setting.getValue();
        if (result != null && result instanceof String) {
            result = expandSettingValue((String) result);
        }

        return result;
    }

    /**
     * Replace any nested token with the format "${token}" with either the setting with the given name
     * or with a System property with the given name.
     *
     * @param value the value to expand
     * @return the expanded value
     */
    private String expandSettingValue(String value) {
        SettingValueExpander valueExpander = new SettingValueExpander(value);
        String token;
        while ((token = valueExpander.nextToken()) != null) {
            Object setting = peek(token);
            if (setting != null) {
                valueExpander.replaceToken(token, setting.toString());
                continue;
            }
            String sysProp = System.getProperty(token);
            if (StringUtils.isNotBlank(sysProp)) {
                valueExpander.replaceToken(token, sysProp);
                continue;
            }
            valueExpander.replaceToken(token, "");
        }
        return valueExpander.getValue();
    }

    /**
     * Updates the database value of the given setting
     * @param template the setting to update
     * @return the updated setting
     */
    @Lock(LockType.WRITE)
    public Setting set(Setting template) {
        Setting setting = em.find(Setting.class, template.getKey());
        if (setting == null) {
            throw new IllegalArgumentException("Non-existing setting " + template.getKey());
        }

        // Update the DB
        setting.setValue(template.getValue());
        setting = em.merge(setting);

        // Invalidate the cache
        evictFromCache(setting.getKey());

        return setting;
    }

    /** Evicts any setting with the given key from the cache **/
    public void evictFromCache(String key) {
        settingsCache.getCache().remove(key);
    }

    /**
     * Updates the database value of the given setting
     * @param key the key of the setting to update
     * @param value the value of the setting to update
     * @return the updated setting
     */
    public Setting set(String key, Object value) {
        return set(new Setting(key, value));
    }

    /**
     * Returns the setting as a String
     *
     * @param setting the source
     * @return the associated value
     */
    public String getString(Setting setting) {
        Object value = get(setting);
        return value == null ? null : (value instanceof String) ? (String) value : value.toString();
    }

    public String getString(String key) {
        return getString(new Setting(key));
    }

    /**
     * Returns the setting as a boolean
     *
     * @param setting the source
     * @return the associated value
     */
    public Boolean getBoolean(Setting setting) {
        Object value = get(setting);
        if (value != null) {
            if (value instanceof Boolean) {
                return (Boolean) value;
            }
            switch (value.toString().toLowerCase()) {
            case "true":
            case "yes":
            case "t":
            case "y":
                return true;
            }
        }
        return null;
    }

    public Boolean getBoolean(String key) {
        return getBoolean(new Setting(key));
    }

    /**
     * Returns the setting as a long
     *
     * @param setting the source
     * @return the associated value
     */
    public Long getLong(Setting setting) {
        Object value = get(setting);
        if (value != null) {
            if (value instanceof Number) {
                return ((Number) value).longValue();
            }
            return Long.valueOf(value.toString());
        }
        return null;
    }

    public Long getLong(String key) {
        return getLong(new Setting(key));
    }

    /**
     * Returns the setting as an integer
     *
     * @param setting the source
     * @return the associated value
     */
    public Integer getInteger(Setting setting) {
        Object value = get(setting);
        if (value != null) {
            if (value instanceof Number) {
                return ((Number) value).intValue();
            }
            return Integer.valueOf(value.toString());
        }
        return null;
    }

    public Integer getInteger(String key) {
        return getInteger(new Setting(key));
    }

    /**
     * Returns the setting as a Double
     *
     * @param setting the source
     * @return the associated value
     */
    public Double getDouble(Setting setting) {
        Object value = get(setting);
        if (value != null) {
            if (value instanceof Number) {
                return ((Number) value).doubleValue();
            }
            return Double.valueOf(value.toString());
        }
        return null;
    }

    public Double getDouble(String key) {
        return getDouble(new Setting(key));
    }

    /**
     * Returns the setting as a Float
     *
     * @param setting the source
     * @return the associated value
     */
    public Float getFloat(Setting setting) {
        Object value = get(setting);
        if (value != null) {
            if (value instanceof Number) {
                return ((Number) value).floatValue();
            }
            return Float.valueOf(value.toString());
        }
        return null;
    }

    public Float getFloat(String key) {
        return getFloat(new Setting(key));
    }

    /**
     * Returns the setting as a Path
     *
     * @param setting the source
     * @return the associated value
     */
    public Path getPath(Setting setting) {
        String value = getString(setting);
        return value == null ? null : Paths.get(value);
    }

    public Path getPath(String key) {
        return getPath(new Setting(key));
    }

    public void setPath(String key, Path path) {
        set(key, path == null ? null : path.toAbsolutePath().toString());
    }

    /**
     * Returns the setting as a Date
     *
     * @param setting the source
     * @return the associated value
     */
    public Date getDate(Setting setting) {
        Long value = getLong(setting);
        return value == null ? null : new Date(value);
    }

    public Date getDate(String key) {
        return getDate(new Setting(key));
    }

    public void setDate(String key, Date date) {
        set(key, date == null ? null : date.getTime());
    }

    /**
     * Returns the setting from a JSON representation
     *
     * @param setting the source
     * @return the associated value
     */
    public <T> T getFromJson(Setting setting, Class<T> dataClass) {
        try {
            return JsonUtils.fromJson(JsonUtils.toJson(get(setting)), dataClass);
        } catch (IOException e) {
            return null;
        }
    }

    public <T> T getFromJson(String key, Class<T> dataClass) {
        return getFromJson(new Setting(key), dataClass);
    }

    public void setAsJson(String key, Object data) {
        try {
            set(key, JsonUtils.toJson(data));
        } catch (IOException ignored) {
        }
    }

    /**
     * Injects the String setting defined by the {@code @Setting} annotation
     *
     * @param ip the injection point
     * @return the String setting value
     */
    @Produces
    @org.niord.core.settings.annotation.Setting
    public String getString(InjectionPoint ip) {
        return getString(ip2setting(ip));
    }

    /**
     * Injects the Boolean setting defined by the {@code @Setting} annotation
     *
     * @param ip the injection point
     * @return the boolean setting value
     */
    @Produces
    @org.niord.core.settings.annotation.Setting
    public Boolean getBoolean(InjectionPoint ip) {
        return getBoolean(ip2setting(ip));
    }

    /**
     * Injects the Long setting defined by the {@code @Setting} annotation
     *
     * @param ip the injection point
     * @return the Long setting value
     */
    @Produces
    @org.niord.core.settings.annotation.Setting
    public Long getLong(InjectionPoint ip) {
        return getLong(ip2setting(ip));
    }

    /**
     * Injects the Integer setting defined by the {@code @Setting} annotation
     *
     * @param ip the injection point
     * @return the Long setting value
     */
    @Produces
    @org.niord.core.settings.annotation.Setting
    public Integer getInteger(InjectionPoint ip) {
        return getInteger(ip2setting(ip));
    }

    /**
     * Injects the Float setting defined by the {@code @Setting} annotation
     *
     * @param ip the injection point
     * @return the Float setting value
     */
    @Produces
    @org.niord.core.settings.annotation.Setting
    public Float getFloat(InjectionPoint ip) {
        return getFloat(ip2setting(ip));
    }

    /**
     * Injects the Double setting defined by the {@code @Setting} annotation
     *
     * @param ip the injection point
     * @return the Double setting value
     */
    @Produces
    @org.niord.core.settings.annotation.Setting
    public Double getDouble(InjectionPoint ip) {
        return getDouble(ip2setting(ip));
    }

    /**
     * Injects the Path setting defined by the {@code @Setting} annotation
     *
     * @param ip the injection point
     * @return the Path setting value
     */
    @Produces
    @org.niord.core.settings.annotation.Setting
    public Path getPath(InjectionPoint ip) {
        return getPath(ip2setting(ip));
    }

    /**
     * Injects the Date setting defined by the {@code @Setting} annotation
     *
     * @param ip the injection point
     * @return the Date setting value
     */
    @Produces
    @org.niord.core.settings.annotation.Setting
    public Date getDate(InjectionPoint ip) {
        return getDate(ip2setting(ip));
    }

    /**
     * Converts the injection point into the associated setting with a default value
     *
     * @param ip the injection point
     * @return the associated setting
     */
    private Setting ip2setting(InjectionPoint ip) {
        org.niord.core.settings.annotation.Setting ann = ip.getAnnotated()
                .getAnnotation(org.niord.core.settings.annotation.Setting.class);

        String key = StringUtils.isBlank(ann.value()) ? ip.getMember().getName() : ann.value();
        Object value = null;
        String defVal = ann.defaultValue();
        switch (ann.type()) {
        case String:
        case Password:
            value = ann.defaultValue();
            break;
        case Integer:
            value = defVal.isEmpty() ? null : Integer.valueOf(defVal);
            break;
        case Long:
            value = defVal.isEmpty() ? null : Long.valueOf(defVal);
            break;
        case Float:
            value = defVal.isEmpty() ? null : Float.valueOf(defVal);
            break;
        case Double:
            value = defVal.isEmpty() ? null : Double.valueOf(defVal);
            break;
        case Boolean:
            value = defVal.isEmpty() ? null : Boolean.valueOf(defVal);
            break;
        case Date:
            value = defVal.isEmpty() ? null : Long.valueOf(defVal);
            break;
        case Path:
            value = defVal.isEmpty() ? null : Paths.get(defVal);
            break;
        case json:
            try {
                value = defVal.isEmpty() ? null : new ObjectMapper().readValue(defVal, Object.class);
            } catch (Exception ignored) {
            }
            break;
        }
        return new Setting(key, value, ann.type(), ann.description(), ann.cached(), ann.web(), ann.editable());
    }

}