001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2017-2018 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 com.electronwill.nightconfig.core.file.FileConfig;
022import java.io.File;
023import java.io.IOException;
024import org.jgrapes.core.Channel;
025import org.jgrapes.core.EventPipeline;
026import org.jgrapes.core.events.Start;
027import org.jgrapes.util.events.ConfigurationUpdate;
028import org.jgrapes.util.events.FileChanged;
029import org.jgrapes.util.events.InitialPreferences;
030
031/**
032 * This component provides a store for an application's configuration
033 * backed by a YAML file. The YAML object described by this file 
034 * represents the root directory. If an entry does not start with a
035 * slash, it represents the key of a key value pair. If it does
036 * starts with a slash, the value is another JSON object that
037 * describes the respective subdirectory.
038 * 
039 * The component reads the initial values from {@link File} passed
040 * to the constructor. During application bootstrap, it 
041 * intercepts the {@link Start} event using a handler with  priority 
042 * 999999. When receiving this event, it fires all known preferences 
043 * values on the channels of the start event as a 
044 * {@link InitialPreferences} event, using a new {@link EventPipeline}
045 * and waiting for its completion. Then, allows the intercepted 
046 * {@link Start} event to continue. 
047 * 
048 * Components that depend on configuration values define handlers
049 * for {@link ConfigurationUpdate} events and adapt themselves to the values 
050 * received. Note that due to the intercepted {@link Start} event, the initial
051 * preferences values are received before the {@link Start} event, so
052 * components' configurations can be rearranged before they actually
053 * start doing something.
054 *
055 * Besides initially publishing the stored preferences values,
056 * the component also listens for {@link ConfigurationUpdate} events
057 * on its channel and updates the YAML file (may be suppressed).
058 */
059@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.AvoidDuplicateLiterals",
060    "PMD.GodClass" })
061public class YamlConfigurationStore extends NightConfigStore {
062
063    /**
064     * Creates a new component with its channel set to the given 
065     * channel and the given file. The component handles
066     * {@link ConfigurationUpdate} events and {@link FileChanged}
067     * events for the configuration file (see
068     * @link #NightConfigStore(Channel, File, boolean, boolean)}
069     * 
070     * @param componentChannel the channel 
071     * @param file the file used to store the configuration
072     * @throws IOException
073     */
074    public YamlConfigurationStore(Channel componentChannel, File file)
075            throws IOException {
076        this(componentChannel, file, true);
077    }
078
079    /**
080     * Creates a new component with its channel set to the given 
081     * channel and the given file. The component handles
082     * {@link FileChanged} events for the configuration file (see
083     * @link #NightConfigStore(Channel, File, boolean, boolean)}
084     * 
085     * If `update` is `true`, the configuration file is updated
086     * when {@link ConfigurationUpdate} events are received.  
087     *
088     * @param componentChannel the channel
089     * @param file the file used to store the configuration
090     * @param update if the configuration file is to be updated
091     * @throws IOException Signals that an I/O exception has occurred.
092     */
093    @SuppressWarnings("PMD.ShortVariable")
094    public YamlConfigurationStore(Channel componentChannel, File file,
095            boolean update) throws IOException {
096        this(componentChannel, file, update, true);
097    }
098
099    /**
100     * Creates a new component with its channel set to the given 
101     * channel and the given file.
102     * 
103     * If `update` is `true`, the configuration file is updated
104     * when {@link ConfigurationUpdate} events are received.  
105     * 
106     * If `watch` is `true`, {@link FileChanged} events are processed
107     * and the configuration file is reloaded when it changes. Note
108     * that the generation of the {@link FileChanged} events must
109     * be configured independently (see {@link FileSystemWatcher}).
110     *
111     * @param componentChannel the channel
112     * @param file the file used to store the configuration
113     * @param update if the configuration file is to be updated
114     * @param watch if {@link FileChanged} events are to be processed
115     * @throws IOException Signals that an I/O exception has occurred.
116     */
117    @SuppressWarnings("PMD.ShortVariable")
118    public YamlConfigurationStore(Channel componentChannel, File file,
119            boolean update, boolean watch) throws IOException {
120        super(componentChannel, file, update, watch);
121        // Force load of YamlFormat and its registration. Required for OSGi.
122        com.electronwill.nightconfig.yaml.YamlFormat.defaultInstance();
123        config = FileConfig.builder(file.getAbsolutePath()).sync().concurrent()
124            .build();
125        config.load();
126    }
127
128}