Automatically importing preferences and projects into Eclipse workspaces

18 July 2018 Author: Erik Lievaart In this article I will describe how one can setup an Eclipse workspace without manual steps.

Eclipse does not have global configuration. It never has, and it seems like it never will. It is probably the number one reason why people switch to IntelliJ, but this does not seem to impress the developers of Eclipse. Eclipse does have various export functions, but the programmer is expected to manually click through menus to import the configuration. Eclipse does not have an automatic mechanism for importing projects into a workspace, well it sort of does, we'll get there. But lets go over the alternatives for importing preferences first, you may like them better than my solution.

Preference import alternatives

So people have invented numerous workarounds in order to not have to spend their day clicking through the same menus time and time again.

Here are a couple of alternatives:

What can I say about IntelliJ? It has global properties and tackles the problem properly. If I liked the way IntelliJ works, I would switch. Unfortunately, I do not.

The most common approach seems to be copying the .metadata folder. This is a hidden folder in the workspace that contains all of the configuration of the workspace. The advantages: it is simple, requires no special tooling, has a great coverage of settings. Disadvantages: no versioning, not guaranteed to be stable, suffers from bloating problems. Once upon a time I ran out of disk space and found gigabytes and gigabytes of data in my workspaces. I had a maven plugin gone rogue, storing gigabytes of data. Every time I created a new workspace the data was copied. Some people are smarter about it and only copy the workspace.xmi file (window layout) and the JDT settings.

If you are looking for a high tech solution, then workspace mechanic might be for you: https://marketplace.eclipse.org/content/workspace-mechanic
This no longer maintained plugin automatically synchronizes settings across workspaces. Somewhat of an overly complicated tool for my needs, but some love it.

Writing a plugin that imports preferences is incredibly simple and I will show you how in the next chapter.

writing a plugin to import preferences

So, in order to do plugin development in eclipse, you will want to have eclipse PDE installed. Either install PDE using the marketplace or when you download eclipse, go to the PDE project and get the eclipse download on the project site.

Plugin hello world

I was exploring writing a custom plugin as one of the options and I was pleasantly surprised. Creating a hello world plugin is trivial with the PDE suite. Eclipse really got this right. Simply create a new plugin project: Fill in the wizard and on the last (template) screen choose Hello, World Command: (note: this is just for demo purposes, the import plugin does not actually need any of the templates) Running a custom plugin is as simple as selecting a project and selecting run as > Eclipse Application from the context menu: This starts a new eclipse instance, which works in a separate workspace and has the custom plugin installed. At this point, you will have an extra menu item and an icon in the toolbar that both show a popup with hello world in it. But back to the preferences.

importing preferences

So Eclipse plugins generally have a plugin.xml file in the root, which declares integration with the Eclipse platform. If you double click the file, Eclipse will open the plugin wizard, which makes it a bit easier to edit the plugin.xml file. You can still view the contents of the plugin.xml file by selecting the plugin.xml tab. If you installed the hello world template, then this file contains a blurb of xml registering the menu item and button with Eclipse. Otherwise, this file will be mostly empty.

Unless you want to activate the functionality through the menu or a button, this is all the code required in the plugin.xml file: plugin.xml

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension point="org.eclipse.ui.startup">
      <startup class="com.example.Startup">
      </startup>
   </extension>
</plugin>
This snippet declares a class that runs when the workbench is started. The startup class has to exist and has to implement org.eclipse.ui.IStartup. But first, let's verify that all the relevant dependencies are registered. Under the dependencies tab, please ensure the following plugins are listed: This will add entries to the manifest that ensures the classes of that plugin are made available to ours. If you need to interact with other plugins always ensure they are listed in the dependencies. Otherwise the code will not compile, even if it is correct. Let's test our code with a dummy implementation of the startup handler: HelloStartup.java
package com.example;

