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}