001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006package org.fcrepo.kernel.api.utils; 007 008import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; 009import static java.nio.file.StandardWatchEventKinds.OVERFLOW; 010import static org.apache.commons.lang3.StringUtils.isEmpty; 011import static org.slf4j.LoggerFactory.getLogger; 012 013import java.io.IOException; 014import java.nio.file.FileSystems; 015import java.nio.file.Path; 016import java.nio.file.Paths; 017import java.nio.file.WatchEvent; 018import java.nio.file.WatchKey; 019import java.nio.file.WatchService; 020 021import org.slf4j.Logger; 022 023/** 024 * Abstract configuration class which monitors a file path in order to reload the configuration when it changes. 025 * 026 * @author bbpennel 027 */ 028public abstract class AutoReloadingConfiguration { 029 private static final Logger LOGGER = getLogger(AutoReloadingConfiguration.class); 030 031 protected String configPath; 032 033 private boolean monitorForChanges; 034 035 private Thread monitorThread; 036 037 private boolean monitorRunning; 038 039 /** 040 * Initialize the configuration and set up monitoring 041 * 042 * @throws IOException thrown if the configuration cannot be loaded. 043 * 044 */ 045 public void init() throws IOException { 046 if (isEmpty(configPath)) { 047 return; 048 } 049 050 loadConfiguration(); 051 052 if (monitorForChanges) { 053 monitorForChanges(); 054 } 055 } 056 057 /** 058 * Shut down the change monitoring thread 059 */ 060 public void shutdown() { 061 if (monitorThread != null) { 062 monitorThread.interrupt(); 063 } 064 } 065 066 /** 067 * Load the configuration file. 068 * 069 * @throws IOException thrown if the configuration cannot be loaded. 070 */ 071 protected abstract void loadConfiguration() throws IOException; 072 073 /** 074 * Starts up monitoring of the configuration for changes. 075 */ 076 private void monitorForChanges() { 077 if (monitorRunning) { 078 return; 079 } 080 081 final Path path; 082 try { 083 path = Paths.get(configPath); 084 } catch (final Exception e) { 085 LOGGER.warn("Cannot monitor configuration {}, disabling monitoring; {}", configPath, e.getMessage()); 086 return; 087 } 088 089 if (!path.toFile().exists()) { 090 LOGGER.debug("Configuration {} does not exist, disabling monitoring", configPath); 091 return; 092 } 093 final Path directoryPath = path.getParent(); 094 095 try { 096 final WatchService watchService = FileSystems.getDefault().newWatchService(); 097 directoryPath.register(watchService, ENTRY_MODIFY); 098 099 monitorThread = new Thread(new Runnable() { 100 101 @Override 102 public void run() { 103 try { 104 for (;;) { 105 final WatchKey key; 106 try { 107 key = watchService.take(); 108 } catch (final InterruptedException e) { 109 LOGGER.debug("Interrupted the configuration monitor thread."); 110 break; 111 } 112 113 for (final WatchEvent<?> event : key.pollEvents()) { 114 final WatchEvent.Kind<?> kind = event.kind(); 115 if (kind == OVERFLOW) { 116 continue; 117 } 118 119 // If the configuration file triggered this event, reload it 120 final Path changed = (Path) event.context(); 121 if (changed.equals(path.getFileName())) { 122 LOGGER.info( 123 "Configuration {} has been updated, reloading.", 124 path); 125 try { 126 loadConfiguration(); 127 } catch (final IOException e) { 128 LOGGER.error("Failed to reload configuration {}", configPath, e); 129 } 130 } 131 132 // reset the key 133 final boolean valid = key.reset(); 134 if (!valid) { 135 LOGGER.debug("Monitor of {} is no longer valid", path); 136 break; 137 } 138 } 139 } 140 } finally { 141 try { 142 watchService.close(); 143 } catch (final IOException e) { 144 LOGGER.error("Failed to stop configuration monitor", e); 145 } 146 } 147 monitorRunning = false; 148 } 149 }); 150 } catch (final IOException e) { 151 LOGGER.error("Failed to start configuration monitor", e); 152 } 153 154 monitorThread.start(); 155 monitorRunning = true; 156 } 157 158 /** 159 * Set the file path for the configuration 160 * 161 * @param configPath file path for configuration 162 */ 163 public void setConfigPath(final String configPath) { 164 // Resolve classpath references without spring's help 165 if (configPath != null && configPath.startsWith("classpath:")) { 166 final String relativePath = configPath.substring(10); 167 this.configPath = this.getClass().getResource(relativePath).getPath(); 168 } else { 169 this.configPath = configPath; 170 } 171 } 172 173 /** 174 * Set whether to monitor the configuration file for changes 175 * 176 * @param monitorForChanges flag controlling if to enable configuration monitoring 177 */ 178 public void setMonitorForChanges(final boolean monitorForChanges) { 179 this.monitorForChanges = monitorForChanges; 180 } 181}