public class Startup implements org.eclipse.ui.IStartup {
	@Override
	public void earlyStartup() {
		javax.swing.JOptionPane.showMessageDialog(null, "hello from startup!");
	}
}
If you are getting errors on the singleton directive, simply select the error, press ctrl + 1 and apply the quick fix. You should be able to run the plugin now: Now, I don't necessarily recommend combining SWT with Swing, but this is just for test purposes. All that's left, is for us to import the preferences. Let's add a class that does the job: EclipsePreferences.java
package com.example;

import java.io.InputStream;
import java.util.Map;

import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.preferences.ConfigurationScope;
import org.eclipse.core.runtime.preferences.IExportedPreferences;
import org.eclipse.core.runtime.preferences.IPreferenceFilter;
import org.eclipse.core.runtime.preferences.IPreferencesService;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.core.runtime.preferences.PreferenceFilterEntry;
import org.eclipse.ui.PlatformUI;

public class EclipsePreferences {
    public static void init(InputStream is) throws Exception {
        IPreferencesService service = Platform.getPreferencesService();
        IExportedPreferences prefs = service.readPreferences(is);
        IPreferenceFilter filter = new IPreferenceFilter() {
            @Override
            public String[] getScopes() {
                return new String[] { InstanceScope.SCOPE, ConfigurationScope.SCOPE };
            }
            @Override
            public Map<String, PreferenceFilterEntry[]> getMapping(String scope) {
                return null;
            }
        };
        PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
            try {
                service.applyPreferences(prefs, new IPreferenceFilter[] { filter });
                ResourcesPlugin.getWorkspace().build(IncrementalProjectBuilder.CLEAN_BUILD, null);
            } catch (CoreException e) {
                throw new RuntimeException(e);
            }
        });
    }
}
So, the class gets the PreferenceService, uses it to read an .epf file from disk. Next it applies all the preferences and initiates a clean build. The clean build is not strictly required, but if you change, say, compiler warnings, then the build is required to actualize the problem view. You may have noticed the line EclipsePreferences.java [32:32]
        PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
This ensures everything between the braces is invoked in the UI thread. Traditionally UI frameworks have used single threaded models, where only one thread is allowed to update the UI. This is a convenient way of avoiding many complicated threading issues. Some preferences may impact the UI and I was getting errors without this fix. Now all you have to do is call the EclipsePreferences.init(InputStream) method from the earlyStartup(). I will leave this as an exercise for you, since it really depends on where you want to grab the preferences from. You can grab them from a file or even a URL if you put your defaults on a web site. And with that, you have completed your own plugin that automatically installs your preferences on startup.

There is one extra check I do in my own initialization plugin:
https://github.com/eriklievaart/eclipseinit
I only install the preferences if there aren't any projects in the workspace. This way my workspace is initialized with all my defaults, but I can still make changes that don't get overwritten. In my case, it is always safe to overwrite the preferences if there are no projects in the workspace. Here is how to get the projects:

ResourcesPlugin.getWorkspace().getRoot().getProjects()

Lastly, sometimes you want to add or remove a single preference and you do not want to export the whole file. Similarly, Eclipse may not export a preference you are interested in for whatever reason. Removing preferences is easy, since the .epf file is just a text based file. Adding preferences is similarly easy, but you need to know the names and values required by Eclipse. For this, we can install an IPreferenceChangeListener. This listener will notify you on preference changes. Unfortunately, it only works for existing preferences. When a new preference is added a new node is created. So we also need to add an INodeChangeListener that adds a IPreferenceChangeListener when a node is added. Here is the relevant snippet:

IPreferenceChangeListener listener = new IPreferenceChangeListener() {
    public void preferenceChange(PreferenceChangeEvent e) {
        System.out.println(e.getNode() + "/" + e.getKey() + "=" + e.getNewValue());
    }
};
IPreferencesService prefsService = Platform.getPreferencesService();
IEclipsePreferences root = prefsService.getRootNode();

