001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 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;
020
021import java.math.BigDecimal;
022import java.time.Instant;
023import java.time.format.DateTimeParseException;
024import java.time.temporal.TemporalAccessor;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.HashMap;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Map;
031import java.util.Optional;
032import java.util.Queue;
033import java.util.TreeMap;
034import java.util.regex.Pattern;
035import java.util.stream.Collectors;
036import org.jgrapes.core.Channel;
037import org.jgrapes.core.Component;
038import org.jgrapes.core.Event;
039import org.jgrapes.core.Manager;
040import org.jgrapes.core.annotation.Handler;
041import org.jgrapes.core.annotation.HandlerDefinition.ChannelReplacements;
042import org.jgrapes.util.events.InitialConfiguration;
043
044/**
045 * A base class for configuration stores. Implementing classes must
046 * override one of the methods {@link #structured(String)} or
047 * {@link #values(String)} as the default implementations of either
048 * calls the other. 
049 */
050@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.GodClass" })
051public abstract class ConfigurationStore extends Component {
052
053    public static final Pattern NUMBER = Pattern.compile("^\\d+$");
054
055    /**
056     * Creates a new component with its channel set to itself.
057     */
058    public ConfigurationStore() {
059        // Nothing to do.
060    }
061
062    /**
063     * Creates a new component base with its channel set to the given 
064     * channel. As a special case {@link Channel#SELF} can be
065     * passed to the constructor to make the component use itself
066     * as channel. The special value is necessary as you 
067     * obviously cannot pass an object to be constructed to its 
068     * constructor.
069     *
070     * @param componentChannel the channel that the component's
071     * handlers listen on by default and that 
072     * {@link Manager#fire(Event, Channel...)} sends the event to
073     */
074    public ConfigurationStore(Channel componentChannel) {
075        super(componentChannel);
076    }
077
078    /**
079     * Creates a new component base like {@link #ConfigurationStore(Channel)}
080     * but with channel mappings for {@link Handler} annotations.
081     *
082     * @param componentChannel the channel that the component's
083     * handlers listen on by default and that 
084     * {@link Manager#fire(Event, Channel...)} sends the event to
085     * @param channelReplacements the channel replacements to apply
086     * to the `channels` elements of the {@link Handler} annotations
087     */
088    public ConfigurationStore(Channel componentChannel,
089            ChannelReplacements channelReplacements) {
090        super(componentChannel, channelReplacements);
091    }
092
093    /**
094     * Configuration information should be kept simple. Sometimes, 
095     * however, it is unavoidable to structure the information 
096     * associated with a (logical) key. This can be done by 
097     * reflecting the structure in the names of actual keys, derived
098     * from the logical key. Names such as "key.0", "key.1", "key.2" 
099     * can be used to express that the value associated with "key" 
100     * is a list of values. "key.a", "key.b", "key.c" can be used 
101     * to associate "key" with a map from "a", "b", "c" to some values.
102     * 
103     * This methods looks at all values in the map passed as
104     * argument. If the value is a collection or map, the entry is
105     * converted to several entries following the pattern outlined
106     * above.
107     *
108     * @param structured the map with possibly structured properties
109     * @return the map with flattened properties
110     */
111    public static Map<String, Object> flatten(Map<String, ?> structured) {
112        @SuppressWarnings("PMD.UseConcurrentHashMap")
113        Map<String, Object> result = new HashMap<>();
114        flattenObject(result, null, structured);
115        return result;
116    }
117
118    @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" })
119    private static void flattenObject(Map<String, Object> result,
120            String prefix, Object value) {
121        if (value instanceof Map) {
122            for (var entry : ((Map<Object, Object>) value).entrySet()) {
123                if (entry.getKey().toString().startsWith("/")) {
124                    continue;
125                }
126                flattenObject(result,
127                    Optional.ofNullable(prefix).map(p -> p + ".").orElse("")
128                        + entry.getKey(),
129                    entry.getValue());
130            }
131            return;
132        }
133        if (value instanceof Collection) {
134            int count = 0;
135            for (var item : (Collection<?>) value) {
136                flattenObject(result, prefix + "." + count++, item);
137            }
138            return;
139        }
140        result.put(prefix, value);
141    }
142
143    /**
144     * Same as {@link #structure(Map, boolean)} with `false` as
145     * second argument.
146     *
147     * @param flatProperties the flat properties
148     * @return a map with structured values
149     */
150    @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
151        "PMD.ReturnEmptyCollectionRatherThanNull" })
152    public static Map<String, Object> structure(Map<String, ?> flatProperties) {
153        return structure(flatProperties, false);
154    }
155
156    /**
157     * The reverse operation to {@link #flatten(Map)}. Entries with
158     * key names matching the pattern outlined in {@link #flatten(Map)}
159     * are combined to a single entry with a structured value (map or
160     * list).
161     *
162     * Usually, only key patterns with consecutive numbers starting 
163     * with zero are converted to lists (e.g. `key.0`, `key.1`, `key.2`).
164     * If entries are missing, the values at that level are converted to 
165     * a `Map<Integer,Object>` with the given entries instead. If 
166     * `convertSparse` is `true`, incomplete index sets such as `key.2`,
167     * `key.3`, `key.5` are converted to lists with the available number 
168     * of elements despite the missing entries. 
169     * 
170     * If the derived class overrides {@link #structured(String)},
171     * the leaf values in the returned structure are the values
172     * provided by the overriding implementation (while 
173     * {@link #values(String))} always provides {@link String}s). 
174     * Some configuration formats define types other then string and
175     * therefore value can be e.g. {@link Integer}s or {@link Instant}s.
176     * In order to support the usage of arbitrary configuration store
177     * implementations, values obtained from the data structure returned
178     * by {@link #structure(Map, boolean)} should always be passed 
179     * through {@link #as(Object, Class)}. This method preserves
180     * non-string objects if they match the requested type or
181     * converts the value from its string representation to the
182     * requested type, if possible.
183     *
184     * @param flatProperties the flat properties
185     * @param convertSparse controls conversion to lists
186     * @return a map with structured values
187     */
188    @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
189        "PMD.ReturnEmptyCollectionRatherThanNull" })
190    public static Map<String, Object>
191            structure(Map<String, ?> flatProperties, boolean convertSparse) {
192        if (flatProperties == null) {
193            return null;
194        }
195        @SuppressWarnings("PMD.UseConcurrentHashMap")
196        Map<String, Object> result = new HashMap<>();
197        for (var entry : flatProperties.entrySet()) {
198            // Original key (optionally) consists of dot separated parts
199            var parts = new LinkedList<>(List.of(entry.getKey()
200                .split("\\.(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1)));
201            mergeValue(result, parts, entry.getValue());
202        }
203
204        // Now convert all maps that have only Integer keys to lists
205        for (var entry : result.entrySet()) {
206            entry.setValue(maybeConvert(entry.getValue(), convertSparse));
207        }
208
209        // Return result
210        return result;
211    }
212
213    /**
214     * Similar to {@link ConfigurationStore#structure(Map)} but merges
215     * only a single value into an existing map.
216     *
217     * @param target the target
218     * @param selector the path selector
219     * @param value the value
220     * @return the map
221     */
222    @SuppressWarnings("unchecked")
223    public static Map<String, Object> mergeValue(Map<?, ?> target,
224            String selector, Object value) {
225        // Original key (optionally) consists of dot separated parts
226        var parts = new LinkedList<>(List.of(selector
227            .split("\\.(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1)));
228        mergeValue(target, parts, value);
229
230        // Now convert all maps that have only Integer keys to lists
231        for (var entry : ((Map<String, Object>) target).entrySet()) {
232            entry.setValue(maybeConvert(entry.getValue(), false));
233        }
234        return (Map<String, Object>) target;
235    }
236
237    @SuppressWarnings("unchecked")
238    private static void mergeValue(Map<?, ?> target, Queue<String> parts,
239            Object value) {
240        var part = parts.poll();
241        if (part.startsWith("\"") && part.endsWith("\"")) {
242            part = part.substring(1, part.length() - 1);
243        }
244        Object key = NUMBER.matcher(part).find()
245            ? Integer.parseInt(part)
246            : part;
247        if (parts.isEmpty()) {
248            // Last part (of key), store value
249            ((Map<Object, Object>) target).put(key, value);
250            return;
251        }
252        var newTarget = ((Map<Object, Object>) target)
253            .computeIfAbsent(key, k -> new TreeMap<Object, Object>());
254
255        // Convert list to map
256        if (newTarget instanceof List list) {
257            var asMap = new TreeMap<>();
258            for (var item : list) {
259                asMap.put(asMap.size(), item);
260            }
261            newTarget = asMap;
262            ((Map<Object, Object>) target).put(key, newTarget);
263        }
264        mergeValue((Map<Object, Object>) newTarget, parts, value);
265    }
266
267    @SuppressWarnings({ "unchecked", "PMD.ConfusingTernary" })
268    private static Object maybeConvert(Object value, boolean convertSparse) {
269        if (!(value instanceof TreeMap)) {
270            return value;
271        }
272        List<Object> converted = new ArrayList<>();
273        for (var entry : ((Map<Object, Object>) value).entrySet()) {
274            entry.setValue(maybeConvert(entry.getValue(), convertSparse));
275            if (converted == null) {
276                continue;
277            }
278            if (!(entry.getKey() instanceof Integer)
279                || !convertSparse
280                    && ((Integer) entry.getKey()) != converted.size()) {
281                // Don't convert, leave as Map.
282                converted = null;
283                continue;
284            }
285            converted.add(entry.getValue());
286        }
287        return converted != null ? converted : value;
288    }
289
290    /**
291     * Return the values for a given path if they exist. This
292     * method should only be used in cases where configuration values
293     * are needed before the {@link InitialConfiguration} event is
294     * fired, e.g. while creating the component tree. 
295     * 
296     * @param path the path
297     * @return the values, if defined for the given path
298     */
299    public Optional<Map<String, String>> values(String path) {
300        return structured(path).map(ConfigurationStore::flatten)
301            .map(o -> o.entrySet().stream()
302                .collect(Collectors.toMap(Map.Entry::getKey,
303                    e -> e.getValue().toString())));
304    }
305
306    /**
307     * Return the properties for a given path if they exists
308     * as structured data, see {@link #structure(Map)}.
309     * 
310     * @param path the path
311     * @return the values, if defined for the given path
312     */
313    public Optional<Map<String, Object>> structured(String path) {
314        return values(path).map(ConfigurationStore::structure);
315    }
316
317    /**
318     * If the value is not `null`, return it as the requested type.
319     * The method is successful if the value already is of the
320     * requested type (or a subtype) or if the value is of type
321     * {@link String} and can be converted to the requested type. 
322     * 
323     * Supported types are:
324     * * {@link String}
325     * * {@link Number}, converts from {@link String} using
326     *   {@link BigDecimal#BigDecimal(String)}
327     * * {@link Instant}, converts from {@link TemporalAccessor}
328     *   or from {@link String} using {@link Instant#parse(CharSequence)) 
329     * * `Boolean`, converts from {@link String} using
330     *   {@link Boolean#valueOf(String)}
331     * 
332     * @return the value
333     */
334    @SuppressWarnings({ "unchecked", "PMD.ShortMethodName",
335        "PMD.NPathComplexity" })
336    public static <T> Optional<T> as(Object value, Class<T> requested) {
337        // Handle null.
338        if (value == null) {
339            return Optional.empty();
340        }
341        // Is of requested type?
342        if (requested.isAssignableFrom(value.getClass())) {
343            return Optional.of((T) value);
344        }
345        // Convert to Instant, if requested.
346        if (requested.equals(Instant.class)) {
347            if (value instanceof TemporalAccessor) {
348                return Optional.of((T) Instant.from((TemporalAccessor) value));
349            }
350            try {
351                return Optional.of((T) Instant.parse(value.toString()));
352            } catch (DateTimeParseException e) {
353                return Optional.empty();
354            }
355        }
356        // Convert to String, if requested.
357        if (requested.equals(String.class)) {
358            return Optional.of((T) value.toString());
359        }
360        // Remaining conversions require a string representation.
361        if (!(value instanceof String)) {
362            return Optional.empty();
363        }
364        if (requested.equals(Number.class)) {
365            try {
366                return Optional.of((T) new BigDecimal((String) value));
367            } catch (NumberFormatException e) {
368                return Optional.empty();
369            }
370        }
371        if (requested.equals(Boolean.class)) {
372            return Optional.of((T) Boolean.valueOf((String) value));
373        }
374        return Optional.empty();
375    }
376
377    /**
378     * Short for `as(value, String.class)`.
379     *
380     * @param value the value
381     * @return the optional
382     */
383    public static Optional<String> asString(Object value) {
384        return as(value, String.class);
385    }
386
387    /**
388     * Short for `as(value, Number.class)`.
389     *
390     * @param value the value
391     * @return the optional
392     */
393    public static Optional<Number> asNumber(Object value) {
394        return as(value, Number.class);
395    }
396
397    /**
398     * Short for `as(value, Instant.class)`.
399     *
400     * @param value the value
401     * @return the optional
402     */
403    public static Optional<Instant> asInstant(Object value) {
404        return as(value, Instant.class);
405    }
406
407    /**
408     * Short for `as(value, Boolean.class)`.
409     *
410     * @param value the value
411     * @return the optional
412     */
413    public static Optional<Boolean> asBoolean(Object value) {
414        return as(value, Boolean.class);
415    }
416}