/******************************************************************************* * This file is part of OpenNMS(R). * * Copyright (C) 2007-2011 The OpenNMS Group, Inc. * OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc. * * OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc. * * OpenNMS(R) is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * OpenNMS(R) is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenNMS(R). If not, see: * http://www.gnu.org/licenses/ * * For more information contact: * OpenNMS(R) Licensing <license@opennms.org> * http://www.opennms.org/ * http://www.opennms.com/ *******************************************************************************/ package org.opennms.netmgt.dao.support; import java.io.File; import java.io.IOException; import org.opennms.core.utils.LogUtils; import org.springframework.core.io.Resource; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.util.Assert; /** * <p> * Provides a container for returning an object and reloading the object if * an underlying file has changed. Ideally suited for automatically reloading * configuration files that might be edited outside of the application. * </p> * * <p> * <!-- * Can't use generics in @see and @link tags. See Sun bug 5096551: * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5096551 * Leaving off the <T> bits and using Object seem to work okay. * --> * There are two constructors: * <ol> * <li>{@link #FileReloadContainer(Object, Resource, FileReloadCallback) * FileReloadContainer(T, Resource, FileReloadCallback<T>)} * is used for objects having an underlying resource and are reloadable</li> * <li>{@link #FileReloadContainer(Object) FileReloadContainer(T)} is used * for objects that either do not have an underlying file or are otherwise * not reloadable</li> * </ol> * The second constructor is provided for convenience so that reloadable and * non-reloadable data of the same type can be handled similarly (the only * difference in code is at initialization time when one constructor or the * other is used). * </p> * * <p> * If the first constructor is used, the Resource will be stored for later * reloading. If {@link Resource#getFile() Resource.getFile()} does not * throw an exception, the returned File object will be stored and * {@link File#lastModified() File.lastModified()} will be called every time * the {@link #getObject() getObject()} method is called to see if the file * has changed. If the file has changed, the last modified time is updated * and the reload callback, {@link FileReloadCallback#reload(Object, File) * FileReloadCallback.reload}, is called. If it returns a non-null object, * the new object is stored and it gets returned to the caller. If a null * object is returned, the stored object isn't modified and the old object * is returned to the caller. * </p> * * <p> * If an unchecked exception is thrown by the reload callback, it will be * caught, logged, and a {@link DataAccessResourceFailureException} with * a cause of the unchecked exception. This will propogate up to the * caller of the getObject method. If you do not want unchecked exceptions * on reloads to propogate up to the caller of getObject, they need to be * caught within the reload method. Returning a null in the case of errors * is a good alternative in this case. * </p> * * @author dj@opennms.org * @param <T> the class of the inner object that is stored in this container */ public class FileReloadContainer<T> { private static final long DEFAULT_RELOAD_CHECK_INTERVAL = 1000; private T m_object; private Resource m_resource; private File m_file; private long m_lastModified; private FileReloadCallback<T> m_callback; private long m_reloadCheckInterval = DEFAULT_RELOAD_CHECK_INTERVAL; private long m_lastReloadCheck; /** * Creates a new container with an object and a file underlying that * object. If reloadCheckInterval is set to a non-negative value * (default is 1000 milliseconds), the last modified timestamp on * the file will be checked and the * {@link FileReloadCallback#reload(Object, File) reload} * on the callback will be called when the file is modified. The * check will be performed when {@link #getObject()} is called and * at least reloadCheckInterval milliseconds have passed. * * @param object object to be stored in this container * @param callback {@link FileReloadCallback#reload(Object, File) reload} * will be called when the underlying file object is modified * @throws java.lang.IllegalArgumentException if object, file, or callback are null * @param resource a {@link org.springframework.core.io.Resource} object. * @param <T> a T object. */ public FileReloadContainer(final T object, final Resource resource, final FileReloadCallback<T> callback) { Assert.notNull(object, "argument object cannot be null"); Assert.notNull(resource, "argument file cannot be null"); Assert.notNull(callback, "argument callback cannot be null"); m_object = object; m_resource = resource; m_callback = callback; try { m_file = resource.getFile(); m_lastModified = m_file.lastModified(); } catch (final IOException e) { // Do nothing... we'll fall back to using the InputStream LogUtils.infof(this, e, "Resource '%s' does not seem to have an underlying File object; assuming this is not an auto-reloadable file resource", resource); } m_lastReloadCheck = System.currentTimeMillis(); } /** * Creates a new container with an object which has no underlying file. * This will not auto-reload. * * @param object object to be stored in this container * @throws java.lang.IllegalArgumentException if object is null */ public FileReloadContainer(final T object) { Assert.notNull(object, "argument object cannot be null"); m_object = object; } /** * Get the object in this container. If the object is backed by a file, * the last modified time on the file will be checked, and if it has * changed the object will be reloaded. * * @return object in this container * @throws org.springframework.dao.DataAccessResourceFailureException if an unchecked exception * is received while trying to reload the object from the underlying file */ public T getObject() throws DataAccessResourceFailureException { checkForUpdates(); return m_object; } private synchronized void checkForUpdates() throws DataAccessResourceFailureException { if (m_file == null || m_reloadCheckInterval < 0 || System.currentTimeMillis() < (m_lastReloadCheck + m_reloadCheckInterval)) { return; } m_lastReloadCheck = System.currentTimeMillis(); if (m_file.lastModified() <= m_lastModified) { return; } reload(); } /** * Force a reload of the configuration. */ public synchronized void reload() { /* * Always update the timestamp, even if we have an error. * XXX What if someone is writing the file while we are reading it, * we get an error, and the (correct) file is written completely * within the same second, so lastModified doesn't get updated. */ m_lastModified = m_file.lastModified(); final T object; try { object = m_callback.reload(m_object, m_resource); } catch (Throwable t) { final String message = String.format("Failed reloading data for object '%s' from file '%s'. Unexpected Throwable received while issuing reload.", m_object, m_file.getAbsolutePath()); LogUtils.errorf(this, t, message); throw new DataAccessResourceFailureException(message, t); } if (object == null) { LogUtils.infof(this, "Not updating object for file '%s' due to reload callback returning null.", m_file.getAbsolutePath()); } else { m_object = object; } } /** * Get the file underlying the object in this container, if any. * * @return if the container was created with an underlying file the * file will be returned, otherwise null */ public File getFile() { return m_file; } /** * Get the reload check interval. * * @return reload check interval in milliseconds. A negative value * indicates that automatic reload checks are not performed and the * file will only be reloaded if {@link #reload()} is explicitly called. */ public long getReloadCheckInterval() { return m_reloadCheckInterval; } /** * Set the reload check interval. * * @param reloadCheckInterval reload check interval in milliseconds. A negative value * indicates that automatic reload checks are not performed and the * file will only be reloaded if {@link #reload()} is explicitly called. */ public void setReloadCheckInterval(final long reloadCheckInterval) { m_reloadCheckInterval = reloadCheckInterval; } }