IPreferenceNodeVisitor addingVisitor = new IPreferenceNodeVisitor() {
    public boolean visit(IEclipsePreferences node) {
        if (listener != null) {
            node.addPreferenceChangeListener(listener);
            node.addNodeChangeListener(new INodeChangeListener() {
                public void removed(NodeChangeEvent event) {
                }
                public void added(NodeChangeEvent e) {
                    IEclipsePreferences node = (IEclipsePreferences) e.getChild();
                    node.addPreferenceChangeListener(listener);
                }
            });
        }
        return true;
    }
};

try {
    root.accept(addingVisitor);
} catch (BackingStoreException e) {
    e.printStackTrace();
}
So, that completes the guide to importing preferences into an empty workspace. Please note that window positioning is not stored in preferences and is a completely different beast. I do not have a clean solution for that and the best solution I know is copying the workbench.xmi from the .metadata folder. I will discuss the problems with configuring window positions at the end of this blog. But there are still things we need to automate, like installing Eclipse and importing projects. I will start with importing projects, because there is the most value to be had there.

Automatically importing projects into Eclipse

First lets go over our options. Eclipse does not have a way of importing existing projects into workspaces. Yes, there is the import project wizard, which allows you to engage in a click-a-ton. And there is the import working set feature, which might be a solution for you. You still need to select a file, but the file can contain a list of projects to import. There is one huge limitation on the working set wizard. The projects must be under (some kind of) version control. I have my sources under version, but not the entire project. So I cannot use the wizard. Even then, it is still not unattended. Remember good programmers do not automate their workspace They have a deep love for repetitive manual clicking. This kind of dogmatic thinking is pervasive in the Eclipse community. I was expecting there to be some sort of import project command line flag. There is none.

Now eclipse uses several files to define a project. We need to make a distinction between the files that define the project and the files that link the project to the workspace. The project is defined by the .project and .classpath files in the project directory. .project defines the location of the sources on the file system, but also tells Eclipse which programming language to enable for the project. .classpath defines the java classpath. It tells Eclipse which (sub) folders contain java files that need to be compiled. Additionaly, it may contain dependencies with their optional source and javadoc locations. Both files are simple xml files that could be written by hand or generated. Maven can generate them for you. Unfortunately, the workspace link is in binary format and a little hard to fake.

Therefore, it is a lot easier to create the project links from a plugin. This is the relevant code snippet:

File directory = ...
Path path = new Path(directory.getAbsolutePath());
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IProjectDescription description = workspace.loadProjectDescription(path);

IProject project = workspace.getRoot().getProject(description.getName());
project.create(description, null);
project.open(null);
This will load the project (with existing .classpath and .project) located at the specified directory into the workspace. Deciding which projects to load into the current workspace is implementation specific and you will have to design a solution. My solution was to use a simple config file with lines containing only the name of the workspace and the names of the projects:
workspace1 projecta projectc
workspace2 projectb projectc
When eclipse initializes, I read the name of the folder containing the workspace:
workspace.getRoot().getLocation().lastSegment()
And use it to select a line in the config file. Everything on the line after the workspace is then a project. All projects are in the same directory by convention, so simply append the project name to the directory and load it. You are free to design your own solution, but you will need some mechanism to lookup the projects based on the workspace. I run the code on every start of Eclipse, in case I decide to add a project at a later time.

Installing and running Eclipse

The Eclipse site by default guides you to the full SDK, which is currently a wonderful 230MB package of bloatware. The java installation is considerably smaller, but still contains more than I need. Neither version contains PDE for plugin development, you would have to use the marketplace to get them. You can actually install plugins unattended using the director. This makes it possible to create a minimal eclipse installation: eclipse.sh
#!/bin/sh

die() {
    echo >&2 "$@"
    exit 1
}

[ "$#" -eq 1 ] || die "exactly 1 argument required [INSTALL_DIR]"
[ -e "$1" ] && die "*warning* Aborting! location exists, eclipse already installed?"

