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.commons.configuration2.reloading;
018
019import java.io.File;
020import java.net.MalformedURLException;
021import java.net.URL;
022
023import org.apache.commons.configuration2.io.FileHandler;
024import org.apache.commons.configuration2.io.FileLocatorUtils;
025
026/**
027 * <p>
028 * A specialized implementation of {@code ReloadingDetector} which monitors a file specified by a {@link FileHandler}.
029 * </p>
030 * <p>
031 * An instance of this class is passed a {@code FileHandler} at construction time. Each time the
032 * {@code isReloadingRequired()} method is called, it checks whether the {@code FileHandler} points to a valid location.
033 * If this is the case, the file's last modification time is obtained and compared with the last stored time. If it has
034 * changed, a reload operation should be performed.
035 * </p>
036 * <p>
037 * Because file I/O may be expensive it is possible to configure a refresh delay as a time in milliseconds. This is the
038 * minimum interval between two checks. If the {@code isReloadingRequired()} method is called in shorter intervals, it
039 * does not perform a check, but directly returns <b>false</b>.
040 * </p>
041 * <p>
042 * To initialize an instance either {@code isReloadingRequired()} or {@code reloadingPerformed()} can be called. The
043 * first call of {@code isReloadingRequired} does not perform a check, but obtains the initial modification date of the
044 * monitored file. {@code reloadingPerformed()} always obtains the file's modification date and stores it internally.
045 * </p>
046 *
047 * @since 2.0
048 */
049public class FileHandlerReloadingDetector implements ReloadingDetector {
050    /** Constant for the jar URL protocol. */
051    private static final String JAR_PROTOCOL = "jar";
052
053    /** Constant for the default refresh delay. */
054    private static final int DEFAULT_REFRESH_DELAY_MILLIS = 5000;
055
056    /** The associated file handler. */
057    private final FileHandler fileHandler;
058
059    /** The refresh delay. */
060    private final long refreshDelayMillis;
061
062    /** The last time the configuration file was modified. */
063    private long lastModifiedMillis;
064
065    /** The last time the file was checked for changes. */
066    private long lastCheckedMillis;
067
068    /**
069     * Creates a new instance of {@code FileHandlerReloadingDetector} and initializes it with the {@code FileHandler} to
070     * monitor and the refresh delay. The handler is directly used, no copy is created. So it is possible to change the
071     * location monitored by manipulating the {@code FileHandler} object.
072     *
073     * @param handler the {@code FileHandler} associated with this detector (can be <b>null</b>)
074     * @param refreshDelayMillis the refresh delay; a value of 0 means that a check is performed in all cases
075     */
076    public FileHandlerReloadingDetector(final FileHandler handler, final long refreshDelayMillis) {
077        fileHandler = handler != null ? handler : new FileHandler();
078        this.refreshDelayMillis = refreshDelayMillis;
079    }
080
081    /**
082     * Creates a new instance of {@code FileHandlerReloadingDetector} and initializes it with the {@code FileHandler} to
083     * monitor and a default refresh delay.
084     *
085     * @param handler the {@code FileHandler} associated with this detector (can be <b>null</b>)
086     */
087    public FileHandlerReloadingDetector(final FileHandler handler) {
088        this(handler, DEFAULT_REFRESH_DELAY_MILLIS);
089    }
090
091    /**
092     * Creates a new instance of {@code FileHandlerReloadingDetector} with an uninitialized {@code FileHandler} object. The
093     * file to be monitored has to be set later by manipulating the handler object returned by {@code getFileHandler()}.
094     */
095    public FileHandlerReloadingDetector() {
096        this(null);
097    }
098
099    /**
100     * Gets the {@code FileHandler} associated with this object. The underlying handler is directly returned, so changing
101     * its location also changes the file monitored by this detector.
102     *
103     * @return the associated {@code FileHandler}
104     */
105    public FileHandler getFileHandler() {
106        return fileHandler;
107    }
108
109    /**
110     * Gets the refresh delay. This is a time in milliseconds. The {@code isReloadingRequired()} method first checks
111     * whether the time since the previous check is more than this value in the past. Otherwise, no check is performed. This
112     * is a means to limit file I/O caused by this class.
113     *
114     * @return the refresh delay used by this object
115     */
116    public long getRefreshDelay() {
117        return refreshDelayMillis;
118    }
119
120    /**
121     * {@inheritDoc} This implementation checks whether the associated {@link FileHandler} points to a valid file and
122     * whether the last modification time of this time has changed since the last check. The refresh delay is taken into
123     * account, too; a check is only performed if at least this time has passed since the last check.
124     */
125    @Override
126    public boolean isReloadingRequired() {
127        final long nowMillis = System.currentTimeMillis();
128        if (nowMillis >= lastCheckedMillis + getRefreshDelay()) {
129            lastCheckedMillis = nowMillis;
130
131            final long modifiedMillis = getLastModificationDate();
132            if (modifiedMillis > 0) {
133                if (lastModifiedMillis != 0) {
134                    return modifiedMillis != lastModifiedMillis;
135                }
136                // initialization
137                updateLastModified(modifiedMillis);
138            }
139        }
140
141        return false;
142    }
143
144    /**
145     * {@inheritDoc} This implementation updates the internally stored last modification date with the current modification
146     * date of the monitored file. So the next change is detected when this file is changed again.
147     */
148    @Override
149    public void reloadingPerformed() {
150        updateLastModified(getLastModificationDate());
151    }
152
153    /**
154     * Tells this implementation that the internally stored state should be refreshed. This method is intended to be called
155     * after the creation of an instance.
156     */
157    public void refresh() {
158        updateLastModified(getLastModificationDate());
159    }
160
161    /**
162     * Gets the date of the last modification of the monitored file. A return value of 0 indicates, that the monitored
163     * file does not exist.
164     *
165     * @return the last modification date in milliseconds.
166     */
167    protected long getLastModificationDate() {
168        final File file = getExistingFile();
169        return file != null ? file.lastModified() : 0;
170    }
171
172    /**
173     * Updates the last modification date of the monitored file. The need for a reload is detected only if the file's
174     * modification date is different from this value.
175     *
176     * @param timeMillis the new last modification date
177     */
178    protected void updateLastModified(final long timeMillis) {
179        lastModifiedMillis = timeMillis;
180    }
181
182    /**
183     * Gets the {@code File} object which is monitored by this object. This method is called every time the file's last
184     * modification time is needed. If it returns <b>null</b>, no check is performed. This base implementation obtains the
185     * {@code File} from the associated {@code FileHandler}. It can also deal with URLs to jar files.
186     *
187     * @return the {@code File} to be monitored (can be <b>null</b>)
188     */
189    protected File getFile() {
190        final URL url = getFileHandler().getURL();
191        return url != null ? fileFromURL(url) : getFileHandler().getFile();
192    }
193
194    /**
195     * Gets the monitored {@code File} or <b>null</b> if it does not exist.
196     *
197     * @return the monitored {@code File} or <b>null</b>
198     */
199    private File getExistingFile() {
200        File file = getFile();
201        if (file != null && !file.exists()) {
202            file = null;
203        }
204
205        return file;
206    }
207
208    /**
209     * Helper method for transforming a URL into a file object. This method handles file: and jar: URLs.
210     *
211     * @param url the URL to be converted
212     * @return the resulting file or <b>null </b>
213     */
214    private static File fileFromURL(final URL url) {
215        if (JAR_PROTOCOL.equals(url.getProtocol())) {
216            final String path = url.getPath();
217            try {
218                return FileLocatorUtils.fileFromURL(new URL(path.substring(0, path.indexOf('!'))));
219            } catch (final MalformedURLException mex) {
220                return null;
221            }
222        }
223        return FileLocatorUtils.fileFromURL(url);
224    }
225}