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.collections.JsonMap.*;
018import static org.apache.juneau.common.internal.StringUtils.*;
019import static org.apache.juneau.common.internal.ThrowableUtils.*;
020
021import java.io.*;
022import java.lang.annotation.*;
023import java.lang.reflect.*;
024import java.nio.*;
025import java.nio.channels.*;
026import java.nio.charset.*;
027import java.nio.file.*;
028import java.util.concurrent.*;
029
030import org.apache.juneau.*;
031import org.apache.juneau.collections.*;
032import org.apache.juneau.internal.*;
033import org.apache.juneau.utils.*;
034
035/**
036 * Filesystem-based storage location for configuration files.
037 *
038 * <p>
039 * Points to a file system directory containing configuration files.
040 *
041 * <h5 class='section'>Notes:</h5><ul>
042 *    <li class='note'>This class is thread safe and reusable.
043 * </ul>
044 */
045public class FileStore extends ConfigStore {
046
047   //-------------------------------------------------------------------------------------------------------------------
048   // Static
049   //-------------------------------------------------------------------------------------------------------------------
050
051   /** Default file store, all default values.*/
052   public static final FileStore DEFAULT = FileStore.create().build();
053
054   /**
055    * Creates a new builder for this object.
056    *
057    * @return A new builder.
058    */
059   public static Builder create() {
060      return new Builder();
061   }
062
063   //-------------------------------------------------------------------------------------------------------------------
064   // Builder
065   //-------------------------------------------------------------------------------------------------------------------
066
067   /**
068    * Builder class.
069    */
070   @FluentSetters
071   public static class Builder extends ConfigStore.Builder {
072
073      String directory, extensions;
074      Charset charset;
075      boolean enableWatcher, updateOnWrite;
076      WatcherSensitivity watcherSensitivity;
077
078      /**
079       * Constructor, default settings.
080       */
081      protected Builder() {
082         directory = env("ConfigFileStore.directory", ".");
083         charset = env("ConfigFileStore.charset", Charset.defaultCharset());
084         enableWatcher = env("ConfigFileStore.enableWatcher", false);
085         watcherSensitivity = env("ConfigFileStore.watcherSensitivity", WatcherSensitivity.MEDIUM);
086         updateOnWrite = env("ConfigFileStore.updateOnWrite", false);
087         extensions = env("ConfigFileStore.extensions", "cfg");
088      }
089
090      /**
091       * Copy constructor.
092       *
093       * @param copyFrom The bean to copy from.
094       */
095      protected Builder(FileStore copyFrom) {
096         super(copyFrom);
097         type(copyFrom.getClass());
098         directory = copyFrom.directory;
099         charset = copyFrom.charset;
100         enableWatcher = copyFrom.enableWatcher;
101         watcherSensitivity = copyFrom.watcherSensitivity;
102         updateOnWrite = copyFrom.updateOnWrite;
103         extensions = copyFrom.extensions;
104      }
105
106      /**
107       * Copy constructor.
108       *
109       * @param copyFrom The builder to copy from.
110       */
111      protected Builder(Builder copyFrom) {
112         super(copyFrom);
113         directory = copyFrom.directory;
114         charset = copyFrom.charset;
115         enableWatcher = copyFrom.enableWatcher;
116         watcherSensitivity = copyFrom.watcherSensitivity;
117         updateOnWrite = copyFrom.updateOnWrite;
118         extensions = copyFrom.extensions;
119      }
120
121      @Override /* Context.Builder */
122      public Builder copy() {
123         return new Builder(this);
124      }
125
126      @Override /* Context.Builder */
127      public FileStore build() {
128         return build(FileStore.class);
129      }
130
131      //-----------------------------------------------------------------------------------------------------------------
132      // Properties
133      //-----------------------------------------------------------------------------------------------------------------
134
135      /**
136       * Local file system directory.
137       *
138       * <p>
139       * Identifies the path of the directory containing the configuration files.
140       *
141       * @param value
142       *    The new value for this property.
143       *    <br>The default is the first value found:
144       *    <ul>
145       *       <li>System property <js>"ConfigFileStore.directory"
146       *       <li>Environment variable <js>"CONFIGFILESTORE_DIRECTORY"
147       *       <li><js>"."</js>
148       *    </ul>
149       * @return This object.
150       */
151      public Builder directory(String value) {
152         directory = value;
153         return this;
154      }
155
156      /**
157       * Local file system directory.
158       *
159       * <p>
160       * Identifies the path of the directory containing the configuration files.
161       *
162       * @param value
163       *    The new value for this property.
164       *    <br>The default is the first value found:
165       *    <ul>
166       *       <li>System property <js>"ConfigFileStore.directory"
167       *       <li>Environment variable <js>"CONFIGFILESTORE_DIRECTORY"
168       *       <li><js>"."</js>.
169       *    </ul>
170       * @return This object.
171       */
172      public Builder directory(File value) {
173         directory = value.getAbsolutePath();
174         return this;
175      }
176
177      /**
178       * Charset for external files.
179       *
180       * <p>
181       * Identifies the charset of external files.
182       *
183       * @param value
184       *    The new value for this property.
185       *    <br>The default is the first value found:
186       *    <ul>
187       *       <li>System property <js>"ConfigFileStore.charset"
188       *       <li>Environment variable <js>"CONFIGFILESTORE_CHARSET"
189       *       <li>{@link Charset#defaultCharset()}
190       *    </ul>
191       * @return This object.
192       */
193      public Builder charset(Charset value) {
194         charset = value;
195         return this;
196      }
197
198      /**
199       * Use watcher.
200       *
201       * <p>
202       * Use a file system watcher for file system changes.
203       *
204       * <h5 class='section'>Notes:</h5><ul>
205       *    <li class='note'>Calling {@link FileStore#close()} closes the watcher.
206       * </ul>
207       *
208       * <p>
209       *    The default is the first value found:
210       *    <ul>
211       *       <li>System property <js>"ConfigFileStore.enableWatcher"
212       *       <li>Environment variable <js>"CONFIGFILESTORE_ENABLEWATCHER"
213       *       <li><jk>false</jk>.
214       *    </ul>
215       *
216       * @return This object.
217       */
218      public Builder enableWatcher() {
219         enableWatcher = true;
220         return this;
221      }
222
223      /**
224       * Watcher sensitivity.
225       *
226       * <p>
227       * Determines how frequently the file system is polled for updates.
228       *
229       * <h5 class='section'>Notes:</h5><ul>
230       *    <li class='note'>This relies on internal Sun packages and may not work on all JVMs.
231       * </ul>
232       *
233       * @param value
234       *    The new value for this property.
235       *    <br>The default is the first value found:
236       *    <ul>
237       *       <li>System property <js>"ConfigFileStore.watcherSensitivity"
238       *       <li>Environment variable <js>"CONFIGFILESTORE_WATCHERSENSITIVITY"
239       *       <li>{@link WatcherSensitivity#MEDIUM}
240       *    </ul>
241       * @return This object.
242       */
243      public Builder watcherSensitivity(WatcherSensitivity value) {
244         watcherSensitivity = value;
245         return this;
246      }
247
248      /**
249       * Update-on-write.
250       *
251       * <p>
252       * When enabled, the {@link FileStore#update(String, String)} method will be called immediately following
253       * calls to {@link FileStore#write(String, String, String)} when the contents are changing.
254       * <br>This allows for more immediate responses to configuration changes on file systems that use
255       * polling watchers.
256       * <br>This may cause double-triggering of {@link ConfigStoreListener ConfigStoreListeners}.
257       *
258       * <p>
259       *    The default is the first value found:
260       *    <ul>
261       *       <li>System property <js>"ConfigFileStore.updateOnWrite"
262       *       <li>Environment variable <js>"CONFIGFILESTORE_UPDATEONWRITE"
263       *       <li><jk>false</jk>.
264       *    </ul>
265       *
266       * @return This object.
267       */
268      public Builder updateOnWrite() {
269         updateOnWrite = true;
270         return this;
271      }
272
273      /**
274       * File extensions.
275       *
276       * <p>
277       * Defines what file extensions to search for when the config name does not have an extension.
278       *
279       * @param value
280       *    The new value for this property.
281       *    The default is the first value found:
282       *    <ul>
283       *       <li>System property <js>"ConfigFileStore.extensions"
284       *       <li>Environment variable <js>"CONFIGFILESTORE_EXTENSIONS"
285       *       <li><js>"cfg"</js>
286       *    </ul>
287       * @return This object.
288       */
289      public Builder extensions(String value) {
290         extensions = value;
291         return this;
292      }
293
294      // <FluentSetters>
295
296      @Override /* GENERATED - org.apache.juneau.Context.Builder */
297      public Builder annotations(Annotation...values) {
298         super.annotations(values);
299         return this;
300      }
301
302      @Override /* GENERATED - org.apache.juneau.Context.Builder */
303      public Builder apply(AnnotationWorkList work) {
304         super.apply(work);
305         return this;
306      }
307
308      @Override /* GENERATED - org.apache.juneau.Context.Builder */
309      public Builder applyAnnotations(java.lang.Class<?>...fromClasses) {
310         super.applyAnnotations(fromClasses);
311         return this;
312      }
313
314      @Override /* GENERATED - org.apache.juneau.Context.Builder */
315      public Builder applyAnnotations(Method...fromMethods) {
316         super.applyAnnotations(fromMethods);
317         return this;
318      }
319
320      @Override /* GENERATED - org.apache.juneau.Context.Builder */
321      public Builder cache(Cache<HashKey,? extends org.apache.juneau.Context> value) {
322         super.cache(value);
323         return this;
324      }
325
326      @Override /* GENERATED - org.apache.juneau.Context.Builder */
327      public Builder debug() {
328         super.debug();
329         return this;
330      }
331
332      @Override /* GENERATED - org.apache.juneau.Context.Builder */
333      public Builder debug(boolean value) {
334         super.debug(value);
335         return this;
336      }
337
338      @Override /* GENERATED - org.apache.juneau.Context.Builder */
339      public Builder impl(Context value) {
340         super.impl(value);
341         return this;
342      }
343
344      @Override /* GENERATED - org.apache.juneau.Context.Builder */
345      public Builder type(Class<? extends org.apache.juneau.Context> value) {
346         super.type(value);
347         return this;
348      }
349
350      // </FluentSetters>
351   }
352
353   //-------------------------------------------------------------------------------------------------------------------
354   // Instance
355   //-------------------------------------------------------------------------------------------------------------------
356
357   @Override /* Context */
358   public Builder copy() {
359      return new Builder(this);
360   }
361
362   final String directory, extensions;
363   final Charset charset;
364   final boolean enableWatcher, updateOnWrite;
365   final WatcherSensitivity watcherSensitivity;
366
367   private final File dir;
368   private final WatcherThread watcher;
369   private final ConcurrentHashMap<String,String> cache = new ConcurrentHashMap<>();
370   private final ConcurrentHashMap<String,String> nameCache = new ConcurrentHashMap<>();
371   private final String[] exts;
372
373   /**
374    * Constructor.
375    *
376    * @param builder The builder for this object.
377    */
378   public FileStore(Builder builder) {
379      super(builder);
380      directory = builder.directory;
381      extensions = builder.extensions;
382      charset = builder.charset;
383      enableWatcher = builder.enableWatcher;
384      updateOnWrite = builder.updateOnWrite;
385      watcherSensitivity = builder.watcherSensitivity;
386      try {
387         dir = new File(directory).getCanonicalFile();
388         dir.mkdirs();
389         exts = split(extensions);
390         watcher = enableWatcher ? new WatcherThread(dir, watcherSensitivity) : null;
391         if (watcher != null)
392            watcher.start();
393      } catch (Exception e) {
394         throw asRuntimeException(e);
395      }
396   }
397
398   @Override /* ConfigStore */
399   public synchronized String read(String name) throws IOException {
400      name = resolveName(name);
401
402      Path p = resolveFile(name);
403      name = p.getFileName().toString();
404
405      String s = cache.get(name);
406      if (s != null)
407         return s;
408
409      dir.mkdirs();
410
411      // If file doesn't exist, don't trigger creation.
412      if (! Files.exists(p))
413         return "";
414
415      boolean isWritable = isWritable(p);
416      OpenOption[] oo = isWritable ? new OpenOption[]{READ,WRITE,CREATE} : new OpenOption[]{READ};
417
418      try (FileChannel fc = FileChannel.open(p, oo)) {
419         try (FileLock lock = isWritable ? fc.lock() : null) {
420            ByteBuffer buf = ByteBuffer.allocate(1024);
421            StringBuilder sb = new StringBuilder();
422            while (fc.read(buf) != -1) {
423               sb.append(charset.decode((ByteBuffer)(((Buffer)buf).flip()))); // Fixes Java 11 issue involving overridden flip method.
424               ((Buffer)buf).clear();
425            }
426            s = sb.toString();
427            cache.put(name, s);
428         }
429      }
430
431      return cache.get(name);
432   }
433
434   @Override /* ConfigStore */
435   public synchronized String write(String name, String expectedContents, String newContents) throws IOException {
436      name = resolveName(name);
437
438      // This is a no-op.
439      if (eq(expectedContents, newContents))
440         return null;
441
442      dir.mkdirs();
443
444      Path p = resolveFile(name);
445      name = p.getFileName().toString();
446
447      boolean exists = Files.exists(p);
448
449      // Don't create the file if we're not going to match.
450      if ((!exists) && isNotEmpty(expectedContents))
451         return "";
452
453      if (isWritable(p)) {
454         if (newContents == null)
455            Files.delete(p);
456         else {
457            try (FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE)) {
458               try (FileLock lock = fc.lock()) {
459                  String currentContents = "";
460                  if (exists) {
461                     ByteBuffer buf = ByteBuffer.allocate(1024);
462                     StringBuilder sb = new StringBuilder();
463                     while (fc.read(buf) != -1) {
464                        sb.append(charset.decode((ByteBuffer)((Buffer)buf).flip()));
465                        ((Buffer)buf).clear();
466                     }
467                     currentContents = sb.toString();
468                  }
469                  if (expectedContents != null && ! eq(currentContents, expectedContents)) {
470                     if (currentContents == null)
471                        cache.remove(name);
472                     else
473                        cache.put(name, currentContents);
474                     return currentContents;
475                  }
476                  fc.position(0);
477                  fc.write(charset.encode(newContents));
478               }
479            }
480         }
481      }
482
483      if (updateOnWrite)
484         update(name, newContents);
485      else
486         cache.remove(name);  // Invalidate the cache.
487
488      return null;
489   }
490
491   @Override /* ConfigStore */
492   public synchronized boolean exists(String name) {
493      return Files.exists(resolveFile(name));
494   }
495
496   private Path resolveFile(String name) {
497      return dir.toPath().resolve(resolveName(name));
498   }
499
500   @Override
501   protected String resolveName(String name) {
502      if (! nameCache.containsKey(name)) {
503         String n = null;
504
505         // Does file exist as-is?
506         if (FileUtils.exists(dir, name))
507            n = name;
508
509         // Does name already have an extension?
510         if (n == null) {
511            for (String ext : exts) {
512               if (FileUtils.hasExtension(name, ext)) {
513                  n = name;
514                  break;
515               }
516            }
517         }
518
519         // Find file with the correct extension.
520         if (n == null) {
521            for (String ext : exts) {
522               if (FileUtils.exists(dir, name + '.' + ext)) {
523                  n = name + '.' + ext;
524                  break;
525               }
526            }
527         }
528
529         // If file not found, use the default which is the name with the first extension.
530         if (n == null)
531            n = exts.length == 0 ? name : (name + "." + exts[0]);
532
533         nameCache.put(name, n);
534      }
535      return nameCache.get(name);
536   }
537
538   private synchronized boolean isWritable(Path p) {
539      try {
540         if (! Files.exists(p)) {
541            Files.createDirectories(p.getParent());
542            if (! Files.exists(p))
543               p.toFile().createNewFile();
544         }
545      } catch (IOException e) {
546         return false;
547      }
548      return Files.isWritable(p);
549   }
550
551   @Override /* ConfigStore */
552   public synchronized FileStore update(String name, String newContents) {
553      cache.put(name, newContents);
554      super.update(name, newContents);
555      return this;
556   }
557
558   @Override /* Closeable */
559   public synchronized void close() {
560      if (watcher != null)
561         watcher.interrupt();
562   }
563
564
565   //---------------------------------------------------------------------------------------------
566   // WatcherThread
567   //---------------------------------------------------------------------------------------------
568
569   final class WatcherThread extends Thread {
570      private final WatchService watchService;
571
572      WatcherThread(File dir, WatcherSensitivity s) throws Exception {
573         watchService = FileSystems.getDefault().newWatchService();
574         WatchEvent.Kind<?>[] kinds = new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY};
575         WatchEvent.Modifier modifier = lookupModifier(s);
576         dir.toPath().register(watchService, kinds, modifier);
577      }
578
579      private WatchEvent.Modifier lookupModifier(WatcherSensitivity s) {
580         try {
581            switch(s) {
582               case LOW: return com.sun.nio.file.SensitivityWatchEventModifier.LOW;
583               case MEDIUM: return com.sun.nio.file.SensitivityWatchEventModifier.MEDIUM;
584               case HIGH: return com.sun.nio.file.SensitivityWatchEventModifier.HIGH;
585            }
586         } catch (Exception e) {
587            /* Ignore */
588         }
589         return null;
590
591      }
592
593      @SuppressWarnings("unchecked")
594      @Override /* Thread */
595      public void run() {
596         try {
597            WatchKey key;
598            while ((key = watchService.take()) != null) {
599               for (WatchEvent<?> event : key.pollEvents()) {
600                  WatchEvent.Kind<?> kind = event.kind();
601                  if (kind != OVERFLOW)
602                     FileStore.this.onFileEvent(((WatchEvent<Path>)event));
603               }
604               if (! key.reset())
605                  break;
606            }
607         } catch (Exception e) {
608            throw asRuntimeException(e);
609         }
610      }
611
612      @Override /* Thread */
613      public void interrupt() {
614         try {
615            watchService.close();
616         } catch (IOException e) {
617            throw asRuntimeException(e);
618         } finally {
619            super.interrupt();
620         }
621      }
622   }
623
624   /**
625    * Gets called when the watcher service on this store is triggered with a file system change.
626    *
627    * @param e The file system event.
628    * @throws IOException Thrown by underlying stream.
629    */
630   protected synchronized void onFileEvent(WatchEvent<Path> e) throws IOException {
631      String fn = e.context().getFileName().toString();
632
633      String oldContents = cache.get(fn);
634      cache.remove(fn);
635      String newContents = read(fn);
636      if (! eq(oldContents, newContents)) {
637         update(fn, newContents);
638      }
639   }
640
641   //-----------------------------------------------------------------------------------------------------------------
642   // Other methods
643   //-----------------------------------------------------------------------------------------------------------------
644
645   @Override /* Context */
646   protected JsonMap properties() {
647      return filteredMap("charset", charset, "extensions", extensions, "updateOnWrite", updateOnWrite);
648   }
649}