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