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 static java.nio.file.StandardWatchEventKinds.*;
016import static java.nio.file.StandardOpenOption.*;
017import static org.apache.juneau.internal.StringUtils.*;
018
019import java.io.*;
020import java.nio.*;
021import java.nio.channels.*;
022import java.nio.charset.*;
023import java.nio.file.*;
024import java.util.concurrent.*;
025
026import org.apache.juneau.*;
027
028/**
029 * Filesystem-based storage location for configuration files.
030 * 
031 * <p>
032 * Points to a file system directory containing configuration files.
033 */
034public class ConfigFileStore extends ConfigStore {
035
036   //-------------------------------------------------------------------------------------------------------------------
037   // Configurable properties
038   //-------------------------------------------------------------------------------------------------------------------
039
040   private static final String PREFIX = "ConfigFileStore.";
041
042   /**
043    * Configuration property:  Local file system directory.
044    * 
045    * <h5 class='section'>Property:</h5>
046    * <ul>
047    *    <li><b>Name:</b>  <js>"ConfigFileStore.directory.s"</js>
048    *    <li><b>Data type:</b>  <code>String</code>
049    *    <li><b>Default:</b>  <js>"."</js>
050    *    <li><b>Methods:</b> 
051    *       <ul>
052    *          <li class='jm'>{@link ConfigFileStoreBuilder#directory(String)}
053    *          <li class='jm'>{@link ConfigFileStoreBuilder#directory(File)}
054    *       </ul>
055    * </ul>
056    * 
057    * <h5 class='section'>Description:</h5>
058    * <p>
059    * Identifies the path of the directory containing the configuration files.
060    */
061   public static final String FILESTORE_directory = PREFIX + "directory.s";
062
063   /**
064    * Configuration property:  Charset.
065    * 
066    * <h5 class='section'>Property:</h5>
067    * <ul>
068    *    <li><b>Name:</b>  <js>"ConfigFileStore.charset.s"</js>
069    *    <li><b>Data type:</b>  {@link Charset}
070    *    <li><b>Default:</b>  {@link Charset#defaultCharset()}
071    *    <li><b>Methods:</b> 
072    *       <ul>
073    *          <li class='jm'>{@link ConfigFileStoreBuilder#charset(String)}
074    *          <li class='jm'>{@link ConfigFileStoreBuilder#charset(Charset)}
075    *       </ul>
076    * </ul>
077    * 
078    * <h5 class='section'>Description:</h5>
079    * <p>
080    * Identifies the charset of external files.
081    */
082   public static final String FILESTORE_charset = PREFIX + "charset.s";
083   
084   /**
085    * Configuration property:  Use watcher.
086    * 
087    * <h5 class='section'>Property:</h5>
088    * <ul>
089    *    <li><b>Name:</b>  <js>"ConfigFileStore.useWatcher.b"</js>
090    *    <li><b>Data type:</b>  <code>Boolean</code>
091    *    <li><b>Default:</b>  <jk>false</jk>
092    *    <li><b>Methods:</b> 
093    *       <ul>
094    *          <li class='jm'>{@link ConfigFileStoreBuilder#useWatcher()}
095    *       </ul>
096    * </ul>
097    * 
098    * <h5 class='section'>Description:</h5>
099    * <p>
100    * Use a file system watcher for file system changes.
101    * 
102    * <h5 class='section'>Notes:</h5>
103    * <ul class='spaced-list'>
104    *    <li>Calling {@link #close()} on this object closes the watcher.
105    * </ul>
106    */
107   public static final String FILESTORE_useWatcher = PREFIX + "useWatcher.s";
108   
109   /**
110    * Configuration property:  Watcher sensitivity.
111    * 
112    * <h5 class='section'>Property:</h5>
113    * <ul>
114    *    <li><b>Name:</b>  <js>"ConfigFileStore.watcherSensitivity.s"</js>
115    *    <li><b>Data type:</b>  {@link WatcherSensitivity}
116    *    <li><b>Default:</b>  {@link WatcherSensitivity#MEDIUM}
117    *    <li><b>Methods:</b> 
118    *       <ul>
119    *          <li class='jm'>{@link ConfigFileStoreBuilder#watcherSensitivity(WatcherSensitivity)}
120    *          <li class='jm'>{@link ConfigFileStoreBuilder#watcherSensitivity(String)}
121    *       </ul>
122    * </ul>
123    * 
124    * <h5 class='section'>Description:</h5>
125    * <p>
126    * Determines how frequently the file system is polled for updates.
127    * 
128    * <h5 class='section'>Notes:</h5>
129    * <ul class='spaced-list'>
130    *    <li>This relies on internal Sun packages and may not work on all JVMs.
131    * </ul>
132    */
133   public static final String FILESTORE_watcherSensitivity = PREFIX + "watcherSensitivity.s";
134   
135   /**
136    * Configuration property:  Update-on-write.
137    * 
138    * <h5 class='section'>Property:</h5>
139    * <ul>
140    *    <li><b>Name:</b>  <js>"ConfigFileStore.updateOnWrite.b"</js>
141    *    <li><b>Data type:</b>  <code>Boolean</code>
142    *    <li><b>Default:</b>  <jk>false</jk>
143    *    <li><b>Methods:</b> 
144    *       <ul>
145    *          <li class='jm'>{@link ConfigFileStoreBuilder#updateOnWrite()}
146    *       </ul>
147    * </ul>
148    * 
149    * <h5 class='section'>Description:</h5>
150    * <p>
151    * When enabled, the {@link #update(String, String)} method will be called immediately following
152    * calls to {@link #write(String, String, String)} when the contents are changing.
153    * <br>This allows for more immediate responses to configuration changes on file systems that use
154    * polling watchers.
155    * <br>This may cause double-triggering of {@link ConfigStoreListener ConfigStoreListeners}.
156    */
157   public static final String FILESTORE_updateOnWrite = PREFIX + "updateOnWrite.b";
158
159   
160   //-------------------------------------------------------------------------------------------------------------------
161   // Predefined instances
162   //-------------------------------------------------------------------------------------------------------------------
163
164   /** Default file store, all default values.*/
165   public static final ConfigFileStore DEFAULT = ConfigFileStore.create().build();
166
167
168   //-------------------------------------------------------------------------------------------------------------------
169   // Instance
170   //-------------------------------------------------------------------------------------------------------------------
171   
172   /**
173    * Create a new builder for this object.
174    * 
175    * @return A new builder for this object.
176    */
177   public static ConfigFileStoreBuilder create() {
178      return new ConfigFileStoreBuilder();
179   }
180   
181   @Override /* Context */
182   public ConfigFileStoreBuilder builder() {
183      return new ConfigFileStoreBuilder(getPropertyStore());
184   }
185
186   private final File dir;
187   private final Charset charset;
188   private final WatcherThread watcher;
189   private final boolean updateOnWrite;
190   private final ConcurrentHashMap<String,String> cache = new ConcurrentHashMap<>();
191   
192   /**
193    * Constructor.
194    * 
195    * @param ps The settings for this content store.
196    */
197   protected ConfigFileStore(PropertyStore ps) {
198      super(ps);
199      try {
200         dir = new File(getStringProperty(FILESTORE_directory, ".")).getCanonicalFile();
201         dir.mkdirs();
202         charset = getProperty(FILESTORE_charset, Charset.class, Charset.defaultCharset());
203         updateOnWrite = getBooleanProperty(FILESTORE_updateOnWrite, false);
204         WatcherSensitivity ws = getProperty(FILESTORE_watcherSensitivity, WatcherSensitivity.class, WatcherSensitivity.MEDIUM);
205         watcher = getBooleanProperty(FILESTORE_useWatcher, false) ? new WatcherThread(dir, ws) : null;
206         if (watcher != null)
207            watcher.start();
208      } catch (Exception e) {
209         throw new RuntimeException(e);
210      }
211   }
212   
213   @Override /* ConfigStore */
214   public synchronized String read(String name) throws IOException {
215      String s = cache.get(name);
216      if (s != null)
217         return s;
218      
219      dir.mkdirs();
220      
221      // If file doesn't exist, don't trigger creation.
222      Path p = dir.toPath().resolve(name);
223      if (! Files.exists(p)) 
224         return "";
225      
226      try (FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE)) {
227         try (FileLock lock = fc.lock()) {
228            ByteBuffer buf = ByteBuffer.allocate(1024);
229            StringBuilder sb = new StringBuilder();
230            while (fc.read(buf) != -1) {
231               sb.append(charset.decode((ByteBuffer)(buf.flip())));
232               buf.clear();
233            }
234            s = sb.toString();
235            cache.put(name, s);
236         }
237      }
238      
239      return cache.get(name);
240   }
241
242   @Override /* ConfigStore */
243   public synchronized String write(String name, String expectedContents, String newContents) throws IOException {
244
245      // This is a no-op.
246      if (isEquals(expectedContents, newContents))
247         return null;
248
249      dir.mkdirs();
250      Path p = dir.toPath().resolve(name);
251      
252      boolean exists = Files.exists(p);
253      
254      // Don't create the file if we're not going to match.
255      if ((!exists) && (!isEmpty(expectedContents)))
256         return "";
257      
258      try (FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE)) {
259         try (FileLock lock = fc.lock()) {
260            String currentContents = "";
261            if (exists) {
262               ByteBuffer buf = ByteBuffer.allocate(1024);
263               StringBuilder sb = new StringBuilder();
264               while (fc.read(buf) != -1) {
265                  sb.append(charset.decode((ByteBuffer)(buf.flip())));
266                  buf.clear();
267               }
268               currentContents = sb.toString();
269            }
270            if (expectedContents != null && ! isEquals(currentContents, expectedContents)) {
271               if (currentContents == null)
272                  cache.remove(name);
273               else
274                  cache.put(name, currentContents);
275               return currentContents;
276            }
277            fc.position(0);
278            fc.write(charset.encode(newContents));
279         }
280      }
281      
282      if (updateOnWrite)
283         update(name, newContents);
284      else 
285         cache.remove(name);  // Invalidate the cache.
286      
287      return null;
288   }
289      
290   @Override /* ConfigStore */
291   public synchronized ConfigFileStore update(String name, String newContents) {
292      cache.put(name, newContents);
293      super.update(name, newContents);
294      return this;
295   }
296
297   @Override /* Closeable */
298   public synchronized void close() {
299      if (watcher != null)
300         watcher.interrupt();
301   }
302   
303   
304   //---------------------------------------------------------------------------------------------
305   // WatcherThread
306   //---------------------------------------------------------------------------------------------
307
308   final class WatcherThread extends Thread {
309      private final WatchService watchService;
310      
311      WatcherThread(File dir, WatcherSensitivity s) throws Exception {
312         watchService = FileSystems.getDefault().newWatchService();
313         WatchEvent.Kind<?>[] kinds = new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY};
314         WatchEvent.Modifier modifier = lookupModifier(s);
315         dir.toPath().register(watchService, kinds, modifier);
316      }
317      
318      @SuppressWarnings("restriction")
319      private WatchEvent.Modifier lookupModifier(WatcherSensitivity s) {
320         try {
321            switch(s) {
322               case LOW: return com.sun.nio.file.SensitivityWatchEventModifier.LOW;
323               case MEDIUM: return com.sun.nio.file.SensitivityWatchEventModifier.MEDIUM;
324               case HIGH: return com.sun.nio.file.SensitivityWatchEventModifier.HIGH;
325            }
326         } catch (Exception e) {
327            /* Ignore */
328         }
329         return null;
330         
331      }
332      
333      @SuppressWarnings("unchecked")
334      @Override /* Thread */
335      public void run() {
336          try {
337            WatchKey key;
338            while ((key = watchService.take()) != null) {
339                for (WatchEvent<?> event : key.pollEvents()) {
340                    WatchEvent.Kind<?> kind = event.kind();
341                    if (kind != OVERFLOW) 
342                        ConfigFileStore.this.onFileEvent(((WatchEvent<Path>)event));
343                }
344                if (! key.reset())
345                     break;
346            }
347         } catch (Exception e) {
348            e.printStackTrace();
349            throw new RuntimeException(e);
350         }
351      };
352      
353      @Override /* Thread */
354      public void interrupt() {
355         try {
356            watchService.close();
357         } catch (IOException e) {
358            throw new RuntimeException(e);
359         } finally {
360            super.interrupt();
361         }
362      }
363   }
364   
365   /**
366    * Gets called when the watcher service on this store is triggered with a file system change.
367    * 
368    * @param e The file system event.
369    * @throws IOException
370    */
371   protected synchronized void onFileEvent(WatchEvent<Path> e) throws IOException {
372      String fn = e.context().getFileName().toString();
373      
374      String oldContents = cache.get(fn);
375      cache.remove(fn);
376      String newContents = read(fn);
377      if (! isEquals(oldContents, newContents)) {
378         update(fn, newContents);
379      }
380   }
381}