001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2017-2022 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.util.events;
020
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.Map;
025import java.util.Optional;
026import java.util.Set;
027import java.util.concurrent.ConcurrentHashMap;
028import java.util.stream.Collectors;
029import org.jgrapes.core.Event;
030import org.jgrapes.core.Manager;
031import org.jgrapes.util.ConfigurationStore;
032
033/**
034 * An event to indicate that configuration information has been
035 * updated.
036 * 
037 * Configuration information provided by this event is organized
038 * by paths and associated key/value pairs. The path information
039 * should be used by components to select the information important
040 * to them. Often, a component simply matches the path from the event
041 * with its own path in the component hierarchy 
042 * (see {@link Manager#componentPath()}). But paths can also be used
043 * to structure information in a way that is completely independent of
044 * the implementation's structure as the filtering is completely up
045 * to the component.
046 */
047@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
048public class ConfigurationUpdate extends Event<Void> {
049
050    @SuppressWarnings("PMD.UseConcurrentHashMap")
051    private final Map<String, Map<String, Object>> structuredValues
052        = new HashMap<>();
053    private final Map<String, Map<String, Object>> flattenedCache
054        = new ConcurrentHashMap<>();
055
056    /**
057     * Return all paths affected by this event.
058     * 
059     * @return the paths
060     */
061    @SuppressWarnings("PMD.ConfusingTernary")
062    public Set<String> paths() {
063        synchronized (structuredValues) {
064            return new HashSet<>(structuredValues.keySet());
065        }
066    }
067
068    private Optional<Map<String, Object>> flattened(String path) {
069        return Optional.ofNullable(flattenedCache.computeIfAbsent(path,
070            p -> ConfigurationStore.flatten(structuredValues.get(path))));
071    }
072
073    /**
074     * Return the properties for a given path if any exist.
075     * If a property has a structured value (list or collection),
076     * the values are returned as several entries as described in
077     * {@link ConfigurationStore#flatten(Map)}. All values are
078     * converted to their string representation.
079     * 
080     * @param path the path
081     * @return the updated values or `null` if the path has been
082     * removed (implies the removal of all values for that path).
083     */
084    public Optional<Map<String, String>> values(String path) {
085        if (structuredValues.get(path) == null) {
086            return Optional.empty();
087        }
088        Map<String, Object> result = flattened(path).get();
089        return Optional.of(result).map(o -> o.entrySet().stream()
090            .collect(
091                Collectors.toMap(Map.Entry::getKey, e -> ConfigurationStore
092                    .as(e.getValue(), String.class).orElse(null))));
093    }
094
095    /**
096     * Return the value with the given path and key if it exists
097     * and is of or can be converted to the requested type.
098     *
099     * @param <T> the generic type
100     * @param path the path
101     * @param key the key
102     * @param as the as
103     * @return the optional
104     */
105    @SuppressWarnings("PMD.ShortVariable")
106    public <T> Optional<T> value(String path, String key, Class<T> as) {
107        return flattened(path)
108            .flatMap(map -> ConfigurationStore.as(map.get(key), as));
109    }
110
111    /**
112     * Return the value with the given path and key if it exists as string.
113     * 
114     * @param path the path
115     * @param key the key
116     * @return the value
117     */
118    public Optional<String> value(String path, String key) {
119        return value(path, key, String.class);
120    }
121
122    /**
123     * Return the properties for a given path if they exists as
124     * a map with (possibly) structured values (see 
125     * {@link ConfigurationStore#structured(String)}). The type
126     * of the value depends on the configuration store used.
127     * Some configuration stores support types other than string,
128     * some don't. Too avoid any problems, it is strongly recommended
129     * to call {@link ConfigurationStore#as(Object, Class)} for any
130     * value obtained from the result of this method.
131     * 
132     * @param path the path
133     * @return the updated values or `null` if the path has been
134     * removed (implies the removal of all values for that path).
135     */
136    public Optional<Map<String, Object>> structured(String path) {
137        if (structuredValues.get(path) == null) {
138            return Optional.empty();
139        }
140        return Optional
141            .of(Collections.unmodifiableMap(structuredValues.get(path)));
142    }
143
144    /**
145     * Set new (updated), possibly structured configuration values (see
146     * {@link ConfigurationStore#structure(Map)} for the given path.
147     * Any information associated with the path before the invocation
148     * of this method is replaced.  
149     * 
150     * @param path the value's path
151     * @return the event for easy chaining
152     */
153    @SuppressWarnings("unchecked")
154    public ConfigurationUpdate set(String path, Map<String, ?> values) {
155        if (path == null || !path.startsWith("/")) {
156            throw new IllegalArgumentException("Path must start with \"/\".");
157        }
158        structuredValues.put(path, (Map<String, Object>) values);
159        return this;
160    }
161
162    /**
163     * Add a new (or updated) configuration value for the given path
164     * and key.
165     * 
166     * @param path the value's path
167     * @param selector the key or the path within the structured value
168     * @param value the value
169     * @return the event for easy chaining
170     */
171    public ConfigurationUpdate add(String path, String selector, Object value) {
172        if (path == null || !path.startsWith("/")) {
173            throw new IllegalArgumentException("Path must start with \"/\".");
174        }
175        synchronized (structuredValues) {
176            @SuppressWarnings("PMD.UseConcurrentHashMap")
177            Map<String, Object> scoped = structuredValues
178                .computeIfAbsent(path, newKey -> new HashMap<String, Object>());
179            ConfigurationStore.mergeValue(scoped, selector, value);
180            flattenedCache.remove(path);
181        }
182        return this;
183    }
184
185    /**
186     * Associate the given path with `null`. This signals to handlers 
187     * that the path has been removed from the configuration.
188     * 
189     * @param path the path that has been removed
190     * @return the event for easy chaining
191     */
192    public ConfigurationUpdate removePath(String path) {
193        if (path == null || !path.startsWith("/")) {
194            throw new IllegalArgumentException("Path must start with \"/\".");
195        }
196        structuredValues.put(path, null);
197        return this;
198    }
199
200}