Przykład historii lokalnej

Najlepszym sposobem poznania interfejsu API synchronizacji jest utworzenie prostego przykładu, który faktycznie działa. W tym przykładzie zostanie utworzona strona w widoku synchronizacji, która będzie wyświetlać najnowszy stan historii lokalnej dla wszystkich plików w obszarze roboczym. Synchronizacja historii lokalnej będzie odbywać się automatycznie w momencie zmian w obszarze roboczym i można będzie otworzyć edytor porównawczy w celu przeglądania i scalania tych zmian. Dodana zostanie ponadto niestandardowa dekoracja w celu wyświetlania ostatniego znacznika czasu elementu historii lokalnej oraz akcja przywracania plików obszaru roboczego do ich ostatniego zapisanego stanu w historii lokalnej. Jest to doskonały przykład, ponieważ składnica wariantów zasobów jest już dostępna i nie trzeba się nią zajmować.

W pozostałej części tego przykładu użyty zostanie działający przykład. Ta strona zawierać będzie większą część kodu źródłowego. Pełny kod źródłowy można znaleźć w pakiecie historii lokalnej modułu dodatkowego org.eclipse.team.examples.filesystem. Odpowiedni projekt można pobrać z repozytorium CVS i używać jako odniesienia podczas lektury tego kursu. Zastrzeżenie: Kod źródłowy w przykładowym module dodatkowym może się zmieniać. Aby uzyskać kopię zgodną z tym przykładem, można pobrać projekt ze znacznikiem wersji 3.0 (najprawdopodobniej R3_0) lub znacznikiem daty 28 czerwca 2004.

Przegląd historii lokalnej

Na tym zrzucie ekranu widać synchronizację historii lokalnej w widoku Synchronizacja. W widoku tym można przeglądać różnice między zasobem lokalnym i ostatnim stanem w historii. Ma on niestandardową dekorację do wyświetlania znacznika czasu powiązanego z wpisem historii lokalnej oraz niestandardową akcję przywracania treści pliku do stanu w historii lokalnej. Używany jest standardowy widok Synchronizacja, który udostępnia możliwości wprowadzania adnotacji dotyczących problemów, skompresowany układ folderów i przyciski nawigacyjne.

Definiowanie wariantów na potrzeby historii lokalnej

Pierwszym krokiem jest zdefiniowanie wariantu, który będzie reprezentował elementy z historii lokalnej. Umożliwi to funkcjom API synchronizacji dostęp do treści z historii lokalnej, dzięki czemu można będzie ją porównać z treścią bieżącą i wyświetlić.

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

Nie było to trudne, ponieważ interfejs IFileState już umożliwia dostęp do treści pliku z historii lokalnej (to jest implementuje interfejs IStorage). Ogólnie tworzenie wariantu polega na podaniu sposobu dostępu do treści, określeniu identyfikatora treści, który będzie wyświetlany użytkownikowi i umożliwi identyfikację wariantu, oraz zdefiniowaniu nazwy. Metoda asBytes() jest wymagana tylko wtedy, gdy wariant ma być utrwalany między sesjami.

Teraz należy utworzyć moduł porównujący, który umożliwi obliczenie obiektu SyncInfo w celu porównania zasobów lokalnych z ich wariantami. I tym razem jest to proste, ponieważ istnienie stanu historii lokalnej pozwala wnioskować, że treść stanu historii lokalnej różni się od bieżącej treści pliku. Wynika to ze specyfikacji historii lokalnej, która mówi, że stan historii lokalnej nie zostanie utworzony, jeśli plik nie zmieni się.

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

Ponieważ wiadomo, że istnienie stanu historii lokalnej oznacza, że jest on inny niż zasób lokalny, można po prostu zwrócić wartość false podczas porównywania pliku z jego stanem historii lokalnej. Ponadto synchronizacja z historią lokalną jest tylko dwukierunkowa, ponieważ nie ma dostępu do zasobu bazowego i metoda porównywania dwóch wariantów zasobów nie jest używana.