INSTALL_DIR="$1"
TARBALL=eclipse-platform-4.7.3a-linux-gtk-x86_64.tar.gz

mkdir -p $INSTALL_DIR
if [ ! -f $TARBALL ]
then
    wget http://mirror.csclub.uwaterloo.ca/eclipse/eclipse/downloads/drops4/R-4.7.3a-201803300640/$TARBALL
fi
tar -v -xf "$TARBALL" -C "$INSTALL_DIR" --strip 1

echo "\nUsing director to install java development tools, this may take a while..."
$INSTALL_DIR/eclipse -noSplash -application org.eclipse.equinox.p2.director \
    -repository http://download.eclipse.org/eclipse/updates/4.7 -installIUs org.eclipse.jdt.feature.group
Simply invoke the script and pass the installation directory as an argument.

The script uses the link to a mirror and will break if the mirror changes. The safest way to install Eclipse on a linux installation, is by using the package manager. For example, on Ubuntu:

sudo apt-get install -y eclipse
The only problem is, that you have no idea what version and what plugins you are getting. If you don't care about it that much, then the package manager solution is best. We might solve the mirror problems by storing the eclipse installation package on a server. But then you might as well zip the version with your minimal plugins included. That is going to be way faster than installing the plugins using the marketplace. Just download the zip and unpack.

I use a launcher script to start Eclipse. This has a couple of advantages. First of all, I never get the open workspace dialog, because I use the -data argument to specify a workspace:

./eclipse -data /tmp/workspace
Normally, the workspace dialog creates a new empty workspace in the provided location, if it doesn't already exist. One spelling error and you have an extra workspace. To make things worse, even if you delete the workspace, the incorrect location stays in the suggestion list for a long time. My launcher script verifies that the directory exists and only then starts eclipse bypassing the workspace dialog. The second thing the launcher does, is always start Eclipse in the java perspective:
./eclipse -perspective org.eclipse.jdt.ui.JavaPerspective
It is the only perspective I want to start with. I do use the debug perspective, but it is no place to start a session. The default perspectives (for use with the -perspective flag) are listed on my cheat sheet: http://eriklievaart.com/cheat/java/other/eclipse.html If you want to get a list of all the perspectives available in your eclipse distribution, you can use the following script: perspective.sh
#!/bin/sh
for jar in $(find "$1" -name '*.jar')
do
        plugin=$(zipinfo -l "$jar" plugin.xml 2> /dev/null)
        if [ "$plugin" = "" ]
        then
                continue
        fi
        preferences=$(unzip -q -c "$jar" 'plugin.xml' | xmlstarlet sel -t -v '//perspective/@id')
        if [ "$preferences" != "" ]
        then
                echo "$preferences"
        fi
done
Simply call the script and pass the location of the eclipse directory as an argument.

Eclipse sources and javadoc

When coding Eclipse plugins, you sometimes want to look at the Eclipse source code for examples. If you have reasonable certainty which eclipse plugin has the source code you need, you can clone the git repo. For example:
git clone http://git.eclipse.org/gitroot/jdt/eclipse.jdt.git
The list of available git repos can be found here: http://git.eclipse.org/c/ and here is a link to the Eclipse git manual: http://wiki.eclipse.org/Git

However, if you want to get the sources for multiple bundles, there is an easier way to go about it. If you download the fully bloated SDK version of eclipse, then it actually includes all of the sources. The java files that is, not necessarily the files you would need to build eclipse. Don't get me wrong, I think shipping sources and documentation with a product is a good thing. Every bundle in the plugin directory has an accompanying bundle with ".source_" in the name. The following script will find all the source bundles and extract each of them into a separate directory: sources.sh

#!/bin/sh

eclipse_home="$1"
eclipse_plugins="$eclipse_home/plugins"

