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 }