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