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(); } }