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.team.examples.filesystem
plug-in. You can check the project out from the CVS 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.
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 it's 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.