001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2022 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.loader.microprofile.config.configsource;
018
019import java.lang.reflect.Type;
020
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.NoSuchElementException;
028import java.util.Objects;
029
030import org.microbean.loader.api.Loader;
031
032import org.microbean.loader.spi.Provider;
033import org.microbean.loader.spi.Value;
034
035import org.microbean.path.Path;
036import org.microbean.path.Path.Element;
037
038import org.eclipse.microprofile.config.spi.ConfigSource;
039import org.eclipse.microprofile.config.spi.Converter;
040
041/**
042 * A {@link Provider} that uses a <a
043 * href="https://github.com/eclipse/microprofile-config"
044 * target="_parent">MicroProfile Config</a> {@link ConfigSource} and
045 * <a href="https://microprofile.io/microprofile-config/"
046 * target="_parent">MicroProfile Config</a> {@link Converter}s to
047 * {@linkplain #get(Loader, Path) produce} {@link Value}s.
048 *
049 * @author <a href="https://about.me/lairdnelson"
050 * target="_parent">Laird Nelson</a>
051 *
052 * @see #get(Loader, Path)
053 *
054 * @see ConfigSource#getValue(String)
055 *
056 * @see Converter#convert(String)
057 */
058public class ConfigSourceProvider implements Provider {
059
060  private final ConfigSource configSource;
061
062  private final Map<Type, Converter<?>> converters;
063
064  /**
065   * Creates a new {@link ConfigSourceProvider}.
066   *
067   * @param configSource the {@link ConfigSource} whose {@linkplain
068   * ConfigSource#getValue(String) getValue(String)} method will be
069   * used to back {@link Value}s produced by the {@link #get(Loader,
070   * Path)} method; must not be {@code null}
071   *
072   * @param converters a {@link Map} of {@link Converter}s indexed by
073   * their conversion {@link Type}; may be {@code null}
074   *
075   * @exception NullPointerException if {@code configSource} is {@code
076   * null}
077   */
078  public ConfigSourceProvider(final ConfigSource configSource,
079                              final Map<? extends Type, ? extends Converter<?>> converters) {
080    super();
081    this.configSource = Objects.requireNonNull(configSource, "configSource");
082    if (converters == null || converters.isEmpty()) {
083      this.converters = Map.of();
084    } else {
085      this.converters = Map.copyOf(converters);
086    }
087  }
088
089  /**
090   * Creates a new {@link ConfigSourceProvider}.
091   *
092   * @param configSource the {@link ConfigSource} whose {@linkplain
093   * ConfigSource#getValue(String) getValue(String)} method will be
094   * used to back {@link Value}s produced by the {@link #get(Loader,
095   * Path)} method; must not be {@code null}
096   *
097   * @param converterRegistrations a {@link Collection} of {@link
098   * ConverterRegistration}s; may be {@code null}
099   *
100   * @exception NullPointerException if {@code configSource} is {@code
101   * null}
102   *
103   * @see ConverterRegistration
104   */
105  public ConfigSourceProvider(final ConfigSource configSource,
106                              final Collection<? extends ConverterRegistration<?>> converterRegistrations) {
107    super();
108    this.configSource = Objects.requireNonNull(configSource, "configSource");
109    if (converterRegistrations == null || converterRegistrations.isEmpty()) {
110      this.converters = Map.of();
111    } else {
112      final Map<Type, ConverterRegistration<?>> map = new HashMap<>();
113      for (final ConverterRegistration<?> cr : converterRegistrations) {
114        final Type type = cr.type();
115        ConverterRegistration<?> existing = map.get(type);
116        if (existing == null || existing.priority() < cr.priority()) {
117          map.put(type, cr);
118        }
119      }
120      this.converters = Collections.unmodifiableMap(map);
121    }
122  }
123
124  @Override // Provider
125  public final Type lowerBound() {
126    return this.converters.isEmpty() ? String.class : Object.class;
127  }
128
129  @Override // Provider
130  public final Value<?> get(final Loader<?> requestor, final Path<? extends Type> absolutePath) {
131    final Type pathType = absolutePath.qualified();
132    // We could do fancy assignability checks here for converters, but
133    // the MicroProfile Config API makes users expect an exact match.
134    // That is, in the native MicroProfile Config API, users asking
135    // for a CharSequence conversion expect that if there is not a
136    // Converter registered for CharSequence explicitly, even if there
137    // is one registered for, say, String, the call will fail.
138    //
139    // See https://github.com/eclipse/microprofile-config/issues/740
140    // and https://github.com/eclipse/microprofile-config/issues/448.
141    final Converter<?> converter = this.converters.get(pathType);
142    if (converter == null) {
143      if (pathType instanceof Class<?> c && c.isAssignableFrom(String.class)) {
144        final Element<?> absolutePathLastElement = absolutePath.lastElement();   
145        final Element<? extends Type> lastElement =
146          Element.of(absolutePathLastElement.qualifiers(), String.class, absolutePathLastElement.name());
147        final List<Element<?>> elements;
148        if (absolutePath.size() == 1) {
149          elements = List.of();
150        } else {
151          elements = new ArrayList<>(absolutePath.size() - 1);
152          for (int i = 0; i < elements.size(); i++) {
153            elements.add(absolutePath.get(i));
154          }
155        }
156        final String configPath = configPath(absolutePath);
157        return
158          new Value<>(() -> {
159              final String s = this.configSource.getValue(configPath);
160              if (s == null) {
161                // Sadly, among its many flaws is the fact that
162                // MicroProfile Config conflates null with absence.
163                throw new NoSuchElementException(configPath);
164              }
165              return s;
166          }, Path.of(absolutePath.qualifiers(), elements, lastElement));
167      }
168    } else {
169      final String configPath = configPath(absolutePath);
170      return
171        new Value<>(() -> {
172            final String s = this.configSource.getValue(configPath);
173            if (s == null) {
174              // Sadly, among its many flaws is the fact that
175              // MicroProfile Config conflates null with absence.
176              throw new NoSuchElementException(configPath);
177            }
178            return converter.convert(s);
179        }, absolutePath);
180    }
181    return null;
182  }
183
184  private static final String configPath(final Path<?> path) {
185    return path.stream()
186      .map(Element::name)
187      .reduce((s1, s2) -> String.join(".", s1, s2))
188      .orElse(null);
189  }
190
191  /**
192   * A coupling of a {@link Type}, a priority (as defined in the <a
193   * href="https://javadoc.io/static/org.eclipse.microprofile.config/microprofile-config-api/3.0.1/org/eclipse/microprofile/config/spi/Converter.html#priority">MicroProfile
194   * Config</a> specification), and a {@link Converter}.
195   *
196   * <p>For convenience, this class also implements the {@link
197   * Converter} interface itself.</p>
198   *
199   * @author <a href="https://about.me/lairdnelson"
200   * target="_parent">Laird Nelson</a>
201   *
202   * @see ConfigSourceProvider#ConfigSourceProvider(ConfigSource, Collection)
203   */
204  public static final class ConverterRegistration<T> implements Converter<T> {
205
206    /**
207     * The version of this class for {@link java.io.Serializable
208     * serialization} purposes.
209     */
210    private static final long serialVersionUID = 1L;
211
212    /**
213     * The conversion type of this registration.
214     *
215     * @nullability This field is never {@code null}.
216     */
217    private final Type type;
218
219    /**
220     * The priority of this registration.
221     */
222    private final int priority;
223
224    /**
225     * A {@link Converter} to which all work is delegated.
226     *
227     * @nullability This field is never {@code null}.
228     */
229    private final Converter<? extends T> converter;
230
231    /**
232     * Creates a new {@link ConverterRegistration} with a priority of
233     * {@code 100}, following the conventions of the <a
234     * href="https://javadoc.io/static/org.eclipse.microprofile.config/microprofile-config-api/3.0.1/org/eclipse/microprofile/config/spi/Converter.html#priority">MicroProfile
235     * Config</a> specification
236     *
237     * @param type the conversion type; must not be {@code null}
238     *
239     * @param converter the {@link Converter}; must not be {@code
240     * null}
241     *
242     * @see #ConverterRegistration(Type, int, Converter)
243     */
244    public ConverterRegistration(final Type type, final Converter<? extends T> converter) {
245      this(type, 100, converter);
246    }
247
248    /**
249     * Creates a new {@link ConverterRegistration}.
250     *
251     * @param type the conversion type; must not be {@code null}
252     *
253     * @param priority the priority (as defined in the <a
254     * href="https://javadoc.io/static/org.eclipse.microprofile.config/microprofile-config-api/3.0.1/org/eclipse/microprofile/config/spi/Converter.html#priority">MicroProfile
255     * Config</a> specification)
256     *
257     * @param converter the {@link Converter}; must not be {@code
258     * null}
259     */
260    public ConverterRegistration(final Type type, final int priority, final Converter<? extends T> converter) {
261      super();
262      this.type = Objects.requireNonNull(type, "type");
263      this.priority = priority;
264      this.converter = Objects.requireNonNull(converter, "converter");
265    }
266
267    /**
268     * Returns the conversion type of this {@link
269     * ConverterRegistration}.
270     *
271     * @return the conversion type
272     *
273     * @nullability This method never returns {@code null}.
274     *
275     * @idempotency This method is idempotent and deterministic.
276     *
277     * @threadsafety This method is safe for concurrent use by
278     * multiple threads.
279     */
280    public final Type type() {
281      return this.type;
282    }
283
284    /**
285     * Returns the priority of this {@link ConverterRegistration}.
286     *
287     * @return the priority
288     *
289     * @idempotency This method is idempotent and deterministic.
290     *
291     * @threadsafety This method is safe for concurrent use by
292     * multiple threads.
293     */
294    public final int priority() {
295      return this.priority;
296    }
297
298    /**
299     * Converts the supplied {@link String}-typed configuration value
300     * to the conversion type {@linkplain #ConverterRegistration(Type,
301     * int, Converter) supplied at construction time} using the {@link
302     * Converter} {@linkplain #ConverterRegistration(Type, int,
303     * Converter) supplied at construction time} and returns the
304     * result of the conversion.
305     *
306     * @param configurationValue the value to convert; must not be
307     * {@code null}
308     *
309     * @return the conversion
310     *
311     * @exception NullPointerException if {@code configurationValue}
312     * is {@code null}
313     *
314     * @exception IllegalArgumentException if conversion could not
315     * occur for any reason
316     *
317     * @nullability This method may return {@code null}.
318     *
319     * @idempotency No guarantees are made about idempotency or
320     * determinism.
321     *
322     * @threadsafety No guarantees are made about thread safety.
323     *
324     * @see Converter#convert(String)
325     */
326    @Override // Converter<T>
327    public final T convert(final String configurationValue) {
328      return this.converter.convert(configurationValue);
329    }
330    
331  }
332  
333}