@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) {
		addAction(group, id, action, text, icon, 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)
	 * @param constraints   constraints telling IntelliJ where to position the menu action
	 */
	private void addAction(DefaultActionGroup group, String id, AnAction action, String text, Icon icon, Constraints constraints) {
		action.getTemplatePresentation().setText(text);
		if (icon != null) {
			action.getTemplatePresentation().setIcon(icon);
		}
		if (actionManager.getAction(id) != null) {
			actionManager.unregisterAction(id);
		}
		try {
			actionManager.registerAction(id, action);
		}
		catch(Throwable t) {
			LOG.warn("Error adding action " + id, t);
		}
		if (constraints == null) {
			group.add(action);
		}
		else {
			group.add(action, constraints);
		}
	}

	/**
	 * Creates and registers the OpenCms menu for the tools menu as an action group
	 */
	private void registerToolsMenu() {
		openCmsMainMenu = (OpenCmsMainMenu)actionManager.getAction(OPENCMS_MENU_ID);
		if (openCmsMainMenu != null) {
			openCmsMainMenu.setPlugin(this);
		}
		if (openCmsMainMenu == null) {
			DefaultActionGroup toolsMenu = (DefaultActionGroup)actionManager.getAction("ToolsMenu");
			openCmsMainMenu = OpenCmsMainMenu.getInstance(this);
			addAction(toolsMenu, OPENCMS_MENU_ID, openCmsMainMenu, "O_penCms", null, null);
		}
		openCmsMainMenu.registerModuleActions();
	}

	/**
	 * Creates and registers the OpenCms menu for the project popup as an action group
	 */
	private void registerProjectPopupMenu() {
		OpenCmsProjectPopupMenu openCmsProjectPopupMenu = (OpenCmsProjectPopupMenu)actionManager.getAction(PROJECT_POPUP_MENU_ID);
		if (openCmsProjectPopupMenu == null) {
			DefaultActionGroup projectPopup = (DefaultActionGroup)actionManager.getAction(IdeActions.GROUP_PROJECT_VIEW_POPUP);
			openCmsProjectPopupMenu = new OpenCmsProjectPopupMenu(this);
			addAction(projectPopup, PROJECT_POPUP_MENU_ID, openCmsProjectPopupMenu, "_OpenCms", MENU_ICON, new Constraints(Anchor.BEFORE, "RevealIn"));
			projectPopup.add(Separator.getInstance(), new Constraints(Anchor.AFTER, PROJECT_POPUP_MENU_ID));
		}
	}

	/**
	 * Creates and registers the OpenCms menu for the editor popup as an action group
	 */
	private void registerEditorPopupMenu() {
		OpenCmsEditorPopupMenu openCmsEditorPopupMenu = (OpenCmsEditorPopupMenu)actionManager.getAction(EDITOR_POPUP_MENU_ID);
		if (openCmsEditorPopupMenu == null) {
			DefaultActionGroup editorPopup = (DefaultActionGroup)actionManager.getAction(IdeActions.GROUP_EDITOR_POPUP);
			openCmsEditorPopupMenu = new OpenCmsEditorPopupMenu(this);
			addAction(editorPopup, EDITOR_POPUP_MENU_ID, openCmsEditorPopupMenu, "_OpenCms", MENU_ICON, new Constraints(Anchor.AFTER, "ChangeFileEncodingAction"));
			editorPopup.addAction(Separator.getInstance(), new Constraints(Anchor.BEFORE, EDITOR_POPUP_MENU_ID));
		}
	}

	/**
	 * Creates and registers the OpenCms menu for the editor tab popup as an action group
	 */
	private void registerEditorTabPopupMenu() {
		OpenCmsEditorTabPopupMenu openCmsEditorTabPopupMenu = (OpenCmsEditorTabPopupMenu)actionManager.getAction(TAB_POPUP_MENU_ID);
		if (openCmsEditorTabPopupMenu == null) {
			// DefaultActionGroup editorTabPopup = (DefaultActionGroup)actionManager.getAction(IdeActions.GROUP_EDITOR_TAB_POPUP);
			DefaultActionGroup editorTabPopup = (DefaultActionGroup)actionManager.getAction("EditorTabPopupMenuEx");
			openCmsEditorTabPopupMenu = new OpenCmsEditorTabPopupMenu(this);
			editorTabPopup.addAction(Separator.getInstance());
			// addAction(editorTabPopup, TAB_POPUP_MENU_ID, openCmsEditorTabPopupMenu, "_OpenCms", MENU_ICON, new Constraints(Anchor.AFTER, "UnsplitAll"));
			addAction(editorTabPopup, TAB_POPUP_MENU_ID, openCmsEditorTabPopupMenu, "_OpenCms", MENU_ICON, new Constraints(Anchor.BEFORE, "RunContextPopupGroup"));
			// editorTabPopup.addAction(Separator.getInstance(), new Constraints(Anchor.BEFORE, TAB_POPUP_MENU_ID));
			editorTabPopup.addAction(Separator.getInstance(), new Constraints(Anchor.AFTER, TAB_POPUP_MENU_ID));
		}
	}

	public boolean checkWebappRootConfiguration(boolean showDialog) {
		boolean configOK = true;
		OpenCmsPluginConfigurationData config = getPluginConfiguration();
		if (config != null && config.isOpenCmsPluginEnabled()) {
			File file = new File(config.getWebappRoot());
			// show an error message if the webapp root folder was not found
			if (!file.exists()) {
				configOK = false;
				if (showDialog) {
					Messages.showDialog("The Webapp Root was not found.\nPlease check the OpenCms Webapp Root in the OpenCms Plugin settings.", "OpenCms Plugin - Configuration Error", new String[]{"Ok"}, 0, Messages.getErrorIcon());
				}
			}
			else {
				// check if the OpenCms configuration path exists
				file = new File(config.getWebappRoot() + OpenCmsConfiguration.CONFIGPATH);
				if (!file.exists()) {
					configOK = false;
					if (showDialog) {
						Messages.showDialog("The OpenCms configuration was not found.\nPlease check the OpenCms Webapp Root in the OpenCms Plugin settings.", "Error", new String[]{"Ok"}, 0, Messages.getErrorIcon());
					}
				}
			}
		}
		return configOK;
	}

	/**
	 * Does some cleanup, called by IntelliJ.
	 */
	@Override
	public void disposeComponent() {
		LOG.info("OpenCmsPlugin: disposeComponent called, project: " + project.getName());
		project = null;
		configurationData = null;
		clearOpenCmsConfiguration();
		openCmsModules = null;
		vfsAdapter = null;
		pluginConnector = null;
		toolWindow = null;
		console = null;
		actionManager = null;
	}

	/**
	 * Returns the component's name.
	 * @return  "OpenCmsPlugin.MainComponent"
	 */
	@NotNull
	@Override
	public String getComponentName() {
		LOG.info("OpenCmsPlugin: getComponentName called, project: " + project.getName());
		return "OpenCmsPlugin.MainComponent";
	}

	/**
	 * Returns the IntelliJ project
	 * @return  the IntelliJ project
	 */
	public Project getProject() {
		return project;
	}

	/**
	 * Returns the helper object to retrieve configuration data from the OpenCms configuration
	 * @return  the OpenCms configuration helper object
	 */
	public OpenCmsConfiguration getOpenCmsConfiguration() {
		if (openCmsConfiguration == null) {
			initOpenCmsConfiguration();
		}
		return openCmsConfiguration;
	}

	/**
	 * Returns the plugin's project level configuration data
	 * @return  the project level configuration data
	 */
	public OpenCmsPluginConfigurationData getPluginConfiguration() {
		if (configurationData == null) {
			configurationData = new OpenCmsPluginConfigurationData();
		}
		return configurationData;
	}

	/**
	 * Returns the container for all OpenCms modules configured in the project
	 * @return  the OpenCms module container
	 */
	public OpenCmsModules getOpenCmsModules() {
		return openCmsModules;
	}

	/**
	 * refreshes all OpenCms modules (e.g. after configuration changes)
	 */
	public void refreshOpenCmsModules() {
		openCmsModules.refreshAllModules();
	}

	/**
	 * Returns the adapter used to sync the RFS with the OpenCms VFS
	 * @return  the VFS adapter for sync actions
	 */
	public VfsAdapter getVfsAdapter() {
		if (vfsAdapter == null) {
			OpenCmsPluginConfigurationData config = getPluginConfiguration();
			if (config != null && config.isOpenCmsPluginEnabled() && config.getPassword() != null && config.getPassword().length() > 0) {
				vfsAdapter = new VfsAdapter(config.getRepository(), config.getUsername(), config.getPassword());
			}
		}
		return vfsAdapter;
	}

	/**
	 * Returns the connector used to retrieve module or resource information from OpenCms and execute actions in
	 * OpenCms (e.g. publishing)
	 * @return the OpenCms plugin connector for information retrieval and publishing
	 * @deprecated the old connector (via JSP) will be removed once all functionality is moved to the new connector
	 *             service (via Servlet)
	 */
	@Deprecated
	public OpenCmsPluginConnector getPluginConnector() {
		return pluginConnector;
	}

	/**
	 * Sets the connector used to retrieve module or resource information from OpenCms and execute actions in
	 * OpenCms (e.g. publishing)
	 *
	 * @param pluginConnector the OpenCms plugin connector for information retrieval and publishing
	 * @deprecated the old connector (via JSP) will be removed once all functionality is moved to the new connector
	 *             service (via Servlet)
	 */
	@Deprecated
	public void setPluginConnector(OpenCmsPluginConnector pluginConnector) {
		this.pluginConnector = pluginConnector;
	}

	/**
	 * Returns the IDE connector client that may be used to execute actions on the local OpenCms instance (must be
	 * running) (new with version 1.7). Right now the connector client 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.
	 * @return the IDE connector client
	 */
	public IDEConnectorClient getConnectorClient() {
		if (connectorClient == null) {
			initConnectorClient();
		}
		return connectorClient;
	}

	/**
	 * Initializes the connector client
	 */
	private void initConnectorClient() {
		OpenCmsPluginConfigurationData config = getPluginConfiguration();
		if (config != null && config.isOpenCmsPluginEnabled() && config.isPluginConnectorServiceEnabled() && StringUtils.isNotBlank(config.getConnectorServiceUrl())) {
			IDEConnectorClientConfiguration clientConfiguration = new IDEConnectorClientConfiguration();
			clientConfiguration.setConnectorServiceBaseUrl(config.getConnectorServiceUrl());
			connectorClient = new IDEConnectorClient(clientConfiguration);
		}
	}
	
	
	
	/**
	 * Sets the connector client used to access the new IDE Connector Service (e.g. for importing modules)
	 * @param connectorClient
	 */
	public void setConnectorClient(IDEConnectorClient connectorClient) {
		this.connectorClient = connectorClient;
	}

	/**
	 * Initializes the plugin's tool window
	 */
	private void initToolWindow() {
		if (project == null) {
			LOG.info("Unable to initialize the tool window since the project is null");
			return;
		}
		ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project);
		if (toolWindowManager == null) {
			LOG.info("ToolWindowManager could not be retrieved from the project, it may not be registered yet.");
			return;
		}
		toolWindow = toolWindowManager.getToolWindow(OpenCmsPlugin.TOOLWINDOW_ID);
		if (toolWindow == null) {
			OpenCmsPluginConfigurationData config = getPluginConfiguration();
			toolWindow = toolWindowManager.registerToolWindow(OpenCmsPlugin.TOOLWINDOW_ID, false, ToolWindowAnchor.BOTTOM);
			toolWindow.setIcon(new ImageIcon(this.getClass().getResource("/icons/opencms_13.png")));
			toolWindow.setAvailable(config != null && config.isOpenCmsPluginEnabled(), null);
			OpenCmsPluginToolWindowFactory toolWindowFactory = new OpenCmsPluginToolWindowFactory();
			toolWindowFactory.createToolWindowContent(project, toolWindow);
		}
	}

	/**
	 * Returns the plugin's tool window
	 * @return  the plugin's tool window
	 */
	public ToolWindow getToolWindow() {
		if (toolWindow == null) {
			initToolWindow();
		}
		return toolWindow;
	}

	/**
	 * Activates or deactivates the plugin's tool window
	 * @param available <code>true</code> to activate and <code>false</code> to deactivate the tool window
	 */
	public void setToolWindowAvailable(boolean available) {
		ToolWindow toolWindow = getToolWindow();
		try {
			toolWindow.setAvailable(available, null);
		}
		catch (Exception e) {
			LOG.warn("Exception activating the ToolWindow: " + e.getMessage());
		}
	}

	/**
	 * Activates the plugin's tool window to display the console
	 */
	public void showConsole() {
		if (toolWindow == null || !toolWindow.isActive()) {
			getToolWindow().activate(null);
		}
	}

	/**
	 * Returns the console used to log OpenCms actions like sync or publish
	 * @return  the console used to log OpenCms actions like sync or publish
	 */
	public OpenCmsToolWindowConsole getConsole() {
		// if the console has not been initialized ...
		if (console == null) {
			initToolWindow();  // ... initialize the tool window (containing the console)
		}
		return console;
	}

	/**
	 * Sets the console used to log OpenCms actions like sync or publish
	 * @param console  the console used to log OpenCms actions like sync or publish
	 */
	public void setConsole(OpenCmsToolWindowConsole console) {
		this.console = console;
	}

	/**
	 * Returns the current project level configuration state.
	 *
	 * @return the OpenCmsPluginConfigurationData object
	 */
	@Nullable
	@Override
	public OpenCmsPluginConfigurationData getState() {
		LOG.info("OpenCmsPlugin: getState called, project: " + project.getName());
		return configurationData;
	}

	/**
	 * Loads the project level configuration state contained in the given configuration data.
	 *
	 * @param configurationData the project level configuration data to load
	 */
	@Override
	public void loadState(OpenCmsPluginConfigurationData configurationData) {
		LOG.info("OpenCmsPlugin: loadState called, project: " + project.getName());
		XmlSerializerUtil.copyBean(configurationData, getPluginConfiguration());
	}

}
 public class OpenCmsPluginConfigurationData {

	private boolean openCmsPluginEnabled = false;
    private String repository;
    private String username;
    private String password;
    private String webappRoot;
	private String defaultLocalVfsRoot;
	private String moduleNamingScheme;
	private SyncMode defaultSyncMode;
	private String ignoredFiles;
	private String[] ignoredFilesArray;
	private String ignoredPaths;
	private String[] ignoredPathsArray;
	private String moduleZipTargetFolderPath;
	private boolean pluginConnectorEnabled;
	private String connectorUrl;
	private boolean pluginConnectorServiceEnabled;
	private String connectorServiceUrl;
	private AutoPublishMode autoPublishMode;
	private boolean pullMetadataEnabled;
	private String manifestRoot;
	private boolean useMetaDateVariablesEnabled;
	private boolean useMetaIdVariablesEnabled;
	private boolean useMetaVariablesEnabled;

	/**
	 * Flag denoting if the plugin is enabled for the project.
	 * @return  <code>true</code> if the plugin is enabled for the project, <code>false</code> otherwise
	 */
	public boolean isOpenCmsPluginEnabled() {
		return openCmsPluginEnabled;
	}

	/**
	 * Sets the flag denoting if the plugin is enabled for the project.
	 * @param openCmsPluginEnabled  <code>true</code> if the plugin is to be enabled for the project,
	 *                              <code>false</code> otherwise
	 */
	public void setOpenCmsPluginEnabled(boolean openCmsPluginEnabled) {
		this.openCmsPluginEnabled = openCmsPluginEnabled;
	}

	/**
	 * Returns the configured CMIS-Repository used to sync files and/or folders to and from the OpenCms VFS.
	 * @return the configured CMIS-Repository, e.g. "http://localhost:8080/opencms/cmisatom/cmis-offline/"
	 */
	public String getRepository() {
        return repository;
    }

	/**
	 * Sets the CMIS-Repository to be used to sync files and/or folders to and from the OpenCms VFS.
	 * @param repository the CMIS-Repository, e.g. "http://localhost:8080/opencms/cmisatom/cmis-offline/"
	 */
    public void setRepository(String repository) {
        this.repository = repository;
    }

	/**
	 * Returns the configured OpenCms username
	 * @return the configured OpenCms username (e.g. "Admin")
	 */
    public String getUsername() {
        return username;
    }

	/**
	 * Sets the OpenCms username to be used to login to OpenCms
	 * @param username an OpenCms username (e.g. "Admin")
	 */
    public void setUsername(String username) {
        this.username = username;
    }

	/**
	 * Returns the configured OpenCms password
	 * @return the configured OpenCms password (e.g. "admin")
	 */
   public String getPassword() {
        return password;
    }

	/**
	 * Sets the OpenCms password to be used to login to OpenCms
	 * @param password the OpenCms user's password (e.g. "admin")
	 */
    public void setPassword(String password) {
        this.password = password;
    }

	/**
	 * Returns the configured webapp root
	 * @return the configured webapp root
	 */
    public String getWebappRoot() {
	    return stripTrailingSeparator(webappRoot);
    }

	/**
	 * Sets the webapp root
	 * @param webappRoot the root of the local OpenCms webapp
	 */
    public void setWebappRoot(String webappRoot) {
        this.webappRoot = webappRoot;
    }

	/**
	 * Returns the configured default local VFS root. That's where VFS files are stored under the module root. Modules
	 * can configure differing VFS roots.
	 * @return the configured default local VFS root (e.g. "src/main/vfs")
	 * @see OpenCmsModuleConfigurationData#getLocalVfsRoot()
	 */
	public String getDefaultLocalVfsRoot() {
		return defaultLocalVfsRoot;
	}

	/**
	 * Sets the default local VFS root. That's where VFS files are stored under the module root. Modules can configure
	 * differing VFS roots.
	 * @param defaultLocalVfsRoot the default local VFS root (e.g. "src/main/vfs")
	 * @see OpenCmsModuleConfigurationData#setLocalVfsRoot(String)
	 */
	public void setDefaultLocalVfsRoot(String defaultLocalVfsRoot) {
		this.defaultLocalVfsRoot = defaultLocalVfsRoot;
	}

	/**
	 * Returns the configured naming scheme for modules. If no specific module name is provided in the module
	 * configuration, that's the scheme used to generate the OpenCms module name by replacing ${modulename} with the
	 * IntelliJ module name.
	 * @return the module naming scheme (e.g. "com.yourcompany.opencms.${modulename})
	 * @see com.mediaworx.intellij.opencmsplugin.configuration.OpenCmsModuleConfigurationData#getModuleName() ()
	 */
	public String getModuleNamingScheme() {
		return moduleNamingScheme;
	}

	/**
	 * Sets the naming scheme to be used to generate OpenCms module names. The variable ${modulename} is replaced by
	 * the IntelliJ module name.
	 * @param moduleNamingScheme the naming scheme for OpenCms module names
	 */
	public void setModuleNamingScheme(String moduleNamingScheme) {
		this.moduleNamingScheme = moduleNamingScheme;
	}

	/**
	 * Returns the configured default Sync Mode. The Sync Mode can be PUSH, SYNC or PULL. Modules can configure
	 * differing Sync Modes.
	 * @return the configured Sync Mode
	 * @see OpenCmsModuleConfigurationData#getSyncMode()
	 */
	public SyncMode getDefaultSyncMode() {
		return defaultSyncMode != null ? defaultSyncMode : SyncMode.SYNC;
	}

	/**
	 * Sets the default Sync Mode. The Sync Mode can be PUSH, SYNC or PULL. Modules can configure differing Sync Modes.
	 * @param defaultSyncMode the SyncMode (PUSH, SYNC or PULL)
	 * @see OpenCmsModuleConfigurationData#setSyncMode(SyncMode)
	 */
	public void setDefaultSyncMode(SyncMode defaultSyncMode) {
		this.defaultSyncMode = defaultSyncMode;
	}

	/**
	 * Gets the configured ignored file regex list (one String, separated by Newlines). Files matching one of these
	 * regular expressions are not synced to/from the OpenCms VFS.
	 * @return  the configured ignored file list
	 * @see #getIgnoredFilesArray()
	 */
	public String getIgnoredFiles() {
		return ignoredFiles;
	}

	/**
	 * Gets the configured ignored file regex list as a String Array. Files matching one of these regular expressions
	 * are not synced to/from the OpenCms VFS.
	 * @return  the configured ignored file list split into a String Array.
	 * @see #getIgnoredFiles()
	 */
	public String[] getIgnoredFilesArray() {
		return ignoredFilesArray;
	}

	/**
	 * Sets the ignored file regex list. Files matching one of these regular expressions are not synced to/from the
	 * OpenCms VFS.
	 * @param ignoredFiles the ignored file regex list (one String, separated by Newlines)
	 */
	public void setIgnoredFiles(String ignoredFiles) {
		this.ignoredFiles = ignoredFiles;
		if (ignoredFiles != null && ignoredFiles.length() > 0) {
			ignoredFilesArray = StringUtils.split(ignoredFiles.trim());
		}
		else {
			ignoredFilesArray = new String[0];
		}
	}

	/**
	 * Gets the configured ignored folders regex list (one String, separated by Newlines). Folders matching one of these
	 * regular expressions are not synced to/from the OpenCms VFS.
	 * @return  the configured ignored folders list
	 * @see #getIgnoredFilesArray()
	 */
	public String getIgnoredPaths() {
		return ignoredPaths;
	}

	/**
	 * Gets the configured ignored folders regex list as a String Array. Folders matching one of these regular
	 * expressions are not synced to/from the OpenCms VFS.
	 * @return  the configured ignored folders list split into a String Array.
	 * @see #getIgnoredFiles()
	 */
	public String[] getIgnoredPathsArray() {
		return ignoredPathsArray;
	}

	/**
	 * Sets the ignored folders regex list. Folders matching one of these regular expressions are not synced to/from the
	 * OpenCms VFS.
	 * @param ignoredPaths the ignored folders regex list (one String, separated by Newlines)
	 */
	public void setIgnoredPaths(String ignoredPaths) {
		this.ignoredPaths = ignoredPaths;
		if (ignoredPaths != null && ignoredPaths.length() > 0) {
			ignoredPathsArray = StringUtils.split(ignoredPaths.trim());
		}
		else {
			ignoredPathsArray = new String[0];
		}
	}

	/**
	 * Gets the configured folder that is used to store packaged module zips
	 * @return folder that is used to store packaged module zips
	 */
	public String getModuleZipTargetFolderPath() {
		return moduleZipTargetFolderPath;
	}

	public void setModuleZipTargetFolderPath(String moduleZipTargetFolderPath) {
		this.moduleZipTargetFolderPath = moduleZipTargetFolderPath;
	}

	/**
	 * Flag denoting if the Plugin Connector is enabled
	 * @return  <code>true</code> if the Plugin Connector is enabled, <code>false</code> otherwise
	 */
	public boolean isPluginConnectorEnabled() {
		return pluginConnectorEnabled;
	}

	/**
	 * Sets the flag denoting if the Plugin Connector is enabled
	 * @param pluginConnectorEnabled <code>true</code> if the Plugin Connector should be enabled, <code>false</code>
	 *                               otherwise
	 */
	public void setPluginConnectorEnabled(boolean pluginConnectorEnabled) {
		this.pluginConnectorEnabled = pluginConnectorEnabled;
	}


	/**
	 * Flag denoting if the Plugin Connector Service is enabled (requires Plugin Connector 1.5)
	 * @return  <code>true</code> if the Plugin Connector is enabled, <code>false</code> otherwise
	 */
	public boolean isPluginConnectorServiceEnabled() {
		return pluginConnectorServiceEnabled;
	}

	/**
	 * Sets the flag denoting if the Plugin Connector Service is enabled
	 * @param pluginConnectorServiceEnabled <code>true</code> if the Plugin Connector Service should be enabled,
	 *                                      <code>false</code> otherwise
	 */
	public void setPluginConnectorServiceEnabled(boolean pluginConnectorServiceEnabled) {
		this.pluginConnectorServiceEnabled = pluginConnectorServiceEnabled;
	}


	/**
	 * Gets the configured Url under which the old Plugin Connector JSP can be called.
	 *
	 * @return the configured Url under which the Plugin Connector can be called
	 */
	public String getConnectorUrl() {
		return connectorUrl;
	}

	/**
	 * Sets the Url under which the old Plugin Connector JSP can be called.
	 *
	 * @param connectorUrl the Plugin Connector JSP's Url (e.g.
	 *                     http://localhost:8080/opencms/opencms/system/modules/com.mediaworx.opencms.ideconnector/connector.jsp)
	 */
	public void setConnectorUrl(String connectorUrl) {
		this.connectorUrl = connectorUrl;
	}

	/**
	 * Gets the configured Url under which the new Plugin Connector Service can be called.
	 *
	 * @return the configured Url under which the Plugin Connector can be called
	 */
	public String getConnectorServiceUrl() {
		return connectorServiceUrl;
	}

	/**
	 * Sets the Url under which the new Plugin Connector Service can be called.
	 *
	 * @param connectorServiceUrl the Plugin Connector Service Url (e.g. http://localhost:8080/opencms/ideConnector)
	 */
	public void setConnectorServiceUrl(String connectorServiceUrl) {
		this.connectorServiceUrl = connectorServiceUrl;
	}

	/**
	 * Gets the configured Auto Publish Mode
	 * @return the configured Auto Publish Mode (OFF, FILECHANGE, ALL)
	 */
	public AutoPublishMode getAutoPublishMode() {
		return autoPublishMode != null ? autoPublishMode : AutoPublishMode.FILECHANGE;
	}

	/**
	 * Sets the Auto Publish Mode
	 * @param autoPublishMode the Auto Publish Mode to be used by the plugin (OFF, FILECHANGE, ALL)
	 */
	public void setAutoPublishMode(AutoPublishMode autoPublishMode) {
		this.autoPublishMode = autoPublishMode;
	}


	/**
	 * Flag denoting if pulling OpenCms module and resource meta data is enabled
	 * @return  <code>true</code> if pulling meta data is enabled, <code>false</code> otherwise
	 */
	public boolean isPullMetadataEnabled() {
		return pullMetadataEnabled;
	}

	/**
	 * Sets the flag denoting if pulling OpenCms module and resource meta data is enabled
	 * @param pullMetadataEnabled <code>true</code> if pulling meta data should be enabled, <code>false</code> otherwise
	 */
	public void setPullMetadataEnabled(boolean pullMetadataEnabled) {
		this.pullMetadataEnabled = pullMetadataEnabled;
	}

	/**
	 * Gets the configured path under which OpenCms module manifest files (<code>manifest.xml</code>) and resource
	 * meta data are to be stored (relative to the module's root path).
	 * @return  the configured manifest root path
	 */
	public String getManifestRoot() {
		return manifestRoot;
	}

	/**
	 * Sets the path under which OpenCms module manifest files (<code>manifest.xml</code>) and resource meta data are
	 * to be stored (relative to the module's root path).
	 * @param manifestRoot the manifest root path to be used
	 */
	public void setManifestRoot(String manifestRoot) {
		this.manifestRoot = manifestRoot;
	}

	/**
	 * Gets the flag denoting if using placeholders instead of dates in resource meta data is enabled
	 * @return <code>true</code> if using placeholders is enabled, <code>false</code> otherwise
	 */
	public boolean isUseMetaDateVariablesEnabled() {
		return useMetaVariablesEnabled || useMetaDateVariablesEnabled;
	}

	/**
	 * Sets the flag denoting if using placeholders instead of dates in resource meta data is enabled
	 * @param useMetaDateVariablesEnabled  <code>true</code> if using placeholders should be enabled,
	 *                                     <code>false</code> otherwise
	 */
	public void setUseMetaDateVariablesEnabled(boolean useMetaDateVariablesEnabled) {
		this.useMetaDateVariablesEnabled = useMetaDateVariablesEnabled;
		if (!useMetaDateVariablesEnabled) {
			this.useMetaVariablesEnabled = false;
		}
	}

	/**
	 * Gets the flag denoting if using placeholders instead of UUIDs in resource meta data is enabled
	 *
	 * @return <code>true</code> if using placeholders is enabled, <code>false</code> otherwise
	 */
	public boolean isUseMetaIdVariablesEnabled() {
		return useMetaVariablesEnabled || useMetaIdVariablesEnabled;
	}

	/**
	 * Sets the flag denoting if using placeholders instead of UUIDs in resource meta data is enabled
	 *
	 * @param useMetaIdVariablesEnabled <code>true</code> if using placeholders should be enabled, <code>false</code>
	 *                                  otherwise
	 */
	public void setUseMetaIdVariablesEnabled(boolean useMetaIdVariablesEnabled) {
		this.useMetaIdVariablesEnabled = useMetaIdVariablesEnabled;
		if (!useMetaIdVariablesEnabled) {
			this.useMetaVariablesEnabled = false;
		}
	}

	/**
	 * Gets the flag denoting if using placeholders instead of UUIDs and dates in resource meta data is enabled
	 *
	 * @return <code>true</code> if using placeholders is enabled, <code>false</code> otherwise
	 * @deprecated no longer used, but needed for downward compatibility
	 */
	public boolean isUseMetaVariablesEnabled() {
		return useMetaVariablesEnabled || (this.useMetaDateVariablesEnabled && this.useMetaIdVariablesEnabled);
	}

	/**
	 * Sets the flag denoting if using placeholders instead of UUIDs and dates in resource meta data is enabled
	 *
	 * @param useMetaVariablesEnabled <code>true</code> if using placeholders should be enabled, <code>false</code>
	 *                                otherwise
	 * @deprecated no longer used, but needed for downward compatibility
	 */
	public void setUseMetaVariablesEnabled(boolean useMetaVariablesEnabled) {
		this.useMetaVariablesEnabled = useMetaVariablesEnabled;
		if (useMetaVariablesEnabled) {
			this.useMetaDateVariablesEnabled = true;
			this.useMetaIdVariablesEnabled = true;
		}
	}

	/**
	 * Internal utility method to strip trailing path separators from path Strings.
	 * @param s the path String
	 * @return  the path String with trailing path separators stripped.
	 */
	private String stripTrailingSeparator(String s) {
		if (s != null && (s.endsWith("\\") || s.endsWith("/"))) {
			return s.substring(0, s.length() - 1);
		}
		return s;
	}

} public enum  AutoPublishMode {
	OFF,
	FILECHANGE,
	ALL
} public class CmsConnectionException extends Exception {

	/**
	 * Creates a new CmsConnectionException with the given message
	 * @param message the error message
	 */
	public CmsConnectionException(String message) {
		super(message);
	}

	/**
	 * Creates a new CmsConnectionException with the given message
	 * @param message the error message
	 * @param cause Original Exception/Throwable that led to the exception
	 */
	public CmsConnectionException(String message, Throwable cause) {
		super(message, cause);
	}

} public class CmsPermissionDeniedException extends Exception {

	/**
	 * Creates a new CmsPermissionDeniedException with the given message
	 * @param message the error message
	 */
	public CmsPermissionDeniedException(String message) {
		super(message);
	}

	/**
	 * Creates a new CmsPermissionDeniedException with the given message
	 * @param message the error message
	 * @param cause Original Exception/Throwable that led to the exception
	 */
	public CmsPermissionDeniedException(String message, Throwable cause) {
		super(message, cause);
	}
} public class OpenCmsConnectorException extends Exception {

	/**
	 * Creates a new OpenCmsConnectorException with the given message
	 * @param message the error message
	 */
	public OpenCmsConnectorException(String message) {
		super(message);
	}

	/**
	 * Creates a new OpenCmsConnectorException with the given message
	 * @param message the error message
	 * @param cause Original Exception/Throwable that led to the exception
	 */
	public OpenCmsConnectorException(String message, Throwable cause) {
		super(message, cause);
	}
} public class OpenCmsModule implements OpenCmsConfiguration.ConfigurationChangeListener {

	private static final Logger LOG = LoggerFactory.getLogger(OpenCmsModule.class);

	private OpenCmsPlugin plugin;
	private String moduleBasePath;
	private OpenCmsConfiguration openCmsConfig;
	private OpenCmsPluginConfigurationData pluginConfig;
	private OpenCmsModuleConfigurationData moduleConfig;
	private String moduleName;

	private List<OpenCmsModuleExportPoint> exportPoints;
	private List<String> moduleResources;
	private String localVfsRoot;

	/**
	 * Creates a new OpenCms module
	 * @param plugin            the current plugin instance
	 * @param moduleBasePath    absolute base path of the module on the local file system
	 */
	public OpenCmsModule(OpenCmsPlugin plugin, String moduleBasePath) {
		this.plugin = plugin;
		this.moduleBasePath = moduleBasePath;

		pluginConfig = plugin.getPluginConfiguration();
		openCmsConfig = plugin.getOpenCmsConfiguration();
		openCmsConfig.registerConfigurationChangeListener(this);
	}

	/**
	 * Initializes the module with the given configuration data (from the module's configuration dialog)
	 * @param moduleConfig the module's configuration data
	 */
	public void init(OpenCmsModuleConfigurationData moduleConfig) {
		this.moduleConfig = moduleConfig;

		if (moduleConfig.isUseProjectDefaultModuleNameEnabled()) {
			String moduleNamingScheme = pluginConfig.getModuleNamingScheme();
			String moduleFolderName = StringUtils.substringAfterLast(this.moduleBasePath, "/");
			if (moduleNamingScheme != null && moduleNamingScheme.length() > 0) {
				moduleName = moduleNamingScheme.replaceAll(Pattern.quote("${modulename}"), moduleFolderName);
			}
			else {
				moduleName = moduleFolderName;
			}
		}
		else {
			moduleName = moduleConfig.getModuleName();
		}

		exportPoints = openCmsConfig.getExportPointsForModule(moduleName);
		moduleResources = openCmsConfig.getModuleResourcesForModule(moduleName);

		String relativeVfsRoot;

		if (moduleConfig.isUseProjectDefaultVfsRootEnabled()) {
			relativeVfsRoot = pluginConfig.getDefaultLocalVfsRoot();
		}
		else {
			relativeVfsRoot = moduleConfig.getLocalVfsRoot();
		}
		localVfsRoot = this.moduleBasePath + "/" + relativeVfsRoot;
	}

	/**
	 * refreshes the module after module configuration changes
	 * @param moduleConfig  the updated configuration data
	 */
	public void refresh(OpenCmsModuleConfigurationData moduleConfig) {
		// for now a refresh just does the same as init
		init(moduleConfig);
	}

	/**
	 * refreshes the module with unchanged configuration data
	 */
	public void refresh() {
		// for now a refresh just does the same as init
		init(moduleConfig);
	}

	/**
	 * @return the module's package name name, e.g. com.mycompany.opencms.contenttypes
	 */
	public String getModuleName() {
		return moduleName;
	}

	/**
	 * @return  absolute base path of the module on the local file system
	 */
	public String getModuleBasePath() {
		return moduleBasePath;
	}

	/**
	 * @return absolute local path to the module's vfs folder
	 */
	public String getLocalVfsRoot() {
		return localVfsRoot;
	}

	public String getExportImportSiteRoot() {
		String exportImportSiteRoot = moduleConfig.getExportImportSiteRoot();
		if (StringUtils.isBlank(exportImportSiteRoot)) {
			exportImportSiteRoot = "/";
		}
		return exportImportSiteRoot;
	}

	/**
	 * @return absolute local path to the folder containing the module's meta data
	 */
	public String getManifestRoot() {
		return moduleBasePath + "/" + plugin.getPluginConfiguration().getManifestRoot();
	}

	/**
	 * @return the module's sync mode as configured in either the global plugin configuration or the module's
	 *         configuration
	 */
	public SyncMode getSyncMode() {
		if (moduleConfig.isUseProjectDefaultSyncModeEnabled()) {
			return pluginConfig.getDefaultSyncMode();
		}
		else {
			return moduleConfig.getSyncMode();
		}
	}

	/**
	 * @return <code>true</code> if the module configuration defines a specific module version to use,
	 *         <code>false</code> otherwise
	 */
	public boolean isSetSpecificModuleVersionEnabled() {
		return moduleConfig.isSetSpecificModuleVersionEnabled();
	}

	/**
	 * @return the module version configured for this module (if any)
	 */
	public String getModuleVersion() {
		return moduleConfig.getModuleVersion();
	}

	/**
	 * @return a list of export points configured for this module
	 */
	public List<OpenCmsModuleExportPoint> getExportPoints() {
		return exportPoints;
	}

	/**
	 * @return list of the module resource paths configured for this module (relative to VFS root)
	 */
	public List<String> getModuleResources() {
		return moduleResources;
	}

	/**
	 * Checks if the given file is placed inside a module resource path
	 * @param file  the file to check
	 * @return  <code>true</code> if the file is contained in one of the module's resource paths,
	 *          <code>false</code> otherwise
	 */
	public boolean isFileModuleResource(File file) {
		return isPathModuleResource(file.getPath());
	}

	/**
	 * Checks if the given path is inside a module resource path
	 *
	 * @param resourcePath the path to check
	 * @return <code>true</code> if the path is contained in one of the module's resource paths,
	 * <code>false</code> otherwise
	 */
	public boolean isPathModuleResource(String resourcePath) {
		resourcePath = PluginTools.ensureUnixPath(resourcePath);
		for (String moduleResourcePath : getModuleResources()) {
			String localModuleResourcePath = getLocalVfsRoot() + moduleResourcePath;
			if ((resourcePath + "/").endsWith(localModuleResourcePath)) {
				resourcePath = resourcePath + "/";
			}
			LOG.info("localModuleResourcePath: " + localModuleResourcePath);
			LOG.info("resourcePath:     " + resourcePath);
			if (resourcePath.startsWith(localModuleResourcePath)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * checks if the given file represents the module's root folder
	 * @param file  the file to check
	 * @return <code>true</code> if the file represents the module's root folder, <code>false</code> otherwise
	 */
	public boolean isFileModuleRoot(File file) {
		if (!file.isDirectory()) {
			return false;
		}
		String filePath = PluginTools.ensureUnixPath(file.getPath());
		return filePath.equals(moduleBasePath);
	}

	/**
	 * checks if the given path is the module's root folder
	 *
	 * @param path the path to check
	 * @return <code>true</code> if the path is the module's root folder, <code>false</code> otherwise
	 */
	public boolean isPathModuleRoot(String path) {
		if (StringUtils.isBlank(path)) {
			return false;
		}
		// RTASK: check for trailing slashes on either path. How is moduleBasePath stored? How should it be stored?
		return path.equals(moduleBasePath);
	}

	/**
	 * checks if the given file is placed under the local VFS root
	 * @param file  the file to check
	 * @return <code>true</code> if the file is contained in the local VFS root, <code>false</code> otherwise
	 */
	public boolean isFileInVFSPath(File file) {
		if (VfsFileAnalyzer.fileOrPathIsIgnored(plugin.getPluginConfiguration(), file)) {
			return false;
		}
		String filePath = PluginTools.ensureUnixPath(file.getPath());
		return filePath.startsWith(localVfsRoot);
	}

	/**
	 * checks if the given path is under the local VFS root
	 *
	 * @param path the path to check
	 * @return <code>true</code> if the path is contained in the local VFS root, <code>false</code> otherwise
	 */
	public boolean isPathInVFSPath(String path) {
		String filename = StringUtils.substringAfterLast(path, "/");
		if (VfsFileAnalyzer.fileOrPathIsIgnored(plugin.getPluginConfiguration(), path, filename)) {
			return false;
		}
		return path.startsWith(localVfsRoot);
	}


	/**
	 * @param file the file for which the VFS is to be retrieved
	 * @return the VFS relative path for the given file
	 */
	public String getVfsPathForFile(final File file) {
		String filepath = PluginTools.ensureUnixPath(file.getPath());
		return getVfsPathForRealPath(filepath);
	}

	/**
	 * @param path a local root path
	 * @return the VFS relative path for the given local path
	 */
	public String getVfsPathForRealPath(final String path) {
		String relativeName;
		try {
			relativeName = path.substring(localVfsRoot.length());
		}
		catch (StringIndexOutOfBoundsException e) {
			LOG.warn("There was a problem loading the VFS path for the path " + path + " - localVfsRoot: " + localVfsRoot);
			relativeName = "/";
		}
		if (relativeName.length() == 0) {
			relativeName = "/";
		}
		return PluginTools.addVfsSiteRootToLocalPath(this, relativeName);
	}

	/**
	 * @return the path of the newest module zip in the target folder, null if no module zip exists
	 */
	public String findNewestModuleZipPath() {
		String zipParentPath = getModuleBasePath() + "/" + plugin.getPluginConfiguration().getModuleZipTargetFolderPath();
		Collection<File> moduleZips = FileUtils.listFiles(new File(zipParentPath), new String[]{"zip"}, false);
		File newestModuleZip = null;
		for (File moduleZip : moduleZips) {
			if (newestModuleZip == null || FileUtils.isFileNewer(moduleZip, newestModuleZip)) {
				newestModuleZip = moduleZip;
			}
		}
		if (newestModuleZip != null) {
			return newestModuleZip.getPath();
		}
		else {
			return null;
		}
	}


	/**
	 * handles changes to the module configuration, refreshes export points and module resources
	 * @param changeType    the type of the changed OpenCms configuration (right now only MODULECONFIGURATION)
	 */
	@Override
	public void handleOpenCmsConfigurationChange(OpenCmsConfiguration.ConfigurationChangeType changeType) {
		if (changeType == OpenCmsConfiguration.ConfigurationChangeType.MODULECONFIGURATION) {
			exportPoints = openCmsConfig.getExportPointsForModule(moduleName);
			moduleResources = openCmsConfig.getModuleResourcesForModule(moduleName);
		}
	}

} public class OpenCmsModuleExportPoint {

	private String vfsSource;
	private String rfsTarget;

	/**
	 * Creates a new module export point
	 * @param vfsSource the VFS resource path for this export point
	 * @param rfsTarget the target path in the real file system, relative to the webapp
	 */
	public OpenCmsModuleExportPoint(String vfsSource, String rfsTarget) {
		this.vfsSource = vfsSource;
		this.rfsTarget = rfsTarget;
	}

	/**
	 * Returns the VFS resource path for this export point
	 * @return  the VFS resource path for this export point
	 */
	public String getVfsSource() {
		return vfsSource;
	}

	/**
	 * Returns the path in the real file system, relative to the webapp
	 * @return  the target path in the real file system, relative to the webapp
	 */
	public String getRfsTarget() {
		return rfsTarget;
	}


	/**
	 * returns the export path for  the given resource relative to the webapp.
	 * @param resourcePath  path to the resource within the export point
	 * @return  export path for the resource relative to the webapp
	 */
	public String getTargetPathForVfsResource(String resourcePath) {
		if (!resourcePath.startsWith(vfsSource)) {
			return null;
		}
		return rfsTarget + resourcePath.substring(vfsSource.length());
	}
} public class OpenCmsModules {

	private static final Logger LOG = Logger.getInstance(OpenCmsModules.class);

	OpenCmsPlugin plugin;
	List<OpenCmsModuleExportPoint> allExportPoints;

	private Map<String, OpenCmsModule> openCmsModuleMap = new LinkedHashMap<String, OpenCmsModule>();

	/**
	 * Creates a new OpenCmsModules repository
	 * @param plugin  the current plugin instance
	 */
	public OpenCmsModules(OpenCmsPlugin plugin) {
		this.plugin = plugin;
	}

	/**
	 * registers the module with the given base path
	 * @param moduleBasePath the module's base path
	 * @param moduleConfig   the configuration data from the module's configuration dialog
	 */
	public void registerModule(String moduleBasePath, OpenCmsModuleConfigurationData moduleConfig) {
		LOG.info("registering module: " + moduleBasePath);
		moduleBasePath = PluginTools.ensureUnixPath(moduleBasePath);
		allExportPoints = null;
		if (!moduleConfig.isOpenCmsModuleEnabled()) {
			return;
		}
		OpenCmsModule ocmsModule;
		if (openCmsModuleMap.containsKey(moduleBasePath)) {
			ocmsModule = openCmsModuleMap.get(moduleBasePath);
			ocmsModule.refresh(moduleConfig);
		}
		else {
			ocmsModule = new OpenCmsModule(plugin, moduleBasePath);
			ocmsModule.init(moduleConfig);
			openCmsModuleMap.put(moduleBasePath, ocmsModule);
		}
	}

	/**
	 * removes the OpenCms module linked to the IntelliJ module with the given base path from the repository
	 * @param moduleBasePath the IntelliJ module's basePath
	 */
	public void unregisterModule(String moduleBasePath) {
		LOG.info("unregistering module: " + moduleBasePath);
		allExportPoints = null;
		openCmsModuleMap.remove(moduleBasePath);
	}

	/**
	 * @return a Collection of all modules in the repository
	 */
	public Collection<OpenCmsModule> getAllModules() {
		return openCmsModuleMap.values();
	}

	/*
		Compare paths ending with a / to avoid matching on another
		module that begins with the same prefix.
		For example, consider this ModuleMap:
		- a.b.c.a
		- a.b.c
		- a.b.c.d
		Without the / appending, a request for a.b.c.d/src/main/vfs/example.txt
		would yield the module a.b.c responsible.
		 */

	/**
	 * get the module owning a specific File
	 * @param file
	 * @return
	 */
	public OpenCmsModule getModuleForFile(File file) {
		if (file == null) {
			return null;
		}
		String filePath = PluginTools.ensureUnixPath(file.getPath());
		if(file.isDirectory() && filePath.substring(filePath.length() - 1) != "/") {
			filePath = filePath + "/";
		}
		LOG.info("getModuleForFile(file="+filePath+")");
		for (String basePath : openCmsModuleMap.keySet()) {
			String compareBasePath = basePath;
			if(compareBasePath.substring(compareBasePath.length() - 1) != "/") {
				compareBasePath = compareBasePath + "/";
			}
			LOG.info("Comparing " + filePath + " against "+compareBasePath);
			if (filePath.startsWith(compareBasePath)) {
				LOG.info("Match");
				return openCmsModuleMap.get(basePath); //use basePath here as the module registration may or may not have been with a trailing /
			}
		}
		LOG.warn("No matching module could be found for " + filePath);
		return null;
	}

	/**
	 * @param path any path inside the module (local root path)
	 * @return the module containing the path
	 */
	public OpenCmsModule getModuleForPath(String path) {
		if (path == null) {
			LOG.error("getModuleForPath called with path = null");
			return null;
		}
		File fsFile = new File(path);
		if(!fsFile.exists()) {
			LOG.error("getModuleForPath(" + path + ") called on a non-existent file");
			return null;
		}
		if(fsFile.isDirectory() && path.substring(path.length() - 1) != "/") {
			path = path + "/";
		}
		LOG.info("getModuleForPath(path=" + path + ")");
		for (String basePath : openCmsModuleMap.keySet()) {
			String compareBasePath = basePath;
			if(compareBasePath.substring(compareBasePath.length() - 1) != "/") {
				compareBasePath = compareBasePath + "/";
			}
			LOG.info("Comparing " + path + " against "+compareBasePath);
			if (path.startsWith(compareBasePath)) {
				LOG.info("Match");
				return openCmsModuleMap.get(basePath); //use basePath here as the module registration may or may not have been with a trailing /
			}
		}
		LOG.warn("No matching module could be found for " + path);
		return null;
	}

	/**
	 * @param moduleBasePath the modules base path
	 * @return the module linked to the base path
	 */
	public OpenCmsModule getModuleForBasePath(String moduleBasePath) {
		if (moduleBasePath == null) {
			return null;
		}
		return openCmsModuleMap.get(moduleBasePath);
	}

	/**
	 * @param file  the file to check
	 * @return <code>true</code> if the file is contained in an OpenCms module, <code>false</code> otherwise
	 */
	public boolean isFileOpenCmsModuleResource(final File file) {
		OpenCmsModule ocmsModule = getModuleForFile(file);
		if (ocmsModule == null) {
			LOG.info("No module configured for the file " + PluginTools.ensureUnixPath(file.getPath()));
			return false;
		}
		return ocmsModule.isFileModuleResource(file);
	}

	/**
	 * @param resourcePath path pf the resource to check
	 * @return the export point for the given resource path, <code>null</code> if no export point is configured for
	 *         the resource
	 */
	public OpenCmsModuleExportPoint getExportPointForVfsResource(String resourcePath) {
		for (OpenCmsModuleExportPoint exportPoint : getAllExportPoints()) {
			if (resourcePath.startsWith(exportPoint.getVfsSource())) {
				return exportPoint;
			}
		}
		return null;
	}

	/**
	 * @return List of all export points defined by all the modules in the repository
	 */
	public List<OpenCmsModuleExportPoint> getAllExportPoints() {
		if (allExportPoints == null) {
			allExportPoints = new ArrayList<OpenCmsModuleExportPoint>();
			for (OpenCmsModule ocmsModule : getAllModules()) {
				List<OpenCmsModuleExportPoint> exportPoints = ocmsModule.getExportPoints();
				if (exportPoints != null) {
					for (OpenCmsModuleExportPoint exportPoint : exportPoints) {
						allExportPoints.add(exportPoint);
					}
				}
			}
		}
		return allExportPoints;
	}

	/**
	 * refreshes all modules (e.g. after configuration changes)
	 */
	public void refreshAllModules() {
		for (OpenCmsModule ocmsModule : getAllModules()) {
			ocmsModule.refresh();
		}
		allExportPoints = null;
	}
} public class SyncJob implements Runnable {

	private static final Logger LOG = Logger.getInstance(SyncJob.class);
	public static final String ERROR_PREFIX = "ERROR: ";

	private static final String CLASSES_PATH = "WEB-INF/classes";
	private static final String IDE_CONNECTOR_PACKAGE = "com.mediaworx.opencms.ideconnector";
	private static final Set<String> IDE_CONNECTOR_PARENT_PATHS = new HashSet<String>();
	static {
		String[] ideConnectorPathFolders = IDE_CONNECTOR_PACKAGE.split("\\.");
		StringBuilder ideConnectorParentPath = new StringBuilder(CLASSES_PATH);
		IDE_CONNECTOR_PARENT_PATHS.add(ideConnectorParentPath.toString());
		for (String ideConnectorPathFolder : ideConnectorPathFolders) {
			ideConnectorParentPath.append("/").append(ideConnectorPathFolder);
			IDE_CONNECTOR_PARENT_PATHS.add(ideConnectorParentPath.toString());
		}
	}

	private OpenCmsPlugin plugin;
	private OpenCmsToolWindowConsole console;
	private OpenCmsPluginConfigurationData config;
	private VfsAdapter adapter;

	private SyncList syncList;
	private boolean pullMetadataOnly;
	private List<SyncEntity> refreshEntityList;
	private List<ExportEntity> exportList;
	private List<String> publishList;
	private boolean publish;

	/**
	 * Creates a new SyncJob for the given SyncList
	 * @param plugin   the current plugin instance
	 * @param syncList list of SyncEntities to be synced by this SyncJob
	 */
	public SyncJob(OpenCmsPlugin plugin, SyncList syncList) {
		this.plugin = plugin;
		config = plugin.getPluginConfiguration();
		publish = config.isPluginConnectorEnabled() && config.getAutoPublishMode() == AutoPublishMode.ALL;
		adapter = plugin.getVfsAdapter();
		this.refreshEntityList = new ArrayList<SyncEntity>();
		this.exportList = new ArrayList<ExportEntity>();
		this.publishList = new ArrayList<String>();
		setSyncList(syncList);
	}

	/**
	 * Starts the SyncJob (done as a separate thread)
	 */
	public void run() {
		console = plugin.getConsole();

		int step = 1;
		int numSteps = 1;
		if (config.isPluginConnectorEnabled() && config.isPullMetadataEnabled()) {
			numSteps += 1;
		}
		if (syncList.isSyncModuleMetaData()) {
			numSteps += 2;
		}
		if (!pullMetadataOnly && publish) {
			numSteps += 1;
		}
		if (!pullMetadataOnly && numExportEntities() > 0) {
			numSteps += 1;
		}


		// ######## SYNC FILES / FOLDERS ################################
		if (!pullMetadataOnly) {
			console.info("Step " + (step++) + "/" + numSteps + ": Syncing files and folders");
			for (SyncEntity entity : syncList) {
				doSync(entity);
			}
			console.info("---- Sync finished ----\n");
		}
		// ######## OR CLEAN UP META DATA FOLDERS ################################
		else {
			console.info("Step " + (step++) + "/" + numSteps + ": Clean meta data folders for affected modules");
			for (OpenCmsModule ocmsModule : syncList.getOcmsModules()) {
				console.info("Cleaning meta data folder for " + ocmsModule.getModuleName());
				cleanupModuleMetaFolder(ocmsModule);
			}
		}

		if (syncList.isSyncModuleMetaData()) {
			// ######## PULL MODULE MANIFESTS ################################
			console.info("Step " + (step++) + "/" + numSteps + ": Pull module manifests");
			pullModuleManifests();
			console.info("---- Module manifest pull finished ----\n");

			// ######## PULL META INFOS FOR MODULE RESOURCE PARENTS ################################
			console.info("Step " + (step++) + "/" + numSteps + ": Pulling resource meta data for module resource path ancestors from OpenCms");
			pullModuleResourcePathAncestorMetaInfos();
			console.info("---- Pull of meta data for module resource path ancestors finished ----\n");
		}

		// ######## PULL RESOURCE VFS META INFORMATION ################################
		if (config.isPluginConnectorEnabled() && config.isPullMetadataEnabled()) {
			console.info("Step " + (step++) + "/" + numSteps + ": Pulling resource meta data from OpenCms");
			pullResourceMetaInfos();
			console.info("---- Resource meta info pull finished ----\n");
		}

		if (!pullMetadataOnly) {
			// ######## PUBLISHING ###########################################
			if (publish) {
				console.info("Step " + step + "/" + numSteps + ": Publishing");

				if (publishList.size() > 0) {
					try {
						plugin.getPluginConnector().publishResources(publishList, false);
						console.info("A direct publish session was started successfully");
					}
					catch (OpenCmsConnectorException e) {
						console.error(e.getMessage());
					}
					catch (IOException e) {
						LOG.warn("There was an exception while publishing resources after sync", e);
						console.error("There was an exception while publishing resources after syncing. Is OpenCms running? Please have a look at the OpenCms log file and/or the IntelliJ log file.");
					}
				}
				else {
					console.info("No resources need publishing");
				}

				console.info("---- Publish finished ----\n");
			}

			// ######## EXPORT POINT HANDLING ################################
			if (numExportEntities() > 0) {
				console.info("Step " + step + "/" + numSteps + ": Handling export points");

				for (ExportEntity entity : exportList) {
					doExportPointHandling(entity);
				}
				console.info("---- Copying of ExportPoints finished ----\n");
			}
		}

		// ######## REFRESH IDEA FILESYSTEM ##############################
		if (hasRefreshEntities()) {
			List<SyncEntity> pullEntityList = getRefreshEntityList();
			List<File> refreshFiles = new ArrayList<File>(pullEntityList.size());

			for (SyncEntity entity : pullEntityList) {
				refreshFiles.add(entity.getFile());
			}

			try {
				LocalFileSystem.getInstance().refreshIoFiles(refreshFiles);
			}
			catch (Exception e) {
				// if there's an exception then the file was not found.
			}
		}
		console.info("#### SYNC FINISHED ####");
	}

	private void setSyncList(SyncList syncList) {
		this.syncList = syncList;
		this.pullMetadataOnly = syncList.isPullMetaDataOnly();

		if (!syncList.isPullMetaDataOnly()) {
			for (SyncEntity entity : syncList) {
				if (entity.getSyncAction() == SyncAction.PULL || entity.getSyncAction() == SyncAction.DELETE_RFS) {
					this.refreshEntityList.add(entity);
				}
				addSyncEntityToExportListIfNecessary(entity);
			}
		}
	}

	/**
	 * @return the List of SyncEntities handled by this SyncJob
	 */
	public SyncList getSyncList() {
		return syncList;
	}

	private void addSyncEntityToExportListIfNecessary(SyncEntity syncEntity) {

		// if publishing is enabled, pushed and deleted entities don't have to be handled (this is done by OpenCms)
		if (publish && (syncEntity.getSyncAction() == SyncAction.PUSH || syncEntity.getSyncAction() == SyncAction.DELETE_VFS)) {
			return;
		}

		List<OpenCmsModuleExportPoint> exportPoints = syncEntity.getOcmsModule().getExportPoints();

		if (exportPoints != null) {

			String localModuleVfsRoot = syncEntity.getOcmsModule().getLocalVfsRoot();
			String entityVfsPath = syncEntity.getVfsPath();

			for (OpenCmsModuleExportPoint exportPoint : exportPoints) {
				String vfsSource = exportPoint.getVfsSource();
				if (entityVfsPath.startsWith(vfsSource)) {
					String destination = exportPoint.getRfsTarget();
					String relativePath = entityVfsPath.substring(vfsSource.length());
					ExportEntity exportEntity = new ExportEntity();
					exportEntity.setSourcePath(localModuleVfsRoot+entityVfsPath);
					exportEntity.setTargetPath(config.getWebappRoot() + "/" + destination + relativePath);
					exportEntity.setVfsPath(entityVfsPath);
					exportEntity.setDestination(destination);
					exportEntity.setToBeDeleted(syncEntity.getSyncAction().isDeleteAction());
					addExportEntity(exportEntity);
				}
			}
		}
	}

	private void addExportEntity(ExportEntity entity) {
		exportList.add(entity);
	}

	private List<SyncEntity> getRefreshEntityList() {
		return refreshEntityList;
	}

	private int getNumRefreshEntities() {
		return refreshEntityList.size();
	}

	private boolean hasRefreshEntities() {
		return getNumRefreshEntities() > 0;
	}

	private int numExportEntities() {
		return exportList.size();
	}


	private void doSync(SyncEntity entity) {
		if (entity.getSyncAction() == SyncAction.PUSH) {
			doPush(entity);
			if (publish) {
				publishList.add(entity.getVfsPath());
			}
		}
		else if (entity.getSyncAction() == SyncAction.PULL) {
			doPull(entity);
		}
		else if (entity.getSyncAction() == SyncAction.DELETE_RFS) {
			doDeleteFromRfs(entity);
		}
		else if (entity.getSyncAction() == SyncAction.DELETE_VFS) {
			doDeleteFromVfs(entity);
			if (publish) {
				publishList.add(entity.getVfsPath());
			}
		}
	}

	private void doPush(SyncEntity entity) {

		boolean success = false;
		String errormessage = null;

		if (entity.isFolder()) {
			try {
				adapter.createFolder(entity.getVfsPath());
				success = true;
			}
			catch (Exception e) {
				errormessage = "Error pushing Folder "+entity.getVfsPath()+"\n"+e.getMessage();
			}
		}
		else if (entity.isFile()) {
			try {
				adapter.pushFile(entity);
				success = true;
			}
			catch (CmsPushException e) {
				errormessage = e.getMessage();
			}
		}

		if (success) {
			StringBuilder confirmation = new StringBuilder();
			confirmation.append("PUSH: ").append(entity.getVfsPath()).append(" pushed to VFS");
			if (entity.replaceExistingEntity()) {
				confirmation.append(" replacing an existing entity");
			}
			console.info(confirmation.toString());
		}
		else {
			console.error("PUSH FAILED! " + errormessage);
		}
	}

	private void doPull(SyncEntity entity) {
		StringBuilder confirmation = new StringBuilder();

		if (entity.isFolder()) {
			try {
				FileUtils.forceMkdir(new File(entity.getRfsPath()));
			} catch (IOException e) {
				console.error("ERROR: couldn't create local directory " + entity.getRfsPath());
				LOG.warn("There was an Exception creating a local directory", e);
		   }
		}
		else {
			adapter.pullFile(entity);
		}

		confirmation.append("PULL: ").append(entity.getVfsPath()).append(" pulled from VFS to ").append(entity.getOcmsModule().getLocalVfsRoot());
		if (entity.replaceExistingEntity()) {
			confirmation.append(" replacing an existing entity");
		}

		console.info(confirmation.toString());
	}

	private void doDeleteFromRfs(SyncEntity entity) {
		StringBuilder confirmation = new StringBuilder("DELETE ").append(entity.getVfsPath()).append(" from ").append(entity.getOcmsModule().getLocalVfsRoot()).append(" (not in the VFS) - ");
		File rfsFile = entity.getFile();
		if (FileUtils.deleteQuietly(rfsFile)) {
			confirmation.append(" SUCCESS");
			console.info(confirmation.toString());
		}
		else {
			confirmation.insert(0, "ERROR: ");
			confirmation.append(" FAILED!");
			console.error(confirmation.toString());
		}
	}

	private void doDeleteFromVfs(SyncEntity entity) {
		StringBuilder confirmation = new StringBuilder("DELETE ").append(entity.getVfsPath()).append(" (not in the RFS) - ");
		if (adapter.deleteResource(entity.getVfsPath())) {
			confirmation.append(" SUCCESS");
			console.info(confirmation.toString());
		}
		else {
			confirmation.insert(0, "ERROR: ");
			confirmation.append(" FAILED!");
			console.error(confirmation.toString());
		}
	}

	private void pullResourceMetaInfos() {
		// build lists with all resources for which meta information is to be pulled / deleted
		ArrayList<String> pullEntityList = new ArrayList<String>();
		for (SyncEntity entity : syncList) {
			if (!entity.getSyncAction().isDeleteAction()) {
				pullEntityList.add(entity.getVfsPath());
			}
		}

		try {
			HashMap<String, String> metaInfos = plugin.getPluginConnector().getResourceInfos(pullEntityList);
			for (SyncEntity entity : syncList) {
				doMetaInfoHandling(console, metaInfos, entity);
			}
		}
		catch (OpenCmsConnectorException e) {
			console.error(e.getMessage());
		}
		catch (IOException e) {
			console.error("There was an error retrieving resource meta infos from OpenCms");
			LOG.warn("IOException while trying to retrieve meta infos", e);
		}
	}

	private void doMetaInfoHandling(OpenCmsToolWindowConsole console, Map<String,String> metaInfos, SyncEntity entity) {
		String metaInfoFilePath = entity.getMetaInfoFilePath();
		File metaInfoFile = new File(metaInfoFilePath);

		if (entity.getSyncAction().isDeleteAction()) {
			FileUtils.deleteQuietly(metaInfoFile);
			console.info("DELETE: " + metaInfoFilePath);
			return;
		}

		if (metaInfos.containsKey(entity.getVfsPath())) {
			if (entity instanceof SyncFolder) {
				String metaFolderPath = ((SyncFolder)entity).getMetaInfoFolderPath();
				File metaFolder = new File(metaFolderPath);
				if (!metaFolder.exists()) {
					try {
						FileUtils.forceMkdir(metaFolder);
					}
					catch (IOException e) {
						String message = "ERROR: cant create meta info directory " + metaFolderPath;
						console.error(message);
						LOG.warn(message, e);
						return;
					}
				}
			}
			try {
				String metaInfoStr = PluginTools.ensureUnixNewline(metaInfos.get(entity.getVfsPath())) + "\n";
				FileUtils.writeStringToFile(metaInfoFile, metaInfoStr, "UTF-8");
			}
			catch (IOException e) {
				String message = "ERROR: cant create meta info file " + metaInfoFilePath;
				console.error(message);
				LOG.warn(message, e);
				return;
			}

		}
		else {
			String message = entity.getVfsPath() + " not found in meta info map.";
			console.error(message);
			return;
		}
		console.info("PULL: Meta info file pulled: " + metaInfoFilePath);
	}

	private void pullModuleResourcePathAncestorMetaInfos() {
		List<OpenCmsModuleResource> resourcePathParents = new ArrayList<OpenCmsModuleResource>();
		for (OpenCmsModule ocmsModule : syncList.getOcmsModules()) {
			Set<String> handledParents = new HashSet<String>();
			for (String resourcePath : ocmsModule.getModuleResources()) {
				addParentFolderToResourcePaths(resourcePath, ocmsModule, resourcePathParents, handledParents);
			}
		}

		if (resourcePathParents.size() > 0) {
			try {
				Map<String,String> resourceInfos = plugin.getPluginConnector().getModuleResourceInfos(resourcePathParents);

				for (OpenCmsModuleResource resourceParent : resourcePathParents) {
					SyncFolder syncFolder = new SyncFolder(resourceParent.getOpenCmsModule(), resourceParent.getResourcePath(), null, null, SyncAction.PULL, false);
					doMetaInfoHandling(console, resourceInfos, syncFolder);
				}
			}
			catch (OpenCmsConnectorException e) {
				console.error(e.getMessage());
			}
			catch (IOException e) {
				Messages.showDialog("There was an error pulling the meta information for module resource ancestor folders from OpenCms.\nIs the connector module installed?",
						"Error", new String[]{"Ok"}, 0, Messages.getErrorIcon());
				LOG.warn("There was an Exception pulling the meta information for module resource ancestor folders", e);
			}
		}
	}

	private void addParentFolderToResourcePaths(String resourcePath, OpenCmsModule ocmsModule,
												List<OpenCmsModuleResource> resourcePathParents, Set<String> handledParents) {
		if (resourcePath.endsWith("/")) {
			resourcePath = resourcePath.substring(0, resourcePath.length() - 1);
		}
		if (resourcePath.lastIndexOf("/") > 0) {  // > 0 is right because the "/" at position 0 has to be ignored
			String parentPath = resourcePath.substring(0, resourcePath.lastIndexOf("/"));
			if (!handledParents.contains(parentPath)) {
				resourcePathParents.add(new OpenCmsModuleResource(ocmsModule, parentPath));
				handledParents.add(parentPath);
				if (parentPath.contains("/")) {
					addParentFolderToResourcePaths(parentPath, ocmsModule, resourcePathParents, handledParents);
				}
			}
		}
	}

	private void pullModuleManifests() {

		// collect the module names in a List
		List<String> moduleNames = new ArrayList<String>(syncList.getOcmsModules().size());
		for (OpenCmsModule ocmsModule : syncList.getOcmsModules()) {
			moduleNames.add(ocmsModule.getModuleName());
		}

		if (moduleNames.size() > 0) {
			try {
				// pull the module manifests
				Map<String,String> manifestInfos = plugin.getPluginConnector().getModuleManifests(moduleNames);

				for (OpenCmsModule ocmsModule : syncList.getOcmsModules()) {
					if (manifestInfos.containsKey(ocmsModule.getModuleName())) {
						// put the manifest to a file
						String manifestPath = ocmsModule.getManifestRoot() + "/manifest_stub.xml";
						String manifest = manifestInfos.get(ocmsModule.getModuleName());
						if (ocmsModule.isSetSpecificModuleVersionEnabled() && StringUtils.isNotEmpty(ocmsModule.getModuleVersion())) {
							manifest = manifest.replaceFirst("<version>[^<]*</version>", "<version>" + Matcher.quoteReplacement(ocmsModule.getModuleVersion()) + " </version>");
						}
						manifest = PluginTools.ensureUnixNewline(manifest) + "\n";
						FileUtils.writeStringToFile(new File(manifestPath), manifest, StandardCharsets.UTF_8);
						console.info("PULL: " + manifestPath + " pulled from OpenCms");
					}
					else {
						LOG.warn("No manifest found for module " + ocmsModule.getModuleName());
					}
				}
			}
			catch (OpenCmsConnectorException e) {
				console.error(e.getMessage());
			}
			catch (IOException e) {
				Messages.showDialog("There was an error pulling the module manifest files from OpenCms.\nIs the connector module installed?",
						"Error", new String[]{"Ok"}, 0, Messages.getErrorIcon());
				LOG.warn("There was an Exception pulling the module manifests", e);
			}
		}
	}

	private static boolean isParentOfIdeConnectorPath(String path) {
		for (String ideConnectorParentPath : IDE_CONNECTOR_PARENT_PATHS) {
			if (path.endsWith(ideConnectorParentPath)) {
				return true;
			}
		}
		return false;
	}

	private void doExportPointHandling(ExportEntity entity) {
		StringBuilder confirmation = new StringBuilder();
		StringBuilder notice = new StringBuilder();

		if (!entity.isToBeDeleted()) {
			confirmation.append("Copy of ").append(entity.getVfsPath()).append(" to ").append(entity.getDestination()).append(" - ");
			File file = new File(entity.getSourcePath());
			if (file.exists()) {
				if (file.isFile()) {
					try {
						FileUtils.copyFile(file, new File(entity.getTargetPath()));
						confirmation.append("SUCCESS");
					} catch (IOException e) {
						confirmation.insert(0, ERROR_PREFIX);
						confirmation.append("FAILED (").append(e.getMessage()).append(")");
					}
				}
				else if (file.isDirectory()) {
					try {
						FileUtils.copyDirectory(file, new File(entity.getTargetPath()));
						confirmation.append("SUCCESS");
					} catch (IOException e) {
						confirmation.insert(0, ERROR_PREFIX);
						confirmation.append("FAILED (").append(e.getMessage()).append(")");
					}
				}
			}
			else {
				confirmation.append(" - FILE NOT FOUND");
			}
		}
		else {
			String targetPath = entity.getTargetPath();
			String vfsPath = entity.getVfsPath();

			deleteExportedResource(vfsPath, targetPath, confirmation, notice);
		}
		if (confirmation.indexOf(ERROR_PREFIX) > -1) {
			console.error(confirmation.toString());
		}
		else {
			console.info(confirmation.toString());
		}
		if (notice.length() > 0) {
			console.notice(notice.toString());
		}
	}

	/**
	 * Deletes a resource file that was exported via export point handling
	 * @param vfsPath       the resource's VFS path
	 * @param targetPath    the export point's target path within the WebApp
	 * @param confirmation  StringBuilder the log is added to
	 * @param notice        String builder for an additional notice that may be displayed under the log. This is used
	 *                      for a warning message if the deletion was skipper because a path containing the IDE
	 *                      connector would be deleted.
	 */
	public static void deleteExportedResource(String vfsPath, String targetPath, StringBuilder confirmation, StringBuilder notice) {
		confirmation.append("Resource ").append(vfsPath).append(" removed, deletion of exported file ")
				.append(targetPath).append(" - ");
		// check if the target path is something that shouldn't be deleted
		if (isParentOfIdeConnectorPath(targetPath)) {
			confirmation.append("SKIPPED (paths containing the IDE connector must not be deleted)");
			notice.append("Deletion of the folder ").append(targetPath).append(" was skipped. Please delete any unwanted exported sub resources manually.");
		}
		else {
			File file = new File(targetPath);
			if (file.exists()) {
				if (FileUtils.deleteQuietly(file)) {
					confirmation.append("SUCCESS");
				}
				else {
					confirmation.insert(0, ERROR_PREFIX);
					confirmation.append("FAILED");
				}
			}
			else {
				confirmation.append("NOT NECESSARY (doesn't exist)");
			}
		}
	}

	private void cleanupModuleMetaFolder(OpenCmsModule ocmsModule) {
		if (ocmsModule != null) {
			File metaFolder = new File(ocmsModule.getManifestRoot());
			if (!metaFolder.isDirectory()) {
				return;
			}
			File[] metaFiles = metaFolder.listFiles();
			if (metaFiles == null || metaFiles.length == 0) {
				return;
			}
			for (File metaFile : metaFiles) {
				FileUtils.deleteQuietly(metaFile);
			}
		}
	}

} public class VfsAdapter {

	private static final Logger LOG = Logger.getInstance(VfsAdapter.class);

	/** the CMIS session */
	private Session session;

	/** boolean flag denoting if the adapter is connected */
	private boolean connected;

	/** repository URL, for OpenCms CMIS usually "http://localhost:8080/opencms/cmisatom/cmis-offline/" */
	private String atompubUrl;

	/** OpenCms user with sufficient privileges to read/write from/to the VFS, e.g. "Admin" */
	private String user;

	/** the OpenCms user's password */
	private String password;

	/**
	 * creates a new VfsAdapter that may be connected by calling {@link #startSession()}
	 * @param atompubUrl repository URL, for OpenCms CMIS usually "http://localhost:8080/opencms/cmisatom/cmis-offline/"
	 * @param user       OpenCms user with sufficient privileges to read/write from/to the VFS, e.g. "Admin"
	 * @param password   the OpenCms user's password
	 */
	public VfsAdapter(String atompubUrl, String user, String password) {

	    if (atompubUrl == null || atompubUrl.length() == 0) {
		    throw new IllegalArgumentException("parameter atompubUrl must not be null or empty");
	    }
	    if (user == null || user.length() == 0) {
		    throw new IllegalArgumentException("parameter user must not be null or empty");
	    }
	    if (password == null || password.length() == 0) {
		    throw new IllegalArgumentException("parameter password must not be null or empty");
	    }

	    this.atompubUrl = atompubUrl;
	    this.user = user;
		this.password = password;
	}

	public void setUser(String user) {
		this.user = user;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	/**
	 * starts the CMIS session that is used to push or pull files/folders
	 */
	public void startSession() throws CmsConnectionException {

	    if (password != null && password.length() > 0) {

		    Map<String, String> sessionParams = new HashMap<String, String>();

	        // Create a SessionFactory and set up the SessionParameter map
		    SessionFactory sessionFactory = SessionFactoryImpl.newInstance();

	        // user credentials
	        sessionParams.put(SessionParameter.USER, user);
	        sessionParams.put(SessionParameter.PASSWORD, password);

	        // repository
	        sessionParams.put(SessionParameter.ATOMPUB_URL, atompubUrl);
	        sessionParams.put(SessionParameter.BINDING_TYPE, BindingType.ATOMPUB.value());

		    try {
			    // find all the repositories at this URL - there should only be one.
			    List<Repository> repositories = sessionFactory.getRepositories(sessionParams);
			    for (Repository r : repositories) {
				    LOG.info("Found repository: " + r.getName());
			    }

			    // create session with the first (and only) repository
			    Repository repository = repositories.get(0);
			    sessionParams.put(SessionParameter.REPOSITORY_ID, repository.getId());
			    sessionParams.put(SessionParameter.CONNECT_TIMEOUT, "500");

			    LOG.info("Starting CMIS session using repository " + atompubUrl);
			    this.session = sessionFactory.createSession(sessionParams);

			    if (this.session != null) {
				    connected = true;
			    }
			    else {
				    connected = false;
				    LOG.info("Error: CMIS session is null");
			    }
		    }
		    catch (Exception e) {
			    LOG.info("Exception connecting to VFS", e);
			    connected = false;
			    throw new CmsConnectionException("Connection to OpenCms VFS failed. Is OpenCms running?");
		    }
		}
	}

	/**
	 * checks if a VFS resource exists at the given path
	 * @param path  the path to be checked (full root path, e.g.
	 *              <code>/system/modules/com.mycompany.mymodule/classes/messages.properties</code>)
	 * @return  <code>true</code> if the resource exists in the VFS, <code>false</code> otherwise
	 */
	public boolean exists(String path) {
	    if (!connected) {
		    LOG.warn("not connected");
		    return false;
	    }
		if (!path.startsWith("/")) {
			return false;
		}
	    try {
	        session.getObjectByPath(path);
	        return true;
	    }
	    catch (CmisObjectNotFoundException e) {
	        return false;
	    }
	}

	/**
	 * retrieves (pulls) the VFS resource at the given path
	 * @param path  path of the resource to be pulled
	 * @return  the VFS resource
	 * @throws CmsPermissionDeniedException
	 */
	public CmisObject getVfsObject(String path) throws CmsPermissionDeniedException {
	    if (!connected) {
		    LOG.warn("not connected");
		    return null;
	    }
		path = PluginTools.ensureUnixPath(path);
	    try {
	        return session.getObjectByPath(path);
	    }
	    catch (CmisObjectNotFoundException e) {
	        return null;
	    }
	    catch (CmisPermissionDeniedException e) {
		    LOG.warn("Permission denied, can't access "+path, e);
		    throw new CmsPermissionDeniedException("Permission denied, can't access "+path, e);
	    }
		catch (CmisConnectionException e) {
			Messages.showDialog("Error connecting to the VFS" + e.getMessage() + "\nIs OpenCms running?",
					"Error", new String[]{"Ok"}, 0, Messages.getErrorIcon());
			LOG.warn("Error connecting to the VFS", e);
			connected = false;
			return null;
		}
	}

	/**
	 * retrieves a VFS folder, creating it if it doesn't exist
	 * @param path  the path of the folder to be retrieved
	 * @return  the VFS folder (may be newly created)
	 */
	private Folder getOrCreateFolder(String path) {
	    if (!connected) {
		    LOG.warn("not connected");
		    return null;
	    }

	    // check if the folder exists
	    try {
	        return (Folder)session.getObjectByPath(path);
	    }
	    // if the folder does not exist, create it
	    catch (CmisObjectNotFoundException e) {
	        String parentPath = path.substring(0, path.lastIndexOf("/"));
	        String foldername = path.substring(path.lastIndexOf("/") + 1, path.length());
	        LOG.info("creating folder "+path);
	        LOG.info("parent path "+parentPath);
	        LOG.info("foldername "+foldername);

	        Folder parent;

	        try {
	            parent = (Folder)session.getObjectByPath(parentPath);
	        }
	        catch (CmisObjectNotFoundException e2) {
	            parent = getOrCreateFolder(parentPath);
	        }

	        Map<String, String> newFolderProps = new HashMap<String, String>();
	        newFolderProps.put(PropertyIds.OBJECT_TYPE_ID, BaseTypeId.CMIS_FOLDER.value());
	        newFolderProps.put(PropertyIds.NAME, foldername);
	        return parent.createFolder(newFolderProps);
	    }
	}

	/**
	 * creates a folder in the VFS and returns it. If the folder already exists, the existing folder is returned
	 * @param path  the folder's VFS path (full root path, e.g.
	 *              <code>/system/modules/com.mycompany.mymodule/classes</code>)
	 * @return  the newly created folder (or the folder that existed previously)
	 */
	public Folder createFolder(String path) {
		return getOrCreateFolder(path);
	}

	/**
	 * pushes a file from the RFS to the VFS
	 * @param entity    the sync entity representing the file to be pushed
	 * @return  a CMIS document of the newly created VFS file
	 * @throws CmsPushException
	 */
	public Document pushFile(SyncEntity entity) throws CmsPushException {
		if (!connected) {
			LOG.info("not connected");
			return null;
		}

		File rfsFile = entity.getFile();
		FileInputStream rfsFileInputStream = null;
		Document vfsFile = null;
		long vfsFileModifiedTime = 0;

		try {
			rfsFileInputStream = new FileInputStream(rfsFile);
			String mimetype = new MimetypesFileTypeMap().getContentType(rfsFile);

			ContentStream contentStream = session.getObjectFactory().createContentStream(rfsFile.getName(),
					rfsFile.length(), mimetype, rfsFileInputStream);

			// if the file already exists in the VFS ...
			if (entity.replaceExistingEntity()) {
				// ... update its content
				vfsFile = (Document)entity.getVfsObject();
				vfsFile.setContentStream(contentStream, true, true);
			}
			// if the file doesn't exist in the VFS
			else {
				// ... get the parent folder object from the VFS
				String parentPath = entity.getVfsPath().substring(0, entity.getVfsPath().lastIndexOf("/"));
				Folder parent = getOrCreateFolder(parentPath);

				// ... and create the file as Document Object under the parent folder
				Map<String, Object> properties = new HashMap<String, Object>();
				properties.put(PropertyIds.OBJECT_TYPE_ID, BaseTypeId.CMIS_DOCUMENT.value());
				properties.put(PropertyIds.NAME, rfsFile.getName());
				vfsFile = parent.createDocument(properties, contentStream, VersioningState.NONE);
			}

			// Set file modification date in the VFS to the RFS file date
	        // This does not work when using OpenCms since OpenCms always sets the date of the CMIS change event
			// That's why the file date in the RFS has to be set to the VFS date (see finally block)
			/*
			Map<String, Object> dateProperties = new HashMap<String, Object>(1);
			GregorianCalendar modifiedGC = new GregorianCalendar();
			modifiedGC.setTime(new Date(rfsFile.lastModified()));
			dateProperties.put(PropertyIds.LAST_MODIFICATION_DATE, modifiedGC);
			vfsFile.updateProperties(dateProperties, false);
			*/

			vfsFileModifiedTime = vfsFile.getLastModificationDate().getTimeInMillis();
		}
		catch (FileNotFoundException e) {
			LOG.info("File not found.");
		}
		catch(CmisNameConstraintViolationException e) {
			throw new CmsPushException("Could not push entity "+entity.getVfsPath()+", there was a problem with the resource name.\n"+e.getMessage(), e);
		}
		catch(CmisRuntimeException e) {
			throw new CmsPushException("Could not push entity "+entity.getVfsPath()+", there may be an issue with a lock or an XML validation issue. Look at the OpenCms log file to find out what went wrong.\n"+e.getMessage(), e);
		}
		finally {
			try {
				if (rfsFileInputStream != null) {
					rfsFileInputStream.close();
				}

				if (vfsFileModifiedTime > 0) {
					// Since setting the modification Date on the VFS file ain't possible, set the date for the RFS file
					if (rfsFile.setLastModified(vfsFileModifiedTime)) {
						LOG.info("Setting lastModificationDate successful");
					}
					else {
						LOG.info("Setting lastModificationDate NOT successful");
					}
				}
			}
			catch (IOException e) {
				// do nothing
			}
		}

		return vfsFile;
	}

	/**
	 * pulls a VFS file to the RFS
	 * @param syncEntity    the sync entity representing the file to be pulled
	 */
	public void pullFile(SyncEntity syncEntity) {
	    if (!connected) {
		    LOG.info("not connected");
		    return;
	    }
	    Document document = (Document)syncEntity.getVfsObject();

	    LOG.info("Pulling "+syncEntity.getVfsPath()+" to "+syncEntity.getOcmsModule().getLocalVfsRoot());

	    InputStream is = document.getContentStream().getStream();
	    File rfsFile = createRealFile(syncEntity);
	    OutputStream os = null;
	    try {
	        os = new FileOutputStream(rfsFile);
	        byte[] buffer = new byte[4096];
	        for (int n; (n = is.read(buffer)) != -1; ) {
	            os.write(buffer, 0, n);
	        }
	    }
	    catch (IOException e) {
	        LOG.info("There was an Exception writing to the local file " + syncEntity.getRfsPath() + ": " + e + "\n" + e.getMessage());
	    }
	    finally {
	        try {
	            is.close();
	        }
	        catch (IOException e) {
	            // Do nothing
	        }
		    if (os != null) {
			    try {
				    os.close();
			    }
			    catch (IOException e) {
				    // Do nothing
			    }
		    }
	        if (!rfsFile.setLastModified(document.getLastModificationDate().getTimeInMillis())) {
		        LOG.info("there was an error setting the modification date for " + syncEntity.getRfsPath());
	        }
	    }
	}


	/**
	 * creates a local file (or folder) for this sync entitiy, if none exists
	 * @param syncEntity the SyncEntity for which the real file/folder is to be created
	 * @return the file handle for the newly created file/folder (or the file/folder that already existed)
	 */
	public File createRealFile(SyncEntity syncEntity) {
		File realFile = new File(syncEntity.getRfsPath());
		if (!realFile.exists()) {
			try {
				if (syncEntity.isFolder()) {
					if (!realFile.mkdirs()) {
						LOG.warn("The directories for " + syncEntity.getRfsPath() + " could not be created");
					}
				}
				else {
					File parentFolder = realFile.getParentFile();
					if (!parentFolder.exists()) {
						FileUtils.forceMkdir(parentFolder);
					}
					if (!realFile.createNewFile()) {
						LOG.warn("The file " + syncEntity.getRfsPath() + " could not be created");
					}
				}
			}
			catch (IOException e) {
				LOG.warn("There was an Exception creating the local file " + syncEntity.getRfsPath(), e);
			}
		}
		syncEntity.setFile(realFile);
		return realFile;
	}



	/**
	 * deletes a file or folder from the VFS
	 * @param vfsPath   the path of the resource to be deleted (full root path, e.g.
	 *              <code>/system/modules/com.mycompany.mymodule/formatters/delete_me.jsp</code>)
	 */
	public boolean deleteResource(String vfsPath) {
	    if (!connected) {
		    LOG.warn("not connected");
		    return false;
	    }
		boolean success = false;
	    CmisObject vfsFile = null;
	    try {
		    vfsFile = getVfsObject(vfsPath);
	    }
	    catch (CmsPermissionDeniedException e) {
		    LOG.warn("Can't delete " + vfsPath + ", permission denied", e);
	    }
	    if (vfsFile != null) {
		    // Folders
	        if (vfsFile instanceof Folder) {
	            LOG.info("Deleting the following folder from the VFS: "+vfsPath);
	            List<String> failedResourcePaths = ((Folder)vfsFile).deleteTree(true, UnfileObject.DELETE, true);
		        success = failedResourcePaths == null || failedResourcePaths.size() <= 0;
	        }
	        // Files
	        else {
	            LOG.info("Deleting the following file from the VFS: "+vfsPath);
	            vfsFile.delete();
		        success = true;
	        }
	    }
		return success;
	}

	/**
	 * checks if the adapter is connected (a CMIS session is active)
	 * @return  <code>true</code> id the adapter is connected, <code>false</code> otherwise
	 */
	public boolean isConnected() {
		if (!connected) {
			return false;
		}
		if (session == null) {
			return false;
		}
		Folder folder = null;
		try {
			folder = session.getRootFolder();
		}
		catch (CmisConnectionException e) {
			LOG.info("Can't read CMIS repository root folder, not connected", e);
			connected = false;
		}
		catch (CmisObjectNotFoundException e) {
			LOG.info("Can't read CMIS repository root folder, not connected", e);
			connected = false;
		}
		catch (CmisPermissionDeniedException e) {
			LOG.info("CMIS says permission denied, not connected", e);
			connected = false;
		}
	    return folder != null && connected;
	}

	/**
	 * clears the CMIS session cache
	 */
	public void clearCache() {
	    session.clear();
	}


	/**
	 * logs the CMIS capabilities, for debugging purposes
	 */
	public void logCapabilities() {
		LOG.info("Printing repository capabilities...");
		final RepositoryInfo repInfo = session.getRepositoryInfo();
		RepositoryCapabilities cap = repInfo.getCapabilities();
		LOG.info("\nNavigation Capabilities");
		LOG.info("-----------------------");
		LOG.info("Get descendants supported: " + (cap.isGetDescendantsSupported() ? "true" : "false"));
		LOG.info("Get folder tree supported: " + (cap.isGetFolderTreeSupported() ? "true" : "false"));
		LOG.info("\nObject Capabilities");
		LOG.info("-----------------------");
		LOG.info("Content Stream: " + cap.getContentStreamUpdatesCapability().value());
		LOG.info("Changes: " + cap.getChangesCapability().value());
		LOG.info("Renditions: " + cap.getRenditionsCapability().value());
		LOG.info("\nFiling Capabilities");
		LOG.info("-----------------------");
		LOG.info("Multifiling supported: " + (cap.isMultifilingSupported() ? "true" : "false"));
		LOG.info("Unfiling supported: " + (cap.isUnfilingSupported() ? "true" : "false"));
		LOG.info("Version specific filing supported: " + (cap.isVersionSpecificFilingSupported() ? "true" : "false"));
		LOG.info("\nVersioning Capabilities");
		LOG.info("-----------------------");
		LOG.info("PWC searchable: " + (cap.isPwcSearchableSupported() ? "true" : "false"));
		LOG.info("PWC updatable: " + (cap.isPwcUpdatableSupported() ? "true" : "false"));
		LOG.info("All versions searchable: " + (cap.isAllVersionsSearchableSupported() ? "true" : "false"));
		LOG.info("\nQuery Capabilities");
		LOG.info("-----------------------");
		LOG.info("Query: " + cap.getQueryCapability().value());
		LOG.info("Join: " + cap.getJoinCapability().value());
		LOG.info("\nACL Capabilities");
		LOG.info("-----------------------");
		LOG.info("ACL: " + cap.getAclCapability().value());
		LOG.info("End of  repository capabilities");
	}

	/**
	 * logs the properties of a CMIS object, for debugging purposes
	 */
	public void logCmisObjectProperties(CmisObject object) {
		List<Property<?>> properties = object.getProperties();
		for (Property<?> property : properties) {
			LOG.info("Property: "+property.getDisplayName()+" ("+property.getLocalName()+") - "+property.getValue().toString());
		}
	}
} public class OpenCmsToolWindowConsole extends JTextPane {

	private static final Logger LOG = Logger.getInstance(OpenCmsToolWindowConsole.class);

	private JScrollPane scrollPane;
	private StyledDocument consoleDocument;
	private SimpleAttributeSet errorAttributes;
	private SimpleAttributeSet noticeAttributes;

	public OpenCmsToolWindowConsole() {
		super();
		init();
	}

	public void setScrollPane(JScrollPane scrollPane) {
		this.scrollPane = scrollPane;
	}

	private void init() {
		setEditable(false);

		consoleDocument = getStyledDocument();
		errorAttributes = new SimpleAttributeSet();
		StyleConstants.setForeground(errorAttributes, Color.RED);
		StyleConstants.setBold(errorAttributes, true);

		noticeAttributes = new SimpleAttributeSet();
		StyleConstants.setForeground(noticeAttributes, Color.BLUE);
		StyleConstants.setBold(noticeAttributes, true);
	}

	public void clear() {
		setText("");
	}

	public void append(String str) {
		append(str, null);
	}

	public void info(String str) {
		append(str + "\n", null);
	}

	public void notice(String str) {
		append(str + "\n", noticeAttributes);
	}

	public void error(String str) {
		append(str + "\n", errorAttributes);
	}

	private void append(String str, AttributeSet attributeSet) {
		UIUtil.invokeLaterIfNeeded(new ConsoleAppender(str, attributeSet));
	}

	public StyledDocument getConsoleDocument() {
		return consoleDocument;
	}

	private class ConsoleAppender implements Runnable {

		String stringToAppend;
		AttributeSet attributeSet;

		private ConsoleAppender(String stringToAppend, AttributeSet attributeSet) {
			this.stringToAppend = stringToAppend;
			this.attributeSet = attributeSet;
		}

		@Override
		public void run() {
			try {
				consoleDocument.insertString(consoleDocument.getLength(), stringToAppend, attributeSet);
			}
			catch (BadLocationException e) {
				LOG.warn("Exception while appending content to the console", e);
			}
			if (scrollPane != null && scrollPane.getVerticalScrollBar() != null) {
				JScrollBar scrollbar = scrollPane.getVerticalScrollBar();
				scrollPane.getVerticalScrollBar().setValue(scrollbar.getMaximum() - scrollbar.getVisibleAmount());
			}
		}

	}
}
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.mediaworx.intellij.opencmsplugin.OpenCmsPlugin;
import com.mediaworx.intellij.opencmsplugin.configuration.OpenCmsPluginConfigurationData;
import com.mediaworx.intellij.opencmsplugin.connector.AutoPublishMode;
import com.mediaworx.intellij.opencmsplugin.exceptions.CmsConnectionException;
import com.mediaworx.intellij.opencmsplugin.exceptions.CmsPermissionDeniedException;
import com.mediaworx.intellij.opencmsplugin.exceptions.OpenCmsConnectorException;
import com.mediaworx.intellij.opencmsplugin.opencms.OpenCmsModule;
import com.mediaworx.intellij.opencmsplugin.opencms.OpenCmsModuleExportPoint;
import com.mediaworx.intellij.opencmsplugin.opencms.OpenCmsModules;
import com.mediaworx.intellij.opencmsplugin.sync.SyncJob;
import com.mediaworx.intellij.opencmsplugin.sync.VfsAdapter;
import com.mediaworx.intellij.opencmsplugin.toolwindow.OpenCmsToolWindowConsole;
import com.mediaworx.opencms.moduleutils.manifestgenerator.OpenCmsModuleManifestGenerator;
import org.apache.chemistry.opencmis.client.api.CmisObject;
import org.apache.chemistry.opencmis.client.api.FileableCmisObject;
import org.apache.chemistry.opencmis.client.api.Folder;
import org.apache.chemistry.opencmis.commons.PropertyIds;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.*;
/*
 * 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.listeners;



/**
 * Handles changes collected by the {@link OpenCmsModuleFileChangeListener} and asks the user if those changes should
 * be reflected in the OpenCms VFS. Implements runnable so that an instance can be used in
 * <code>ApplicationManager.getApplication().invokeLater()</code> to avoid a crash that was introduced in IntelliJ in
 * late 2016, see
 * <a href="https://github.com/JetBrains/intellij-community/commit/905e2faabc568831fe6596b8a2991c96f095f255">
 *     https://github.com/JetBrains/intellij-community/commit/905e2faabc568831fe6596b8a2991c96f095f255
 * </a> (presenting dialogs in file write/change events, possible before this commit, was now prohibited)
 *
 * @author initial author: Kai Widmann - widmann@mediaworx.com, 20.11.17
 */
public class OpenCmsModuleFileChangeHandler implements Runnable {

	private static final Logger LOG = Logger.getInstance(OpenCmsModuleFileChangeHandler.class);

	private OpenCmsPlugin plugin;
	private OpenCmsPluginConfigurationData config;
	private OpenCmsModules openCmsModules;
	private OpenCmsToolWindowConsole console;

	private VfsAdapter vfsAdapter;
	private List<VfsFileDeleteInfo> vfsFilesToBeDeleted;
	private List<VfsFileMoveInfo> vfsFilesToBeMoved;
	private List<VfsFileRenameInfo> vfsFilesToBeRenamed;
	private List<File> refreshFiles;

	private boolean refreshOpenCmsModuleConfiguration = false;

	/**
	 * Handler used to reflect file changes like deletes, moves and renames made in the RFS in the OpenCms VFS (after
	 * asking the user)
	 * @param plugin the OpenCms plugin instance
	 */
	OpenCmsModuleFileChangeHandler(OpenCmsPlugin plugin) {
		this.plugin = plugin;
		config = plugin.getPluginConfiguration();
		openCmsModules = plugin.getOpenCmsModules();
		console = plugin.getConsole();

		vfsFilesToBeDeleted = new ArrayList<VfsFileDeleteInfo>();
		vfsFilesToBeMoved = new ArrayList<VfsFileMoveInfo>();
		vfsFilesToBeRenamed = new ArrayList<VfsFileRenameInfo>();
		refreshFiles = new ArrayList<File>();
	}

	@Override
	public void run() {
		handleChanges();
	}

	/**
	 * Handles all the changes collected previously and is also used  to present a dialog asking the user if the file
	 * change should be reflected in the OpenCms VFS as well.
	 */
	void handleChanges() {
		try {
			if (getNumAffected() > 0) {
				boolean isPublishRequired = handleAffectedFiles();

				// Publish the affected VFS resources (if publish is enabled)
				if (isPublishRequired && config.isPluginConnectorEnabled() && config.getAutoPublishMode() != AutoPublishMode.OFF) {