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