001package jmri.util.prefs;
002
003import java.util.ArrayList;
004import java.util.HashMap;
005import java.util.HashSet;
006import java.util.List;
007import java.util.Set;
008import java.util.stream.Collectors;
009import javax.annotation.Nonnull;
010import jmri.InstanceManager;
011import jmri.beans.Bean;
012import jmri.jmrix.ConnectionConfigManager;
013import jmri.profile.Profile;
014import jmri.spi.PreferencesManager;
015
016/**
017 * An abstract PreferencesManager that implements some of the boilerplate that
018 * PreferencesManager implementations would otherwise require.
019 *
020 * @author Randall Wood (C) 2015
021 */
022public abstract class AbstractPreferencesManager extends Bean implements PreferencesManager {
023
024    private final HashMap<Profile, Boolean> initialized = new HashMap<>();
025    private final HashMap<Profile, Boolean> initializing = new HashMap<>();
026    private final HashMap<Profile, List<Exception>> exceptions = new HashMap<>();
027
028    /**
029     * {@inheritDoc}
030     */
031    @Override
032    public boolean isInitialized(Profile profile) {
033        return this.initialized.getOrDefault(profile, false)
034                && this.exceptions.getOrDefault(profile, new ArrayList<>()).isEmpty();
035    }
036
037    /**
038     * {@inheritDoc}
039     */
040    @Override
041    public boolean isInitializedWithExceptions(Profile profile) {
042        return this.initialized.getOrDefault(profile, false)
043                && !this.exceptions.getOrDefault(profile, new ArrayList<>()).isEmpty();
044    }
045
046    /**
047     * {@inheritDoc}
048     */
049    @Override
050    @Nonnull
051    public List<Exception> getInitializationExceptions(Profile profile) {
052        return new ArrayList<>(this.exceptions.getOrDefault(profile, new ArrayList<>()));
053    }
054
055    /**
056     * Test if the manager is being initialized.
057     *
058     * @param profile the profile against which the manager is being initialized
059     *                or null if being initialized for this user regardless of
060     *                profile
061     * @return true if being initialized; false otherwise
062     */
063    protected boolean isInitializing(Profile profile) {
064        return !this.initialized.getOrDefault(profile, false) && this.initializing.getOrDefault(profile, false);
065    }
066
067    /**
068     * {@inheritDoc}
069     * <p>
070     * This implementation includes a default dependency on the
071     * {@link jmri.jmrix.ConnectionConfigManager}.
072     *
073     * @return An set of classes; if there are no dependencies, return an empty
074     *         set instead of null; overriding implementations may add to this
075     *         set directly
076     */
077    @Override
078    @Nonnull
079    public Set<Class<? extends PreferencesManager>> getRequires() {
080        Set<Class<? extends PreferencesManager>> requires = new HashSet<>();
081        requires.add(ConnectionConfigManager.class);
082        return requires;
083    }
084
085    /**
086     * {@inheritDoc}
087     * <p>
088     * This implementation returns the class of the object against which this
089     * method is called.
090     */
091    @Override
092    @Nonnull
093    public Set<Class<?>> getProvides() {
094        Set<Class<?>> provides = new HashSet<>();
095        provides.add(this.getClass());
096        return provides;
097    }
098
099    /**
100     * Set the initialized state for the given profile. Sets
101     * {@link #isInitializing(jmri.profile.Profile)} to false if setting
102     * initialized to false.
103     *
104     * @param profile     the profile to set initialized against
105     * @param initialized the initialized state to set
106     */
107    protected void setInitialized(Profile profile, boolean initialized) {
108        this.initialized.put(profile, initialized);
109        if (initialized) {
110            this.setInitializing(profile, false);
111        }
112    }
113
114    /**
115     * Protect against circular attempts to initialize during initialization.
116     *
117     * @param profile      the profile for which initializing is ongoing
118     * @param initializing the initializing state to set
119     */
120    protected void setInitializing(Profile profile, boolean initializing) {
121        this.initializing.put(profile, initializing);
122    }
123
124    /**
125     * Add an error to the list of exceptions.
126     *
127     * @param profile   the profile against which the manager is being
128     *                  initialized
129     * @param exception the exception to add
130     */
131    protected void addInitializationException(Profile profile, @Nonnull Exception exception) {
132        if (this.exceptions.get(profile) == null) {
133            this.exceptions.put(profile, new ArrayList<>());
134        }
135        this.exceptions.get(profile).add(exception);
136    }
137
138    /**
139     * Require that instances of the specified classes have initialized
140     * correctly. This method should only be called from within
141     * {@link #initialize(jmri.profile.Profile)}, generally immediately after
142     * the PreferencesManager verifies that it is not already initialized. If
143     * this method is within a try-catch block, the exception it generates
144     * should be re-thrown by initialize(profile).
145     *
146     * @param profile the profile against which the manager is being initialized
147     * @param classes the manager classes for which all calling
148     *                {@link #isInitialized(jmri.profile.Profile)} must return
149     *                true against all instances of
150     * @param message the localized message to display if an
151     *                InitializationExcpetion is thrown
152     * @throws InitializationException  if any instance of any class in classes
153     *                                  returns false on isIntialized(profile)
154     * @throws IllegalArgumentException if any member of classes is not also in
155     *                                  the set of classes returned by
156     *                                  {@link #getRequires()}
157     */
158    protected void requiresNoInitializedWithExceptions(Profile profile,
159            @Nonnull Set<Class<? extends PreferencesManager>> classes, @Nonnull String message)
160            throws InitializationException {
161        classes.stream().filter((clazz) -> (!this.getRequires().contains(clazz))).forEach((clazz) -> {
162            throw new IllegalArgumentException(
163                    "Class " + clazz.getClass().getName() + " not marked as required by " + this.getClass().getName());
164        });
165        for (Class<? extends PreferencesManager> clazz : classes) {
166            for (PreferencesManager instance : InstanceManager.getList(clazz)) {
167                if (instance.isInitializedWithExceptions(profile)) {
168                    InitializationException exception = new InitializationException("Refusing to initialize", message);
169                    this.addInitializationException(profile, exception);
170                    this.setInitialized(profile, true);
171                    throw exception;
172                }
173            }
174        }
175    }
176
177    /**
178     * Require that instances of the specified classes have initialized
179     * correctly. This method should only be called from within
180     * {@link #initialize(jmri.profile.Profile)}, generally immediately after
181     * the PreferencesManager verifies that it is not already initialized. If
182     * this method is within a try-catch block, the exception it generates
183     * should be re-thrown by initialize(profile). This calls
184     * {@link #requiresNoInitializedWithExceptions(jmri.profile.Profile, java.util.Set, java.lang.String)}
185     * with the result of {@link #getRequires()} as the set of classes to
186     * require.
187     *
188     * @param profile the profile against which the manager is being initialized
189     * @param message the localized message to display if an
190     *                InitializationExcpetion is thrown
191     * @throws InitializationException if any instance of any class in classes
192     *                                 returns false on isIntialized(profile)
193     */
194    protected void requiresNoInitializedWithExceptions(Profile profile, @Nonnull String message)
195            throws InitializationException {
196        this.requiresNoInitializedWithExceptions(profile, this.getRequires(), message);
197    }
198
199    /**
200     * Convenience method to allow a PreferencesManager to require all other
201     * PreferencesManager in an attempt to be the last PreferencesManager
202     * initialized. Use this method as the body of {@link #getRequires()}.
203     * <p>
204     * <strong>Note</strong> given a set of PreferencesManagers using this
205     * method as the body of {@link #getRequires()}, the order in which those
206     * PreferencesManagers are initialized is non-deterministic.
207     *
208     * @return a set of all PreferencesManagers registered with the
209     *         InstanceManager except this one
210     */
211    @Nonnull
212    protected Set<Class<? extends PreferencesManager>> requireAllOther() {
213        return InstanceManager.getList(PreferencesManager.class).stream()
214                .filter(pm -> !pm.equals(this))
215                .map(pm -> pm.getClass())
216                .collect(Collectors.toSet());
217    }
218}