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 public void init() throws IOException { 055 if (isEmpty(configPath)) { 056 return; 057 } 058 059 loadConfiguration(); 060 061 if (monitorForChanges) { 062 monitorForChanges(); 063 } 064 } 065 066 /** 067 * Shut down the change monitoring thread 068 */ 069 public void shutdown() { 070 if (monitorThread != null) { 071 monitorThread.interrupt(); 072 } 073 } 074 075 /** 076 * Load the configuration file. 077 * 078 * @throws IOException thrown if the configuration cannot be loaded. 079 */ 080 protected abstract void loadConfiguration() throws IOException; 081 082 /** 083 * Starts up monitoring of the configuration for changes. 084 */ 085 private void monitorForChanges() { 086 if (monitorRunning) { 087 return; 088 } 089 090 final Path path; 091 try { 092 path = Paths.get(configPath); 093 } catch (final Exception e) { 094 LOGGER.warn("Cannot monitor configuration {}, disabling monitoring; {}", configPath, e.getMessage()); 095 return; 096 } 097 098 if (!path.toFile().exists()) { 099 LOGGER.debug("Configuration {} does not exist, disabling monitoring", configPath); 100 return; 101 } 102 final Path directoryPath = path.getParent(); 103 104 try { 105 final WatchService watchService = FileSystems.getDefault().newWatchService(); 106 directoryPath.register(watchService, ENTRY_MODIFY); 107 108 monitorThread = new Thread(new Runnable() { 109 110 @Override 111 public void run() { 112 try { 113 for (;;) { 114 final WatchKey key; 115 try { 116 key = watchService.take(); 117 } catch (final InterruptedException e) { 118 LOGGER.debug("Interrupted the configuration monitor thread."); 119 break; 120 } 121 122 for (final WatchEvent<?> event : key.pollEvents()) { 123 final WatchEvent.Kind<?> kind = event.kind(); 124 if (kind == OVERFLOW) { 125 continue; 126 } 127 128 // If the configuration file triggered this event, reload it 129 final Path changed = (Path) event.context(); 130 if (changed.equals(path.getFileName())) { 131 LOGGER.info( 132 "Configuration {} has been updated, reloading.", 133 path); 134 try { 135 loadConfiguration(); 136 } catch (final IOException e) { 137 LOGGER.error("Failed to reload configuration {}", configPath, e); 138 } 139 } 140 141 // reset the key 142 final boolean valid = key.reset(); 143 if (!valid) { 144 LOGGER.debug("Monitor of {} is no longer valid", path); 145 break; 146 } 147 } 148 } 149 } finally { 150 try { 151 watchService.close(); 152 } catch (final IOException e) { 153 LOGGER.error("Failed to stop configuration monitor", e); 154 } 155 } 156 monitorRunning = false; 157 } 158 }); 159 } catch (final IOException e) { 160 LOGGER.error("Failed to start configuration monitor", e); 161 } 162 163 monitorThread.start(); 164 monitorRunning = true; 165 } 166 167 /** 168 * Set the file path for the configuration 169 * 170 * @param configPath file path for configuration 171 */ 172 public void setConfigPath(final String configPath) { 173 // Resolve classpath references without spring's help 174 if (configPath != null && configPath.startsWith("classpath:")) { 175 final String relativePath = configPath.substring(10); 176 this.configPath = this.getClass().getResource(relativePath).getPath(); 177 } else { 178 this.configPath = configPath; 179 } 180 } 181 182 /** 183 * Set whether to monitor the configuration file for changes 184 * 185 * @param monitorForChanges flag controlling if to enable configuration monitoring 186 */ 187 public void setMonitorForChanges(final boolean monitorForChanges) { 188 this.monitorForChanges = monitorForChanges; 189 } 190}