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