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      }
059
060      /**
061       * Copy constructor.
062       *
063       * @param copyFrom The bean to copy from.
064       */
065      protected Builder(ConfigStore copyFrom) {
066         super(copyFrom);
067      }
068
069      /**
070       * Copy constructor.
071       *
072       * @param copyFrom The builder to copy from.
073       */
074      protected Builder(Builder copyFrom) {
075         super(copyFrom);
076      }
077
078      @Override /* Context.Builder */
079      public abstract Builder copy();
080
081      //-----------------------------------------------------------------------------------------------------------------
082      // Properties
083      //-----------------------------------------------------------------------------------------------------------------
084
085      // <FluentSetters>
086
087      @Override /* GENERATED - org.apache.juneau.Context.Builder */
088      public Builder annotations(Annotation...values) {
089         super.annotations(values);
090         return this;
091      }
092
093      @Override /* GENERATED - org.apache.juneau.Context.Builder */
094      public Builder apply(AnnotationWorkList work) {
095         super.apply(work);
096         return this;
097      }
098
099      @Override /* GENERATED - org.apache.juneau.Context.Builder */
100      public Builder applyAnnotations(java.lang.Class<?>...fromClasses) {
101         super.applyAnnotations(fromClasses);
102         return this;
103      }
104
105      @Override /* GENERATED - org.apache.juneau.Context.Builder */
106      public Builder applyAnnotations(Method...fromMethods) {
107         super.applyAnnotations(fromMethods);
108         return this;
109      }
110
111      @Override /* GENERATED - org.apache.juneau.Context.Builder */
112      public Builder cache(Cache<HashKey,? extends org.apache.juneau.Context> value) {
113         super.cache(value);
114         return this;
115      }
116
117      @Override /* GENERATED - org.apache.juneau.Context.Builder */
118      public Builder debug() {
119         super.debug();
120         return this;
121      }
122
123      @Override /* GENERATED - org.apache.juneau.Context.Builder */
124      public Builder debug(boolean value) {
125         super.debug(value);
126         return this;
127      }
128
129      @Override /* GENERATED - org.apache.juneau.Context.Builder */
130      public Builder impl(Context value) {
131         super.impl(value);
132         return this;
133      }
134
135      @Override /* GENERATED - org.apache.juneau.Context.Builder */
136      public Builder type(Class<? extends org.apache.juneau.Context> value) {
137         super.type(value);
138         return this;
139      }
140
141      // </FluentSetters>
142   }
143
144   //-------------------------------------------------------------------------------------------------------------------
145   // Instance
146   //-------------------------------------------------------------------------------------------------------------------
147
148   private final ConcurrentHashMap<String,Set<ConfigStoreListener>> listeners = new ConcurrentHashMap<>();
149   private final ConcurrentHashMap<String,ConfigMap> configMaps = new ConcurrentHashMap<>();
150
151   /**
152    * Constructor.
153    *
154    * @param builder The builder for this object.
155    */
156   protected ConfigStore(Builder builder) {
157      super(builder);
158   }
159
160   /**
161    * Returns the contents of the configuration file.
162    *
163    * @param name The config file name.
164    * @return
165    *    The contents of the configuration file.
166    *    <br>A blank string if the config does not exist.
167    *    <br>Never <jk>null</jk>.
168    * @throws IOException Thrown by underlying stream.
169    */
170   public abstract String read(String name) throws IOException;
171
172   /**
173    * Saves the contents of the configuration file if the underlying storage hasn't been modified.
174    *
175    * @param name The config file name.
176    * @param expectedContents The expected contents of the file.
177    * @param newContents The new contents.
178    * @return
179    *    If <jk>null</jk>, then we successfully stored the contents of the file.
180    *    <br>Otherwise the contents of the file have changed and we return the new contents of the file.
181    * @throws IOException Thrown by underlying stream.
182    */
183   public abstract String write(String name, String expectedContents, String newContents) throws IOException;
184
185   /**
186    * Checks whether the configuration with the specified name exists in this store.
187    *
188    * @param name The config name.
189    * @return <jk>true</jk> if the configuration with the specified name exists in this store.
190    */
191   public abstract boolean exists(String name);
192
193   /**
194    * Registers a new listener on this store.
195    *
196    * @param name The configuration name to listen for.
197    * @param l The new listener.
198    * @return This object.
199    */
200   public synchronized ConfigStore register(String name, ConfigStoreListener l) {
201      name = resolveName(name);
202      Set<ConfigStoreListener> s = listeners.get(name);
203      if (s == null) {
204         s = synced(Collections.newSetFromMap(new IdentityHashMap<>()));
205         listeners.put(name, s);
206      }
207      s.add(l);
208      return this;
209   }
210
211   /**
212    * Unregisters a listener from this store.
213    *
214    * @param name The configuration name to listen for.
215    * @param l The listener to unregister.
216    * @return This object.
217    */
218   public synchronized ConfigStore unregister(String name, ConfigStoreListener l) {
219      name = resolveName(name);
220      Set<ConfigStoreListener> s = listeners.get(name);
221      if (s != null)
222         s.remove(l);
223      return this;
224   }
225
226   /**
227    * Returns a map file containing the parsed contents of a configuration.
228    *
229    * @param name The configuration name.
230    * @return
231    *    The parsed configuration.
232    *    <br>Never <jk>null</jk>.
233    * @throws IOException Thrown by underlying stream.
234    */
235   public synchronized ConfigMap getMap(String name) throws IOException {
236      name = resolveName(name);
237      ConfigMap cm = configMaps.get(name);
238      if (cm != null)
239         return cm;
240      cm = new ConfigMap(this, name);
241      ConfigMap cm2 = configMaps.putIfAbsent(name, cm);
242      if (cm2 != null)
243         return cm2;
244      register(name, cm);
245      return cm;
246   }
247
248   /**
249    * Called when the physical contents of a config file have changed.
250    *
251    * <p>
252    * Triggers calls to {@link ConfigStoreListener#onChange(String)} on all registered listeners.
253    *
254    * @param name The config name (e.g. the filename without the extension).
255    * @param contents The new contents.
256    * @return This object.
257    */
258   public synchronized ConfigStore update(String name, String contents) {
259      name = resolveName(name);
260      Set<ConfigStoreListener> s = listeners.get(name);
261      if (s != null)
262         listeners.get(name).forEach(x -> x.onChange(contents));
263      return this;
264   }
265
266   /**
267    * Convenience method for updating the contents of a file with lines.
268    *
269    * @param name The config name (e.g. the filename without the extension).
270    * @param contentLines The new contents.
271    * @return This object.
272    */
273   public synchronized ConfigStore update(String name, String...contentLines) {
274      name = resolveName(name);
275      StringBuilder sb = new StringBuilder();
276      for (String l : contentLines)
277         sb.append(l).append('\n');
278      return update(name, sb.toString());
279   }
280
281   /**
282    * Subclasses can override this method to convert config names to internal forms.
283    *
284    * <p>
285    * For example, the {@link FileStore} class can take in both <js>"MyConfig"</js> and <js>"MyConfig.cfg"</js>
286    * as names that both resolve to <js>"MyConfig.cfg"</js>.
287    *
288    * @param name The name to resolve.
289    * @return The resolved name.
290    */
291   protected String resolveName(String name) {
292      return name;
293   }
294}