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