Workspace save participation

Workspace save processing is triggered when the workbench is shut down by the user and at other times periodically by the platform.  Plug-ins can participate in the workspace save process so that critical plug-in data is saved to disk whenever the rest of the workspace's persistent data is saved.

The workspace save process can also be used to track changes that occur between activations of your plug-in.

Implementing a save participant

To participate in workspace saving, you must add a save participant to the workspace. This is typically done during your plug-in's startup method.  This is also where you read any state that you might have saved when your plug-in was last shut down.

Let's look at a simple plug-in which will demonstrate the save process.

   package com.example.saveparticipant;

   import org.eclipse.core.runtime.*;
   import org.eclipse.core.resources.*;
   import java.io.File;
   import java.util.*;

   public class MyPlugin extends Plugin {
      private static MyPlugin plugin;

      public MyPlugin(IPluginDescriptor descriptor) {
         super(descriptor);
         plugin = this;
      }

      public static MyPlugin getDefault() {
         return plugin;
      }

      protected void readStateFrom(File target) {
      }

      public void startup() throws CoreException {
         super.startup();
         ISaveParticipant saveParticipant = new MyWorkspaceSaveParticipant();
         ISavedState lastState =
            ResourcesPlugin.getWorkspace().addSaveParticipant(this, saveParticipant);
         if (lastState == null)
            return;
         IPath location = lastState.lookup(new Path("save"));
         if (location == null)
            return;
         // the plugin instance should read any important state from the file.
         File f = getStateLocation().append(location).toFile();
         readStateFrom(f);
      }

      protected void writeImportantState(File target) {
      }
   }

ISaveParticipant defines the protocol for a workspace save participant. Implementors of this interface can provide behavior for different stages of the save process.  Let's look at the stages and how our class WorkspaceSaveParticipant implements each of these steps.

      public void prepareToSave(ISaveContext context) throws CoreException {
      }
      public void saving(ISaveContext context) throws CoreException {
         switch (context.getKind()) {
            case ISaveContext.FULL_SAVE:
               MyPlugin myPluginInstance = MyPlugin.getDefault();
               // save the plug-in state
               int saveNumber = context.getSaveNumber();
               String saveFileName = "save-" + Integer.toString(saveNumber);
               File f = myPluginInstance.getStateLocation().append(saveFileName).toFile();
               // if we fail to write, an exception is thrown and we do not update the path
               myPluginInstance.writeImportantState(f);
               context.map(new Path("save"), new Path(saveFileName));
               context.needSaveNumber();
               break;
            case ISaveContext.PROJECT_SAVE:
               // get the project related to this save operation
               IProject project = context.getProject();
               // save its information, if necessary
               break;
            case ISaveContext.SNAPSHOT:
               // This operation needs to be really fast because
               // snapshots can be requested frequently by the
               // workspace.
               break;
         }
      }

The ISaveContext describes information about the save operation.  There are three kinds of save operations:  FULL_SAVE, SNAPSHOT, and PROJECT_SAVE.  Save participants should be careful to perform the processing appropriate for the kind of save event they have received.  For example, snapshot events may occur quite frequently and are intended to allow plug-ins to save their critical state.  Taking a long time to save state which can be recomputed in the event of a crash will slow down the platform.

A save number is used to create data save files that are named using sequential numbers (save-1, save-2, etc.)  Each save file is mapped to a logical file name (save) that is independent of the save number. Plug-in data is written to the corresponding file and can be retrieved later without knowing the specific save number of the last successful save operation.  Recall that we saw this technique in our  plug-in's startup code:

IPath location = lastState.lookup(new Path("save"));

After we have saved our data and mapped the file name, we call needSaveNumber to indicate that we have actively participated in a workspace save and want to assign a number to the save activity. The save numbers can be used to create data files as above. 

      public void doneSaving(ISaveContext context) {
         MyPlugin myPluginInstance = MyPlugin.getDefault();

         // delete the old saved state since it is not necessary anymore
         int previousSaveNumber = context.getPreviousSaveNumber();
         String oldFileName = "save-" + Integer.toString(previousSaveNumber);
         File f = myPluginInstance.getStateLocation().append(oldFileName).toFile();
         f.delete();
      }

Here, we clean up the save information from the previous save operation.  We use getPreviousSaveNumber to get the save number that was assigned in the previous save operation (not the one we just completed).  We use this number to construct the name of the file that we need to delete.  Note that we do not use the save state's logical file map since we've already mapped our current save file number. 

      public void rollback(ISaveContext context) {
         MyPlugin myPluginInstance = MyPlugin.getDefault();

         // since the save operation has failed, delete the saved state we have just written
         int saveNumber = context.getSaveNumber();
         String saveFileName = "save-" + Integer.toString(saveNumber);
         File f = myPluginInstance.getStateLocation().append(saveFileName).toFile();
         f.delete();
      }

Here, we delete the state that we just saved.  Note that we use the current save number to construct the file name of the file we just saved.  We don't have to worry about the fact that we mapped this file name into the ISaveContext. The platform will discard the context when a save operation fails.

If your plug-in throws an exception at any time during the save lifecycle, it will be removed from the current save operation and will not get any of the remaining lifecycle methods.  For example, if you fail during your saving method, you will not receive a rollback or doneSaving message. 

Using previously saved state

When you add a save participant to the workspace, it will return an ISavedState object, which describes what your plug-in saved during its last save operation (or null if your plug-in has not previously saved any state). This object can be used to access information from the previous save file (using the save number and file map) or to process changes that have occurred between activations of a plug-in.

Accessing the save files

If a file map was used to save logically named files according to the save number, this same map can be used to retrieve the data from the last known save state.

   ISaveParticipant saveParticipant = new MyWorkspaceSaveParticipant();
   ISavedState lastState =
      ResourcesPlugin.getWorkspace().addSaveParticipant(myPluginInstance, saveParticipant);

   if (lastState != null) {
      String saveFileName = lastState.lookup(new Path("save")).toString();
      File f = myPluginInstance.getStateLocation().append(saveFileName).toFile();
      // the plugin instance should read any important state from the file.
      myPluginInstance.readStateFrom(f);
   }

Processing resource deltas between activations

Recall that any number of resource change events could occur in the workspace before your plug-in is ever activated. If you want to know what changes have occurred since your plug-in was deactivated, you can use the save mechanism to do so, even if you don't need to save any other data.

The save participant must request that the platform keep a resource delta on its behalf. This is done as part of the save operation.

   public void saving(ISaveContext context) throws CoreException {
      // no state to be saved by the plug-in, but request a
      // resource delta to be used on next activation.
      context.needDelta();
   }

During plug-in startup, the previous saved state can be accessed and change events will be created for all changes that have occurred since the last save.

   ISaveParticipant saveParticipant = new MyWorkspaceSaveParticipant();
   ISavedState lastState =
      ResourcesPlugin.getWorkspace().addSaveParticipant(myPluginInstance, saveParticipant);
   if (lastState != null) {
      lastState.processResourceChangeEvents(new MyResourceChangeReporter());
   }

The provided class must implement IResourceChangeListener, as described in Tracking resource changes.  The changes since the last save are reported as part of the POST_AUTO_BUILD resource change event.

Note:  Marker changes are not reported in the change events stored in an ISavedState. You must assume that any or all markers have changed since your last state was saved.