    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}