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