Należy zwrócić uwagę, że funkcja obliczania synchronizacji nie wywoła metody porównywania modułu porównującego, gdy wariant nie istnieje (to znaczy ma wartość NULL). Metoda ta jest wywoływana tylko wtedy, gdy oba elementy istnieją. W tym przykładzie sytuacja taka występuje zarówno dla plików, które nie mają historii lokalnej, jak i dla wszystkich folderów (które nigdy nie mają historii lokalnej). Aby sobie z tym poradzić, trzeba zdefiniować własną podklasę klasy SyncInfo, która zmodyfikuje wyliczony stan synchronizacji dla tych przypadków.

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

W ten sposób przesłonięto zachowanie konstruktora i teraz zawsze będzie udostępniany zasób bazowy, który ma wartość NULL (ponieważ używana jest tylko synchronizacja dwukierunkowa), oraz zmodyfikowano obliczenia rodzaju synchronizacji, aby zwracały wartość IN_SYNC, gdy nie ma zasobu zdalnego (ponieważ rozważane są tylko przypadki, gdy istnieje plik lokalny i stan pliku w historii lokalnej).

Tworzenie subskrybenta

Teraz zostanie utworzony subskrybent, który umożliwi dostęp do wariantów zasobów w historii lokalnej. Ponieważ historię lokalną można zapisać dla dowolnego pliku w obszarze roboczym, subskrybent historii lokalnej będzie nadzorował każdy zasób, a zbiorem elementów głównych będą wszystkie projekty w obszarze roboczym. Ponadto nie trzeba będzie umożliwiać odświeżania subskrybenta - historia lokalna zmienia się tylko wtedy, gdy zmienia się treść pliku lokalnego. Dlatego można aktualizować stan synchronizacji zawsze wtedy, gdy istnieje różnica miedzy zasobami (obliczana jest ich delta). Tym samym w tworzonym subskrybencie historii lokalnej można wykorzystać tylko dwie metody: uzyskanie obiektu SyncInfo i przejście obszaru roboczego.

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) {
        // tylko ostatni stan
        variant = new LocalHistoryVariant(states[0]);
      } 
    }
    SyncInfo info = new LocalHistorySyncInfo(resource, variant, comparator);
    info.init();
    return info;
  } catch (CoreException e) {
    throw TeamException.asTeamException(e);
  }
}

Subskrybent zwróci nową instancję klasy SyncInfo, która będzie zawierała najnowszy stan pliku w historii lokalnej. Instancja klasy SyncInfo jest tworzona wraz z wariantem historii lokalnej dla elementu zdalnego. W przypadku projektów, folderów i plików, które nie mają historii lokalnej, nie jest udostępniany żaden wariant zasobu zdalnego, co spowoduje, że zasób zostanie uznany za zsynchronizowany z uwagi na metodę calculateKind w klasie LocalHistorySyncInfo.

Pozostały kod subskrybenta historii lokalnej jest implementacją metody 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);
  }
}

Interesujące w tej metodzie jest to, że zwraca ona nieistniejący element potomny, gdy usunięty zasób ma historię lokalną. Umożliwi to subskrybentowi zwrócenie obiektu SyncInfo dla elementów, które istnieją tylko w historii lokalnej i nie ma ich już w obszarze roboczym.

Dodawanie uczestnika synchronizacji historii lokalnej

Jak dotąd utworzone zostały klasy, które zapewniają dostęp do obiektu SyncInfo dla elementu w historii lokalnej. W kolejnych krokach utworzone zostaną elementy interfejsu użytkownika, które umożliwią wyświetlanie na stronie w widoku synchronizacji ostatniego stanu historii dla każdego elementu w historii lokalnej. Ponieważ subskrybent został już utworzony wcześniej, dodanie tych elementów do widoku synchronizacji nie będzie trudne. Najpierw należy dodać punkt rozszerzenia uczestnika synchronizacji:

<extension
       point="org.eclipse.team.ui.synchronizeParticipants">
