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; 018 019import java.lang.reflect.Type; 020 021import java.util.ArrayList; 022import java.util.List; 023import java.util.Map; 024import java.util.NoSuchElementException; 025import java.util.Properties; 026 027import java.util.function.Supplier; 028 029import org.microbean.invoke.FixedValueSupplier; 030 031import org.microbean.loader.api.Loader; 032 033import org.microbean.loader.spi.AbstractProvider; 034import org.microbean.loader.spi.Value; 035 036import org.microbean.path.Path; 037import org.microbean.path.Path.Element; 038 039import org.microbean.type.JavaTypes; 040import org.microbean.type.Type.CovariantSemantics; 041 042/** 043 * An {@link AbstractProvider} that can return {@link Value}s backed 044 * by System properties. 045 * 046 * <p>While System properties are often casually assumed to be stable 047 * and {@link String}-typed, the exact opposite is true: System 048 * properties may contain arbitrarily-typed {@link Object}s under 049 * arbitrarily-typed keys, and the properties themselves {@linkplain 050 * System#setProperties(Properties) may be replaced} at any point. 051 * This means that all {@link Value}s supplied by this {@link 052 * SystemPropertyProvider} are {@linkplain Value#determinism() 053 * non-deterministic} and may change type and presence from one call 054 * to another.</p> 055 * 056 * <p>It is also worth mentioning explicitly that, deliberately, no 057 * type conversion of any System property value takes place in this 058 * class.</p> 059 * 060 * @author <a href="https://about.me/lairdnelson" 061 * target="_parent">Laird Nelson</a> 062 * 063 * @see System#getProperty(String, String) 064 * 065 * @see System#getProperties() 066 * 067 * @see System#setProperties(Properties) 068 * 069 * @see Properties#getProperty(String, String) 070 * 071 * @see Value#determinism() 072 */ 073public class SystemPropertyProvider extends AbstractProvider { 074 075 076 /* 077 * Instance fields. 078 */ 079 080 081 private final boolean flatKeys; 082 083 private final boolean onlyStrings; 084 085 private final boolean mutable; 086 087 088 /* 089 * Constructors. 090 */ 091 092 093 /** 094 * Creates a new {@link SystemPropertyProvider} that uses flat keys, 095 * does not presume that the only things stored in the System 096 * properties are {@link String}s, and honors the fact that System 097 * properties are mutable. 098 * 099 * @see #SystemPropertyProvider(boolean, boolean, boolean) 100 */ 101 public SystemPropertyProvider() { 102 this(true, false, true); 103 } 104 105 /** 106 * Creates a new {@link SystemPropertyProvider}. 107 * 108 * @param flatKeys whether the key for a system property is derived 109 * from a {@link Path}'s {@linkplain Path#lastElement() last 110 * element}'s {@linkplain Element#name() name} only 111 * 112 * @param onlyStrings whether all System properties are expected to 113 * be {@link String}-typed 114 * 115 * @param mutable whether System properties should be treated as 116 * mutable (which they are, strictly speaking, but for many 117 * applications this may not matter in practice) 118 */ 119 public SystemPropertyProvider(final boolean flatKeys, 120 final boolean onlyStrings, 121 final boolean mutable) { 122 super(onlyStrings ? String.class : null); 123 this.flatKeys = flatKeys; 124 this.onlyStrings = onlyStrings; 125 this.mutable = mutable; 126 } 127 128 129 /* 130 * Instance methods. 131 */ 132 133 134 /** 135 * Returns a {@link Supplier} suitable for the System property 136 * represented by the supplied {@link Path}. 137 * 138 * <p>This method never returns {@code null}. Its overrides may if 139 * they wish.</p> 140 * 141 * <p>The {@linkplain Path.Element#name() name} of the {@linkplain 142 * Path#lastElement() last element} of the supplied {@link Path} is 143 * taken to be the name of the System property value to retrieve. 144 * If the {@linkplain JavaTypes#erase(Type) type erasure} of the 145 * supplied {@link Path}'s {@link Path#qualified() qualified()} 146 * method {@linkplain Class#isAssignableFrom(Class) is assignable 147 * from} {@link String String.class}, then calls will be made by the 148 * returned {@link Value}'s {@link Value#get() get()} method to 149 * {@link System#getProperty(String)} before simple calls to {@link 150 * Properties#get(String) System.getProperties().get(String)}.</p> 151 * 152 * <p>Any {@link Supplier} returned by this method will be 153 * {@linkplain 154 * org.microbean.invoke.OptionalSupplier.Determinism#NON_DETERMINISTIC 155 * non-deterministic}, since system properties may change at any 156 * point. Additionally, if the supplied {@link Path}'s {@linkplain 157 * Path#qualified() type} is not assignable from that borne by a 158 * System property value, then the {@link Supplier} will return 159 * {@code null} from its {@link Value#get() get()} method in such a 160 * case, indicating that the value is present but cannot be 161 * represented. Overrides are strongly encouraged to abide by these 162 * conditions.</p> 163 * 164 * @param requestor the {@link Loader} seeking a {@link Value}; must 165 * not be {@code null} 166 * 167 * @param absolutePath an {@linkplain Path#absolute() absolute 168 * <code>Path</code>} for which a {@link Value} is being sought; 169 * must not be {@code null} 170 * 171 * @return a {@link Supplier} suitable for the System property whose 172 * name is represented by the supplied {@link Path}'s {@linkplain 173 * Path#lastElement() last <code>Element</code>}'s {@linkplain 174 * Path.Element#name() name} 175 * 176 * @exception NullPointerException if an argument for either 177 * parameter is {@code null} 178 * 179 * @nullability This method never returns {@code null} but its 180 * overrides may. 181 * 182 * @threadsafety This method is, and its overrides must be, safe for 183 * concurrent use by multiple threads. 184 * 185 * @idempotency This method is idempotent and deterministic. 186 * Overrides must be idempotent, but need not be deterministic. 187 * {@link Supplier}s returned by this method or its overrides are 188 * <em>not</em> guaranteed to be idempotent or deterministic. 189 */ 190 @Override 191 protected Supplier<?> find(final Loader<?> requestor, final Path<? extends Type> absolutePath) { 192 assert absolutePath.absolute(); 193 assert absolutePath.startsWith(requestor.path()); 194 assert !absolutePath.equals(requestor.path()); 195 196 // This is tricky. System properties suck. 197 // 198 // System properties are mutable. They can come and go at any 199 // point. They can also be of any type. If you set them to null, 200 // the "required" ones get re-initialized to newly sourced values. 201 // And so on. 202 // 203 // Next, while the keys are "flat", they are often naively assumed 204 // to represent a hierarchy. But the naming scheme suggests that 205 // even in the case of the "required" properties the designers 206 // were thinking in terms of hierarchies. 207 // 208 // Anyway, suppose an absolute Path of "/com/foo/bar" (making up 209 // the syntax; "/" is a Path.Element separator; each "word" is a 210 // Path.Element name() value) has a size of 4 (the leading 211 // element's name is always "") (hierarchical). Should this 212 // Provider respond by trying 213 // System.getProperties().getProperty("com.foo.bar")? 214 // 215 // Or should this Provider only accept a Path of "/com.foo.bar" 216 // whose size is 2 (flat), so that the name of only the *last* 217 // element is the property key ("com.foo.bar")? 218 // 219 // (The end call to System.getProperty() is the same, of course.) 220 // 221 // If we do the hierarchy case, we might run into spurious 222 // conflicts between this Provider and 223 // ProxyingProvider. "java.home", for example, is not really a 224 // hierarchy; "java.home" as a flat string is actually the name of 225 // the property in question and just happens to have "." in it. 226 // 227 // On the other hand, if we do the flat case, there isn't an easy 228 // way to override scalar nodes in a tree, and it seems at least 229 // moderately clear that the designers wanted these keys to be 230 // perceived as hierarchical in some fashion. 231 232 final String key = key(absolutePath, this.flatKeys); 233 if (key == null || key.isEmpty()) { 234 // System properties never permit null or empty keys. See 235 // https://github.com/openjdk/jdk/blob/jdk-17+35/src/java.base/share/classes/java/lang/System.java#L1043-L1050. 236 return null; 237 } 238 final Type type = absolutePath.qualified(); 239 if (CovariantSemantics.INSTANCE.assignable(type, String.class)) { 240 if (this.mutable) { 241 return () -> { 242 final String returnValue = System.getProperty(key); 243 if (returnValue == null) { 244 // System.getProperty() is documented to purposely conflate 245 // absence with null for some reason. 246 throw new NoSuchElementException(key); 247 } 248 return returnValue; 249 }; 250 } else { 251 final String value = System.getProperty(key); 252 if (value == null) { 253 // System.getProperty() is documented to purposely conflate 254 // absence with null for some reason. 255 return null; 256 } 257 return FixedValueSupplier.of(value); 258 } 259 } else if (this.onlyStrings) { 260 return null; 261 } else if (this.mutable) { 262 // At this point we know that the requested type is not a 263 // supertype of String. Therefore there's no point in calling 264 // System.getProperty(key) because you wouldn't be able to 265 // assign its (String) return value to the caller doing the 266 // requesting. Therefore defaults are not in play because only 267 // System.getProperty() consults them. Therefore we can treat 268 // the return value of System.getProperties() as just an 269 // ordinary Map. We also know that is is a Properties instance, 270 // so we also know it is fully synchronized on itself. Putting 271 // this all together we can tell definitively when a value has 272 // been explicitly and deliberately set to null versus when it 273 // is absent. 274 return () -> { 275 final Object returnValue; 276 final Map<?, ?> map = System.getProperties(); 277 synchronized (map) { 278 if ((returnValue = map.get(key)) == null && !map.containsKey(key)) { 279 throw new NoSuchElementException(key); 280 } 281 } 282 if (returnValue == null || CovariantSemantics.INSTANCE.assignable(type, returnValue.getClass())) { 283 return returnValue; 284 } 285 return null; 286 }; 287 } else { 288 final Object returnValue; 289 final Map<?, ?> map = System.getProperties(); 290 synchronized (map) { 291 if ((returnValue = map.get(key)) == null && !map.containsKey(key)) { 292 return null; 293 } 294 } 295 if (returnValue == null || CovariantSemantics.INSTANCE.assignable(type, returnValue.getClass())) { 296 return FixedValueSupplier.of(returnValue); 297 } 298 return null; 299 } 300 } 301 302 /** 303 * Overrides the {@link AbstractProvider#path(Loader, Path)} method 304 * to return a (relative) {@link Path} consisting solely of the 305 * {@linkplain Path#lastElement() last element} of the supplied 306 * {@code absolutePath}. 307 * 308 * @param requestor the {@link Loader} seeking a {@link Value}; must 309 * not be {@code null} 310 * 311 * @param absolutePath an {@linkplain Path#absolute() absolute 312 * <code>Path</code>} for which a {@link Value} is being sought; 313 * must not be {@code null} 314 * 315 * @return a {@link Path} that will be {@linkplain 316 * Value#Value(Supplier, Path) used to build a <code>Value</code>} 317 * to be returned by the {@link #get(Loader, Path)} method 318 * 319 * @nullability This method does not, but overrides may, return 320 * {@code null}. 321 * 322 * @idempotency This method is, and its overrides must be, 323 * idempotent and deterministic. 324 * 325 * @threadsafety This method is, and its overrides must be, safe for 326 * concurrent use by multiple threads. 327 * 328 * @see AbstractProvider#path(Loader, Path) 329 */ 330 @Override 331 @SuppressWarnings("unchecked") 332 protected <T extends Type> Path<T> path(final Loader<?> requestor, final Path<T> absolutePath) { 333 return Path.of(absolutePath.lastElement()); 334 } 335 336 337 /* 338 * Static methods. 339 */ 340 341 342 /** 343 * Returns a {@link String} representation of the supplied {@link Path}. 344 * 345 * @param path the {@link Path} in question; must not be {@code 346 * null} 347 * 348 * @param flat whether the key is derived from the supplied {@link 349 * Path}'s {@linkplain Path#lastElement() last element}'s 350 * {@linkplain Element#name() name} only 351 * 352 * @return a {@link String} representation of the supplied {@link 353 * Path} 354 * 355 * @exception NullPointerException if {@code path} is {@code null} 356 * 357 * @nullability This method never returns {@code null}. 358 * 359 * @idempotency This method is idempotent and deterministic. 360 * 361 * @threadsafety This method is safe for concurrent use by multiple 362 * threads. 363 */ 364 protected static final String key(final Path<?> path, final boolean flat) { 365 if (flat) { 366 return path.lastElement().name(); 367 } else { 368 return path.stream() 369 .map(Element::name) 370 .filter(s1 -> !s1.isEmpty()) 371 .reduce((s1, s2) -> String.join(".", s1, s2)) 372 .orElse(""); 373 } 374 } 375 376}