blob: bf75f60bf6226cc2d9bc6b1924cf789b97c7017c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010, 2011 Obeo.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.docs.intent.client.synchronizer.synchronizer;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.emf.cdo.util.InvalidURIException;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.Monitor;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.common.util.WrappedException;
import org.eclipse.emf.compare.Diff;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.mylyn.docs.intent.client.synchronizer.SynchronizerRepositoryClient;
import org.eclipse.mylyn.docs.intent.client.synchronizer.api.contribution.ISynchronizerExtension;
import org.eclipse.mylyn.docs.intent.client.synchronizer.api.contribution.ISynchronizerExtensionRegistry;
import org.eclipse.mylyn.docs.intent.client.synchronizer.factory.SynchronizerStatusFactory;
import org.eclipse.mylyn.docs.intent.client.synchronizer.listeners.GeneratedElementListener;
import org.eclipse.mylyn.docs.intent.client.synchronizer.strategy.DefaultSynchronizerStrategy;
import org.eclipse.mylyn.docs.intent.client.synchronizer.strategy.SynchronizerStrategy;
import org.eclipse.mylyn.docs.intent.collab.common.logger.IIntentLogger.LogType;
import org.eclipse.mylyn.docs.intent.collab.common.logger.IntentLogger;
import org.eclipse.mylyn.docs.intent.collab.handlers.adapters.RepositoryAdapter;
import org.eclipse.mylyn.docs.intent.compare.utils.EMFCompareUtils;
import org.eclipse.mylyn.docs.intent.core.compiler.CompilationMessageType;
import org.eclipse.mylyn.docs.intent.core.compiler.CompilationStatus;
import org.eclipse.mylyn.docs.intent.core.compiler.InstructionTraceabilityEntry;
import org.eclipse.mylyn.docs.intent.core.compiler.TraceabilityIndex;
import org.eclipse.mylyn.docs.intent.core.compiler.TraceabilityIndexEntry;
import org.eclipse.mylyn.docs.intent.core.document.IntentGenericElement;
import org.eclipse.mylyn.docs.intent.core.modelingunit.ResourceDeclaration;
/**
* In charge of comparing the compiled models of the repository with the compiled models generated at the
* location indicated by a Intent ResourceDeclaration.
*
* @author <a href="mailto:alex.lagarde@obeo.fr">Alex Lagarde</a>
* @author <a href="mailto:william.piers@obeo.fr">William Piers</a>
*/
public class IntentSynchronizer {
/**
* The Synchronizer strategy to use, defining several behaviors in case of conflict.
*/
private SynchronizerStrategy synchronizerStrategy;
/**
* Listens generated elements.
*/
private GeneratedElementListener defaultSynchronizedElementListener;
/**
* The repository client.
*/
private SynchronizerRepositoryClient repositoryClient;
/**
* IntentSynchronizer constructor.
*
* @param synchronizerRepositoryClient
* the repositoryClient
*/
public IntentSynchronizer(SynchronizerRepositoryClient synchronizerRepositoryClient) {
this.repositoryClient = synchronizerRepositoryClient;
this.synchronizerStrategy = new DefaultSynchronizerStrategy();
}
/**
* Sets the Synchronizer strategy to use.
*
* @param strategy
* the Synchronizer strategy to use
*/
public void setSynchronizerStrategy(SynchronizerStrategy strategy) {
this.synchronizerStrategy = strategy;
}
/**
* Sets the generatedElement listener, which will notify the Synchronizer if any generatedElement has
* changed.
*
* @param generatedElementListener
* the GeneratedElementListener
*/
public void setGeneratedElementListener(GeneratedElementListener generatedElementListener) {
this.defaultSynchronizedElementListener = generatedElementListener;
}
/**
* Using the given traceability index, compares the compiled models of the repository with the compiled
* models generated at the location indicated by this index ; if the two models aren't similar, adds a
* status representing the differences to the returned list of status.
*
* @param adapter
* the repositoryAdapter to use for getting the repository content
* @param tracabilityIndex
* the Traceability index to use
* @param progressMonitor
* the progress Monitor indicating if this synchronization operation has been canceled
* @return a list containing status relatives to synchronization
* @throws InterruptedException
* if this operation was interrupted
*/
public Collection<? extends CompilationStatus> synchronize(RepositoryAdapter adapter,
TraceabilityIndex tracabilityIndex, Monitor progressMonitor) throws InterruptedException {
final List<CompilationStatus> statusList = new ArrayList<CompilationStatus>();
final Collection<Resource> resourcesToUnload = Sets.newLinkedHashSet();
if (defaultSynchronizedElementListener != null) {
defaultSynchronizedElementListener.clearElementToListen();
}
Iterator<TraceabilityIndexEntry> indexEntryIterator = tracabilityIndex.getEntries().iterator();
while (indexEntryIterator.hasNext()) {
this.stopIfCanceled(progressMonitor);
final TraceabilityIndexEntry indexEntry = indexEntryIterator.next();
// First of all, we clear the old synchronization statuses
if (indexEntry.getResourceDeclaration() != null) {
clearSyncStatusesFromIndexEntry(indexEntry);
// We do not synchronize abstract resources (i.e. resources with no associated URI)
if (indexEntry.getResourceDeclaration().getUri() != null) {
// We then generate the synchronization status for this entry
final Collection<? extends CompilationStatus> synchronizedStatus = synchronize(adapter,
indexEntry, resourcesToUnload, progressMonitor);
statusList.addAll(synchronizedStatus);
}
}
}
// Unload all external resources
for (Resource resource : resourcesToUnload) {
try {
resource.unload();
// CHECKSTYLE:OFF
} catch (Exception e) {
// CHECKSTYLE:ON
IntentLogger.getInstance().logError(e);
}
}
return statusList;
}
/**
* Clears the synchronization statues associated to the given indexEntry (i.e in the ResourceDeclaration
* and the instructions that describes the generated content).
*
* @param indexEntry
* the index Entry containing the instructions to clear
*/
private void clearSyncStatusesFromIndexEntry(TraceabilityIndexEntry indexEntry) {
// We first remove the synchronization statutes associated to the resource Declaration
Iterator<CompilationStatus> statusIterator = indexEntry.getResourceDeclaration()
.getCompilationStatus().iterator();
while (statusIterator.hasNext()) {
CompilationStatus status = statusIterator.next();
if (isSyncStatus(status)) {
statusIterator.remove();
}
}
// Then, for each mapped element
for (EObject containedElement : indexEntry.getContainedElementToInstructions().keySet()) {
// We must remove the synchronization statuses from the instruction that generated this
// element
EList<InstructionTraceabilityEntry> instructionEntries = indexEntry
.getContainedElementToInstructions().get(containedElement);
if (instructionEntries != null) {
for (InstructionTraceabilityEntry instructionTraceabilityEntry : instructionEntries) {
IntentGenericElement instruction = instructionTraceabilityEntry.getInstruction();
if (instruction != null) {
clearSyncStatusesFromInstruction(instruction);
}
}
}
}
}
/**
* Clears the synchronization statues associated to the given instruction.
*
* @param instruction
* the instruction to clear
*/
private void clearSyncStatusesFromInstruction(IntentGenericElement instruction) {
EList<CompilationStatus> compilationStatus = instruction.getCompilationStatus();
if (compilationStatus != null) {
Iterator<CompilationStatus> iterator = compilationStatus.iterator();
while (iterator.hasNext()) {
CompilationStatus status = iterator.next();
if (isSyncStatus(status)) {
iterator.remove();
}
}
}
}
/**
* Returns true if the given status is related to a synchronization issue.
*
* @param status
* the status to test
* @return true if the given status is related to a synchronization issue
*/
private boolean isSyncStatus(CompilationStatus status) {
CompilationMessageType type = status.getType();
return type.equals(CompilationMessageType.SYNCHRONIZER_WARNING)
|| type.equals(CompilationMessageType.SYNCHRONIZER_INFO);
}
/**
* Using the given TraceabilitIndexEntry, compares the model located at the indicated path with the model
* located at the path indicated by the resource declaration.
*
* @param adapter
* the repositoryAdapter to use for getting the repository content
* @param indexEntry
* the indexEntry to use for obtaining synchronization informations
* @param resourcesToUnload
* the resources that should be unloaded after synchronization
* @param progressMonitor
* the progress Monitor indicating if this synchronization operation has been canceled
* @return a list of status relatives to synchronization of the model described in the given indexEntry
* @throws InterruptedException
* if this operation was interrupted
*/
private Collection<? extends CompilationStatus> synchronize(final RepositoryAdapter adapter,
final TraceabilityIndexEntry indexEntry, Collection<Resource> resourcesToUnload,
Monitor progressMonitor) throws InterruptedException {
List<CompilationStatus> statusList = new ArrayList<CompilationStatus>();
boolean continueSynchronization = true;
// Step 1 : getting the repository resource
stopIfCanceled(progressMonitor);
Resource internalResource = getInternalResource(adapter, indexEntry);
stopIfCanceled(progressMonitor);
// Step 2 : getting the generated resource
Resource externalResource = getExternalResource(adapter, indexEntry);
stopIfCanceled(progressMonitor);
// Step 3 : if one of the resource is null,
// we use the strategy to handle these cases
if (internalResource == null) {
final List<Resource> result = new ArrayList<Resource>();
final Resource finalExternalResource = externalResource;
result.add(synchronizerStrategy.handleNullInternalResource(indexEntry.getGeneratedResourcePath(),
finalExternalResource));
if (!result.isEmpty()) {
internalResource = result.get(0);
}
// TODO : we can create here a status if the internal Resource has not been created
continueSynchronization = internalResource != null;
}
if (externalResource == null) {
final List<Resource> result = new ArrayList<Resource>();
final Resource finalInternalResource = internalResource;
Resource handleNullExternalResource = synchronizerStrategy.handleNullExternalResource(indexEntry
.getResourceDeclaration(), finalInternalResource, indexEntry.getResourceDeclaration()
.getUri());
if (handleNullExternalResource != null) {
result.add(handleNullExternalResource);
}
if (!result.isEmpty()) {
externalResource = result.get(0);
} else {
Collection<? extends CompilationStatus> statusForNullExternalresource = synchronizerStrategy
.getStatusForNullExternalResource(indexEntry.getResourceDeclaration(),
indexEntry.getGeneratedResourcePath());
statusList.addAll(statusForNullExternalresource);
updateSynchronizedElementsListeners(getResourceDeclarationURI(indexEntry
.getResourceDeclaration()));
}
// TODO : we can create here a status if the external Resource has not been created
continueSynchronization = externalResource != null;
}
stopIfCanceled(progressMonitor);
// If no resource was null or if the strategy authorizes the operation to continue
if (continueSynchronization) {
if (externalResource.getContents().isEmpty() && !internalResource.getContents().isEmpty()) {
Collection<? extends CompilationStatus> statusForEmptyExternalresource = synchronizerStrategy
.getStatusForEmptyExternalResource(indexEntry.getResourceDeclaration(),
indexEntry.getGeneratedResourcePath());
statusList.addAll(statusForEmptyExternalresource);
updateSynchronizedElementsListeners(getResourceDeclarationURI(indexEntry
.getResourceDeclaration()));
} else if (internalResource.getContents().isEmpty() && !externalResource.getContents().isEmpty()) {
Collection<? extends CompilationStatus> statusForEmptyInternalResource = synchronizerStrategy
.getStatusForEmptyInternalResource(indexEntry.getResourceDeclaration(),
indexEntry.getGeneratedResourcePath());
statusList.addAll(statusForEmptyInternalResource);
updateSynchronizedElementsListeners(getResourceDeclarationURI(indexEntry
.getResourceDeclaration()));
} else {
// Step 4 : comparing those two resources
Resource leftResource = synchronizerStrategy.getLeftResource(internalResource,
externalResource);
Resource rightResource = synchronizerStrategy.getRightResource(internalResource,
externalResource);
try {
List<Diff> differences = null;
stopIfCanceled(progressMonitor);
differences = compareResource(indexEntry, leftResource, rightResource);
stopIfCanceled(progressMonitor);
// Step 5 : creating status from the Diff
statusList = createSynchronizerSatusListFromComparison(indexEntry, differences,
progressMonitor);
resourcesToUnload.add(rightResource);
// CHECKSTYLE:OFF
} catch (Exception e) {
// CHECKSTYLE:ON
// Unloading the external resource if issue was encountered
try {
externalResource.unload();
// CHECKSTYLE:OFF
} catch (Exception e2) {
// CHECKSTYLE:ON
IntentLogger.getInstance().logError(e2);
}
}
// Step 7 : we ask the generated element listener to listen to the external Resource
updateSynchronizedElementsListeners(externalResource.getURI());
}
} else {
stopIfCanceled(progressMonitor);
// TODO we can imagine creating a status, unless it's the responsability of the Strategy
}
return statusList;
}
/**
* Notifies the listeners in charge of detecting any changes made outside of repository that this
* synchronizer wants to listen the resource located at the given uri.
*
* @param uri
* the uri of the resource the synchronizer wants to listen
*/
public void updateSynchronizedElementsListeners(URI uri) {
boolean foundSpecificSynchronizer = false;
// Step 1 : searching through all contributed SynchronizerExtensions
// for an Extension matching the scheme of the given uri
for (ISynchronizerExtension synchronizerExtension : ISynchronizerExtensionRegistry
.getSynchronizerExtensions(uri)) {
if (synchronizerExtension != null) {
synchronizerExtension.addListenedElements(repositoryClient, Sets.newHashSet(uri));
foundSpecificSynchronizer = true;
}
}
// Step 2 : if no synchronizer extensions is define for the given URI, then we notify the generated
// elements listener
if (!foundSpecificSynchronizer && this.defaultSynchronizedElementListener != null) {
this.defaultSynchronizedElementListener.addElementToListen(uri.trimFragment());
}
}
/**
* Ensure the current synchronization operation (represented by the given monitor) hasn't been canceled ;
* if so, throws an InterruptedException.
*
* @param progressMonitor
* the progressMonitor to use for determining if the operation has been canceled
* @throws InterruptedException
* if the operation has been canceled
*/
private void stopIfCanceled(Monitor progressMonitor) throws InterruptedException {
if (progressMonitor.isCanceled()) {
throw new InterruptedException();
}
}
/**
* Creates the synchronization statuses corresponding to the given list of {@link Diff}.
*
* @param indexEntry
* the indexEntry indicating the compared resources
* @param differences
* the list of {@link Diff} between the compared resources
* @param progressMonitor
* the progress Monitor indicating if this synchronization operation has been canceled
* @return a list containing the synchronization statuses corresponding to the given list of {@link Diff}
* @throws InterruptedException
* if the operation has been canceled
*/
private List<CompilationStatus> createSynchronizerSatusListFromComparison(
TraceabilityIndexEntry indexEntry, List<Diff> differences, Monitor progressMonitor)
throws InterruptedException {
Map<IntentGenericElement, Collection<CompilationStatus>> elementToSyncStatus = Maps
.newLinkedHashMap();
List<CompilationStatus> statusList = new ArrayList<CompilationStatus>();
for (Diff difference : differences) {
stopIfCanceled(progressMonitor);
// For each synchronization status relative to the consider Diff
for (CompilationStatus newStatus : SynchronizerStatusFactory.createStatusFromDiff(indexEntry,
difference)) {
stopIfCanceled(progressMonitor);
if (elementToSyncStatus.get(newStatus.getTarget()) == null) {
elementToSyncStatus.put(newStatus.getTarget(), Lists.<CompilationStatus> newArrayList());
}
elementToSyncStatus.get(newStatus.getTarget()).add(newStatus);
statusList.add(newStatus);
}
}
return statusList;
}
/**
* Return the resource containing the compiled model currently inspected (<b>internal</b> resource of the
* repository).
*
* @param adapter
* the repositoryAdapter to use for getting the repository content
* @param indexEntry
* the indexEntry indicating the location of the compiled resource
* @return the resource containing the compiled model currently inspected (<b>internal</b> resource of the
* repository)
*/
private Resource getInternalResource(RepositoryAdapter adapter, TraceabilityIndexEntry indexEntry) {
try {
return adapter.getResource(indexEntry.getGeneratedResourcePath());
// CHECKSTYLE:OFF
} catch (Exception e) {
// CHECKSTYLE:ON
return null;
}
}
/**
* Returns the resource containing the compiled model currently inspected (<b>external</b> resource of the
* repository : can be in a workspace, on internet...).
*
* @param adapter
* the current {@link RepositoryAdapter}
* @param indexEntry
* the indexEntry indicating the location of the compiled resource
* @return the resource containing the compiled model currently inspected (<b>external</b> resource of the
* repository) - can be null if the resource doesn't exist.
*/
private Resource getExternalResource(RepositoryAdapter adapter, TraceabilityIndexEntry indexEntry) {
ResourceSet resourceSet = adapter.getResourceSet();
Resource resource = null;
if (indexEntry.getResourceDeclaration() != null) {
URI externalURI = getResourceDeclarationURI(indexEntry.getResourceDeclaration());
try {
resource = resourceSet.getResource(externalURI, true);
} catch (WrappedException e) {
resource = null;
} catch (InvalidURIException e) {
resource = null;
} catch (IllegalArgumentException e) {
resource = null;
}
}
return resource;
}
/**
* Compares the roots of the given resources and return a list of differences.
*
* @param indexEntry
* the index entry declaring the left & right resources
* @param leftResource
* the <i>"left"</i> resource of the two resources to get compared.
* @param rightResource
* the <i>"right"</i> resource of the two resources to get compared.
* @return a list of Diff corresponding to all the differences between the left resources and the right
* resource
* @throws InterruptedException
* if the comparison is interrupted
*/
private List<Diff> compareResource(TraceabilityIndexEntry indexEntry, Resource leftResource,
Resource rightResource) throws InterruptedException {
List<Diff> differences = Lists.newArrayList();
try {
URI workingCopyResourceURI = URI.createURI(indexEntry.getResourceDeclaration().getUri()
.toString());
Notifier leftTarget = leftResource;
Notifier rightTarget = rightResource;
// If we are synchronizing the whole resource
if (workingCopyResourceURI.hasFragment()) {
leftTarget = leftResource.getContents().iterator().next();
rightTarget = rightResource.getEObject(workingCopyResourceURI.fragment());
}
if (leftTarget != null && rightTarget != null) {
differences.addAll(EMFCompareUtils.compare(leftTarget, rightTarget).getDifferences());
} else {
IntentLogger.getInstance().log(LogType.ERROR,
"Could not synchronize resource " + workingCopyResourceURI + ": resource was null");
}
// CHECKSTYLE:OFF
} catch (Exception e) {
// CHECKSTYLE :ON
IntentLogger.getInstance().logError(e);
}
return differences;
}
/**
* Disposes elements.
*/
public void dispose() {
defaultSynchronizedElementListener.dispose();
}
private static URI getResourceDeclarationURI(ResourceDeclaration resourceDeclaration) {
return URI.createURI(resourceDeclaration.getUri().toString().replace("\"", ""));
}
}