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}