package org.ovirt.engine.ui.frontend.server.gwt.plugin;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ObjectNode;
import org.ovirt.engine.core.common.config.ConfigUtil;
import org.ovirt.engine.core.utils.EngineLocalConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Reads, validates and stores UI plugin descriptor/configuration data.
* <p>
* Note that this class uses {@link EngineLocalConfig} to read local (machine-specific) Engine configuration.
*/
public class PluginDataManager {
// Using 'initialization-on-demand holder' pattern
private static class Holder {
private static final PluginDataManager INSTANCE = new PluginDataManager(
PluginDataManager.resolvePluginDataPath(), PluginDataManager.resolvePluginConfigPath());
}
private static final String UI_PLUGIN_DIR = "ui-plugins"; //$NON-NLS-1$
private static final String JSON_FILE_SUFFIX = ".json"; //$NON-NLS-1$
private static final String CONFIG_FILE_SUFFIX = "-config" + JSON_FILE_SUFFIX; //$NON-NLS-1$
private static final long MISSING_FILE_LAST_MODIFIED = -1L;
private static final Logger log = LoggerFactory.getLogger(PluginDataManager.class);
/**
* Returns UI plugin <em>data path</em>, under which UI plugin descriptor (JSON) files are placed.
*/
public static String resolvePluginDataPath() {
return ConfigUtil.resolvePath(EngineLocalConfig.getInstance().getUsrDir().getAbsolutePath(), UI_PLUGIN_DIR);
}
/**
* Returns UI plugin <em>config path</em>, under which UI plugin configuration (JSON) files are placed.
*/
public static String resolvePluginConfigPath() {
return ConfigUtil.resolvePath(EngineLocalConfig.getInstance().getEtcDir().getAbsolutePath(), UI_PLUGIN_DIR);
}
private final File pluginDataDir;
private final File pluginConfigDir;
private final ObjectMapper mapper;
// Cached plugin data, maps descriptor file names to corresponding object representations
private final AtomicReference<Map<String, PluginData>> dataMapRef;
private PluginDataManager(String pluginDataPath, String pluginConfigPath) {
Map<String, PluginData> map = new HashMap<>();
this.dataMapRef = new AtomicReference<>(map);
this.pluginDataDir = new File(pluginDataPath);
this.pluginConfigDir = new File(pluginConfigPath);
this.mapper = createJsonMapper();
}
ObjectMapper createJsonMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
return mapper;
}
public static PluginDataManager getInstance() {
return Holder.INSTANCE;
}
/**
* Calling this method is equivalent to:
* <pre>
* reloadData();
* return getCurrentData();
* </pre>
*/
public Collection<PluginData> reloadAndGetCurrentData() {
reloadData();
return getCurrentData();
}
/**
* Returns the currently valid descriptor/configuration data as unmodifiable collection.
* <p>
* Use {@link #reloadAndGetCurrentData} in case the caller expects 'recently-up-to-date' data.
*/
public Collection<PluginData> getCurrentData() {
return Collections.unmodifiableCollection(dataMapRef.get().values());
}
/**
* Reloads descriptor/configuration data from local file system if necessary.
* <p>
* No attempts are made with regard to lock-based synchronization. The 'live' data is updated atomically through
* {@linkplain AtomicReference#compareAndSet conditional reference re-assignment}. It may happen that another thread
* has already updated 'live' data, in which case the current thread does nothing. This is offset by better
* performance, assuming that the caller doesn't necessarily need to have 'completely-up-to-date' data at the given
* point in time (having 'recently-up-to-date', but consistent data, should be enough).
*/
public void reloadData() {
// Get a snapshot of current data mappings
Map<String, PluginData> currentDataMapSnapshot = dataMapRef.get();
// Create a local working copy of current data mappings (avoid modifying 'live' data)
Map<String, PluginData> currentDataMapCopy = new HashMap<>(currentDataMapSnapshot);
File[] descriptorFiles = pluginDataDir.listFiles(pathname -> isJsonFile(pathname));
if (descriptorFiles == null) {
log.warn("Cannot list UI plugin descriptor files in '{}'", pluginDataDir.getAbsolutePath()); //$NON-NLS-1$
return;
}
// Reload descriptor/configuration data
reloadData(descriptorFiles, currentDataMapCopy);
// Apply changes through reference assignment
if (!dataMapRef.compareAndSet(currentDataMapSnapshot, currentDataMapCopy)) {
log.warn("It seems that UI plugin data has changed, please reload WebAdmin application"); //$NON-NLS-1$
}
}
void reloadData(File[] descriptorFiles, Map<String, PluginData> currentDataMapCopy) {
Map<String, PluginData> entriesToUpdate = new HashMap<>();
Set<String> keysToRemove = new HashSet<>();
// Optimization: make sure we don't check data that we already processed
Set<String> keysToCheckForRemoval = new HashSet<>(currentDataMapCopy.keySet());
// Compare (possibly added or modified) files against cached data
for (final File df : descriptorFiles) {
final File cf = new File(pluginConfigDir, getConfigurationFileName(df));
String descriptorFilePath = df.getAbsolutePath();
PluginData currentData = currentDataMapCopy.get(descriptorFilePath);
long descriptorLastModified = df.lastModified();
long configurationLastModified = isJsonFile(cf) ? cf.lastModified() : MISSING_FILE_LAST_MODIFIED;
// Check if data needs to be reloaded
boolean reloadDescriptor;
boolean reloadConfiguration;
if (currentDataMapCopy.containsKey(descriptorFilePath)) {
reloadDescriptor = descriptorLastModified > currentData.getDescriptorLastModified();
reloadConfiguration = configurationLastModified > currentData.getConfigurationLastModified();
// Change in descriptor causes reload of configuration
reloadConfiguration |= reloadDescriptor;
// Refresh configuration if the corresponding file has gone missing
reloadConfiguration |= configurationLastModified == MISSING_FILE_LAST_MODIFIED
&& currentData.getConfigurationLastModified() != MISSING_FILE_LAST_MODIFIED;
} else {
reloadDescriptor = true;
reloadConfiguration = true;
}
// Read descriptor data
JsonNode descriptorNode = currentData != null ? currentData.getDescriptorNode() : null;
if (reloadDescriptor) {
log.info("Reading UI plugin descriptor '{}'", df.getAbsolutePath()); //$NON-NLS-1$
descriptorNode = readJsonNode(df);
if (descriptorNode == null) {
// Failed to read descriptor data, nothing we can do about it
continue;
}
} else if (descriptorNode == null) {
log.warn("UI plugin descriptor node is null for '{}'", df.getAbsolutePath()); //$NON-NLS-1$
continue;
}
// Read configuration data
JsonNode configurationNode = currentData != null ? currentData.getConfigurationNode() : null;
if (reloadConfiguration) {
log.info("Reading UI plugin configuration '{}'", cf.getAbsolutePath()); //$NON-NLS-1$
configurationNode = readConfigurationNode(cf);
if (configurationNode == null) {
// Failed to read configuration data, use empty object
configurationNode = createEmptyObjectNode();
}
} else if (configurationNode == null) {
log.warn("UI plugin configuration node is null for '{}'", cf.getAbsolutePath()); //$NON-NLS-1$
continue;
}
// Update data
if (reloadDescriptor || reloadConfiguration) {
PluginData newData = new PluginData(descriptorNode, descriptorLastModified,
configurationNode, configurationLastModified, mapper.getNodeFactory());
// Validate data
boolean dataValid = newData.validate(new PluginData.ValidationCallback() {
@Override
public void descriptorError(String message) {
log.warn("Validation error in '{}': {}", df.getAbsolutePath(), message); //$NON-NLS-1$
}
@Override
public void configurationError(String message) {
log.warn("Validation error in '{}': {}", cf.getAbsolutePath(), message); //$NON-NLS-1$
}
});
if (!dataValid) {
// Data validation failed, nothing we can do about it
continue;
}
entriesToUpdate.put(descriptorFilePath, newData);
}
keysToCheckForRemoval.remove(descriptorFilePath);
}
// Compare cached data against (possibly missing) files
for (String descriptorFilePath : keysToCheckForRemoval) {
File df = new File(descriptorFilePath);
if (!df.exists()) {
// Descriptor data file has gone missing
keysToRemove.add(descriptorFilePath);
}
}
// Perform data updates
currentDataMapCopy.putAll(entriesToUpdate);
currentDataMapCopy.keySet().removeAll(keysToRemove);
}
boolean isJsonFile(File pathname) {
return pathname.isFile() && pathname.canRead() && pathname.getName().endsWith(JSON_FILE_SUFFIX);
}
JsonNode readJsonNode(File file) {
JsonNode node = null;
try {
node = mapper.readValue(file, JsonNode.class);
} catch (IOException e) {
log.warn("Cannot read/parse JSON file '{}': {}", file.getAbsolutePath(), e.getMessage()); //$NON-NLS-1$
log.debug("Exception", e); // $NON-NLS-1$
}
return node;
}
JsonNode readConfigurationNode(File configurationFile) {
return isJsonFile(configurationFile) ? readJsonNode(configurationFile) : null;
}
String getConfigurationFileName(File descriptorFile) {
return descriptorFile.getName().replace(JSON_FILE_SUFFIX, CONFIG_FILE_SUFFIX);
}
ObjectNode createEmptyObjectNode() {
return mapper.getNodeFactory().objectNode();
}
}