001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.config.store;
014
015import static org.apache.juneau.internal.CollectionUtils.*;
016
017import java.io.*;
018import java.lang.annotation.*;
019import java.lang.reflect.*;
020import java.util.*;
021import java.util.concurrent.*;
022
023import org.apache.juneau.*;
024import org.apache.juneau.config.internal.*;
025import org.apache.juneau.internal.*;
026import org.apache.juneau.utils.*;
027
028/**
029 * Represents a storage location for configuration files.
030 *
031 * <p>
032 * Content stores require two methods to be implemented:
033 * <ul class='javatree'>
034 *    <li class='jm'>{@link #read(String)} - Retrieve a config file.
035 *    <li class='jm'>{@link #write(String,String,String)} - ConfigStore a config file.
036 * </ul>
037 *
038 * <h5 class='section'>Notes:</h5><ul>
039 *    <li class='note'>This class is thread safe and reusable.
040 * </ul>
041*/
042public abstract class ConfigStore extends Context implements Closeable {
043
044   //-------------------------------------------------------------------------------------------------------------------
045   // Builder
046   //-------------------------------------------------------------------------------------------------------------------
047
048   /**
049    * Builder class.
050    */
051   @FluentSetters
052   public abstract static class Builder extends Context.Builder {
053
054      /**
055       * Constructor, default settings.
056       */
057      protected Builder() {
058         super();
059      }
060
061      /**
062       * Copy constructor.
063       *
064       * @param copyFrom The bean to copy from.
065       */
066      protected Builder(ConfigStore copyFrom) {
067         super(copyFrom);
068      }
069
070      /**
071       * Copy constructor.
072       *
073       * @param copyFrom The builder to copy from.
074       */
075      protected Builder(Builder copyFrom) {
076         super(copyFrom);
077      }
078
079      @Override /* Context.Builder */
080      public abstract Builder copy();
081
082      //-----------------------------------------------------------------------------------------------------------------
083      // Properties
084      //-----------------------------------------------------------------------------------------------------------------
085
086      // <FluentSetters>
087
088      @Override /* GENERATED - org.apache.juneau.Context.Builder */
089      public Builder annotations(Annotation...values) {
090         super.annotations(values);
091         return this;
092      }
093
094      @Override /* GENERATED - org.apache.juneau.Context.Builder */
095      public Builder apply(AnnotationWorkList work) {
096         super.apply(work);
097         return this;
098      }
099
100      @Override /* GENERATED - org.apache.juneau.Context.Builder */
101      public Builder applyAnnotations(java.lang.Class<?>...fromClasses) {
102         super.applyAnnotations(fromClasses);
103         return this;
104      }
105
106      @Override /* GENERATED - org.apache.juneau.Context.Builder */
107      public Builder applyAnnotations(Method...fromMethods) {
108         super.applyAnnotations(fromMethods);
109         return this;
110      }
111
112      @Override /* GENERATED - org.apache.juneau.Context.Builder */
113      public Builder cache(Cache<HashKey,? extends org.apache.juneau.Context> value) {
114         super.cache(value);
115         return this;
116      }
117
118      @Override /* GENERATED - org.apache.juneau.Context.Builder */
119      public Builder debug() {
120         super.debug();
121         return this;
122      }
123
124      @Override /* GENERATED - org.apache.juneau.Context.Builder */
125      public Builder debug(boolean value) {
126         super.debug(value);
127         return this;
128      }
129
130      @Override /* GENERATED - org.apache.juneau.Context.Builder */
131      public Builder impl(Context value) {
132         super.impl(value);
133         return this;
134      }
135
136      @Override /* GENERATED - org.apache.juneau.Context.Builder */
137      public Builder type(Class<? extends org.apache.juneau.Context> value) {
138         super.type(value);
139         return this;
140      }
141
142      // </FluentSetters>
143   }
144
145   //-------------------------------------------------------------------------------------------------------------------
146   // Instance
147   //-------------------------------------------------------------------------------------------------------------------
148
149   private final ConcurrentHashMap<String,Set<ConfigStoreListener>> listeners = new ConcurrentHashMap<>();
150   private final ConcurrentHashMap<String,ConfigMap> configMaps = new ConcurrentHashMap<>();
151
152   /**
153    * Constructor.
154    *
155    * @param builder The builder for this object.
156    */
157   protected ConfigStore(Builder builder) {
158      super(builder);
159   }
160
161   /**
162    * Returns the contents of the configuration file.
163    *
164    * @param name The config file name.
165    * @return
166    *    The contents of the configuration file.
167    *    <br>A blank string if the config does not exist.
168    *    <br>Never <jk>null</jk>.
169    * @throws IOException Thrown by underlying stream.
170    */
171   public abstract String read(String name) throws IOException;
172
173   /**
174    * Saves the contents of the configuration file if the underlying storage hasn't been modified.
175    *
176    * @param name The config file name.
177    * @param expectedContents The expected contents of the file.
178    * @param newContents The new contents.
179    * @return
180    *    If <jk>null</jk>, then we successfully stored the contents of the file.
181    *    <br>Otherwise the contents of the file have changed and we return the new contents of the file.
182    * @throws IOException Thrown by underlying stream.
183    */
184   public abstract String write(String name, String expectedContents, String newContents) throws IOException;
185
186   /**
187    * Checks whether the configuration with the specified name exists in this store.
188    *
189    * @param name The config name.
190    * @return <jk>true</jk> if the configuration with the specified name exists in this store.
191    */
192   public abstract boolean exists(String name);
193
194   /**
195    * Registers a new listener on this store.
196    *
197    * @param name The configuration name to listen for.
198    * @param l The new listener.
199    * @return This object.
200    */
201   public synchronized ConfigStore register(String name, ConfigStoreListener l) {
202      name = resolveName(name);
203      Set<ConfigStoreListener> s = listeners.get(name);
204      if (s == null) {
205         s = synced(Collections.newSetFromMap(new IdentityHashMap<ConfigStoreListener,Boolean>()));
206         listeners.put(name, s);
207      }
208      s.add(l);
209      return this;
210   }
211
212   /**
213    * Unregisters a listener from this store.
214    *
215    * @param name The configuration name to listen for.
216    * @param l The listener to unregister.
217    * @return This object.
218    */
219   public synchronized ConfigStore unregister(String name, ConfigStoreListener l) {
220      name = resolveName(name);
221      Set<ConfigStoreListener> s = listeners.get(name);
222      if (s != null)
223         s.remove(l);
224      return this;
225   }
226
227   /**
228    * Returns a map file containing the parsed contents of a configuration.
229    *
230    * @param name The configuration name.
231    * @return
232    *    The parsed configuration.
233    *    <br>Never <jk>null</jk>.
234    * @throws IOException Thrown by underlying stream.
235    */
236   public synchronized ConfigMap getMap(String name) throws IOException {
237      name = resolveName(name);
238      ConfigMap cm = configMaps.get(name);
239      if (cm != null)
240         return cm;
241      cm = new ConfigMap(this, name);
242      ConfigMap cm2 = configMaps.putIfAbsent(name, cm);
243      if (cm2 != null)
244         return cm2;
245      register(name, cm);
246      return cm;
247   }
248
249   /**
250    * Called when the physical contents of a config file have changed.
251    *
252    * <p>
253    * Triggers calls to {@link ConfigStoreListener#onChange(String)} on all registered listeners.
254    *
255    * @param name The config name (e.g. the filename without the extension).
256    * @param contents The new contents.
257    * @return This object.
258    */
259   public synchronized ConfigStore update(String name, String contents) {
260      name = resolveName(name);
261      Set<ConfigStoreListener> s = listeners.get(name);
262      if (s != null)
263         listeners.get(name).forEach(x -> x.onChange(contents));
264      return this;
265   }
266
267   /**
268    * Convenience method for updating the contents of a file with lines.
269    *
270    * @param name The config name (e.g. the filename without the extension).
271    * @param contentLines The new contents.
272    * @return This object.
273    */
274   public synchronized ConfigStore update(String name, String...contentLines) {
275      name = resolveName(name);
276      StringBuilder sb = new StringBuilder();
277      for (String l : contentLines)
278         sb.append(l).append('\n');
279      return update(name, sb.toString());
280   }
281
282   /**
283    * Subclasses can override this method to convert config names to internal forms.
284    *
285    * <p>
286    * For example, the {@link FileStore} class can take in both <js>"MyConfig"</js> and <js>"MyConfig.cfg"</js>
287    * as names that both resolve to <js>"MyConfig.cfg"</js>.
288    *
289    * @param name The name to resolve.
290    * @return The resolved name.
291    */
292   protected String resolveName(String name) {
293      return name;
294   }
295}