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}