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}