    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}