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