Local History Example

The best way to understand the Synchronize APIs is to create a simple example that actually works. In this example we will be creating a page in the Synchronize View that will display the latest local history state for all files in the workspace. The local history synchronization will update automatically when changes are made to the workspace, and a compare editor can open to browse, merge, then changes. We will also add a custom decorator to show the last timestamp of the local history element and an action to revert the workspace files to their latest saved local history state. This is an excellent example because we already have a store of resource variants available and we don't have to manage it.

For the remainder of this example we will make use of a running example. Much, but not all, of the source code will be included on this page. The full source code can be found in the local history package of the org.eclipse.ui.examples.filesystem plug-in. You can check the project out from the Git repository and use it as a reference while you are reading this tutorial. Disclaimer: The source code in the example plug-ins may change over time. To get a copy that matches what is used in this example, you can check out the project using the 3.3.2 version tag (most likely R3_3_2) or a date tag of June 10, 2007.

local history overview

This screen shot shows the local history synchronization in the Synchronize View. With it you can browse the changes between the local resource and the latest state in history. It has a custom decorator for displaying the timestamp associated with the local history entry and a custom action to revert your file to the contents in the local history. Notice also that the standard Synchronize View presentation is used which provide problem annotations, compressed folder layout, and navigation buttons.

Defining the variants for local history

The first step is to define a variant to represent the elements from local history. This will allow the synchronize APIs to access the contents from the local history so it can be compared with the current contents and displayed to the user.


public class LocalHistoryVariant implements IResourceVariant {
  private final IFileState state;
  public LocalHistoryVariant(IFileState state) {
    this.state = state;
  }
  public String getName() {
    return state.getName();
  }
  public boolean isContainer() {
    return false;
  }
  public IStorage getStorage(IProgressMonitor monitor) throws TeamException {
    return state;
  }
  public String getContentIdentifier() {
    return DateFormat.getDateTimeInstance().format(new Date(state.getModificationTime()));
  }
  public byte[] asBytes() {
    return null;
  }
  public IFileState getFileState() {
    return state;
  }
}

Since the IFileState interface already provides access to the contents of the file from local history (i.e. implements the IStorage interface), this was easy. Generally, when creating a variant you have to provide a way of accessing the content, a content identifier that will be displayed to the user to identify this variant, and a name. The asBytes() method is only required if persisting the variant between sessions.

Next, let's create a variant comparator that allows the SyncInfo calculation to compare local resources with their variants. Again, this is easy because the existence of a local history state implies that the content of the local history state differs from the current contents of the file. This is because the specification for local history says that it won't create a local history state if the file hasn't changed.


public class LocalHistoryVariantComparator implements IResourceVariantComparator {
  public boolean compare(IResource local, IResourceVariant remote) {
    return false;
  }
  public boolean compare(IResourceVariant base, IResourceVariant remote) {
    return false;
  }
  public boolean isThreeWay() {
    return false;
  }
}

Because we know that the existence of the local history state implies that it is different from the local, we can simply return false when comparing the file to its local history state. Also, synchronization with the local history is only two-way because we don't have access to a base resource so the method for comparing two resource variants is not used.

Note that the synchronize calculation won't call the compare method of the comparator if the variant doesn't exist (i.e. is null). It is only called if both elements exist. In our example, this would occur both for files that don't have a local history and for all folders (which never have a local history). To deal with this, we need to define our own subclass of SyncInfo in order to modify the calculated synchronization state for these cases.


public class LocalHistorySyncInfo extends SyncInfo {
  public LocalHistorySyncInfo(IResource local, IResourceVariant remote, IResourceVariantComparator comparator) {
    super(local, null, remote, comparator);
  }
  protected int calculateKind() throws TeamException {
    if (getRemote() == null)
      return IN_SYNC;
    else
      return super.calculateKind();
  }
}