rm -rf /tmp/unpacked
for jar in $(ls $eclipse_plugins | grep 'source_.*.jar')
do
        name=$(echo $jar | sed 's/.source//')
        base=$(echo $name | sed 's/.v[-_0-9]*.jar$//')
        destination="/tmp/unpacked/$base"
        mkdir -p $destination
        echo "$jar => $destination"
        unzip "$eclipse_plugins/$jar" -d "$destination"

        binary="$eclipse_plugins/$name"
        plugin=$(zipinfo -l "$binary" plugin.xml 2> /dev/null)
        if [ "$plugin" = "" ]
        then
                continue
        fi
        unzip "$binary" plugin.xml -d "$destination"
done
It also scans the implementation bundles for the plugin.xml files. These are very useful to have.

Instead, to extract all of the sources into a shared folder (rather than a folder per bundle): src.sh

#!/bin/sh

plugins="$1/plugins"
allsrc=/tmp/src

rm -rf $allsrc
mkdir -p $allsrc
for jar in $(ls $plugins | grep 'source_.*.jar')
do
        unzip -n "$plugins/$jar" -d "$allsrc"
done

Now, multiple bundles may have a file at the same path (e.g. the plugin.xml or manifest file). For these files the script above will give you only one copy. I did not even include the plugin.xml files, since they would all overwrite the same location. The shared folder solution is useful for one specific case, generating javadoc. The Eclipse javadoc is available online. Annoyingly, they show the javadoc in the help center. Which means you get extra clutter around the javadoc pages. Here is a ant build file you can use to generate the javadoc pages without the clutter: javadoc.xml
<project default="javadoc">
  <target name="javadoc">
    <delete dir="api" />
    <javadoc packagenames="org.eclipse.ui.*" sourcepath="/tmp/src" destdir="api" windowtitle="Eclipse API">
        <doctitle>eclipse javadoc</doctitle>
    </javadoc>
  </target>
</project>
Add the ant build file to an eclipse project, drag it to the ant view and double click the javadoc target to run it. It creates javadoc for the org.eclipse.ui package by default, but the file is easy to understand. Modify the file if you want to create javadoc for other packages.

Perspectives and views

The next thing I like changing is perspectives and views. Generally, I close a bunch of views and move the remaining ones to the locations I want them. The easiest way to start with a particular perspective is using the -perspective flag on the command line. We can change perspectives from our eclipse plugin as well:
IWorkbenchWindow page = window.openPage("org.eclipse.ui.resourcePerspective", ResourcesPlugin.getWorkspace());
hiding a view:
page.hideView(page.findView("org.eclipse.ui.internal.introview"));
The easiest way to find the id of a view you want to open or close, it to use plugin spy. It ships with the PDE version of eclipse. Simply select a view and press alt+shift+F1 to get it.

Unfortunately it is not possible to position views from a plugin. The Eclipse philosophy is that users should be allowed to position views where they want. Plugins should not get in the user's way and I fully agree with that. Or as eclipse puts it:
https://wiki.eclipse.org/FAQ_How_do_I_set_the_size_or_position_of_my_view%3F

So how does Eclipse decide where to allocate views? Well, like it says on the eclipse wiki, the perspective decides the initial position of the view. Here is an example from Eclipse's own source (it does not appear as though they have maximum line lengths): DebugPerspectiveFactory.java

/*******************************************************************************
 * Copyright (c) 2000, 2013 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.debug.internal.ui;


import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.ui.IFolderLayout;
import org.eclipse.ui.IPageLayout;
import org.eclipse.ui.IPerspectiveFactory;
import org.eclipse.ui.console.IConsoleConstants;

/**
 * The debug perspective factory.
 */
public class DebugPerspectiveFactory implements IPerspectiveFactory {

