Пример локальной хронологии

Наилучшим способом разобраться в синхронизации API будет создание простого реально работающего примера. В этом примере в панели Синхронизация будет создана страница, на которой будут отображаться последние версии всех файлов рабочей области, сохраненные в локальной хронологии. Синхронизация и обновление локальной хронологии будет осуществляться автоматически при изменении рабочей области, после чего появится возможность просмотра, слияния и редактирования изменений в редакторе сравнения. Кроме того, мы добавим пользовательский значок оформления, показывающий время последнего изменения элемента локальной хронологии, и действие по возвращению файлов рабочей области к последней версии, сохраненной в локальной хронологии. Это будет отличный пример, поскольку мы уже научились сохранять переменные ресурсов, а теперь научимся ими управлять.

И наконец, пример будет реально работающим. В эту страницу войдет большая часть исходного кода. Полностью его можно найти в пакете локальной хронологии на Web-сайте org.eclipse.team.examples.filesystem. Проект можно извлечь из хранилища CVS и при изучении этого материала пользоваться им как справочником. Оговорка: Исходный код модуля примера может измениться в любое время. Для получения копии, соответствующей этому примеру, следует выбрать проект по версии (3.0) (скорее всего, R3_0) или по дате: 28 июня 2004 г.

Синхронизация локальной хронологии

На этом снимке экрана показана панель Синхронизация с синхронизацией локальной хронологии. В ней можно сравнить локальный ресурс с его последним сохраненным в хронологии состоянием. Системное время, связанное с записью локальной хронологии, отображается с помощью значка оформления, а возвращение файла к последней сохраненной версии осуществляется с помощью пользовательского действия. Также обратите внимание, что в стандартной презентации панели Синхронизация представлены комментарии к задачам, размещение сжатой папки и кнопки навигации.

Определение переменных для локальной хронологии

Вначале следует определить переменную для представления элементов локальной хронологии. С ее помощью будут синхронизироваться API для получения доступа к содержимому локальной хронологии, сравнения его с текущим содержимым и отображения результатов.

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 Long.toString(state.getModificationTime());
}

public byte[] asBytes() {
return null;
}
}

Это будет достаточно просто, поскольку интерфейс IFileState уже предоставляет доступ к содержимому файла из локальной хронологии (т.е. реализует интерфейс IStorage). Вообще, при создании переменной необходим способ доступа к содержимому, ИД содержимого для обозначения этой переменной, и имя. Если переменная должна сохраняться между сеансами, то необходим метод asBytes().

Далее необходимо создать класс сравнения переменных, с помощью которой локальные ресурсы будут сравниваться со своими состояниями и по результатам будет рассчитываться SyncInfo. Это, опять-таки, несложно, поскольку наличие состояния в локальной хронологии подразумевает, что файл изменился. Если файл не менялся, то версия для него в хронологии не создается.

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;
}
}

Поскольку мы знаем, что наличие версии файла в локальной хронологии подразумевает изменение файла, мы можем просто при сравнении файла с версией в хронологии вернуть false. Кроме того, синхронизация с локальной хронологией только двухсторонняя, поскольку у нас нет доступа к базовому ресурсу, и поэтому метод сравнения трех версий не применяется.

Обратите внимание, что расчет синхронизации не вызывает метод сравнения класса сравнения, если переменная не существует (равна нулю). Метод вызывается только при наличии обоих элементов. В нашем примере это может произойти как для файлов, у которых нет локальной хронологии, так и для всех папок (у которых никогда не будет локальной хронологии). В таких случаях для работы с ними необходимо определить собственные производные классы SyncInfo, чтобы изменять рассчитанное состояние синхронизации.

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();
  }
}

Мы переопределили конструктор, чтобы base всегда был равен null (так как применяется только двухстороннее сравнение), и изменили вид расчета синхронизации так, чтобы возвращалось значение IN_SYNC, если он не является удаленным (так как нас интересуют только случаи, когда локальный файл и его версия есть в локальной хронологии).

Создание подписчика

Теперь необходимо создать Подписчик, который будет обеспечивать доступ к переменным ресурсов в локальной хронологии. Так как локальную хронологию можно сохранить для любого файла рабочей области, Подписчик локальной хронологии будет управлять каждым ресурсом, а все проекты в рабочей области будут объявлены корневыми объектами. Так как локальная хронология изменяется только при изменении локального файла, то обновлять Подписчик не нужно. Следовательно, мы будем обновлять версию всякий раз при изменении ресурса. Поэтому в Подписчике локальной хронологии мы рассмотрим только два метода: получение SyncInfo и перемещение по рабочей области.

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);
  }
}

