View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2.reloading;
18  
19  import java.io.File;
20  import java.net.MalformedURLException;
21  import java.net.URL;
22  
23  import org.apache.commons.configuration2.io.FileHandler;
24  import org.apache.commons.configuration2.io.FileLocatorUtils;
25  
26  /**
27   * <p>
28   * A specialized implementation of {@code ReloadingDetector} which monitors a file specified by a {@link FileHandler}.
29   * </p>
30   * <p>
31   * An instance of this class is passed a {@code FileHandler} at construction time. Each time the
32   * {@code isReloadingRequired()} method is called, it checks whether the {@code FileHandler} points to a valid location.
33   * If this is the case, the file's last modification time is obtained and compared with the last stored time. If it has
34   * changed, a reload operation should be performed.
35   * </p>
36   * <p>
37   * Because file I/O may be expensive it is possible to configure a refresh delay as a time in milliseconds. This is the
38   * minimum interval between two checks. If the {@code isReloadingRequired()} method is called in shorter intervals, it
39   * does not perform a check, but directly returns <strong>false</strong>.
40   * </p>
41   * <p>
42   * To initialize an instance either {@code isReloadingRequired()} or {@code reloadingPerformed()} can be called. The
43   * first call of {@code isReloadingRequired} does not perform a check, but obtains the initial modification date of the
44   * monitored file. {@code reloadingPerformed()} always obtains the file's modification date and stores it internally.
45   * </p>
46   *
47   * @since 2.0
48   */
49  public class FileHandlerReloadingDetector implements ReloadingDetector {
50  
51      /** Constant for the jar URL protocol. */
52      private static final String JAR_PROTOCOL = "jar";
53  
54      /** Constant for the default refresh delay. */
55      private static final int DEFAULT_REFRESH_DELAY_MILLIS = 5000;
56  
57      /**
58       * Helper method for transforming a URL into a file object. This method handles file: and jar: URLs.
59       *
60       * @param url the URL to be converted
61       * @return the resulting file or <strong>null </strong>
62       */
63      private static File fileFromURL(final URL url) {
64          if (JAR_PROTOCOL.equals(url.getProtocol())) {
65              final String path = url.getPath();
66              try {
67                  return FileLocatorUtils.fileFromURL(new URL(path.substring(0, path.indexOf('!'))));
68              } catch (final MalformedURLException mex) {
69                  return null;
70              }
71          }
72          return FileLocatorUtils.fileFromURL(url);
73      }
74  
75      /** The associated file handler. */
76      private final FileHandler fileHandler;
77  
78      /** The refresh delay. */
79      private final long refreshDelayMillis;
80  
81      /** The last time the configuration file was modified. */
82      private long lastModifiedMillis;
83  
84      /** The last time the file was checked for changes. */
85      private long lastCheckedMillis;
86  
87      /**
88       * Creates a new instance of {@code FileHandlerReloadingDetector} with an uninitialized {@code FileHandler} object. The
89       * file to be monitored has to be set later by manipulating the handler object returned by {@code getFileHandler()}.
90       */
91      public FileHandlerReloadingDetector() {
92          this(null);
93      }
94  
95      /**
96       * Creates a new instance of {@code FileHandlerReloadingDetector} and initializes it with the {@code FileHandler} to
97       * monitor and a default refresh delay.
98       *
99       * @param handler the {@code FileHandler} associated with this detector (can be <strong>null</strong>)
100      */
101     public FileHandlerReloadingDetector(final FileHandler handler) {
102         this(handler, DEFAULT_REFRESH_DELAY_MILLIS);
103     }
104 
105     /**
106      * Creates a new instance of {@code FileHandlerReloadingDetector} and initializes it with the {@code FileHandler} to
107      * monitor and the refresh delay. The handler is directly used, no copy is created. So it is possible to change the
108      * location monitored by manipulating the {@code FileHandler} object.
109      *
110      * @param handler the {@code FileHandler} associated with this detector (can be <strong>null</strong>)
111      * @param refreshDelayMillis the refresh delay; a value of 0 means that a check is performed in all cases
112      */
113     public FileHandlerReloadingDetector(final FileHandler handler, final long refreshDelayMillis) {
114         fileHandler = handler != null ? handler : new FileHandler();
115         this.refreshDelayMillis = refreshDelayMillis;
116     }
117 
118     /**
119      * Gets the monitored {@code File} or <strong>null</strong> if it does not exist.
120      *
121      * @return the monitored {@code File} or <strong>null</strong>
122      */
123     private File getExistingFile() {
124         File file = getFile();
125         if (file != null && !file.exists()) {
126             file = null;
127         }
128 
129         return file;
130     }
131 
132     /**
133      * Gets the {@code File} object which is monitored by this object. This method is called every time the file's last
134      * modification time is needed. If it returns <strong>null</strong>, no check is performed. This base implementation obtains the
135      * {@code File} from the associated {@code FileHandler}. It can also deal with URLs to jar files.
136      *
137      * @return the {@code File} to be monitored (can be <strong>null</strong>)
138      */
139     protected File getFile() {
140         final URL url = getFileHandler().getURL();
141         return url != null ? fileFromURL(url) : getFileHandler().getFile();
142     }
143 
144     /**
145      * Gets the {@code FileHandler} associated with this object. The underlying handler is directly returned, so changing
146      * its location also changes the file monitored by this detector.
147      *
148      * @return the associated {@code FileHandler}
149      */
150     public FileHandler getFileHandler() {
151         return fileHandler;
152     }
153 
154     /**
155      * Gets the date of the last modification of the monitored file. A return value of 0 indicates, that the monitored
156      * file does not exist.
157      *
158      * @return the last modification date in milliseconds.
159      */
160     protected long getLastModificationDate() {
161         final File file = getExistingFile();
162         return file != null ? file.lastModified() : 0;
163     }
164 
165     /**
166      * Gets the refresh delay. This is a time in milliseconds. The {@code isReloadingRequired()} method first checks
167      * whether the time since the previous check is more than this value in the past. Otherwise, no check is performed. This
168      * is a means to limit file I/O caused by this class.
169      *
170      * @return the refresh delay used by this object
171      */
172     public long getRefreshDelay() {
173         return refreshDelayMillis;
174     }
175 
176     /**
177      * {@inheritDoc} This implementation checks whether the associated {@link FileHandler} points to a valid file and
178      * whether the last modification time of this time has changed since the last check. The refresh delay is taken into
179      * account, too; a check is only performed if at least this time has passed since the last check.
180      */
181     @Override
182     public boolean isReloadingRequired() {
183         final long nowMillis = System.currentTimeMillis();
184         if (nowMillis >= lastCheckedMillis + getRefreshDelay()) {
185             lastCheckedMillis = nowMillis;
186 
187             final long modifiedMillis = getLastModificationDate();
188             if (modifiedMillis > 0) {
189                 if (lastModifiedMillis != 0) {
190                     return modifiedMillis != lastModifiedMillis;
191                 }
192                 // initialization
193                 updateLastModified(modifiedMillis);
194             }
195         }
196 
197         return false;
198     }
199 
200     /**
201      * Tells this implementation that the internally stored state should be refreshed. This method is intended to be called
202      * after the creation of an instance.
203      */
204     public void refresh() {
205         updateLastModified(getLastModificationDate());
206     }
207 
208     /**
209      * {@inheritDoc} This implementation updates the internally stored last modification date with the current modification
210      * date of the monitored file. So the next change is detected when this file is changed again.
211      */
212     @Override
213     public void reloadingPerformed() {
214         updateLastModified(getLastModificationDate());
215     }
216 
217     /**
218      * Updates the last modification date of the monitored file. The need for a reload is detected only if the file's
219      * modification date is different from this value.
220      *
221      * @param timeMillis the new last modification date
222      */
223     protected void updateLastModified(final long timeMillis) {
224         lastModifiedMillis = timeMillis;
225     }
226 }