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      boolean isWritable = isWritable(p);
227      OpenOption[] oo = isWritable ? new OpenOption[]{READ,WRITE,CREATE} : new OpenOption[]{READ};
228
229      try (FileChannel fc = FileChannel.open(p, oo)) {
230         try (FileLock lock = isWritable ? fc.lock() : null) {
231            ByteBuffer buf = ByteBuffer.allocate(1024);
232            StringBuilder sb = new StringBuilder();
233            while (fc.read(buf) != -1) {
234               sb.append(charset.decode((ByteBuffer)(buf.flip())));
235               buf.clear();
236            }
237            s = sb.toString();
238            cache.put(name, s);
239         }
240      }
241
242      return cache.get(name);
243   }
244
245   @Override /* ConfigStore */
246   public synchronized String write(String name, String expectedContents, String newContents) throws IOException {
247
248      // This is a no-op.
249      if (isEquals(expectedContents, newContents))
250         return null;
251
252      dir.mkdirs();
253      Path p = dir.toPath().resolve(name);
254
255      boolean exists = Files.exists(p);
256
257      // Don't create the file if we're not going to match.
258      if ((!exists) && isNotEmpty(expectedContents))
259         return "";
260
261      if (isWritable(p)) {
262         try (FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE)) {
263            try (FileLock lock = fc.lock()) {
264               String currentContents = "";
265               if (exists) {
266                  ByteBuffer buf = ByteBuffer.allocate(1024);
267                  StringBuilder sb = new StringBuilder();
268                  while (fc.read(buf) != -1) {
269                     sb.append(charset.decode((ByteBuffer)(buf.flip())));
270                     buf.clear();
271                  }
272                  currentContents = sb.toString();
273               }
274               if (expectedContents != null && ! isEquals(currentContents, expectedContents)) {
275                  if (currentContents == null)
276                     cache.remove(name);
277                  else
278                     cache.put(name, currentContents);
279                  return currentContents;
280               }
281               fc.position(0);
282               fc.write(charset.encode(newContents));
283            }
284         }
285      }
286
287      if (updateOnWrite)
288         update(name, newContents);
289      else
290         cache.remove(name);  // Invalidate the cache.
291
292      return null;
293   }
294
295   private synchronized boolean isWritable(Path p) {
296      try {
297         if (! Files.exists(p)) {
298            Files.createDirectories(p.getParent());
299            if (! Files.exists(p))
300               p.toFile().createNewFile();
301         }
302      } catch (IOException e) {
303         return false;
304      }
305      return Files.isWritable(p);
306   }
307
308   @Override /* ConfigStore */
309   public synchronized ConfigFileStore update(String name, String newContents) {
310      cache.put(name, newContents);
311      super.update(name, newContents);
312      return this;
313   }
314
315   @Override /* Closeable */
316   public synchronized void close() {
317      if (watcher != null)
318         watcher.interrupt();
319   }
320
321
322   //---------------------------------------------------------------------------------------------
323   // WatcherThread
324   //---------------------------------------------------------------------------------------------
325
326   final class WatcherThread extends Thread {
327      private final WatchService watchService;
328
329      WatcherThread(File dir, WatcherSensitivity s) throws Exception {
330         watchService = FileSystems.getDefault().newWatchService();
331         WatchEvent.Kind<?>[] kinds = new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY};
332         WatchEvent.Modifier modifier = lookupModifier(s);
333         dir.toPath().register(watchService, kinds, modifier);
334      }
335
336      @SuppressWarnings("restriction")
337      private WatchEvent.Modifier lookupModifier(WatcherSensitivity s) {
338         try {
339            switch(s) {
340               case LOW: return com.sun.nio.file.SensitivityWatchEventModifier.LOW;
341               case MEDIUM: return com.sun.nio.file.SensitivityWatchEventModifier.MEDIUM;
342               case HIGH: return com.sun.nio.file.SensitivityWatchEventModifier.HIGH;
343            }
344         } catch (Exception e) {
345            /* Ignore */
346         }
347         return null;
348
349      }
350
351      @SuppressWarnings("unchecked")
352      @Override /* Thread */
353      public void run() {
354          try {
355            WatchKey key;
356            while ((key = watchService.take()) != null) {
357                for (WatchEvent<?> event : key.pollEvents()) {
358                    WatchEvent.Kind<?> kind = event.kind();
359                    if (kind != OVERFLOW)
360                        ConfigFileStore.this.onFileEvent(((WatchEvent<Path>)event));
361                }
362                if (! key.reset())
363                     break;
364            }
365         } catch (Exception e) {
366            e.printStackTrace();
367            throw new RuntimeException(e);
368         }
369      };
370
371      @Override /* Thread */
372      public void interrupt() {
373         try {
374            watchService.close();
375         } catch (IOException e) {
376            throw new RuntimeException(e);
377         } finally {
378            super.interrupt();
379         }
380      }
381   }
382
383   /**
384    * Gets called when the watcher service on this store is triggered with a file system change.
385    *
386    * @param e The file system event.
387    * @throws IOException
388    */
389   protected synchronized void onFileEvent(WatchEvent<Path> e) throws IOException {
390      String fn = e.context().getFileName().toString();
391
392      String oldContents = cache.get(fn);
393      cache.remove(fn);
394      String newContents = read(fn);
395      if (! isEquals(oldContents, newContents)) {
396         update(fn, newContents);
397      }
398   }
399}