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}