	/**
	 * @see IPerspectiveFactory#createInitialLayout(IPageLayout)
	 */
	@Override
	public void createInitialLayout(IPageLayout layout) {

		IFolderLayout consoleFolder = layout.createFolder(IInternalDebugUIConstants.ID_CONSOLE_FOLDER_VIEW, IPageLayout.BOTTOM, (float)0.75, layout.getEditorArea());
		consoleFolder.addView(IConsoleConstants.ID_CONSOLE_VIEW);
		consoleFolder.addView(IPageLayout.ID_TASK_LIST);
		consoleFolder.addPlaceholder(IPageLayout.ID_BOOKMARKS);
		consoleFolder.addPlaceholder(IPageLayout.ID_PROP_SHEET);

		IFolderLayout navFolder= layout.createFolder(IInternalDebugUIConstants.ID_NAVIGATOR_FOLDER_VIEW, IPageLayout.TOP, (float) 0.45, layout.getEditorArea());
		navFolder.addView(IDebugUIConstants.ID_DEBUG_VIEW);
		navFolder.addPlaceholder(IPageLayout.ID_PROJECT_EXPLORER);

		IFolderLayout toolsFolder= layout.createFolder(IInternalDebugUIConstants.ID_TOOLS_FOLDER_VIEW, IPageLayout.RIGHT, (float) 0.50, IInternalDebugUIConstants.ID_NAVIGATOR_FOLDER_VIEW);
		toolsFolder.addView(IDebugUIConstants.ID_VARIABLE_VIEW);
		toolsFolder.addView(IDebugUIConstants.ID_BREAKPOINT_VIEW);
		toolsFolder.addPlaceholder(IDebugUIConstants.ID_EXPRESSION_VIEW);
		toolsFolder.addPlaceholder(IDebugUIConstants.ID_REGISTER_VIEW);

		IFolderLayout outlineFolder= layout.createFolder(IInternalDebugUIConstants.ID_OUTLINE_FOLDER_VIEW, IPageLayout.RIGHT, (float) 0.75, layout.getEditorArea());
		outlineFolder.addView(IPageLayout.ID_OUTLINE);

		layout.addActionSet(IDebugUIConstants.LAUNCH_ACTION_SET);
		layout.addActionSet(IDebugUIConstants.DEBUG_ACTION_SET);

		setContentsOfShowViewMenu(layout);
	}

	/**
	 * Sets the initial contents of the "Show View" menu.
	 */
	protected void setContentsOfShowViewMenu(IPageLayout layout) {
		layout.addShowViewShortcut(IDebugUIConstants.ID_DEBUG_VIEW);
		layout.addShowViewShortcut(IDebugUIConstants.ID_VARIABLE_VIEW);
		layout.addShowViewShortcut(IDebugUIConstants.ID_BREAKPOINT_VIEW);
		layout.addShowViewShortcut(IDebugUIConstants.ID_EXPRESSION_VIEW);
		layout.addShowViewShortcut(IPageLayout.ID_OUTLINE);
		layout.addShowViewShortcut(IConsoleConstants.ID_CONSOLE_VIEW);
		layout.addShowViewShortcut(IPageLayout.ID_TASK_LIST);
	}
}
Unfortunately, that means that we have no way of changing the defaults. It is not that hard to break open the debug jar and replace the class file. But I do not consider this solution better than copying the eclipse metadata.

There is of course the option of defining our own perspective. You can tell eclipse which perspective to use as the debug perspective in the preferences panel. The down side of this approach is that we get an additional perspective in the perspective list. The only function of the new perspective would be different starting positions for the windows. This is not a good reason for creating a whole new perspective.

In the end I decided to copy the workbench metadata:

.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi
The alternatives just don't seem worth the effort.

Links

eclipse entry on the cheat sheet
http://eriklievaart.com/cheat/java/other/eclipse.html
My personal eclipse initialization plugin
https://github.com/eriklievaart/eclipseinit
My personal eclipse plugin for registering a listener that prints preference changes
https://github.com/eriklievaart/eclipselistener