001    /*******************************************************************************
002     * Copyright (c) 2000, 2004 IBM Corporation and others.
003     * All rights reserved. This program and the accompanying materials
004     * are made available under the terms of the Eclipse Public License v1.0
005     * which accompanies this distribution, and is available at
006     * http://www.eclipse.org/legal/epl-v10.html
007     * 
008     * Contributors:
009     *     IBM Corporation - initial API and implementation
010     *******************************************************************************/
011    package org.eclipse.team.internal.ccvs.core;
012    
013    import java.util.*;
014    import org.eclipse.core.resources.*;
015    import org.eclipse.core.runtime.*;
016    import org.eclipse.osgi.util.NLS;
017    import org.eclipse.team.core.RepositoryProvider;
018    import org.eclipse.team.core.TeamException;
019    import org.eclipse.team.core.subscribers.*;
020    import org.eclipse.team.core.synchronize.SyncInfo;
021    import org.eclipse.team.core.synchronize.SyncInfoFilter;
022    import org.eclipse.team.core.variants.*;
023    import org.eclipse.team.internal.ccvs.core.resources.CVSWorkspaceRoot;
024    import org.eclipse.team.internal.ccvs.core.resources.RemoteFile;
025    import org.eclipse.team.internal.ccvs.core.syncinfo.CVSResourceVariantTree;
026    import org.eclipse.team.internal.ccvs.core.syncinfo.ResourceSyncInfo;
027    import org.eclipse.team.internal.ccvs.core.util.Util;
028    
029    /**
030     * A CVSMergeSubscriber is responsible for maintaining the remote trees for a merge into
031     * the workspace. The remote trees represent the CVS revisions of the start and end
032     * points (version or branch) of the merge.
033     * 
034     * This subscriber stores the remote handles in the resource tree sync info slot. When
035     * the merge is cancelled this sync info is cleared.
036     * 
037     * A merge can persist between workbench sessions and thus can be used as an
038     * ongoing merge.
039     * 
040     * TODO: Is the merge subscriber interested in workspace sync info changes?
041     * TODO: Do certain operations (e.g. replace with) invalidate a merge subscriber?
042     * TODO: How to ensure that sync info is flushed when merge roots are deleted?
043     */
044    public class CVSMergeSubscriber extends CVSSyncTreeSubscriber implements IResourceChangeListener, ISubscriberChangeListener {
045    
046            private final class MergeBaseTree extends CVSResourceVariantTree {
047                    // The merge synchronizer has been kept so that those upgrading
048                    // from 3.0 M8 to 3.0 M9 so not lose there ongoing merge state
049                    private PersistantResourceVariantByteStore mergedSynchronizer;
050                    private MergeBaseTree(ResourceVariantByteStore cache, CVSTag tag, boolean cacheFileContentsHint, String syncKeyPrefix) {
051                            super(cache, tag, cacheFileContentsHint);
052                            mergedSynchronizer = new PersistantResourceVariantByteStore(new QualifiedName(SYNC_KEY_QUALIFIER, syncKeyPrefix + "0merged")); //$NON-NLS-1$
053                    }
054                    public IResource[] refresh(IResource[] resources, int depth, IProgressMonitor monitor) throws TeamException {
055                            // Only refresh the base of a resource once as it should not change
056                            List unrefreshed = new ArrayList();
057                            for (int i = 0; i < resources.length; i++) {
058                                    IResource resource = resources[i];
059                                    if (!hasResourceVariant(resource)) {
060                                            unrefreshed.add(resource);
061                                    }
062                            }
063                            if (unrefreshed.isEmpty()) {
064                                    monitor.done();
065                                    return new IResource[0];
066                            }
067                            IResource[] refreshed = super.refresh((IResource[]) unrefreshed.toArray(new IResource[unrefreshed.size()]), depth, monitor);
068                            return refreshed;
069                    }
070                    public IResourceVariant getResourceVariant(IResource resource) throws TeamException {
071                            // Use the merged bytes for the base if there are some
072                            byte[] mergedBytes = mergedSynchronizer.getBytes(resource);
073                            if (mergedBytes != null) {
074                                    byte[] parentBytes = getByteStore().getBytes(resource.getParent());
075                                    if (parentBytes != null) {
076                                            return RemoteFile.fromBytes(resource, mergedBytes, parentBytes);
077                                    }
078                            }
079                            return super.getResourceVariant(resource);
080                    }
081                    
082                    /**
083                     * Mark the resource as merged by making it's base equal the remote
084                     */
085                    public void merged(IResource resource, byte[] remoteBytes) throws TeamException {
086                            if (remoteBytes == null) {
087                                    getByteStore().deleteBytes(resource);
088                            } else {
089                                    getByteStore().setBytes(resource, remoteBytes);
090                            }
091                    }
092                    
093                    /**
094                     * Return true if the remote has already been merged
095                     * (i.e. the base equals the remote).
096                     */
097                    public boolean isMerged(IResource resource, byte[] remoteBytes) throws TeamException {
098                            byte[] mergedBytes = getByteStore().getBytes(resource);
099                            return Util.equals(mergedBytes, remoteBytes);
100                    }
101                    
102                    /* (non-Javadoc)
103                     * @see org.eclipse.team.internal.ccvs.core.syncinfo.CVSResourceVariantTree#dispose()
104                     */
105                    public void dispose() {
106                            mergedSynchronizer.dispose();
107                            super.dispose();
108                    }
109            }
110    
111            public static final String ID = "org.eclipse.team.cvs.ui.cvsmerge-participant"; //$NON-NLS-1$
112            public static final String ID_MODAL = "org.eclipse.team.cvs.ui.cvsmerge-participant-modal"; //$NON-NLS-1$
113            private static final String UNIQUE_ID_PREFIX = "merge-"; //$NON-NLS-1$
114            
115            private CVSTag start, end;
116            private List roots;
117            private CVSResourceVariantTree remoteTree;
118            private MergeBaseTree baseTree;
119            
120            public CVSMergeSubscriber(IResource[] roots, CVSTag start, CVSTag end) {                
121                    this(getUniqueId(), roots, start, end);
122            }
123    
124            private static QualifiedName getUniqueId() {
125                    String uniqueId = Long.toString(System.currentTimeMillis());
126                    return new QualifiedName(ID, "CVS" + UNIQUE_ID_PREFIX + uniqueId); //$NON-NLS-1$
127            }
128            
129            public CVSMergeSubscriber(QualifiedName id, IResource[] roots, CVSTag start, CVSTag end) {              
130                    super(id, NLS.bind(CVSMessages.CVSMergeSubscriber_2, new String[] { start.getName(), end.getName() })); //$NON-NLS-1$ //$NON-NLS-2$
131                    this.start = start;
132                    this.end = end;
133                    this.roots = new ArrayList(Arrays.asList(roots));
134                    initialize();
135            }
136    
137            /* (non-Javadoc)
138             * @see org.eclipse.team.internal.ccvs.core.CVSWorkspaceSubscriber#initialize()
139             */
140            private void initialize() {                     
141                    QualifiedName id = getId();
142                    String syncKeyPrefix = id.getLocalName();
143                    PersistantResourceVariantByteStore remoteSynchronizer = new PersistantResourceVariantByteStore(new QualifiedName(SYNC_KEY_QUALIFIER, syncKeyPrefix + end.getName()));
144                    remoteTree = new CVSResourceVariantTree(remoteSynchronizer, getEndTag(), getCacheFileContentsHint()) {
145                            public IResource[] refresh(IResource[] resources, int depth, IProgressMonitor monitor) throws TeamException {
146                                    // Override refresh to compare file contents
147                                    monitor.beginTask(null, 100);
148                                    try {
149                                            IResource[] refreshed = super.refresh(resources, depth, monitor);
150                                            compareWithRemote(refreshed, Policy.subMonitorFor(monitor, 50));
151                                            return refreshed;
152                                    } finally {
153                                            monitor.done();
154                                    }
155                            }
156                    };
157                    PersistantResourceVariantByteStore baseSynchronizer = new PersistantResourceVariantByteStore(new QualifiedName(SYNC_KEY_QUALIFIER, syncKeyPrefix + start.getName()));
158                    baseTree = new MergeBaseTree(baseSynchronizer, getStartTag(), getCacheFileContentsHint(), syncKeyPrefix);
159                    
160                    ResourcesPlugin.getWorkspace().addResourceChangeListener(this);
161                    CVSProviderPlugin.getPlugin().getCVSWorkspaceSubscriber().addListener(this);
162            }
163    
164            protected SyncInfo getSyncInfo(IResource local, IResourceVariant base, IResourceVariant remote) throws TeamException {
165                    CVSMergeSyncInfo info = new CVSMergeSyncInfo(local, base, remote, this);
166                    info.init();
167                    return info;
168            }
169    
170            public void merged(IResource[] resources) throws TeamException {
171                    for (int i = 0; i < resources.length; i++) {
172                            IResource resource = resources[i];
173                            internalMerged(resource);
174                    }
175                    fireTeamResourceChange(SubscriberChangeEvent.asSyncChangedDeltas(this, resources));
176            }
177            
178            private void internalMerged(IResource resource) throws TeamException {
179                    byte[] remoteBytes = getRemoteByteStore().getBytes(resource);
180                    baseTree.merged(resource, remoteBytes);
181            }
182    
183            /* (non-Javadoc)
184             * @see org.eclipse.team.core.sync.TeamSubscriber#cancel()
185             */
186            public void cancel() {  
187                    ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);              
188                    remoteTree.dispose();
189                    baseTree.dispose();     
190            }
191    
192            /* (non-Javadoc)
193             * @see org.eclipse.team.core.sync.TeamSubscriber#roots()
194             */
195            public IResource[] roots() {
196                    return (IResource[]) roots.toArray(new IResource[roots.size()]);
197            }
198    
199            /* (non-Javadoc)
200             * @see org.eclipse.team.core.sync.TeamSubscriber#isSupervised(org.eclipse.core.resources.IResource)
201             */
202            public boolean isSupervised(IResource resource) throws TeamException {
203                    return getBaseTree().hasResourceVariant(resource) || getRemoteTree().hasResourceVariant(resource); 
204            }
205    
206            public CVSTag getStartTag() {
207                    return start;
208            }
209            
210            public CVSTag getEndTag() {
211                    return end;
212            }
213    
214            /*
215             * What to do when a root resource for this merge changes?
216             * Deleted, Move, Copied
217             * Changed in a CVS way (tag changed, revision changed...)
218             * Contents changed by user
219             * @see IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
220             */
221            public void resourceChanged(IResourceChangeEvent event) {
222                    try {
223                            IResourceDelta delta = event.getDelta();
224                            if(delta != null) {
225                                    delta.accept(new IResourceDeltaVisitor() {
226                                    public boolean visit(IResourceDelta delta) throws CoreException {
227                                            IResource resource = delta.getResource();
228                            
229                                            if (resource.getType()==IResource.PROJECT) {
230                                                    IProject project = (IProject)resource;
231                                                    if (!project.isAccessible()) {
232                                                            return false;
233                                                    }
234                                                    if ((delta.getFlags() & IResourceDelta.OPEN) != 0) {
235                                                            return false;
236                                                    } 
237                                                    if (RepositoryProvider.getProvider(project, CVSProviderPlugin.getTypeId()) == null) {
238                                                            return false;
239                                                    }
240                                            }
241                            
242                                            if (roots.contains(resource)) {
243                                                    if (delta.getKind() == IResourceDelta.REMOVED || delta.getKind() == IResourceDelta.MOVED_TO) {
244                                                            cancel();
245                                                    }
246                                                    // stop visiting children
247                                                    return false;
248                                            }
249                                            // keep visiting children
250                                            return true;
251                                    }
252                            });
253                            }
254                    } catch (CoreException e) {
255                            CVSProviderPlugin.log(e.getStatus());
256                    }
257            }
258    
259            /**
260             * Return whether the given resource has been merged with its 
261             * corresponding remote.
262             * @param resource the local resource
263             * @return boolean
264             * @throws TeamException
265             */
266            public boolean isMerged(IResource resource) throws TeamException {
267                    byte[] remoteBytes = getRemoteByteStore().getBytes(resource);
268                    return baseTree.isMerged(resource, remoteBytes);
269            }
270    
271            /* 
272             * Currently only the workspace subscriber knows when a project has been deconfigured. We will listen for these events
273             * and remove the root then forward to merge subscriber listeners.
274             * (non-Javadoc)
275             * @see org.eclipse.team.core.subscribers.ITeamResourceChangeListener#teamResourceChanged(org.eclipse.team.core.subscribers.TeamDelta[])
276             */
277            public void subscriberResourceChanged(ISubscriberChangeEvent[] deltas) {                
278                    for (int i = 0; i < deltas.length; i++) {
279                            ISubscriberChangeEvent delta = deltas[i];
280                            switch(delta.getFlags()) {
281                                    case ISubscriberChangeEvent.ROOT_REMOVED:
282                                            IResource resource = delta.getResource();
283                                            if(roots.remove(resource))      {
284                                                    fireTeamResourceChange(new ISubscriberChangeEvent[] {delta});
285                                            }                                               
286                                            break;
287                            }
288                    }
289            }
290    
291            /* (non-Javadoc)
292             * @see org.eclipse.team.internal.ccvs.core.CVSSyncTreeSubscriber#getBaseSynchronizationCache()
293             */
294            protected IResourceVariantTree getBaseTree() {
295                    return baseTree;
296            }
297    
298            /* (non-Javadoc)
299             * @see org.eclipse.team.internal.ccvs.core.CVSSyncTreeSubscriber#getRemoteSynchronizationCache()
300             */
301            protected IResourceVariantTree getRemoteTree() {
302                    return remoteTree;
303            }
304            
305            protected  boolean getCacheFileContentsHint() {
306                    return true;
307            }
308    
309            /*
310             * Mark as merged any local resources whose contents match that of the remote resource.
311             */
312            private void compareWithRemote(IResource[] refreshed, IProgressMonitor monitor) throws CVSException, TeamException {
313                    // For any remote changes, if the revision differs from the local, compare the contents.
314                    if (refreshed.length == 0) return;
315                    SyncInfoFilter.ContentComparisonSyncInfoFilter contentFilter =
316                            new SyncInfoFilter.ContentComparisonSyncInfoFilter();
317                    monitor.beginTask(null, refreshed.length * 100);
318                    for (int i = 0; i < refreshed.length; i++) {
319                            IResource resource = refreshed[i];
320                            if (resource.getType() == IResource.FILE) {
321                                    ICVSFile local = CVSWorkspaceRoot.getCVSFileFor((IFile)resource);
322                                    byte[] localBytes = local.getSyncBytes();
323                                    byte[] remoteBytes = getRemoteByteStore().getBytes(resource);
324                                    if (remoteBytes != null 
325                                                    && localBytes != null
326                                                    && local.exists()
327                                                    && !ResourceSyncInfo.getRevision(remoteBytes).equals(ResourceSyncInfo.getRevision(localBytes))
328                                                    && contentFilter.select(getSyncInfo(resource), Policy.subMonitorFor(monitor, 100))) {
329                                            // The contents are equals so mark the file as merged
330                                            internalMerged(resource);
331                                    }
332                            }
333                    }
334                    monitor.done();
335            }
336            
337            
338            private PersistantResourceVariantByteStore getRemoteByteStore() {
339                    return (PersistantResourceVariantByteStore)((CVSResourceVariantTree)getRemoteTree()).getByteStore();
340            }
341            
342            /* (non-Javadoc)
343             * @see java.lang.Object#equals(java.lang.Object)
344             */
345            public boolean equals(Object other) {
346                    if(this == other) return true;
347                    if(! (other instanceof CVSMergeSubscriber)) return false;
348                    CVSMergeSubscriber s = (CVSMergeSubscriber)other;
349                    return getEndTag().equals(s.getEndTag()) && 
350                               getStartTag().equals(s.getStartTag()) && rootsEqual(s);              
351            }
352    }