001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2017–2019 microBean.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.configuration.spi;
018
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.Map;
022import java.util.Objects;
023import java.util.Set;
024
025import java.util.function.Function;
026import java.util.function.Supplier;
027
028import org.microbean.configuration.api.ConfigurationValue;
029
030/**
031 * An {@link AbstractConfiguration} that flexibly loads some kind of
032 * resource, or uses a previously loaded one, to satisfy demands for
033 * configuration property values.
034 *
035 * @author <a href="https://about.me/lairdnelson"
036 * target="_parent">Laird Nelson</a>
037 *
038 * @see #AbstractResourceLoadingConfiguration(Function)
039 *
040 * @see #getValue(Resource, Map, String)
041 */
042public abstract class AbstractResourceLoadingConfiguration<T> extends AbstractConfiguration implements Ranked {
043
044
045  /*
046   * Instance fields.
047   */
048
049
050  private final Function<? super Map<? extends String, ? extends String>, ? extends Resource<? extends T>> resourceLoader;
051
052
053  /*
054   * Constructors.
055   */
056  
057
058  /**
059   * Creates a new {@link AbstractResourceLoadingConfiguration}.
060   *
061   * @param resourceLoader a {@link Function} that accepts a {@link
062   * Map} of requested configuration coordinates and returns a {@link
063   * Resource} that can {@linkplain Resource#get() supply} a source of
064   * configuration values for use by the {@link #getValue(Resource,
065   * Map, String)} method; may be {@code null} in which case all
066   * invocations of the {@link #getValue(Map, String)} method will
067   * return {@code null}
068   *
069   * @see #getValue(Resource, Map, String)
070   *
071   * @see #getValue(Map, String)
072   *
073   * @see Resource
074   */
075  protected AbstractResourceLoadingConfiguration(final Function<? super Map<? extends String, ? extends String>, ? extends Resource<? extends T>> resourceLoader) {
076    super();
077    this.resourceLoader = resourceLoader;
078  }
079
080
081  /*
082   * Instance methods.
083   */
084
085
086  @Override
087  public int getRank() {
088    final int returnValue;
089    if (this.resourceLoader == null) {
090      returnValue = 100;
091    } else {
092      returnValue = this.getRank(this.resourceLoader.apply(null));
093    }
094    return returnValue;
095  }
096
097  protected int getRank(final Resource<? extends T> resource) {
098    return 100;
099  }
100  
101  /**
102   * {@inheritDoc}
103   *
104   * <p>This implementation calls the {@link #getValue(Resource, Map,
105   * String)} method with the return value resulting from the
106   * invocation of the {@link Function} supplied {@linkplain
107   * #AbstractResourceLoadingConfiguration(Function) at construction
108   * time}.
109   *
110   * @param coordinates the requested configuration coordinates; may be {@code null}
111   *
112   * @param name the name of a configuration property for which a
113   * value should be returned; must not be {@code null}
114   *
115   * @exception NullPointerException if {@code name} is {@code null}
116   *
117   * @see #getValue(Resource, Map, String)
118   */
119  @Override
120  public ConfigurationValue getValue(final Map<String, String> coordinates, final String name) {
121    final ConfigurationValue returnValue;
122    if (this.resourceLoader == null) {
123      returnValue = null;
124    } else {
125      returnValue = this.getValue(this.resourceLoader.apply(coordinates), coordinates, name);
126    }
127    return returnValue;
128  }
129
130  @Override
131  public Set<String> getNames() {
132    final Set<String> returnValue;
133    if (this.resourceLoader == null) {
134      returnValue = Collections.emptySet();
135    } else {
136      returnValue = this.getNames(this.resourceLoader.apply(null));
137    }
138    return returnValue;
139  }
140
141  /**
142   * Returns a {@link ConfigurationValue} suitable for the supplied
143   * {@code name} normally sourced in some fashion from the supplied
144   * {@link Resource}, or {@code null} if no such {@link
145   * ConfigurationValue} can be found.
146   *
147   * <p>Implementations of this method are permitted to return {@code
148   * null}.</p>
149   *
150   * <p>Implementations of this method must not call the {@link
151   * #getValue(Map, String)} method or undefined behavior will
152   * result.</p>
153   *
154   * @param resource a {@link Resource} hopefully providing access to
155   * the ultimate source of configuration values for the requested
156   * coordinates; must not be {@code null}
157   *
158   * @param requestedCoordinates for convenience, the same {@link Map}
159   * supplied to the {@link #getValue(Map, String)} method is supplied
160   * here representing the configuration coordinates for which a value
161   * is requested; note that these may very well be different from
162   * {@linkplain Resource#getCoordinates() the configuration
163   * coordinates actually pertaining to the resource}; most
164   * implementations will not need to reference this parameter
165   *
166   * @param name the name of the configuration property for which a
167   * value is to be sought; must not be {@code null}
168   *
169   * @return a suitable {@link ConfigurationValue} or {@code null}
170   *
171   * @exception NullPointerException if {@code resource} or {@code
172   * name} is {@code null}
173   *
174   * @see #getValue(Map, String)
175   *
176   * @see Resource
177   */
178  protected abstract ConfigurationValue getValue(final Resource<? extends T> resource, final Map<String, String> requestedCoordinates, final String name);
179
180  protected abstract Set<String> getNames(final Resource<? extends T> resource);
181
182  /*
183   * Inner and nested classes.
184   */
185
186
187  /**
188   * A {@link Supplier} of a particular kind of resource from which
189   * configuration property values may be retrieved.
190   *
191   * @author <a href="https://about.me/lairdnelson"
192   * target="_parent">Laird Nelson</a>
193   */
194  public static final class Resource<T> implements Supplier<T> {
195
196
197    /*
198     * Instance fields.
199     */
200
201    
202    private final T resource;
203
204    private final Map<String, String> coordinates;
205
206
207    /*
208     * Constructors.
209     */
210
211
212    /**
213     * Creates a new {@link Resource}.
214     *
215     * @param resource the actual underlying source of configuration
216     * property values; may be {@code null}
217     *
218     * @param coordinates the configuration coordinates this {@link
219     * Resource} provides values for; may be {@code null}
220     */
221    public Resource(final T resource, final Map<String, String> coordinates) {
222      super();
223      this.resource = resource;
224      if (coordinates == null) {
225        this.coordinates = null;
226      } else if (coordinates.isEmpty()) {
227        this.coordinates = Collections.emptyMap();
228      } else {
229        this.coordinates = Collections.unmodifiableMap(new HashMap<>(coordinates));
230      }
231    }
232
233
234    /*
235     * Instance methods.
236     */
237    
238
239    /**
240     * Returns the actual underlying source of configuration property
241     * values, typically for use by implementations of the {@link
242     * AbstractResourceLoadingConfiguration#getValue(Resource, Map,
243     * String)} method.
244     *
245     * <p>This method may return {@code null}.</p>
246     *
247     * @return the the actual underlying source of configuration
248     * property values, or {@code null}
249     */
250    public final T get() {
251      return this.resource;
252    }
253
254    /**
255     * Returns an {@linkplain Collections#unmodifiableMap(Map)
256     * immutable} {@link Map} representing the configuration
257     * coordinates for which this {@link Resource} can assist in
258     * providing values.
259     *
260     * <p>This method may return {@code null}.</p>
261     *
262     * @return the configuration coordinates, or {@code null}
263     */
264    public final Map<String, String> getCoordinates() {
265      return this.coordinates;
266    }
267
268    /**
269     * Returns a hashcode for this {@link Resource}.
270     *
271     * @return a hashcode for this {@link Resource}
272     *
273     * @see #equals(Object)
274     */
275    @Override
276    public int hashCode() {
277      int hashCode = 17;
278
279      Object resource = this.get();
280      int c = resource == null ? 0 : resource.hashCode();
281      hashCode = 37 * hashCode + c;
282
283      Object coordinates = this.getCoordinates();
284      c = coordinates == null ? 0 : coordinates.hashCode();
285      hashCode = 37 * hashCode + c;
286      
287      return hashCode;
288    }
289
290    /**
291     * Returns {@code true} if this {@link Resource} is equal to the
292     * supplied {@link Object}.
293     *
294     * @param other the {@link Object} to test; may be {@code null} in
295     * which case {@code false} will be returned
296     *
297     * @return {@code true} if this {@link Resource} is equal to the
298     * supplied {@link Object}; {@code false} otherwise
299     */
300    @Override
301    public boolean equals(final Object other) {
302      if (this == other) {
303        return true;
304      } else if (other instanceof Resource) {
305        final Resource<?> her = (Resource<?>)other;
306
307        final Object myResource = this.get();
308        if (myResource == null) {
309          if (her.get() != null) {
310            return false;
311          }
312        } else if (!myResource.equals(her.get())) {
313          return false;
314        }
315
316        final Object coordinates = this.getCoordinates();
317        if (coordinates == null) {
318          if (her.getCoordinates() != null) {
319            return false;
320          }
321        } else if (!coordinates.equals(her.getCoordinates())) {
322          return false;
323        }
324
325        return true;
326      } else {
327        return false;
328      }
329    }
330    
331  }
332  
333}