001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  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 implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.settings;
018
019import java.util.*;
020import java.util.concurrent.*;
021
022/**
023 * A {@link PropertySource} decorator that adds <b>relaxed binding</b> over a delegate source.
024 *
025 * <p>
026 * When a property lookup misses against the delegate, this source retries with a small set of
027 * <i>canonical name variants</i> derived from the requested name, so a single logical key can be supplied in any of
028 * the common environment conventions and still resolve.  This is the mechanism that lets a Kubernetes-style
029 * environment variable such as {@code MY_SECTION_MY_KEY} satisfy a lookup for {@code MySection/myKey}, or
030 * {@code MY_PROP} satisfy {@code my.prop} / {@code myProp}.
031 *
032 * <h5 class='section'>Candidate order</h5>
033 * <p>
034 * The requested name is always tried <b>verbatim first</b>, so exact-match behavior is preserved unchanged (an
035 * already-canonical key resolves with no transformation, and a source that defines the literal name wins).  Only on a
036 * miss are the relaxed variants tried, in this order:
037 * <ol>
038 *    <li>verbatim (the requested name, unchanged);
039 *    <li><b>upper underscore</b> &mdash; every run of non-alphanumeric characters (and every lower&rarr;upper camel-case
040 *       boundary) becomes a single {@code _}, then the whole thing is upper-cased.  This is the dominant env-var form:
041 *       {@code my.prop}/{@code my-prop}/{@code myProp}/{@code MySection/myKey} &rarr; {@code MY_PROP} /
042 *       {@code MY_SECTION_MY_KEY};
043 *    <li><b>lower dotted</b> &mdash; the same separator collapse but joined with {@code .} and lower-cased
044 *       ({@code MY_PROP} &rarr; {@code my.prop}), so a dotted-property lookup can be satisfied by an env-style key and
045 *       vice-versa.
046 * </ol>
047 *
048 * <p>
049 * Because the boundary between "section" and "key" in a flat relaxed name (e.g. the underscores in
050 * {@code MY_SECTION_MY_KEY}) is inherently ambiguous, this decorator deliberately works in the
051 * <b>lookup-key &rarr; candidate-name</b> direction only (the well-defined direction): the caller asks for a known
052 * logical key and the decorator generates the environment-convention spellings to probe.  It never tries to reverse a
053 * flat env name back into a {@code section/key} pair.
054 *
055 * <h5 class='section'>Usage</h5>
056 * <p class='bjava'>
057 *    <jc>// Wrap the system-env source so env vars bind relaxedly.</jc>
058 *    <jv>settings</jv> = Settings.<jsm>create</jsm>().addSource(<jk>new</jk> RelaxedPropertySource(Settings.<jsf>SYSTEM_ENV_SOURCE</jsf>)).build();
059 * </p>
060 *
061 * @since 10.0.0
062 */
063public class RelaxedPropertySource implements PropertySource {
064
065   private final PropertySource delegate;
066
067   /**
068    * Constructor.
069    *
070    * @param delegate The wrapped source that the generated candidate names are probed against.  Must not be <jk>null</jk>.
071    */
072   public RelaxedPropertySource(PropertySource delegate) {
073      this.delegate = Objects.requireNonNull(delegate, "delegate");
074   }
075
076   @Override
077   public PropertyLookupResult get(String name) {
078      if (name == null)
079         return PropertyLookupResult.missing();
080      for (var candidate : candidates(name)) {
081         var r = delegate.get(candidate);
082         if (r.isPresent())
083            return r;
084      }
085      return PropertyLookupResult.missing();
086   }
087
088   private static final ConcurrentHashMap<String,List<String>> CANDIDATES = new ConcurrentHashMap<>();
089
090   /**
091    * Generates the ordered, de-duplicated list of candidate names for a requested name (verbatim first).
092    *
093    * <p>
094    * The result is <b>unmodifiable</b> and memoized per name, with the same soft cap and pass-through rule as
095    * {@link CachingPropertySource} (names beyond {@link CachingPropertySource#MAX_ENTRIES} are computed on every call).
096    *
097    * @param name The requested property name.  Must not be <jk>null</jk>.
098    * @return The candidate names to probe, in priority order.  Unmodifiable.
099    * @throws NullPointerException If <c>name</c> is <jk>null</jk>.
100    * @since 10.0.0
101    */
102   public static List<String> candidates(String name) {
103      var cached = CANDIDATES.get(name);  // throws NullPointerException for a null name
104      if (cached != null)
105         return cached;
106      var computed = compute(name);
107      if (CANDIDATES.size() < CachingPropertySource.MAX_ENTRIES) {
108         var previous = CANDIDATES.putIfAbsent(name, computed);
109         if (previous != null)
110            return previous;
111      }
112      return computed;
113   }
114
115   private static List<String> compute(String name) {
116      var out = new ArrayList<String>(3);
117      out.add(name);
118      var tokens = tokenize(name);
119      if (! tokens.isEmpty()) {
120         addIfNew(out, String.join("_", tokens).toUpperCase(Locale.ROOT));
121         addIfNew(out, String.join(".", tokens).toLowerCase(Locale.ROOT));
122      }
123      return List.copyOf(out);
124   }
125
126   /**
127    * Splits a name into lowercase-normalized word tokens, breaking on any non-alphanumeric run and on each
128    * lower&rarr;upper camel-case boundary.  E.g. {@code "MySection/myKey"} &rarr; {@code [my, section, my, key]}.
129    */
130   private static List<String> tokenize(String name) {
131      var tokens = new ArrayList<String>();
132      var sb = new StringBuilder();
133      char prev = 0;
134      for (var i = 0; i < name.length(); i++) {
135         var c = name.charAt(i);
136         if (! Character.isLetterOrDigit(c)) {
137            // Separator (., -, /, _, space, …): flush the current token.
138            if (! sb.isEmpty()) {
139               tokens.add(sb.toString());
140               sb.setLength(0);
141            }
142         } else {
143            // camelCase boundary: lower/digit followed by upper starts a new token.
144            if (! sb.isEmpty() && Character.isUpperCase(c) && (Character.isLowerCase(prev) || Character.isDigit(prev))) {
145               tokens.add(sb.toString());
146               sb.setLength(0);
147            }
148            sb.append(Character.toLowerCase(c));
149         }
150         prev = c;
151      }
152      if (! sb.isEmpty())
153         tokens.add(sb.toString());
154      return tokens;
155   }
156
157   private static void addIfNew(List<String> out, String candidate) {
158      if (! out.contains(candidate))
159         out.add(candidate);
160   }
161}