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.*;
027import org.apache.juneau.annotation.*;
028import org.apache.juneau.collections.*;
029import org.apache.juneau.internal.*;
030
031/**
032 * Filesystem-based storage location for configuration files.
033 *
034 * <p>
035 * Points to a file system directory containing configuration files.
036 */
037@ConfigurableContext
038public class ConfigFileStore extends ConfigStore {
039
040   //-------------------------------------------------------------------------------------------------------------------
041   // Configurable properties
042   //-------------------------------------------------------------------------------------------------------------------
043
044   static final String PREFIX = "ConfigFileStore";
045
046   /**
047    * Configuration property:  Local file system directory.
048    *
049    * <h5 class='section'>Property:</h5>
050    * <ul class='spaced-list'>
051    *    <li><b>ID:</b>  {@link org.apache.juneau.config.store.ConfigFileStore#FILESTORE_directory FILESTORE_directory}
052    *    <li><b>Name:</b>  <js>"ConfigFileStore.directory.s"</js>
053    *    <li><b>Data type:</b>  <c>String</c>
054    *    <li><b>System property:</b>  <c>ConfigFileStore.directory</c>
055    *    <li><b>Environment variable:</b>  <c>CONFIGFILESTORE_DIRECTORY</c>
056    *    <li><b>Default:</b>  <js>"."</js>
057    *    <li><b>Methods:</b>
058    *       <ul>
059    *          <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#directory(String)}
060    *          <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#directory(File)}
061    *       </ul>
062    * </ul>
063    *
064    * <h5 class='section'>Description:</h5>
065    * <p>
066    * Identifies the path of the directory containing the configuration files.
067    */
068   public static final String FILESTORE_directory = PREFIX + ".directory.s";
069
070   /**
071    * Configuration property:  Charset.
072    *
073    * <h5 class='section'>Property:</h5>
074    * <ul class='spaced-list'>
075    *    <li><b>ID:</b>  {@link org.apache.juneau.config.store.ConfigFileStore#FILESTORE_charset FILESTORE_charset}
076    *    <li><b>Name:</b>  <js>"ConfigFileStore.charset.s"</js>
077    *    <li><b>Data type:</b>  {@link java.nio.charset.Charset}
078    *    <li><b>System property:</b>  <c>ConfigFileStore.charset</c>
079    *    <li><b>Environment variable:</b>  <c>CONFIGFILESTORE_CHARSET</c>
080    *    <li><b>Default:</b>  {@link java.nio.charset.Charset#defaultCharset()}
081    *    <li><b>Methods:</b>
082    *       <ul>
083    *          <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#charset(String)}
084    *          <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#charset(Charset)}
085    *       </ul>
086    * </ul>
087    *
088    * <h5 class='section'>Description:</h5>
089    * <p>
090    * Identifies the charset of external files.
091    */
092   public static final String FILESTORE_charset = PREFIX + ".charset.s";
093
094   /**
095    * Configuration property:  Use watcher.
096    *
097    * <h5 class='section'>Property:</h5>
098    * <ul class='spaced-list'>
099    *    <li><b>ID:</b>  {@link org.apache.juneau.config.store.ConfigFileStore#FILESTORE_useWatcher FILESTORE_useWatcher}
100    *    <li><b>Name:</b>  <js>"ConfigFileStore.useWatcher.b"</js>
101    *    <li><b>Data type:</b>  <jk>boolean</jk>
102    *    <li><b>System property:</b>  <c>ConfigFileStore.useWatcher</c>
103    *    <li><b>Environment variable:</b>  <c>CONFIGFILESTORE_USEWATCHER</c>
104    *    <li><b>Default:</b>  <jk>false</jk>
105    *    <li><b>Methods:</b>
106    *       <ul>
107    *          <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#useWatcher()}
108    *       </ul>
109    * </ul>
110    *
111    * <h5 class='section'>Description:</h5>
112    * <p>
113    * Use a file system watcher for file system changes.
114    *
115    * <ul class='notes'>
116    *    <li>Calling {@link #close()} on this object closes the watcher.
117    * </ul>
118    */
119   public static final String FILESTORE_useWatcher = PREFIX + ".useWatcher.s";
120
121   /**
122    * Configuration property:  Watcher sensitivity.
123    *
124    * <h5 class='section'>Property:</h5>
125    * <ul class='spaced-list'>
126    *    <li><b>ID:</b>  {@link org.apache.juneau.config.store.ConfigFileStore#FILESTORE_watcherSensitivity FILESTORE_watcherSensitivity}
127    *    <li><b>Name:</b>  <js>"ConfigFileStore.watcherSensitivity.s"</js>
128    *    <li><b>Data type:</b>  {@link org.apache.juneau.config.store.WatcherSensitivity}
129    *    <li><b>System property:</b>  <c>ConfigFileStore.watcherSensitivity</c>
130    *    <li><b>Environment variable:</b>  <c>CONFIGFILESTORE_WATCHERSENSITIVITY</c>
131    *    <li><b>Default:</b>  {@link org.apache.juneau.config.store.WatcherSensitivity#MEDIUM}
132    *    <li><b>Methods:</b>
133    *       <ul>
134    *          <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#watcherSensitivity(WatcherSensitivity)}
135    *          <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#watcherSensitivity(String)}
136    *       </ul>
137    * </ul>
138    *
139    * <h5 class='section'>Description:</h5>
140    * <p>
141    * Determines how frequently the file system is polled for updates.
142    *
143    * <ul class='notes'>
144    *    <li>This relies on internal Sun packages and may not work on all JVMs.
145    * </ul>
146    */
147   public static final String FILESTORE_watcherSensitivity = PREFIX + ".watcherSensitivity.s";
148
149   /**
150    * Configuration property:  Update-on-write.
151    *
152    * <h5 class='section'>Property:</h5>
153    * <ul class='spaced-list'>
154    *    <li><b>ID:</b>  {@link org.apache.juneau.config.store.ConfigFileStore#FILESTORE_updateOnWrite FILESTORE_updateOnWrite}
155    *    <li><b>Name:</b>  <js>"ConfigFileStore.updateOnWrite.b"</js>
156    *    <li><b>Data type:</b>  <jk>boolean</jk>
157    *    <li><b>System property:</b>  <c>ConfigFileStore.updateOnWrite</c>
158    *    <li><b>Environment variable:</b>  <c>CONFIGFILESTORE_UPDATEONWRITE</c>
159    *    <li><b>Default:</b>  <jk>false</jk>
160    *    <li><b>Methods:</b>
161    *       <ul>
162    *          <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#updateOnWrite()}
163    *       </ul>
164    * </ul>
165    *
166    * <h5 class='section'>Description:</h5>
167    * <p>
168    * When enabled, the {@link #update(String, String)} method will be called immediately following
169    * calls to {@link #write(String, String, String)} when the contents are changing.
170    * <br>This allows for more immediate responses to configuration changes on file systems that use
171    * polling watchers.
172    * <br>This may cause double-triggering of {@link ConfigStoreListener ConfigStoreListeners}.
173    */
174   public static final String FILESTORE_updateOnWrite = PREFIX + ".updateOnWrite.b";
175
176   /**
177    * Configuration property:  File extensions.
178    *
179    * <h5 class='section'>Property:</h5>
180    * <ul class='spaced-list'>
181    *    <li><b>ID:</b>  {@link org.apache.juneau.config.store.ConfigFileStore#FILESTORE_extensions FILESTORE_extensions}
182    *    <li><b>Name:</b>  <js>"ConfigFileStore.extensions.s"</js>
183    *    <li><b>Data type:</b>  <c>String</c> (comma-delimited)
184    *    <li><b>System property:</b>  <c>ConfigFileStore.extensions</c>
185    *    <li><b>Environment variable:</b>  <c>CONFIGFILESTORE_EXTENSIONS</c>
186    *    <li><b>Default:</b>  <js>"cfg"</js>
187    *    <li><b>Methods:</b>
188    *       <ul>
189    *          <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#extensions(String)}
190    *       </ul>
191    * </ul>
192    *
193    * <h5 class='section'>Description:</h5>
194    * <p>
195    * Defines what file extensions to search for when the config name does not have an extension.
196    */
197   public static final String FILESTORE_extensions = PREFIX + ".extensions.s";
198
199   //-------------------------------------------------------------------------------------------------------------------
200   // Predefined instances
201   //-------------------------------------------------------------------------------------------------------------------
202
203   /** Default file store, all default values.*/
204   public static final ConfigFileStore DEFAULT = ConfigFileStore.create().build();
205
206
207   //-------------------------------------------------------------------------------------------------------------------
208   // Instance
209   //-------------------------------------------------------------------------------------------------------------------
210
211   /**
212    * Create a new builder for this object.
213    *
214    * @return A new builder for this object.
215    */
216   public static ConfigFileStoreBuilder create() {
217      return new ConfigFileStoreBuilder();
218   }
219
220   @Override /* Context */
221   public ConfigFileStoreBuilder builder() {
222      return new ConfigFileStoreBuilder(getPropertyStore());
223   }
224
225   private final File dir;
226   private final Charset charset;
227   private final WatcherThread watcher;
228   private final boolean updateOnWrite;
229   private final ConcurrentHashMap<String,String> cache = new ConcurrentHashMap<>();
230   private final ConcurrentHashMap<String,String> nameCache = new ConcurrentHashMap<>();
231   private final String[] extensions;
232
233   /**
234    * Constructor.
235    *
236    * @param ps The settings for this content store.
237    */
238   protected ConfigFileStore(PropertyStore ps) {
239      super(ps);
240      try {
241         dir = new File(getStringProperty(FILESTORE_directory, ".")).getCanonicalFile();
242         dir.mkdirs();
243         charset = getProperty(FILESTORE_charset, Charset.class, Charset.defaultCharset());
244         updateOnWrite = getBooleanProperty(FILESTORE_updateOnWrite, false);
245         extensions = getCdlProperty(FILESTORE_extensions, "cfg");
246         WatcherSensitivity ws = getProperty(FILESTORE_watcherSensitivity, WatcherSensitivity.class, WatcherSensitivity.MEDIUM);
247         watcher = getBooleanProperty(FILESTORE_useWatcher, false) ? new WatcherThread(dir, ws) : null;
248         if (watcher != null)
249            watcher.start();
250
251      } catch (Exception e) {
252         throw new RuntimeException(e);
253      }
254   }
255
256   @Override /* ConfigStore */
257   public synchronized String read(String name) throws IOException {
258      name = resolveName(name);
259
260      Path p = resolveFile(name);
261      name = p.getFileName().toString();
262
263      String s = cache.get(name);
264      if (s != null)
265         return s;
266
267      dir.mkdirs();
268
269      // If file doesn't exist, don't trigger creation.
270      if (! Files.exists(p))
271         return "";
272
273      boolean isWritable = isWritable(p);
274      OpenOption[] oo = isWritable ? new OpenOption[]{READ,WRITE,CREATE} : new OpenOption[]{READ};
275
276      try (FileChannel fc = FileChannel.open(p, oo)) {
277         try (FileLock lock = isWritable ? fc.lock() : null) {
278            ByteBuffer buf = ByteBuffer.allocate(1024);
279            StringBuilder sb = new StringBuilder();
280            while (fc.read(buf) != -1) {
281               sb.append(charset.decode((ByteBuffer)(buf.flip())));
282               buf.clear();
283            }
284            s = sb.toString();
285            cache.put(name, s);
286         }
287      }
288
289      return cache.get(name);
290   }
291
292   @Override /* ConfigStore */
293   public synchronized String write(String name, String expectedContents, String newContents) throws IOException {
294      name = resolveName(name);
295
296      // This is a no-op.
297      if (isEquals(expectedContents, newContents))
298         return null;
299
300      dir.mkdirs();
301
302      Path p = resolveFile(name);
303      name = p.getFileName().toString();
304
305      boolean exists = Files.exists(p);
306
307      // Don't create the file if we're not going to match.
308      if ((!exists) && isNotEmpty(expectedContents))
309         return "";
310
311      if (isWritable(p)) {
312         if (newContents == null)
313            Files.delete(p);
314         else {
315            try (FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE)) {
316               try (FileLock lock = fc.lock()) {
317                  String currentContents = "";
318                  if (exists) {
319                     ByteBuffer buf = ByteBuffer.allocate(1024);
320                     StringBuilder sb = new StringBuilder();
321                     while (fc.read(buf) != -1) {
322                        sb.append(charset.decode((ByteBuffer)(buf.flip())));
323                        buf.clear();
324                     }
325                     currentContents = sb.toString();
326                  }
327                  if (expectedContents != null && ! isEquals(currentContents, expectedContents)) {
328                     if (currentContents == null)
329                        cache.remove(name);
330                     else
331                        cache.put(name, currentContents);
332                     return currentContents;
333                  }
334                  fc.position(0);
335                  fc.write(charset.encode(newContents));
336               }
337            }
338         }
339      }
340
341      if (updateOnWrite)
342         update(name, newContents);
343      else
344         cache.remove(name);  // Invalidate the cache.
345
346      return null;
347   }
348
349   @Override /* ConfigStore */
350   public synchronized boolean exists(String name) {
351      return Files.exists(resolveFile(name));
352   }
353
354   private Path resolveFile(String name) {
355      return dir.toPath().resolve(resolveName(name));
356   }
357
358   @Override
359   protected String resolveName(String name) {
360      if (! nameCache.containsKey(name)) {
361         String n = null;
362
363         // Does file exist as-is?
364         if (FileUtils.exists(dir, name))
365            n = name;
366
367         // Does name already have an extension?
368         if (n == null) {
369            for (String ext : extensions) {
370               if (FileUtils.hasExtension(name, ext)) {
371                  n = name;
372                  break;
373               }
374            }
375         }
376
377         // Find file with the correct extension.
378         if (n == null) {
379            for (String ext : extensions) {
380               if (FileUtils.exists(dir, name + '.' + ext)) {
381                  n = name + '.' + ext;
382                  break;
383               }
384            }
385         }
386
387         // If file not found, use the default which is the name with the first extension.
388         if (n == null)
389            n = extensions.length == 0 ? name : (name + "." + extensions[0]);
390
391         nameCache.put(name, n);
392      }
393      return nameCache.get(name);
394   }
395
396   private synchronized boolean isWritable(Path p) {
397      try {
398         if (! Files.exists(p)) {
399            Files.createDirectories(p.getParent());
400            if (! Files.exists(p))
401               p.toFile().createNewFile();
402         }
403      } catch (IOException e) {
404         return false;
405      }
406      return Files.isWritable(p);
407   }
408
409   @Override /* ConfigStore */
410   public synchronized ConfigFileStore update(String name, String newContents) {
411      cache.put(name, newContents);
412      super.update(name, newContents);
413      return this;
414   }
415
416   @Override /* Closeable */
417   public synchronized void close() {
418      if (watcher != null)
419         watcher.interrupt();
420   }
421
422
423   //---------------------------------------------------------------------------------------------
424   // WatcherThread
425   //---------------------------------------------------------------------------------------------
426
427   final class WatcherThread extends Thread {
428      private final WatchService watchService;
429
430      WatcherThread(File dir, WatcherSensitivity s) throws Exception {
431         watchService = FileSystems.getDefault().newWatchService();
432         WatchEvent.Kind<?>[] kinds = new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY};
433         WatchEvent.Modifier modifier = lookupModifier(s);
434         dir.toPath().register(watchService, kinds, modifier);
435      }
436
437      @SuppressWarnings("restriction")
438      private WatchEvent.Modifier lookupModifier(WatcherSensitivity s) {
439         try {
440            switch(s) {
441               case LOW: return com.sun.nio.file.SensitivityWatchEventModifier.LOW;
442               case MEDIUM: return com.sun.nio.file.SensitivityWatchEventModifier.MEDIUM;
443               case HIGH: return com.sun.nio.file.SensitivityWatchEventModifier.HIGH;
444            }
445         } catch (Exception e) {
446            /* Ignore */
447         }
448         return null;
449
450      }
451
452      @SuppressWarnings("unchecked")
453      @Override /* Thread */
454      public void run() {
455          try {
456            WatchKey key;
457            while ((key = watchService.take()) != null) {
458                for (WatchEvent<?> event : key.pollEvents()) {
459                    WatchEvent.Kind<?> kind = event.kind();
460                    if (kind != OVERFLOW)
461                        ConfigFileStore.this.onFileEvent(((WatchEvent<Path>)event));
462                }
463                if (! key.reset())
464                     break;
465            }
466         } catch (Exception e) {
467            e.printStackTrace();
468            throw new RuntimeException(e);
469         }
470      };
471
472      @Override /* Thread */
473      public void interrupt() {
474         try {
475            watchService.close();
476         } catch (IOException e) {
477            throw new RuntimeException(e);
478         } finally {
479            super.interrupt();
480         }
481      }
482   }
483
484   /**
485    * Gets called when the watcher service on this store is triggered with a file system change.
486    *
487    * @param e The file system event.
488    * @throws IOException Thrown by underlying stream.
489    */
490   protected synchronized void onFileEvent(WatchEvent<Path> e) throws IOException {
491      String fn = e.context().getFileName().toString();
492
493      String oldContents = cache.get(fn);
494      cache.remove(fn);
495      String newContents = read(fn);
496      if (! isEquals(oldContents, newContents)) {
497         update(fn, newContents);
498      }
499   }
500
501   //-----------------------------------------------------------------------------------------------------------------
502   // Other methods.
503   //-----------------------------------------------------------------------------------------------------------------
504
505   @Override /* Context */
506   public OMap toMap() {
507      return super.toMap()
508         .a("ConfigFileStore", new DefaultFilteringOMap()
509            .a("charset", charset)
510            .a("extensions", extensions)
511            .a("updateOnWrite", updateOnWrite)
512         );
513   }
514}