gt: 		addAction(group, id, action, text, icon, null);
context: /*
   * This file is part of the OpenCms plugin for IntelliJ by mediaworx.
   *
   * For further information about the OpenCms plugin for IntelliJ, please
   * see the project website at GitHub:
   * https://github.com/mediaworx/opencms-intellijplugin
   *
   * Copyright (C) 2007-2016 mediaworx berlin AG (http://www.mediaworx.com)
   *
   * This program is free software; you can redistribute it and/or modify it
   * under the terms of the GNU General Public License as published by the
   * Free Software Foundation; either version 3 of the License, or (at your
   * option) any later version.
   *
   * This program is distributed in the hope that it will be useful, but
   * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
   * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
   * for more details.
   *
   * You should have received a copy of the GNU General Public License along
   * with this program; if not, write to the Free Software Foundation, Inc.,
   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
   */
  
  package com.mediaworx.intellij.opencmsplugin;
  
  import com.intellij.openapi.actionSystem.*;
  import com.intellij.openapi.application.ApplicationManager;
  import com.intellij.openapi.components.*;
  import com.intellij.openapi.diagnostic.Logger;
  import com.intellij.openapi.module.Module;
  import com.intellij.openapi.module.ModuleManager;
  import com.intellij.openapi.project.Project;
  import com.intellij.openapi.ui.Messages;
  import com.intellij.openapi.vfs.VirtualFileManager;
  import com.intellij.openapi.vfs.VirtualFileManagerListener;
  import com.intellij.openapi.wm.ToolWindow;
  import com.intellij.openapi.wm.ToolWindowAnchor;
  import com.intellij.openapi.wm.ToolWindowManager;
  import com.intellij.util.messages.MessageBus;
  import com.intellij.util.messages.MessageBusConnection;
  import com.intellij.util.xmlb.XmlSerializerUtil;
  import com.mediaworx.intellij.opencmsplugin.actions.menus.OpenCmsEditorPopupMenu;
  import com.mediaworx.intellij.opencmsplugin.actions.menus.OpenCmsEditorTabPopupMenu;
  import com.mediaworx.intellij.opencmsplugin.actions.menus.OpenCmsMainMenu;
  import com.mediaworx.intellij.opencmsplugin.actions.menus.OpenCmsProjectPopupMenu;
  import com.mediaworx.intellij.opencmsplugin.configuration.OpenCmsModuleConfigurationComponent;
  import com.mediaworx.intellij.opencmsplugin.configuration.OpenCmsModuleConfigurationData;
  import com.mediaworx.intellij.opencmsplugin.configuration.OpenCmsPluginConfigurationData;
  import com.mediaworx.intellij.opencmsplugin.connector.OpenCmsPluginConnector;
  import com.mediaworx.intellij.opencmsplugin.listeners.OpenCmsModuleFileChangeListener;
  import com.mediaworx.intellij.opencmsplugin.opencms.OpenCmsConfiguration;
  import com.mediaworx.intellij.opencmsplugin.opencms.OpenCmsModules;
  import com.mediaworx.intellij.opencmsplugin.sync.SyncMode;
  import com.mediaworx.intellij.opencmsplugin.sync.VfsAdapter;
  import com.mediaworx.intellij.opencmsplugin.tools.PluginTools;
  import com.mediaworx.intellij.opencmsplugin.toolwindow.OpenCmsPluginToolWindowFactory;
  import com.mediaworx.intellij.opencmsplugin.toolwindow.OpenCmsToolWindowConsole;
  import com.mediaworx.opencms.ideconnector.client.IDEConnectorClient;
  import com.mediaworx.opencms.ideconnector.client.IDEConnectorClientConfiguration;
  import org.apache.commons.io.FileUtils;
  import org.apache.commons.lang3.StringUtils;
  import org.jetbrains.annotations.NotNull;
  import org.jetbrains.annotations.Nullable;
  import org.json.simple.JSONObject;
  import org.json.simple.parser.JSONParser;
  import org.json.simple.parser.ParseException;
  
  import javax.swing.*;
  import java.io.File;
  import java.io.IOException;
  import java.nio.charset.StandardCharsets;
  import java.util.Map;
  import java.util.TimerTask;
  import java.util.TreeMap;
  
  /**
   * OpenCms plugin for IntelliJ providing IntelliJ menu actions to sync resources to and from the OpenCms VFS, to publish
   * resources right from your IDE (using an additional OpenCms module), to create module manifest files, to package
   * OpenCms modules into module zips and to import those module zips to your local OpenCms instance.
   * OpenCms menu actions are provided in four different locations:
   * <ul>
   *     <li>At the main menu</li>
   *     <li>In the project window (left)</li>
   *     <li>In the main editor window (center)</li>
   *     <li>In the file tabs (usually on top of the editor)</li>
   * </ul>
   *
   * The configuration of the plugin is done on two levels:
   * <br /><br />
   * <strong>Project level</strong>
   * <br />
   * Some general options like the path to your local OpenCms webapp, your OpenCms user credentials or generic settings
   * are done on the project level. These options can be found under File > Settings > Tools > OpenCms Plugin.
   * are done on the project level. These options can be found under File > Settings > Tools > OpenCms Plugin.
   * <br /><br />
   * <strong>Module level</strong>
   * <br />
   * Module specific settings are configured for each separate module under File > Project Structure > Modules > [name of
   * the module] > Tab "OpenCms Module". Some module settings can be configured globally on the project level and changed
   * for individual modules on the module level.
   *
   * <p>See the plugin's <a href="https://github.com/mediaworx/opencms-intellijplugin/wiki">Wiki</a> on GitHub for more
   * information.</p>
   *
   * @author Kai Widmann, 2007-2016 mediaworx berlin AG
   */
  @State(
  	name = "OpenCmsPluginConfigurationData",
  	storages = {
  		@Storage( StoragePathMacros.WORKSPACE_FILE),
  		@Storage( "opencms.xml")
  	}
  )
  public class OpenCmsPlugin implements ProjectComponent, PersistentStateComponent<OpenCmsPluginConfigurationData> {
  
  	private static final Logger LOG = Logger.getInstance(OpenCmsPlugin.class);
  
  	public static final String TOOLWINDOW_ID = "OpenCms";
  	public static final String OPENCMS_MODULE_CONFIG_FILE = "opencms-module-config.json";
  
  	private static final String OPENCMS_MENU_ID = "OpenCmsPlugin.ActionMenu";
  	private static final String PROJECT_POPUP_MENU_ID = "OpenCmsPlugin.ProjectPopupMenu";
  	private static final String EDITOR_POPUP_MENU_ID = "OpenCmsPlugin.EditorPopupMenu";
  	private static final String TAB_POPUP_MENU_ID = "OpenCmsPlugin.TabsPopupGroup";
  
  	private static final Icon MENU_ICON = new ImageIcon(OpenCmsPlugin.class.getResource("/icons/opencms_menu.png"));
  
  	/** After IntelliJ module changes we'll wait a while before OpenCms modules get updated */
  	private static final int MODULE_CHANGE_UPDATE_DELAY = 1000;
  
  	/** The IntelliJ project */
  	private Project project;
  	
  	private VirtualFileManagerListener virtualFileManagerListener;
  	
  	/** Helper object to retrieve configuration data from the OpenCms configuration */
  	private OpenCmsConfiguration openCmsConfiguration;
  
  	/** Configuration data from the configuration form, is stored and loaded via PersistentStateComponent methods */
  	private OpenCmsPluginConfigurationData configurationData;
  
  	/** Container for all OpenCms modules configured in the project */
  	private OpenCmsModules openCmsModules;
  
  	/** Adapter used to sync the RFS with the OpenCms VFS */
  	private VfsAdapter vfsAdapter;
  
  	/**
  	 * Connector used to retrieve module or resource information from OpenCms and execute actions in OpenCms
  	 * (e.g. publishing), backed by the OpenCms module "com.mediaworx.opencms.ideconnector"
  	 */
  	private OpenCmsPluginConnector pluginConnector;
  
  	/**
  	 * Client used to execute actions in OpenCms (new with version 1.7). Right now it runs side by side with the old
  	 * {@link #pluginConnector} and adds some additional functionality (importing modules). In a future release the
  	 * connectorClient is planned to replace the old pluginConnector.
  	 */
  	private IDEConnectorClient connectorClient;
  
  	/**
  	 * The main menu
  	 */
  	private OpenCmsMainMenu openCmsMainMenu;
  
  	/**
  	 * ToolWindow for the OpenCms plugin
  	 */
  	private ToolWindow toolWindow;
  
  	/**
  	 * Console used to log OpenCms actions like sync or publish
  	 */
  	private OpenCmsToolWindowConsole console;
  
  	/** IntelliJ's action manager */
  	private ActionManager actionManager;
  
  	/** json parser used to parse the OpenCms module configuration file content */
  	private final JSONParser jsonParser = new JSONParser();
  
  
  	/**
  	 * timer used to execute delayed refreshes of OpenCms modules after IntelliJ modules have been changed
  	 */
  	private final java.util.Timer moduleUpdateTimer = new java.util.Timer();
  
  	/**
  	 * task used to execute delayed OpenCms module updates after IntelliJ modules have been changed
  	 */
  	private TimerTask currentModuleUpdateTimerTask;
  
  	/**
  	 * Set to <code>true</code> the first time the OpenCms plugin is enabled
  	 */
  	private boolean wasInitialized = false;
  
  	/**
  	 * Creates a new plugin instance, called by IntelliJ
  	 * @param project   the IntelliJ project
  	 */
  	public OpenCmsPlugin(Project project) {
  		LOG.info("OpenCmsPlugin: Initializing OpenCmsPlugin with project " + project.getName());
  		this.project = project;
  		openCmsModules = new OpenCmsModules(this);
  	}
  
  	/**
  	 * Initializes the plugin, called by IntelliJ
  	 */
  	@Override
  	public void initComponent() {
  		LOG.info("OpenCmsPlugin: initComponent called, project: " + project.getName());
  		actionManager = ActionManager.getInstance();
  	}
  
  	private void initOpenCmsConfiguration() {
  		clearOpenCmsConfiguration();
  		OpenCmsPluginConfigurationData config = getPluginConfiguration();
  		openCmsConfiguration = new OpenCmsConfiguration(config);
  		openCmsConfiguration.startMonitoringConfigurationChanges();
  	}
  
  	private void clearOpenCmsConfiguration() {
  		if (openCmsConfiguration != null) {
  			openCmsConfiguration.stopMonitoringConfigurationChanges();
  			openCmsConfiguration = null;
  		}
  	}
  
  
  	/**
  	 * Enables the plugin (if it is enabled in the project level configuration), called by IntelliJ whenever a project
  	 * is opened.
  	 */
  	@Override
  	public void projectOpened() {
  		LOG.info("OpenCmsPlugin: projectOpened called, project: " + project.getName());
  		OpenCmsPluginConfigurationData config = getPluginConfiguration();
  		if (config != null && config.isOpenCmsPluginEnabled()) {
  			enable();
  		}
  	}
  
  	/**
  	 * Disables the plugin, unregisters module actions and stops monitoring configuration changes, called by IntelliJ
  	 * whenever a project is closed.
  	 */
  	@Override
  	public void projectClosed() {
  		LOG.info("OpenCmsPlugin: projectClosed called, project: " + project.getName());
  		if (wasInitialized) {
  			openCmsMainMenu.unregisterModuleActions();
  			openCmsConfiguration.stopMonitoringConfigurationChanges();
  			disable();
  		}
  	}
  
  	/**
  	 * Enables the plugin for the current project. Initializes the plugin and its actions if initialization was not
  	 * done before. If the plugin connector is activated in the project level configuration it gets initialized as well.
  	 */
  	public void enable() {
  		initOpenCmsConfiguration();
  		initializeOpenCmsModules();
  
  		if (!wasInitialized) {
  			OpenCmsPluginConfigurationData config = getPluginConfiguration();
  			if (config != null && config.isOpenCmsPluginEnabled()) {
  
  				if (config.isPluginConnectorEnabled()) {
  					pluginConnector =   new OpenCmsPluginConnector(
  												config.getConnectorUrl(),
  												config.getUsername(),
  												config.getPassword(),
  												config.isUseMetaDateVariablesEnabled(),
  												config.isUseMetaIdVariablesEnabled()
  										);
  				}
  			}
  			registerListeners();
  			registerMenus();
  			wasInitialized = true;
  
  			checkWebappRootConfiguration(true);
  		}
  		else {
  			setToolWindowAvailable(true);
  		}
  	}
  
  	/**
  	 * Disables the plugin. The only thing done here is deactivating the plugin's tool window, menus are disabled
  	 * automatically because all actions are hidden if the plugin is deactivated
  	 */
  	public void disable() {
  		clearOpenCmsConfiguration();
  
  		if (wasInitialized) {
  			setToolWindowAvailable(false);
  		}
  	}
  
  	/**
  	 * initializes all OpenCms modules that are configured in the IntelliJ modules (option "Is OpenCms module" checked)
  	 * and removes modules that aren't configured as OpenCms modules
  	 */
  	private synchronized void initializeOpenCmsModules() {
  		LOG.info("OpenCmsPlugin: initializeOpenCmsModules called, project: " + project.getName());
  
  		ModuleManager moduleManager = ModuleManager.getInstance(project);
  		Module[] modules = moduleManager.getModules();
  
  		// put all modules in a tree map with their name as key and the module as value to be able to get an alphabetically sorted module list
  		Map<String, Module> ideaModuleMap = new TreeMap<>();
  		for (Module module : modules) {
  			ideaModuleMap.put(module.getName(), module);
  		}
  
  
  		for (Module ideaModule : ideaModuleMap.values()) {
  			String moduleBasePath = PluginTools.getModuleContentRoot(ideaModule);
  			if (StringUtils.isNotBlank(moduleBasePath)) {
  				OpenCmsModuleConfigurationData moduleConfig = null;
  
  				boolean isModuleConfigReadFromFile = false;
  				String moduleConfigFilePath = moduleBasePath + "/" + OPENCMS_MODULE_CONFIG_FILE;
  				File moduleConfigFile = new File(moduleConfigFilePath);
  				if (moduleConfigFile.exists() && moduleConfigFile.isFile()) {
  					try {
  						String moduleConfigurationJson = FileUtils.readFileToString(moduleConfigFile, StandardCharsets.UTF_8);
  						try {
  							JSONObject moduleJson = (JSONObject)jsonParser.parse(moduleConfigurationJson);
  							moduleConfig = readOpenCmsModuleConfiguration(moduleJson, ideaModule.getName());
  							isModuleConfigReadFromFile = true;
  						}
  						catch (ParseException e) {
  							LOG.error("Can't parse the module configuration file " + moduleConfigFilePath, e);
  						}
  						catch (Exception e) {
  							LOG.error("Unexpected exception parsing the OpenCms module configuration " + moduleConfigFilePath, e);
  						}
  					}
  					catch (IOException e) {
  						// this should never happen, since UTF-8 should exist. Let's log it anyway
  						LOG.warn("Charset UTF-8 not found, can't read " + moduleConfigFilePath);
  					}
  				}
  
  				if (!isModuleConfigReadFromFile) {
  					OpenCmsModuleConfigurationComponent configurationComponent = ideaModule.getComponent(OpenCmsModuleConfigurationComponent.class);
  					moduleConfig = configurationComponent.getState();
  				}
  
  				boolean validModule = false;
  				if (moduleConfig != null && moduleConfig.isOpenCmsModuleEnabled()) {
  					getOpenCmsModules().registerModule(moduleBasePath, moduleConfig);
  					validModule = true;
  				}
  				if (!validModule) {
  					getOpenCmsModules().unregisterModule(moduleBasePath);
  				}
  			}
  			else {
  				LOG.warn(String.format("Module %s doesn't have a valid content root", ideaModule.getName()));
  			}
  		}
  	}
  
  	/**
  	 * converts the json object containing the OpenCms module configuration to an instance of
  	 * OpenCmsModuleConfigurationData
  	 * @param moduleJson the json object containing the OpenCms module configuration
  	 * @return OpenCmsModuleConfigurationData
  	 */
  	private OpenCmsModuleConfigurationData readOpenCmsModuleConfiguration(JSONObject moduleJson, String intelliJModuleName) {
  		OpenCmsModuleConfigurationData moduleConfig;
  		String openCmsModuleName = (String)moduleJson.get("openCmsModuleName");
  		String localVfsRoot = (String)moduleJson.get("localVfsRoot");
  		String exportImportSiteRoot = (String)moduleJson.get("exportImportSiteRoot");
  		String syncMode = (String)moduleJson.get("syncMode");
  		String moduleVersion = (String)moduleJson.get("moduleVersion");
  
  		moduleConfig = new OpenCmsModuleConfigurationData();
  		moduleConfig.setOpenCmsModuleEnabled(true);
  
  		// handle the OpenCms module name (default or specific)
  		moduleConfig.setUseProjectDefaultModuleNameEnabled(StringUtils.isBlank(openCmsModuleName) || "default".equals(openCmsModuleName));
  		if (!moduleConfig.isUseProjectDefaultModuleNameEnabled()) {
  			moduleConfig.setModuleName(openCmsModuleName);
  		}
  
  		// handle the local VFS root (default or specific)
  		moduleConfig.setUseProjectDefaultVfsRootEnabled(StringUtils.isBlank(localVfsRoot) || "default".equals(localVfsRoot));
  		if (!moduleConfig.isUseProjectDefaultVfsRootEnabled()) {
  			moduleConfig.setLocalVfsRoot(localVfsRoot);
  		}
  
  		// handle the export/import site root
  		moduleConfig.setExportImportSiteRoot(StringUtils.isNotEmpty(exportImportSiteRoot) ? exportImportSiteRoot : "/");
  
  		// handle the sync mode (default, PUSH, SYNC or PULL)
  		if (
  				StringUtils.isEmpty(syncMode) ||
  				!(syncMode.equals("default") || syncMode.equals("PUSH") || syncMode.equals("SYNC") || syncMode.equals("PULL"))
  			) {
  			LOG.warn("invalid sync mode in opencms module configuration for module " + intelliJModuleName);
  			syncMode = "default";
  		}
  		moduleConfig.setUseProjectDefaultSyncModeEnabled("default".equals(syncMode));
  		if (!moduleConfig.isUseProjectDefaultSyncModeEnabled()) {
  			moduleConfig.setSyncMode(SyncMode.valueOf(syncMode));
  		}
  
  		// handle the module version
  		moduleConfig.setSetSpecificModuleVersionEnabled(StringUtils.isNotBlank(syncMode));
  		if (moduleConfig.isSetSpecificModuleVersionEnabled()) {
  			moduleConfig.setModuleVersion(moduleVersion);
  		}
  
  		return moduleConfig;
  	}
  
  	/**
  	 * Handles delayed refresh of the OpenCms modules after IntelliJ module configuration changes. If multiple IntelliJ
  	 * modules have been changed, the delayed execution is stopped and a new scheduled task is queued, so the refresh is
  	 * done only once.
  	 */
  	public void queueOpenCmsModuleUpdate() {
  		if (currentModuleUpdateTimerTask != null) {
  			currentModuleUpdateTimerTask.cancel();
  		}
  		currentModuleUpdateTimerTask = new TimerTask() {
  			@Override
  			public void run() {
  				LOG.info("OpenCmsPlugin: running timed OpenCms module refresh now: " + project.getName());
  				currentModuleUpdateTimerTask = null;
  				initializeOpenCmsModules();
  				if (openCmsMainMenu != null) {
  					openCmsMainMenu.registerModuleActions();
  				}
  			}
  		};
  		moduleUpdateTimer.schedule(currentModuleUpdateTimerTask, MODULE_CHANGE_UPDATE_DELAY);
  	}
  
  
  	/**
  	 * Registers the OpenCms menus for MainMenu, ProjectPopup, EditorPopup and EditorTabPopup
  	 */
  	private void registerMenus() {
  		registerToolsMenu();
  		registerProjectPopupMenu();
  		registerEditorPopupMenu();
  		registerEditorTabPopupMenu();
  	}
  
  	/**
  	 * Registers listeners for IntelliJ events. The listeners only listen for file change events and only file
  	 * deletions, renames and moves are handled (depending on the project level configuration these changes may
  	 * be synced back to OpenCms).
  	 */
  	private void registerListeners() {
  		MessageBus bus = ApplicationManager.getApplication().getMessageBus();
  		MessageBusConnection connection = bus.connect();
  		OpenCmsModuleFileChangeListener fileChangeListener = new OpenCmsModuleFileChangeListener(this);
  		connection.subscribe(VirtualFileManager.VFS_CHANGES, fileChangeListener);
  	}
  
  	/**
  	 * Adds a menu action to a given menu group.
  	 * @param group     the menu group the action should be added to
  	 * @param id        the action's identifier
  	 * @param action    the action itself
  	 * @param text      text to be displayed in the menu
  	 */
  	public void addAction(DefaultActionGroup group, String id, AnAction action, String text) {
  		addAction(group, id, action, text, null, null);
  	}
  
  	/**
  	 * Adds a menu action to a given menu group.
  	 * @param group     the menu group the action should be added to
  	 * @param id        the action's identifier
  	 * @param action    the action itself
  	 * @param text      text to be displayed in the menu
  	 * @param icon      icon to be displayed with the menu action (usually displayed befor the text)
  	 */
  	private void addAction(DefaultActionGroup group, String id, AnAction action, String text, Icon icon) {
  