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