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 java.util.Collections.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.Shorts.*;
022
023import java.io.*;
024import java.util.*;
025import java.util.concurrent.*;
026
027import org.apache.juneau.config.format.*;
028import org.apache.juneau.config.internal.*;
029import org.apache.juneau.marshall.*;
030
031/**
032 * Represents a storage location for configuration files.
033 *
034 * <p>
035 * Content stores require two methods to be implemented:
036 * <ul class='javatree'>
037 *    <li class='jm'>{@link #read(String)} - Retrieve a config file.
038 *    <li class='jm'>{@link #write(String,String,String)} - Store a config file.
039 * </ul>
040 *
041 * <h5 class='section'>Notes:</h5><ul>
042 *    <li class='note'>This class is thread safe and reusable.
043 * </ul>
044*/
045@SuppressWarnings({
046   "rawtypes", // The protected ConfigStore(Builder) constructor takes a raw Builder so subclasses can pass their concrete Builder<SELF> without naming the self-type
047   "resource" // ConfigStore manages Closeable resources
048})
049public abstract class ConfigStore extends Context implements Closeable {
050
051   /**
052    * Builder class.
053    */
054   @SuppressWarnings({
055      "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name.
056   })
057   public abstract static class Builder<SELF extends Builder<SELF>> extends Context.Builder<SELF> {
058
059      /**
060       * Constructor, default settings.
061       */
062      protected Builder() {}
063
064      /**
065       * Copy constructor.
066       *
067       * @param copyFrom The builder to copy from.
068       */
069      protected Builder(Builder<?> copyFrom) {
070         super(copyFrom);
071      }
072
073      /**
074       * Copy constructor.
075       *
076       * @param copyFrom The bean to copy from.
077       */
078      protected Builder(ConfigStore copyFrom) {
079         super(copyFrom);
080      }
081
082   }
083
084   private final ConcurrentHashMap<String,ConfigMap> configMaps = new ConcurrentHashMap<>();
085   private final ConcurrentHashMap<String,Set<ConfigStoreListener>> listeners = new ConcurrentHashMap<>();
086
087   // Tracks the chain of configs currently being constructed on this thread so that a circular import
088   // (a config that directly or indirectly imports itself) can be detected and reported cleanly instead
089   // of recursing infinitely through getMap()->new ConfigMap()->load()->getMap() until the stack overflows.
090   private static final ThreadLocal<Set<String>> LOADING = ThreadLocal.withInitial(LinkedHashSet::new);
091
092   /**
093    * Constructor.
094    *
095    * @param builder The builder for this object.
096    *    <br>Cannot be <jk>null</jk>.
097    */
098   protected ConfigStore(Builder builder) {
099      super(builder);
100   }
101
102   /**
103    * Checks whether the configuration with the specified name exists in this store.
104    *
105    * @param name The config name.
106    * @return <jk>true</jk> if the configuration with the specified name exists in this store.
107    */
108   public abstract boolean exists(String name);
109
110   /**
111    * Returns a map file containing the parsed contents of a configuration.
112    *
113    * @param name The configuration name.
114    * @return
115    *    The parsed configuration.
116    *    <br>Never <jk>null</jk>.
117    * @throws IOException Thrown by underlying stream.
118    */
119   public synchronized ConfigMap getMap(String name) throws IOException {
120      return getMap(name, IniConfigFormat.INSTANCE);
121   }
122
123   /**
124    * Returns a map file containing the parsed contents of a configuration.
125    *
126    * @param name The configuration name.
127    * @param format
128    *    The configuration format.
129    *    <br>Can be <jk>null</jk> (defaults to {@link IniConfigFormat}).
130    * @return The parsed configuration.
131    * @throws IOException Thrown by underlying stream.
132    */
133   @SuppressWarnings({
134      "javabugs:S2259" // False positive: 'format2' is never null; a null 'format' defaults to the static final IniConfigFormat.INSTANCE.
135   })
136   public synchronized ConfigMap getMap(String name, ConfigFormat format) throws IOException {
137      name = resolveName(name);
138      var format2 = format == null ? IniConfigFormat.INSTANCE : format;
139      var key = format2.id() + ":" + name;
140      var cm = configMaps.get(key);
141      if (nn(cm))
142         return cm;
143
144      // Detect circular imports before recursing into ConfigMap construction.
145      var loading = LOADING.get();
146      if (! loading.add(key))
147         throw new ConfigException("Import loop detected in configuration:  %s", importChain(loading, name));
148      try {
149         cm = new ConfigMap(this, name, format2);
150      } finally {
151         loading.remove(key);
152         if (loading.isEmpty())
153            LOADING.remove();
154      }
155
156      var cm2 = configMaps.putIfAbsent(key, cm);
157      if (nn(cm2))
158         return cm2;
159      register(name, cm);
160      return cm;
161   }
162
163   // Renders the chain of configs currently being loaded (e.g. "B -> A2 -> A1 -> A2") for a circular-import error message.
164   // The entries in the loading set are cache keys of the form "<formatId>:<name>", so the format prefix is stripped.
165   private static String importChain(Set<String> loading, String reentry) {
166      var sb = new StringBuilder();
167      for (var key : loading)
168         sb.append(key.substring(key.indexOf(':') + 1)).append(" -> ");
169      return sb.append(reentry).toString();
170   }
171
172   /**
173    * Returns the contents of the configuration file.
174    *
175    * @param name The config file name.
176    * @return
177    *    The contents of the configuration file.
178    *    <br>A blank string if the config does not exist.
179    *    <br>Never <jk>null</jk>.
180    * @throws IOException Thrown by underlying stream.
181    */
182   public abstract String read(String name) throws IOException;
183
184   /**
185    * Registers a new listener on this store.
186    *
187    * @param name
188    *    The configuration name to listen for.
189    *    <br>Cannot be <jk>null</jk>.
190    * @param l The new listener.
191    * @return This object.
192    */
193   public synchronized ConfigStore register(String name, ConfigStoreListener l) {
194      name = resolveName(name);
195      var s = listeners.computeIfAbsent(name, k -> synced(newSetFromMap(new IdentityHashMap<>())));
196      s.add(l);
197      return this;
198   }
199
200   /**
201    * Unregisters a listener from this store.
202    *
203    * @param name
204    *    The configuration name to listen for.
205    *    <br>Cannot be <jk>null</jk>.
206    * @param l The listener to unregister.
207    * @return This object.
208    */
209   public synchronized ConfigStore unregister(String name, ConfigStoreListener l) {
210      name = resolveName(name);
211      var s = listeners.get(name);
212      if (nn(s))
213         s.remove(l);
214      return this;
215   }
216
217   /**
218    * Called when the physical contents of a config file have changed.
219    *
220    * <p>
221    * Triggers calls to {@link ConfigStoreListener#onChange(String)} on all registered listeners.
222    *
223    * @param name
224    *    The config name (e.g. the filename without the extension).
225    *    <br>Cannot be <jk>null</jk>.
226    * @param contents The new contents.
227    * @return This object.
228    */
229   public synchronized ConfigStore update(String name, String contents) {
230      name = resolveName(name);
231      var s = listeners.get(name);
232      if (nn(s))
233         listeners.get(name).forEach(x -> x.onChange(contents));
234      return this;
235   }
236
237   /**
238    * Convenience method for updating the contents of a file with lines.
239    *
240    * @param name
241    *    The config name (e.g. the filename without the extension).
242    *    <br>Cannot be <jk>null</jk>.
243    * @param contentLines The new contents.
244    * @return This object.
245    */
246   public synchronized ConfigStore update(String name, String...contentLines) {
247      name = resolveName(name);
248      var sb = new StringBuilder();
249      for (var l : contentLines)
250         sb.append(l).append('\n');
251      return update(name, sb.toString());
252   }
253
254   /**
255    * Saves the contents of the configuration file if the underlying storage hasn't been modified.
256    *
257    * @param name The config file name.
258    * @param expectedContents The expected contents of the file.
259    * @param newContents The new contents.
260    * @return
261    *    If <jk>null</jk>, then we successfully stored the contents of the file.
262    *    <br>Otherwise the contents of the file have changed and we return the new contents of the file.
263    * @throws IOException Thrown by underlying stream.
264    */
265   public abstract String write(String name, String expectedContents, String newContents) throws IOException;
266
267   /**
268    * Returns the contents that listeners will be notified with after the specified contents are written.
269    *
270    * <p>
271    * Stores that notify listeners with text other than what was written (e.g. after merging in overlays) should
272    * override this method.
273    *
274    * <p>
275    * This is called before the write, so any processing that fails (e.g. merging unparseable written text) fails
276    * before anything is persisted.
277    *
278    * @param name The config name.
279    * @param written The contents being written.
280    * @return The contents that listeners will receive.  The default implementation returns <c>written</c>.
281    * @throws IOException Thrown by underlying stream.
282    */
283   public String getNotifiedContents(String name, String written) throws IOException {
284      return written;
285   }
286
287   /**
288    * Subclasses can override this method to convert config names to internal forms.
289    *
290    * <p>
291    * For example, the {@link FileStore} class can take in both <js>"MyConfig"</js> and <js>"MyConfig.cfg"</js>
292    * as names that both resolve to <js>"MyConfig.cfg"</js>.
293    *
294    * @param name The name to resolve.
295    * @return The resolved name.
296    */
297   protected String resolveName(String name) {
298      return name;
299   }
300}