We have overridden the constructor to always provide a base that is null (since we are only using two-way comparison) and we have modified the synchronization kind calculation to return IN_SYNC if there is no remote (since we only care about the cases where there is a local file and a file state in the local history.

Creating the Subscriber

Now we will create a Subscriber that will provide access to the resource variants in the local history. Since local history can be saved for any file in the workspace, the local history Subscriber will supervise every resource and the set of roots will be all projects in the workspace. Also, there is no need to provide the ability to refresh the subscriber since the local history changes only when the contents of a local file changes. Therefore, we can update our state whenever a resource delta occurs. That leaves only two interesting method on our local history subscriber: obtaining a SyncInfo and traversing the workspace.


public SyncInfo getSyncInfo(IResource resource) throws TeamException {
  try {
    IResourceVariant variant = null;
    if(resource.getType() == IResource.FILE) {
      IFile file = (IFile)resource;
      IFileState[] states = file.getHistory(null);
      if(states.length > 0) {
        // last state only
        variant = new LocalHistoryVariant(states[0]);
      } 
    }
    SyncInfo info = new LocalHistorySyncInfo(resource, variant, comparator);
    info.init();
    return info;
  } catch (CoreException e) {
    throw TeamException.asTeamException(e);
  }
}

The Subscriber will return a new SyncInfo instance that will contain the latest state of the file in local history. The SyncInfo is created with a local history variant for the remote element. For projects, folders and files with no local history, no remote resource variant is provided, which will result in the resource being considered in-sync due to the calculateKind method in our LocalHistorySyncInfo.

The remaining code in the local history subscriber is the implementation of the members method:


public IResource[] members(IResource resource) throws TeamException {
    try {
      if(resource.getType() == IResource.FILE)
        return new IResource[0];
      IContainer container = (IContainer)resource;
      List existingChildren = new ArrayList(Arrays.asList(container.members()));
      existingChildren.addAll(Arrays.asList(container.findDeletedMembersWithHistory(IResource.DEPTH_INFINITE, null)));
      return (IResource[]) existingChildren.toArray(new IResource[existingChildren.size()]);
    } catch (CoreException e) {
      throw TeamException.asTeamException(e);
    }
}

The interesting detail of this method is that it will return non-existing children if a deleted resource has local history. This will allow our Subscriber to return SyncInfo for elements that only exist in local history and are no longer in the workspace.

Adding a Local History Synchronize Participant

So far we have created the classes which provide access to SyncInfo for elements in local history. Next, we will create the UI elements that will allow us to have a page in the Synchronize View to display the last history state for every element in local history. Since we have a Subscriber, adding this to the Synchronize View is easy. Let's start by adding an synchronize participant extension point:


<extension
  point="org.eclipse.team.ui.synchronizeParticipants">
  <participant
      persistent="false"
      icon="icons/full/wizards/synced.gif"
      class="org.eclipse.team.examples.localhistory.LocalHistoryParticipant"
      name="Latest From Local History"
      id="org.eclipse.team.synchronize.example"/>
</extension>

Next we have to implement the LocalHistoryParticipant. It will subclass SubscriberParticipant which will provide all the default behavior for collecting SyncInfo from the subscriber and updating sync states when workspace changes occur. In addition, we will add an action to revert the workspace resources to the latest in local history.

First, we will look at how a custom action is added to the participant.


public static final String CONTEXT_MENU_CONTRIBUTION_GROUP = "context_group_1"; //$NON-NLS-1$
  
private class LocalHistoryActionContribution extends SynchronizePageActionGroup {
  public void initialize(ISynchronizePageConfiguration configuration) {
    super.initialize(configuration);
    appendToGroup(
      ISynchronizePageConfiguration.P_CONTEXT_MENU, CONTEXT_MENU_CONTRIBUTION_GROUP, 
      new SynchronizeModelAction("Revert to latest in local history", configuration) { //$NON-NLS-1$
        protected SynchronizeModelOperation getSubscriberOperation(ISynchronizePageConfiguration configuration, IDiffElement[] elements) {
          return new RevertAllOperation(configuration, elements);
        }
      });
  }
}

Here we are adding a specific SynchronizeMoidelAction and operation. The behavior we get for free here is the ability to run in the background and show busy status for the nodes that are being worked on. The action reverts all resources in the workspace to their latest state in local history. The action is added by adding an action contribution to the participants configuration. The configuration is used to describe the properties used to build the participant page that will display the actual synchronize UI.

The participant will initialize the configuration as follows in order to add the local history action group to the context menu:


protected void initializeConfiguration(ISynchronizePageConfiguration configuration) {
  super.initializeConfiguration(configuration);
  configuration.addMenuGroup(
    ISynchronizePageConfiguration.P_CONTEXT_MENU, 
    CONTEXT_MENU_CONTRIBUTION_GROUP);
  configuration.addActionContribution(new LocalHistoryActionContribution());
  configuration.addLabelDecorator(new LocalHistoryDecorator());	
}

Now lets look at how we can provide a custom decoration. The last line of the above method registers the following decorator with the page's configuration.


private class LocalHistoryDecorator extends LabelProvider implements ILabelDecorator {
  public String decorateText(String text, Object element) {
    if(element instanceof ISynchronizeModelElement) {
      ISynchronizeModelElement node = (ISynchronizeModelElement)element;
      if(node instanceof IAdaptable) {
        SyncInfo info = (SyncInfo)((IAdaptable)node).getAdapter(SyncInfo.class);
        if(info != null) {
          LocalHistoryVariant state = (LocalHistoryVariant)info.getRemote();
          return text+ " ("+ state.getContentIdentifier() + ")";
        }
      }
    }
    return text;
  }
		
  public Image decorateImage(Image image, Object element) {
    return null;
  }
}

The decorator extracts the resource from the model element that appears in the synchronize view and appends the content identifier of the local history resource variant to the text label that appears in the view.

The last and final piece is to provide a wizard that will create the local history participant. The Team Synchronizing perspective defines a global synchronize action that allows users to quickly create a synchronization. In addition, the ability to create synchronizations in available from the Synchronize view toolbar. To start, create a synchronizeWizards extension point:


<extension
  point="org.eclipse.team.ui.synchronizeWizards">
  <wizard
    class="org.eclipse.team.examples.localhistory.LocalHistorySynchronizeWizard"
    icon="icons/full/wizards/synced.gif"
    description="Synchronize resources with their previous contents in the local history"
    name="Synchronize with Latest From Local History"
    id="ExampleSynchronizeSupport.wizard1"/>
</extension>

This will add our wizard to the list and in the wizards performFinish() method we will simply create our participant and add it to the synchronize manager.


LocalHistoryParticipant participant = new LocalHistoryParticipant();
ISynchronizeManager manager = TeamUI.getSynchronizeManager();
manager.addSynchronizeParticipants(new ISynchronizeParticipant[] {participant});
ISynchronizeView view = manager.showSynchronizeViewInActivePage();
view.display(participant);

Conclusion

This is a simple example of using the synchronize APIs and we have glossed over some of the details in order to make the example easier to understand. Writing responsive and accurate synchronization support is non-trivial, the hardest part being the management of synchronization information and the notification of synchronization state changes. The user interface, if the one associated with SubscriberParticipants is adequate, is the easy part once the Subscriber implementation is complete. For more examples please refer to the org.eclipse.team.example.filesystem plug-in and browse the subclasses in the workspace of Subscriber and ISynchronizeParticipant.

The next section describes some class and interfaces that can help you write a Subscriber from scratch including how to cache synchronization states between workbench sessions.