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