001/*
002 * The contents of this file are subject to the license and copyright
003 * detailed in the LICENSE and NOTICE files at the root of the source
004 * tree.
005 */
006package org.fcrepo.kernel.api.utils;
007
008import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
009import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
010import static org.apache.commons.lang3.StringUtils.isEmpty;
011import static org.slf4j.LoggerFactory.getLogger;
012
013import java.io.IOException;
014import java.nio.file.FileSystems;
015import java.nio.file.Path;
016import java.nio.file.Paths;
017import java.nio.file.WatchEvent;
018import java.nio.file.WatchKey;
019import java.nio.file.WatchService;
020
021import org.slf4j.Logger;
022
023/**
024 * Abstract configuration class which monitors a file path in order to reload the configuration when it changes.
025 *
026 * @author bbpennel
027 */
028public abstract class AutoReloadingConfiguration {
029    private static final Logger LOGGER = getLogger(AutoReloadingConfiguration.class);
030
031    protected String configPath;
032
033    private boolean monitorForChanges;
034
035    private Thread monitorThread;
036
037    private boolean monitorRunning;
038
039    /**
040     * Initialize the configuration and set up monitoring
041     *
042     * @throws IOException thrown if the configuration cannot be loaded.
043     *
044     */
045    public void init() throws IOException {
046        if (isEmpty(configPath)) {
047            return;
048        }
049
050        loadConfiguration();
051
052        if (monitorForChanges) {
053            monitorForChanges();
054        }
055    }
056
057    /**
058     * Shut down the change monitoring thread
059     */
060    public void shutdown() {
061        if (monitorThread != null) {
062            monitorThread.interrupt();
063        }
064    }
065
066    /**
067     * Load the configuration file.
068     *
069     * @throws IOException thrown if the configuration cannot be loaded.
070     */
071    protected abstract void loadConfiguration() throws IOException;
072
073    /**
074     * Starts up monitoring of the configuration for changes.
075     */
076    private void monitorForChanges() {
077        if (monitorRunning) {
078            return;
079        }
080
081        final Path path;
082        try {
083            path = Paths.get(configPath);
084        } catch (final Exception e) {
085            LOGGER.warn("Cannot monitor configuration {}, disabling monitoring; {}", configPath, e.getMessage());
086            return;
087        }
088
089        if (!path.toFile().exists()) {
090            LOGGER.debug("Configuration {} does not exist, disabling monitoring", configPath);
091            return;
092        }
093        final Path directoryPath = path.getParent();
094
095        try {
096            final WatchService watchService = FileSystems.getDefault().newWatchService();
097            directoryPath.register(watchService, ENTRY_MODIFY);
098
099            monitorThread = new Thread(new Runnable() {
100
101                @Override
102                public void run() {
103                    try {
104                        for (;;) {
105                            final WatchKey key;
106                            try {
107                                key = watchService.take();
108                            } catch (final InterruptedException e) {
109                                LOGGER.debug("Interrupted the configuration monitor thread.");
110                                break;
111                            }
112
113                            for (final WatchEvent<?> event : key.pollEvents()) {
114                                final WatchEvent.Kind<?> kind = event.kind();
115                                if (kind == OVERFLOW) {
116                                    continue;
117                                }
118
119                                // If the configuration file triggered this event, reload it
120                                final Path changed = (Path) event.context();
121                                if (changed.equals(path.getFileName())) {
122                                    LOGGER.info(
123                                            "Configuration {} has been updated, reloading.",
124                                            path);
125                                    try {
126                                        loadConfiguration();
127                                    } catch (final IOException e) {
128                                        LOGGER.error("Failed to reload configuration {}", configPath, e);
129                                    }
130                                }
131
132                                // reset the key
133                                final boolean valid = key.reset();
134                                if (!valid) {
135                                    LOGGER.debug("Monitor of {} is no longer valid", path);
136                                    break;
137                                }
138                            }
139                        }
140                    } finally {
141                        try {
142                            watchService.close();
143                        } catch (final IOException e) {
144                            LOGGER.error("Failed to stop configuration monitor", e);
145                        }
146                    }
147                    monitorRunning = false;
148                }
149            });
150        } catch (final IOException e) {
151            LOGGER.error("Failed to start configuration monitor", e);
152        }
153
154        monitorThread.start();
155        monitorRunning = true;
156    }
157
158    /**
159     * Set the file path for the configuration
160     *
161     * @param configPath file path for configuration
162     */
163    public void setConfigPath(final String configPath) {
164        // Resolve classpath references without spring's help
165        if (configPath != null && configPath.startsWith("classpath:")) {
166            final String relativePath = configPath.substring(10);
167            this.configPath = this.getClass().getResource(relativePath).getPath();
168        } else {
169            this.configPath = configPath;
170        }
171    }
172
173    /**
174     * Set whether to monitor the configuration file for changes
175     *
176     * @param monitorForChanges flag controlling if to enable configuration monitoring
177     */
178    public void setMonitorForChanges(final boolean monitorForChanges) {
179        this.monitorForChanges = monitorForChanges;
180    }
181}