Подписчик вернет новый экземпляр SyncInfo, в котором будет содержаться последняя версия файла, сохраненная в локальной хронологии. SyncInfo для удаленного элемента создается с переменной локальной хронологии. Для проектов, папок и файлов, для которых нет локальной хронологии, переменной удаленного ресурса не будет, поэтому такие ресурсы будут считаться уже синхронизированными с помощью метода calculateKind в LocalHistorySyncInfo.

Оставшаяся часть кода в Подписчике локальной хронологии - это реализация метода members:

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);
  }
}

Интересный момент: если версия удаленного ресурса была сохранена в локальной хронологии, то метод вернет несуществующего потомка. Такой подход позволяет Подписчику вернуть SyncInfo для элементов, удаленных из рабочей области и существующих только в локальной хронологии.

Добавление агента синхронизации локальной хронологии

Итак, созданы классы для предоставления доступа к SyncInfo элементов локальной хронологии. Далее следует создать элементы UI, с помощью которых в панели Синхронизация будет создана страница, содержащая последние версии элементов локальной хронологии. Необходимо добавить созданный Подписчик в панель Синхронизация. Сначала добавим точку расширения агента синхронизации:

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

Далее реализуем LocalHistoryParticipant. Это класс, производный от SubscriberParticipant, задающий поведение по умолчанию для сбора SyncInfo от подписчика и обновления версий при изменении рабочей области. Кроме этого необходимо добавить действие для возвращения ресурсов рабочей области к последним версиям из локальной хронологии.

Вначале следует разобраться, как добавлять пользовательское действие.

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);
        }
      });
  }
}

Мы добавили SynchronizeMoidelAction и действие. В результате возникает возможность выполнения в фоновом режиме и отображения состояния занятости для узлов, работающих в данный момент. Действие возвращает все ресурсы рабочей области к последним версиям, сохраненным в локальной хронологии. Действие добавлено путем добавления в конфигурацию участников action contribution. Конфигурация служит для описания свойств, на основе которых формируется страница агента, отображающая реальный синхронизированный UI.

Агент будет инициализировать конфигурацию, служащую для добавления в контекстное меню группы действия локальной хронологии:

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());
}

Теперь можно заняться пользовательским оформлением. В последней строке приведенного выше метода регистрируется следующий значок оформления.

public 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;
}
}

Класс значка оформления извлекает ресурс из элемента модели, появившегося в панели синхронизации, и добавляет ИД содержимого переменной ресурса локальной хронологии к текстовой метке.

И наконец, осталось создать мастер, с помощью которого будет создаваться агент локальной хронологии. В проекции Синхронизация совместной работы определено глобальное действие по синхронизации, позволяющее пользователям быстро создавать синхронизацию. Кроме того, синхронизацию можно создать с помощью панели инструментов панели Синхронизация. В первую очередь создадим точку расширения synchronizeWizards:

<extension
point="org.eclipse.team.ui.synchronizeWizards">
<wizard
class="org.eclipse.team.synchronize.example.LocalHistorySynchronizeWizard"
icon="synced.png"
description="Создание синхронизации всех ресурсов рабочей области с последними версиями из локальной хронологии"
name="Latest From Local History Synchronize"
id="ExampleSynchronizeSupport.wizard1"/>
</extension>

Мастер добавляется в список, и теперь можно просто создать участника и добавить его к диспетчеру синхронизации с помощью метода finish().

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

Заключение

Мы разобрали несложный пример синхронизации с помощью API синхронизации. Для упрощения его понимания для некоторых моментов даны пояснения. Написание поддержки точной и гибкой синхронизации - задача нетривиальная, самая сложная часть которой - это управление информацией синхронизации и извещение об изменениях версий. Как только будет реализован Подписчик, останется самое простое - пользовательский интерфейс (если он правильно связан с SubscriberParticipants). Дополнительные примеры можно найти в модуле org.eclipse.team.example.filesystem - производный класс в рабочей среде Подписчика и ISynchronizeParticipant.

В следующем разделе описываются некоторые классы и интерфейсы, помогающие написать Подписчик с самого начала, в том числе кэширование состояний между сеансами рабочей среды.