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.config.store;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.util.*;
023
024import org.apache.juneau.config.format.*;
025import org.apache.juneau.config.internal.*;
026import org.apache.juneau.marshall.*;
027
028/**
029 * A {@link ConfigStore} decorator that layers <b>profile overlays</b> over a delegate store's base config.
030 *
031 * <p>
032 * Given a set of active profiles (e.g. {@code ["stage"]} or {@code ["base","cloud"]}) and a base config name
033 * {@code N}, a {@code read(N)} returns the base contents from the delegate with each active profile's
034 * {@code N-<profile>} overlay merged on top &mdash; profile wins over base, last-active-profile wins over earlier
035 * ones (see {@link ProfileMerge}).  Reads for any name <i>other</i> than the configured base name pass straight
036 * through to the delegate.
037 *
038 * <p>
039 * <b>Live reload:</b> the store registers a listener on the delegate for the base name <i>and</i> each profile name,
040 * so a change to the base file or any active profile file re-runs the merge and re-fires the change to this store's
041 * own listeners (the {@link ConfigMap} sees a fresh merged contents and reloads).
042 *
043 * <p>
044 * The profile file name is derived from the base name by inserting {@code -<profile>} before the extension
045 * (e.g. base {@code my.cfg} + profile {@code stage} &rarr; {@code my-stage.cfg}); a base with no extension simply
046 * appends {@code -<profile>}.  Writes, existence checks, and name resolution delegate to the wrapped store.
047 *
048 * @since 10.0.0
049 */
050@SuppressWarnings({
051   "resource" // Decorator owns the wrapped delegate store and closes it in close(); the delegate's lifecycle is managed here, not leaked.
052})
053public class ProfileConfigStore extends ConfigStore {
054
055   /**
056    * Builder class.
057    */
058   public static class Builder extends ConfigStore.Builder<Builder> {
059
060      ConfigStore delegate;
061      String baseName;
062      List<String> profiles = l();
063      ConfigFormat format = IniConfigFormat.INSTANCE;
064
065      /** Constructor, default settings. */
066      protected Builder() {}
067
068      /**
069       * Copy constructor.
070       *
071       * @param copyFrom The builder to copy from.  Cannot be <jk>null</jk>.
072       */
073      protected Builder(Builder copyFrom) {
074         super(reqnn("copyFrom", copyFrom));
075         this.delegate = copyFrom.delegate;
076         this.baseName = copyFrom.baseName;
077         this.profiles = new ArrayList<>(copyFrom.profiles);
078         this.format = copyFrom.format;
079      }
080
081      /**
082       * Copy constructor.
083       *
084       * @param copyFrom The bean to copy from.  Cannot be <jk>null</jk>.
085       */
086      protected Builder(ProfileConfigStore copyFrom) {
087         super(reqnn("copyFrom", copyFrom));
088         type(copyFrom.getClass());
089         this.delegate = copyFrom.delegate;
090         this.baseName = copyFrom.baseName;
091         this.profiles = new ArrayList<>(copyFrom.profiles);
092         this.format = copyFrom.format;
093      }
094
095      /**
096       * Sets the wrapped delegate store that supplies the base + profile file contents.
097       *
098       * @param value The delegate store.
099       * @return This object.
100       */
101      public Builder delegate(ConfigStore value) { delegate = value; return this; }
102
103      /**
104       * Sets the base config name whose {@code -<profile>} overlays are merged.
105       *
106       * @param value The base config name.
107       * @return This object.
108       */
109      public Builder baseName(String value) { baseName = value; return this; }
110
111      /**
112       * Sets the active profiles, in activation order (last-active-profile wins).
113       *
114       * @param value
115       *    The active profile names.
116       *    <br>Can be <jk>null</jk> (treated as no active profiles).
117       * @return This object.
118       */
119      public Builder profiles(List<String> value) { profiles = value == null ? l() : new ArrayList<>(value); return this; }
120
121      /**
122       * Sets the config format used to parse the base + profile files.
123       *
124       * @param value
125       *    The format.
126       *    <br>Can be <jk>null</jk> (defaults to {@link IniConfigFormat}).
127       * @return This object.
128       */
129      public Builder format(ConfigFormat value) { format = value == null ? IniConfigFormat.INSTANCE : value; return this; }
130
131      @Override /* Overridden from Context.Builder<?> */
132      public ProfileConfigStore build() {
133         return build(ProfileConfigStore.class);
134      }
135
136      @Override /* Overridden from Context.Builder<?> */
137      public Builder copy() {
138         return new Builder(this);
139      }
140   }
141
142   /**
143    * Creates a new builder for this object.
144    *
145    * @return A new builder.
146    */
147   public static Builder create() {
148      return new Builder();
149   }
150
151   private final ConfigStore delegate;
152   private final String baseName;
153   private final List<String> profiles;
154   private final ConfigFormat format;
155   private final ConfigStoreListener reloadListener;
156
157   /**
158    * Constructor.
159    *
160    * @param builder The builder for this object.
161    *    <br>Cannot be <jk>null</jk>.
162    */
163   public ProfileConfigStore(Builder builder) {
164      super(builder);
165      this.delegate = reqnn("delegate", builder.delegate);
166      this.baseName = reqnn("baseName", builder.baseName);
167      this.profiles = List.copyOf(builder.profiles);
168      this.format = builder.format;
169
170      // Re-merge + re-notify whenever the base or any active profile file changes underneath us.
171      this.reloadListener = contents -> {
172         try {
173            update(baseName, mergedContents());
174         } catch (IOException e) {  // HTT: re-merge IO failure only on a real FileStore IO error mid-reload; not reproducible with in-memory stores.
175            throw new ConfigException(e, "Failed to re-merge profile overlays for '%s'", baseName);
176         }
177      };
178      delegate.register(baseName, reloadListener);
179      for (var p : profiles)
180         delegate.register(profileName(p), reloadListener);
181   }
182
183   @Override /* Overridden from Context */
184   public Builder copy() {
185      return new Builder(this);
186   }
187
188   @Override /* Overridden from Closeable */
189   public void close() throws IOException {
190      delegate.unregister(baseName, reloadListener);
191      for (var p : profiles)
192         delegate.unregister(profileName(p), reloadListener);
193      delegate.close();
194   }
195
196   @Override /* Overridden from ConfigStore */
197   public boolean exists(String name) {
198      return delegate.exists(name);
199   }
200
201   @Override /* Overridden from ConfigStore */
202   public String read(String name) throws IOException {
203      if (neq(name, baseName))
204         return delegate.read(name);
205      return mergedContents();
206   }
207
208   @Override /* Overridden from ConfigStore */
209   public String write(String name, String expectedContents, String newContents) throws IOException {
210      return delegate.write(name, expectedContents, newContents);
211   }
212
213   @Override /* Overridden from ConfigStore */
214   public String getNotifiedContents(String name, String written) throws IOException {
215      if (neq(name, baseName))
216         return written;
217      return mergeWithOverlays(written);
218   }
219
220   /**
221    * Reads the base + each active profile from the delegate and merges them (profile-wins, last-active-wins).
222    */
223   private String mergedContents() throws IOException {
224      return mergeWithOverlays(delegate.read(baseName));
225   }
226
227   /**
228    * Merges the specified base text with the current contents of each active profile overlay.
229    */
230   @SuppressWarnings({
231      "java:S9391" // Loop body throws checked IOException, which a stream lambda cannot propagate.
232   })
233   private String mergeWithOverlays(String base) throws IOException {
234      if (profiles.isEmpty())
235         return base;
236      var overlays = new ArrayList<String>(profiles.size());
237      for (var p : profiles)
238         overlays.add(delegate.read(profileName(p)));
239      return ProfileMerge.merge(delegate, baseName, base, overlays, format);
240   }
241
242   /**
243    * Derives the profile file name from the base name by inserting {@code -<profile>} before the extension.
244    *
245    * @param profile The profile name.
246    * @return The profile config name.
247    */
248   String profileName(String profile) {
249      var dot = baseName.lastIndexOf('.');
250      if (dot < 0)
251         return baseName + "-" + profile;
252      return baseName.substring(0, dot) + "-" + profile + baseName.substring(dot);
253   }
254}