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