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 java.io.*;
016import java.util.*;
017import java.util.concurrent.*;
018
019import org.apache.juneau.*;
020import org.apache.juneau.annotation.*;
021import org.apache.juneau.collections.*;
022import org.apache.juneau.config.internal.*;
023
024/**
025 * Represents a storage location for configuration files.
026 *
027 * <p>
028 * Content stores require two methods to be implemented:
029 * <ul class='javatree'>
030 *    <li class='jm'>{@link #read(String)} - Retrieve a config file.
031 *    <li class='jm'>{@link #write(String,String,String)} - ConfigStore a config file.
032 * </ul>
033 */
034@ConfigurableContext
035public abstract class ConfigStore extends Context implements Closeable {
036
037   //-------------------------------------------------------------------------------------------------------------------
038   // Configurable properties
039   //-------------------------------------------------------------------------------------------------------------------
040
041   static final String PREFIX = "ConfigStore";
042
043   //-------------------------------------------------------------------------------------------------------------------
044   // Instance
045   //-------------------------------------------------------------------------------------------------------------------
046
047   private final ConcurrentHashMap<String,Set<ConfigStoreListener>> listeners = new ConcurrentHashMap<>();
048   private final ConcurrentHashMap<String,ConfigMap> configMaps = new ConcurrentHashMap<>();
049
050   /**
051    * Constructor.
052    *
053    * @param ps The settings for this content store.
054    */
055   protected ConfigStore(PropertyStore ps) {
056      super(ps, false);
057   }
058
059   /**
060    * Returns the contents of the configuration file.
061    *
062    * @param name The config file name.
063    * @return
064    *    The contents of the configuration file.
065    *    <br>A blank string if the config does not exist.
066    *    <br>Never <jk>null</jk>.
067    * @throws IOException Thrown by underlying stream.
068    */
069   public abstract String read(String name) throws IOException;
070
071   /**
072    * Saves the contents of the configuration file if the underlying storage hasn't been modified.
073    *
074    * @param name The config file name.
075    * @param expectedContents The expected contents of the file.
076    * @param newContents The new contents.
077    * @return
078    *    If <jk>null</jk>, then we successfully stored the contents of the file.
079    *    <br>Otherwise the contents of the file have changed and we return the new contents of the file.
080    * @throws IOException Thrown by underlying stream.
081    */
082   public abstract String write(String name, String expectedContents, String newContents) throws IOException;
083
084   /**
085    * Checks whether the configuration with the specified name exists in this store.
086    *
087    * @param name The config name.
088    * @return <jk>true</jk> if the configuration with the specified name exists in this store.
089    */
090   public abstract boolean exists(String name);
091
092   /**
093    * Registers a new listener on this store.
094    *
095    * @param name The configuration name to listen for.
096    * @param l The new listener.
097    * @return This object (for method chaining).
098    */
099   public synchronized ConfigStore register(String name, ConfigStoreListener l) {
100      name = resolveName(name);
101      Set<ConfigStoreListener> s = listeners.get(name);
102      if (s == null) {
103         s = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<ConfigStoreListener,Boolean>()));
104         listeners.put(name, s);
105      }
106      s.add(l);
107      return this;
108   }
109
110   /**
111    * Unregisters a listener from this store.
112    *
113    * @param name The configuration name to listen for.
114    * @param l The listener to unregister.
115    * @return This object (for method chaining).
116    */
117   public synchronized ConfigStore unregister(String name, ConfigStoreListener l) {
118      name = resolveName(name);
119      Set<ConfigStoreListener> s = listeners.get(name);
120      if (s != null)
121         s.remove(l);
122      return this;
123   }
124
125   /**
126    * Returns a map file containing the parsed contents of a configuration.
127    *
128    * @param name The configuration name.
129    * @return
130    *    The parsed configuration.
131    *    <br>Never <jk>null</jk>.
132    * @throws IOException Thrown by underlying stream.
133    */
134   public synchronized ConfigMap getMap(String name) throws IOException {
135      name = resolveName(name);
136      ConfigMap cm = configMaps.get(name);
137      if (cm != null)
138         return cm;
139      cm = new ConfigMap(this, name);
140      ConfigMap cm2 = configMaps.putIfAbsent(name, cm);
141      if (cm2 != null)
142         return cm2;
143      register(name, cm);
144      return cm;
145   }
146
147   /**
148    * Called when the physical contents of a config file have changed.
149    *
150    * <p>
151    * Triggers calls to {@link ConfigStoreListener#onChange(String)} on all registered listeners.
152    *
153    * @param name The config name (e.g. the filename without the extension).
154    * @param contents The new contents.
155    * @return This object (for method chaining).
156    */
157   public synchronized ConfigStore update(String name, String contents) {
158      name = resolveName(name);
159      Set<ConfigStoreListener> s = listeners.get(name);
160      if (s != null)
161         for (ConfigStoreListener l : listeners.get(name))
162            l.onChange(contents);
163      return this;
164   }
165
166   /**
167    * Convenience method for updating the contents of a file with lines.
168    *
169    * @param name The config name (e.g. the filename without the extension).
170    * @param contentLines The new contents.
171    * @return This object (for method chaining).
172    */
173   public synchronized ConfigStore update(String name, String...contentLines) {
174      name = resolveName(name);
175      StringBuilder sb = new StringBuilder();
176      for (String l : contentLines)
177         sb.append(l).append('\n');
178      return update(name, sb.toString());
179   }
180
181   /**
182    * Subclasses can override this method to convert config names to internal forms.
183    *
184    * <p>
185    * For example, the {@link ConfigFileStore} class can take in both <js>"MyConfig"</js> and <js>"MyConfig.cfg"</js>
186    * as names that both resolve to <js>"MyConfig.cfg"</js>.
187    *
188    * @param name The name to resolve.
189    * @return The resolved name.
190    */
191   protected String resolveName(String name) {
192      return name;
193   }
194
195   /**
196    * Unused.
197    */
198   @Override /* Context */
199   public final Session createSession(SessionArgs args) {
200      throw new NoSuchMethodError();
201   }
202
203   /**
204    * Unused.
205    */
206   @Override /* Context */
207   public final SessionArgs createDefaultSessionArgs() {
208      throw new NoSuchMethodError();
209   }
210
211   //-----------------------------------------------------------------------------------------------------------------
212   // Other methods.
213   //-----------------------------------------------------------------------------------------------------------------
214
215   @Override /* Context */
216   public OMap toMap() {
217      return super.toMap()
218         .a("ConfigStore", new DefaultFilteringOMap()
219         );
220   }
221}