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