<participant
persistent="false"
icon="synced.png"
class="org.eclipse.team.synchronize.example.LocalHistoryParticipant"
name="Najnowsze z historii lokalnej"
id="org.eclipse.team.synchronize.example"/>
</extension>

Następnie należy zaimplementować klasę LocalHistoryParticipant. Będzie to podklasa klasy SubscriberParticipant, która udostępni całe domyślne zachowanie na potrzeby gromadzenia obiektów SyncInfo z subskrybenta i aktualizowania stanów synchronizacji, gdy wystąpią zmiany w obszarze roboczym. Ponadto dodana zostanie akcja przywracająca zasoby obszaru roboczego do ostatniego stanu w historii lokalnej.

Najpierw dobrze będzie zobaczyć, jak dodaje się niestandardową akcję do uczestnika.

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

Teraz dodawana jest konkretna akcja SynchronizeModelAction i operacja. Uzyskane przy okazji działanie to możliwość uruchamiania w tle i wyświetlania statusu zajętości dla węzłów, dla których trwa przetwarzanie. Ta akcja przywraca wszystkie zasoby w obszarze roboczym do ich ostatniego stanu w historii lokalnej. Dodanie akcji polega na dodaniu elementu wnoszonego akcji do konfiguracji uczestników. Konfiguracja służy do opisu właściwości używanych do zbudowania strony uczestnika, która będzie wyświetlać rzeczywisty interfejs użytkownika synchronizacji.

Uczestnik zainicjuje konfigurację w poniższy sposób w celu dodania grupy akcji historii lokalnej do menu kontekstowego:

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

Teraz można zająć się dodaniem niestandardowej dekoracji. Ostatni wiersz powyższej metody powoduje zarejestrowanie poniższej dekoracji w konfiguracji strony.

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

Dekoracja wyodrębnia zasób z elementu modelu wyświetlanego w widoku synchronizacji i dołącza identyfikator treści wariantu zasobu historii lokalnej do etykiety tekstowej wyświetlanej w widoku.

Końcowy etap to udostępnienie kreatora, który utworzy uczestnika historii lokalnej. Perspektywa Synchronizowanie zespołów definiuje globalną akcję synchronizacji, która umożliwia użytkownikom szybkie tworzenie synchronizacji. Możliwość tworzenia synchronizacji jest dodatkowo dostępna z paska narzędzi widoku synchronizacji. Na początek należy utworzyć punkt rozszerzenia synchronizeWizards:

<extension
point="org.eclipse.team.ui.synchronizeWizards">
<wizard
class="org.eclipse.team.synchronize.example.LocalHistorySynchronizeWizard"
icon="synced.png"
description="Synchronizuje wszystkie zasoby w obszarze roboczym względem stanu w historii lokalnej"
name="Synchronizacja z najnowszym stanem w historii lokalnej"
id="ExampleSynchronizeSupport.wizard1"/>
</extension>

Spowoduje to dodanie kreatora do listy. W metodzie finish() kreatora wystarczy utworzyć uczestnika i dodać go do menedżera synchronizacji.

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

Wnioski

Powyżej przedstawiono prosty przykład użycia interfejsu API synchronizacji. Aby łatwiej było go zrozumieć, pominięto opis niektórych szczegółów. Napisanie dobrze działającej i dokładnej procedury obsługi synchronizacji nie jest proste, a najtrudniejszą częścią jest zarządzanie informacjami o synchronizacji i powiadamianie o zmianach stanu synchronizacji. Interfejs użytkownika, o ile ten powiązany z klasą SubscriberParticipants jest odpowiedni, to łatwiejsza część zadania, gdy implementacja subskrybenta jest zakończona. Więcej przykładów można znaleźć, przeglądając w obszarze roboczym podklasy klas Subscriber i ISynchronizeParticipant modułu dodatkowego org.eclipse.team.example.filesystem.

W następnej sekcji opisano niektóre klasy i interfejsy, które mogą pomóc w napisaniu subskrybenta od podstaw, a także sposób przechowywania w pamięci podręcznej stanów synchronizacji między sesjami środowiska roboczego.