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