1 /*******************************************************************************
2  * Copyright (c) 2000, 2009 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.team.internal.ccvs.core;
15 
16 import java.io.*;
17 import java.net.URI;
18 import java.util.*;
19 
20 import org.eclipse.core.resources.*;
21 import org.eclipse.core.resources.team.*;
22 import org.eclipse.core.runtime.*;
23 import org.eclipse.core.runtime.Status;
24 import org.eclipse.core.runtime.jobs.ISchedulingRule;
25 import org.eclipse.osgi.util.NLS;
26 import org.eclipse.team.core.RepositoryProvider;
27 import org.eclipse.team.core.TeamException;
28 import org.eclipse.team.core.history.IFileHistoryProvider;
29 import org.eclipse.team.internal.ccvs.core.client.*;
30 import org.eclipse.team.internal.ccvs.core.client.Command.KSubstOption;
31 import org.eclipse.team.internal.ccvs.core.client.Command.LocalOption;
32 import org.eclipse.team.internal.ccvs.core.client.listeners.*;
33 import org.eclipse.team.internal.ccvs.core.filehistory.CVSFileHistoryProvider;
34 import org.eclipse.team.internal.ccvs.core.resources.CVSWorkspaceRoot;
35 import org.eclipse.team.internal.ccvs.core.resources.EclipseSynchronizer;
36 import org.eclipse.team.internal.ccvs.core.syncinfo.*;
37 import org.eclipse.team.internal.ccvs.core.util.*;
38 import org.eclipse.team.internal.core.streams.CRLFtoLFInputStream;
39 import org.eclipse.team.internal.core.streams.LFtoCRLFInputStream;
40 
41 /**
42  * CVS implementation of {@link RepositoryProvider}
43  */
44 public class CVSTeamProvider extends RepositoryProvider {
45 
46 	private static final ResourceRuleFactory RESOURCE_RULE_FACTORY = new CVSResourceRuleFactory();
47 
48 	private static final boolean IS_CRLF_PLATFORM = Arrays.equals(
49 		System.getProperty("line.separator").getBytes(), new byte[] { '\r', '\n' }); //$NON-NLS-1$
50 
51 	public static final IStatus OK = new Status(IStatus.OK, CVSProviderPlugin.ID, 0, CVSMessages.ok, null);
52 
53 	private CVSWorkspaceRoot workspaceRoot;
54 	private IProject project;
55 
56 	private static MoveDeleteHook moveDeleteHook= new MoveDeleteHook();
57 	private static CVSCoreFileModificationValidator fileModificationValidator;
58 	private static CVSFileHistoryProvider fileHistoryProvider;
59 
60 	// property used to indicate whether new directories should be discovered for the project
61 	private final static QualifiedName FETCH_ABSENT_DIRECTORIES_PROP_KEY =
62 		new QualifiedName("org.eclipse.team.cvs.core", "fetch_absent_directories");  //$NON-NLS-1$  //$NON-NLS-2$
63 	// property used to indicate whether the project is configured to use Watch/edit
64 	private final static QualifiedName WATCH_EDIT_PROP_KEY =
65 		new QualifiedName("org.eclipse.team.cvs.core", "watch_edit");  //$NON-NLS-1$  //$NON-NLS-2$
66 
67 	/**
68 	 * Session property key used to indicate that the project, although not officially shared,
69 	 * is a target of a CVS operation.
70 	 */
71 	private static final QualifiedName TEMP_SHARED = new QualifiedName(CVSProviderPlugin.ID, "tempShare"); //$NON-NLS-1$
72 
73 	/**
74 	 * Return whether the project is mapped to CVS or is the target of a CVS operation
75 	 * that will most likely lead to the project being shared.
76 	 * @param project the project
77 	 * @return whether the project is mapped to CVS or is the target of a CVS operation
78 	 * that will most likely lead to the project being shared
79 	 */
isSharedWithCVS(IProject project)80 	public static boolean isSharedWithCVS(IProject project) {
81 		if (project.isAccessible()) {
82 			if (RepositoryProvider.isShared(project)) {
83 				RepositoryProvider provider = RepositoryProvider.getProvider(project, CVSProviderPlugin.getTypeId());
84 				if (provider != null)
85 					return true;
86 			}
87 			try {
88 				Object sessionProperty = project.getSessionProperty(TEMP_SHARED);
89 				return sessionProperty != null && sessionProperty.equals(Boolean.TRUE);
90 			} catch (CoreException e) {
91 				CVSProviderPlugin.log(e);
92 			}
93 		}
94 		return false;
95 	}
96 
97 	/**
98 	 * Mark the project as being a target of a CVS operation so the sync info management
99 	 * will occur.
100 	 * @param project the project
101 	 */
markAsTempShare(IProject project)102 	public static void markAsTempShare(IProject project) {
103 		if (RepositoryProvider.isShared(project))
104 			return;
105 		try {
106 			project.setSessionProperty(CVSTeamProvider.TEMP_SHARED, Boolean.TRUE);
107 		} catch (CoreException e) {
108 			CVSProviderPlugin.log(e);
109 		}
110 	}
111 
112 	/**
113 	 * Return the file modification validator used for all CVS repository providers.
114 	 * @return the file modification validator used for all CVS repository providers
115 	 */
internalGetFileModificationValidator()116 	protected static CVSCoreFileModificationValidator internalGetFileModificationValidator() {
117 		if (CVSTeamProvider.fileModificationValidator == null) {
118 			CVSTeamProvider.fileModificationValidator = new CVSCoreFileModificationValidator();
119 		}
120 		return CVSTeamProvider.fileModificationValidator;
121 	}
122 
123 	/**
124 	 * No-arg Constructor for IProjectNature conformance
125 	 */
CVSTeamProvider()126 	public CVSTeamProvider() {
127 	}
128 
129 	@Override
deconfigure()130 	public void deconfigure() {
131 	}
132 
133 	@Override
deconfigured()134 	public void deconfigured() {
135 		// when a nature is removed from the project, notify the synchronizer that
136 		// we no longer need the sync info cached. This does not affect the actual CVS
137 		// meta directories on disk, and will remain unless a client calls unmanage().
138 		try {
139 			EclipseSynchronizer.getInstance().deconfigure(getProject(), null);
140 			internalSetWatchEditEnabled(null);
141 			internalSetFetchAbsentDirectories(null);
142 		} catch(CVSException e) {
143 			// Log the exception and let the disconnect continue
144 			CVSProviderPlugin.log(e);
145 		}
146 		ResourceStateChangeListeners.getListener().projectDeconfigured(getProject());
147 	}
148 	@Override
getProject()149 	public IProject getProject() {
150 		return project;
151 	}
152 
153 	@Override
setProject(IProject project)154 	public void setProject(IProject project) {
155 		this.project = project;
156 		this.workspaceRoot = new CVSWorkspaceRoot(project);
157 		// We used to check to see if the project had CVS folders and log
158 		// if it didn't However, in some scenarios, the project can be mapped
159 		// before the CVS folders have been created (see bug 173610)
160 	}
161 
162 	/**
163 	 * Return the remote location to which the receiver's project is mapped.
164 	 */
getRemoteLocation()165 	public ICVSRepositoryLocation getRemoteLocation() throws CVSException {
166 		try {
167 			return workspaceRoot.getRemoteLocation();
168 		} catch (CVSException e) {
169 			// If we can't get the remote location, we should disconnect since nothing can be done with the provider
170 			try {
171 				RepositoryProvider.unmap(project);
172 			} catch (TeamException ex) {
173 				CVSProviderPlugin.log(ex);
174 			}
175 			// We need to trigger a decorator refresh
176 			throw e;
177 		}
178 	}
179 
getCVSWorkspaceRoot()180 	public CVSWorkspaceRoot getCVSWorkspaceRoot() {
181 		return workspaceRoot;
182 	}
183 
184 	/*
185 	 * Generate an exception if the resource is not a child of the project
186 	 */
checkIsChild(IResource resource)187 	private void checkIsChild(IResource resource) throws CVSException {
188 		if (!isChildResource(resource))
189 			throw new CVSException(new Status(IStatus.ERROR, CVSProviderPlugin.ID, TeamException.UNABLE,
190 				NLS.bind(CVSMessages.CVSTeamProvider_invalidResource, (new Object[] {resource.getFullPath().toString(), project.getName()})),
191 				null));
192 	}
193 
194 	/*
195 	 * Get the arguments to be passed to a commit or update
196 	 */
getValidArguments(IResource[] resources, LocalOption[] options)197 	private String[] getValidArguments(IResource[] resources, LocalOption[] options) throws CVSException {
198 		List<String> arguments = new ArrayList<>(resources.length);
199 		for (IResource resource : resources) {
200 			checkIsChild(resource);
201 			IPath cvsPath = resource.getFullPath().removeFirstSegments(1);
202 			if (cvsPath.segmentCount() == 0) {
203 				arguments.add(Session.CURRENT_LOCAL_FOLDER);
204 			} else {
205 				arguments.add(cvsPath.toString());
206 			}
207 		}
208 		return arguments.toArray(new String[arguments.size()]);
209 	}
210 
getCVSArguments(IResource[] resources)211 	private ICVSResource[] getCVSArguments(IResource[] resources) {
212 		ICVSResource[] cvsResources = new ICVSResource[resources.length];
213 		for (int i = 0; i < cvsResources.length; i++) {
214 			cvsResources[i] = CVSWorkspaceRoot.getCVSResourceFor(resources[i]);
215 		}
216 		return cvsResources;
217 	}
218 
219 	/*
220 	 * This method expects to be passed an InfiniteSubProgressMonitor
221 	 */
setRemoteRoot(ICVSRepositoryLocation location, IProgressMonitor monitor)222 	public void setRemoteRoot(ICVSRepositoryLocation location, IProgressMonitor monitor) throws TeamException {
223 
224 		// Check if there is a differnece between the new and old roots
225 		final String root = location.getLocation(false);
226 		if (root.equals(workspaceRoot.getRemoteLocation()))
227 			return;
228 
229 		try {
230 			workspaceRoot.getLocalRoot().run(progress -> {
231 				try {
232 					// 256 ticks gives us a maximum of 1024 which seems reasonable for folders is a project
233 					progress.beginTask(null, 100);
234 					final IProgressMonitor monitor1 = Policy.infiniteSubMonitorFor(progress, 100);
235 					monitor1.beginTask(null, 256);
236 
237 					// Visit all the children folders in order to set the root in the folder sync info
238 					workspaceRoot.getLocalRoot().accept(new ICVSResourceVisitor() {
239 						public void visitFile(ICVSFile file) throws CVSException {}
240 						public void visitFolder(ICVSFolder folder) throws CVSException {
241 							monitor1.worked(1);
242 							FolderSyncInfo info = folder.getFolderSyncInfo();
243 							if (info != null) {
244 								monitor1.subTask(NLS.bind(CVSMessages.CVSTeamProvider_updatingFolder, new String[] { info.getRepository() }));
245 								MutableFolderSyncInfo newInfo = info.cloneMutable();
246 								newInfo.setRoot(root);
247 								folder.setFolderSyncInfo(newInfo);
248 								folder.acceptChildren(this);
249 							}
250 						}
251 					});
252 				} finally {
253 					progress.done();
254 				}
255 			}, monitor);
256 		} finally {
257 			monitor.done();
258 		}
259 	}
260 
261 	/*
262 	 * Helper to indicate if the resource is a child of the receiver's project
263 	 */
isChildResource(IResource resource)264 	private boolean isChildResource(IResource resource) {
265 		return resource.getProject().getName().equals(project.getName());
266 	}
267 
configureProject()268 	public void configureProject() throws CoreException {
269 		getProject().setSessionProperty(TEMP_SHARED, null);
270 		ResourceStateChangeListeners.getListener().projectConfigured(getProject());
271 	}
272 	/**
273 	 * Sets the keyword substitution mode for the specified resources.
274 	 * <p>
275 	 * Applies the following rules in order:
276 	 * </p>
277 	 * <ul>
278 	 *   <li>If a file is not managed, skips it.</li>
279 	 *   <li>If a file is not changing modes, skips it.</li>
280 	 *   <li>If a file is being changed from binary to text, corrects line delimiters
281 	 *       then commits it, then admins it.</li>
282 	 *   <li>If a file is added, changes the resource sync information locally.</li>
283 	 *   <li>Otherwise commits the file (with FORCE to create a new revision), then admins it.</li>
284 	 * </ul>
285 	 * All files that are admin'd are committed with FORCE to prevent other developers from
286 	 * casually trying to commit pending changes to the repository without first checking out
287 	 * a new copy.  This is not a perfect solution, as they could just as easily do an UPDATE
288 	 * and not obtain the new keyword sync info.
289 	 *
290 	 * @param changeSet a map from IFile to KSubstOption
291 	 * @param monitor the progress monitor
292 	 * @return a status code indicating success or failure of the operation
293 	 *
294 	 * @throws TeamException
295 	 */
setKeywordSubstitution(final Map changeSet, final String comment, IProgressMonitor monitor)296 	public IStatus setKeywordSubstitution(final Map /* from IFile to KSubstOption */ changeSet,
297 		final String comment,
298 		IProgressMonitor monitor) throws TeamException {
299 		final IStatus[] result = new IStatus[] { ICommandOutputListener.OK };
300 		workspaceRoot.getLocalRoot().run(monitor1 -> {
301 			final Map /* from KSubstOption to List of String */ filesToAdmin = new HashMap();
302 			final Collection<ICVSFile> filesToCommitAsText = new HashSet<>(); // need fast lookup
303 			final boolean useCRLF = IS_CRLF_PLATFORM && (CVSProviderPlugin.getPlugin().isUsePlatformLineend());
304 
305 			/*** determine the resources to be committed and/or admin'd ***/
306 			for (Iterator it1 = changeSet.entrySet().iterator(); it1.hasNext();) {
307 				Map.Entry entry1 = (Map.Entry) it1.next();
308 				IFile file = (IFile) entry1.getKey();
309 				KSubstOption toKSubst1 = (KSubstOption) entry1.getValue();
310 
311 				// only set keyword substitution if resource is a managed file
312 				checkIsChild(file);
313 				ICVSFile mFile = CVSWorkspaceRoot.getCVSFileFor(file);
314 				if (! mFile.isManaged()) continue;
315 
316 				// only set keyword substitution if new differs from actual
317 				byte[] syncBytes = mFile.getSyncBytes();
318 				KSubstOption fromKSubst = ResourceSyncInfo.getKeywordMode(syncBytes);
319 				if (toKSubst1.equals(fromKSubst)) continue;
320 
321 				// change resource sync info immediately for an outgoing addition
322 				if (ResourceSyncInfo.isAddition(syncBytes)) {
323 					mFile.setSyncBytes(ResourceSyncInfo.setKeywordMode(syncBytes, toKSubst1), ICVSFile.UNKNOWN);
324 					continue;
325 				}
326 
327 				// nothing do to for deletions
328 				if (ResourceSyncInfo.isDeletion(syncBytes)) continue;
329 
330 				// file exists remotely so we'll have to commit it
331 				if (fromKSubst.isBinary() && ! toKSubst1.isBinary()) {
332 					// converting from binary to text
333 					cleanLineDelimiters(file, useCRLF, new NullProgressMonitor()); // XXX need better progress monitoring
334 					// remember to commit the cleaned resource as text before admin
335 					filesToCommitAsText.add(mFile);
336 				}
337 				// remember to admin the resource
338 				List list1 = (List) filesToAdmin.get(toKSubst1);
339 				if (list1 == null) {
340 					list1 = new ArrayList();
341 					filesToAdmin.put(toKSubst1, list1);
342 				}
343 				list1.add(mFile);
344 			}
345 
346 			/*** commit then admin the resources ***/
347 			// compute the total work to be performed
348 			int totalWork = filesToCommitAsText.size() + 1;
349 			for (Iterator it2 = filesToAdmin.values().iterator(); it2.hasNext();) {
350 				List list2 = (List) it2.next();
351 				totalWork += list2.size();
352 				totalWork += 1; // Add 1 for each connection that needs to be made
353 			}
354 			if (totalWork != 0) {
355 				monitor1.beginTask(CVSMessages.CVSTeamProvider_settingKSubst, totalWork);
356 				try {
357 					// commit files that changed from binary to text
358 					// NOTE: The files are committed as text with conversions even if the
359 					//       resource sync info still says "binary".
360 					if (!filesToCommitAsText.isEmpty()) {
361 						Session session1 = new Session(workspaceRoot.getRemoteLocation(), workspaceRoot.getLocalRoot(), true /* output to console */);
362 						session1.open(Policy.subMonitorFor(monitor1, 1), true /* open for modification */);
363 						try {
364 							String keywordChangeComment = comment;
365 							if (keywordChangeComment == null || keywordChangeComment.length() == 0)
366 								keywordChangeComment = CVSMessages.CVSTeamProvider_changingKeywordComment;
367 							result[0] = Command.COMMIT.execute(
368 								session1,
369 								Command.NO_GLOBAL_OPTIONS,
370 								new LocalOption[] { Command.DO_NOT_RECURSE, Commit.FORCE,
371 									Command.makeArgumentOption(Command.MESSAGE_OPTION, keywordChangeComment) },
372 								filesToCommitAsText.toArray(new ICVSResource[filesToCommitAsText.size()]),
373 								filesToCommitAsText,
374 								null,
375 								Policy.subMonitorFor(monitor1, filesToCommitAsText.size()));
376 						} finally {
377 							session1.close();
378 						}
379 
380 						// if errors were encountered, abort
381 						if (! result[0].isOK()) return;
382 					}
383 
384 					// admin files that changed keyword substitution mode
385 					// NOTE: As confirmation of the completion of a command, the server replies
386 					//       with the RCS command output if a change took place.  Rather than
387 					//       assume that the command succeeded, we listen for these lines
388 					//       and update the local ResourceSyncInfo for the particular files that
389 					//       were actually changed remotely.
390 					for (Iterator it3 = filesToAdmin.entrySet().iterator(); it3.hasNext();) {
391 						Map.Entry entry2 = (Map.Entry) it3.next();
392 						final KSubstOption toKSubst2 = (KSubstOption) entry2.getKey();
393 						final List list3 = (List) entry2.getValue();
394 						// do it
395 						Session session2 = new Session(workspaceRoot.getRemoteLocation(), workspaceRoot.getLocalRoot(), true /* output to console */);
396 						session2.open(Policy.subMonitorFor(monitor1, 1), true /* open for modification */);
397 						try {
398 							result[0] = Command.ADMIN.execute(
399 								session2,
400 								Command.NO_GLOBAL_OPTIONS,
401 								new LocalOption[] { toKSubst2 },
402 								(ICVSResource[]) list3.toArray(new ICVSResource[list3.size()]),
403 								new AdminKSubstListener(toKSubst2),
404 								Policy.subMonitorFor(monitor1, list3.size()));
405 						} finally {
406 							session2.close();
407 						}
408 						// if errors were encountered, abort
409 						if (! result[0].isOK()) return;
410 					}
411 				} finally {
412 					monitor1.done();
413 				}
414 			}
415 		}, Policy.monitorFor(monitor));
416 		return result[0];
417 	}
418 
419 	/**
420 	 * This method translates the contents of a file from binary into text (ASCII).
421 	 * Fixes the line delimiters in the local file to reflect the platform's
422 	 * native encoding.  Performs CR/LF -&gt; LF or LF -&gt; CR/LF conversion
423 	 * depending on the platform but does not affect delimiters that are
424 	 * already correctly encoded.
425 	 */
cleanLineDelimiters(IFile file, boolean useCRLF, IProgressMonitor progress)426 	public static void cleanLineDelimiters(IFile file, boolean useCRLF, IProgressMonitor progress)
427 		throws CVSException {
428 		try {
429 			// convert delimiters in memory
430 			ByteArrayOutputStream bos = new ByteArrayOutputStream();
431 			InputStream is = new BufferedInputStream(file.getContents());
432 			try {
433 				// Always convert CR/LF into LFs
434 				is = new CRLFtoLFInputStream(is);
435 				if (useCRLF) {
436 					// For CR/LF platforms, translate LFs to CR/LFs
437 					is = new LFtoCRLFInputStream(is);
438 				}
439 				for (int b; (b = is.read()) != -1;) bos.write(b);
440 				bos.close();
441 			} finally {
442 				is.close();
443 			}
444 			// write file back to disk with corrected delimiters if changes were made
445 			ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
446 			file.setContents(bis, false /*force*/, false /*keepHistory*/, progress);
447 		} catch (CoreException e) {
448 			throw CVSException.wrapException(file, CVSMessages.CVSTeamProvider_cleanLineDelimitersException, e);
449 		} catch (IOException e) {
450 			throw CVSException.wrapException(file, CVSMessages.CVSTeamProvider_cleanLineDelimitersException, e);
451 		}
452 	}
453 
454 	@Override
getID()455 	public String getID() {
456 		return CVSProviderPlugin.getTypeId();
457 	}
458 
459 	@Override
getMoveDeleteHook()460 	public IMoveDeleteHook getMoveDeleteHook() {
461 		return moveDeleteHook;
462 	}
463 
464 	@Override
getFileModificationValidator()465 	public IFileModificationValidator getFileModificationValidator() {
466 		return getFileModificationValidator2();
467 	}
468 
469 	@Override
getFileModificationValidator2()470 	public FileModificationValidator getFileModificationValidator2() {
471 		return internalGetFileModificationValidator();
472 	}
473 
474 	/**
475 	 * Checkout (cvs edit) the provided resources so they can be modified locally and committed.
476 	 * This will make any read-only resources in the list writable and will notify the server
477 	 * that the file is being edited. This notification may be done immediately or at some
478 	 * later point depending on whether contact with the server is possible at the time of
479 	 * invocation or the value of the notify server parameter.
480 	 *
481 	 * The recurse parameter is equivalent to the cvs local options -l (<code>true</code>) and
482 	 * -R (<code>false</code>). The notifyServer parameter can be used to defer server contact
483 	 * until the next command. This may be appropriate if no shell or progress monitor is available
484 	 * to the caller. The notification bit field indicates what temporary watches are to be used while
485 	 * the file is being edited. The possible values that can be ORed together are ICVSFile.EDIT,
486 	 * ICVSFile.UNEDIT and ICVSFile.COMMIT. There pre-ORed convenience values ICVSFile.NO_NOTIFICATION
487 	 * and ICVSFile.NOTIFY_ON_ALL are also available.
488 	 *
489 	 * @param resources the resources to be edited
490 	 * @param recurse indicates whether to recurse (-R) or not (-l)
491 	 * @param notifyServer indicates whether to notify the server now, if possible,
492 	 *     or defer until the next command.
493 	 * @param notifyForWrittable
494 	 * @param notification the temporary watches.
495 	 * @param progress progress monitor to provide progress indication/cancellation or <code>null</code>
496 	 * @exception CVSException if this method fails.
497 	 * @since 2.1
498 	 *
499 	 * @see CVSTeamProvider#unedit
500 	 */
edit(IResource[] resources, boolean recurse, boolean notifyServer, final boolean notifyForWritable, final int notification, IProgressMonitor progress)501 	public void edit(IResource[] resources, boolean recurse, boolean notifyServer, final boolean notifyForWritable, final int notification, IProgressMonitor progress) throws CVSException {
502 		final int notify;
503 		if (notification == ICVSFile.NO_NOTIFICATION) {
504 			if (CVSProviderPlugin.getPlugin().isWatchOnEdit()) {
505 				notify = ICVSFile.NOTIFY_ON_ALL;
506 			} else {
507 				notify = ICVSFile.NO_NOTIFICATION;
508 			}
509 		} else {
510 			notify = notification;
511 		}
512 		notifyEditUnedit(resources, recurse, notifyServer, new ICVSResourceVisitor() {
513 			public void visitFile(ICVSFile file) throws CVSException {
514 				if (notifyForWritable || file.isReadOnly())
515 					file.edit(notify, notifyForWritable, Policy.monitorFor(null));
516 			}
517 			public void visitFolder(ICVSFolder folder) throws CVSException {
518 				// nothing needs to be done here as the recurse will handle the traversal
519 			}
520 		}, null /* no scheduling rule */, progress);
521 	}
522 
523 	/**
524 	 * Unedit the given resources. Any writable resources will be reverted to their base contents
525 	 * and made read-only and the server will be notified that the file is no longer being edited.
526 	 * This notification may be done immediately or at some
527 	 * later point depending on whether contact with the server is possible at the time of
528 	 * invocation or the value of the notify server parameter.
529 	 *
530 	 * The recurse parameter is equivalent to the cvs local options -l (<code>true</code>) and
531 	 * -R (<code>false</code>). The notifyServer parameter can be used to defer server contact
532 	 * until the next command. This may be appropriate if no shell or progress monitor is available
533 	 * to the caller.
534 	 *
535 	 * @param resources the resources to be unedited
536 	 * @param recurse indicates whether to recurse (-R) or not (-l)
537 	 * @param notifyServer indicates whether to notify the server now, if possible,
538 	 *     or defer until the next command.
539 	 * @param progress progress monitor to provide progress indication/cancellation or <code>null</code>
540 	 * @exception CVSException if this method fails.
541 	 * @since 2.1
542 	 *
543 	 * @see CVSTeamProvider#edit
544 	 */
unedit(IResource[] resources, boolean recurse, boolean notifyServer, IProgressMonitor progress)545 	public void unedit(IResource[] resources, boolean recurse, boolean notifyServer, IProgressMonitor progress) throws CVSException {
546 		notifyEditUnedit(resources, recurse, notifyServer, new ICVSResourceVisitor() {
547 			public void visitFile(ICVSFile file) throws CVSException {
548 				if (!file.isReadOnly())
549 					file.unedit(Policy.monitorFor(null));
550 			}
551 			public void visitFolder(ICVSFolder folder) throws CVSException {
552 				// nothing needs to be done here as the recurse will handle the traversal
553 			}
554 		}, getProject() /* project scheduling rule */, progress);
555 	}
556 
557 	/*
558 	 * This method captures the common behavior between the edit and unedit methods.
559 	 */
notifyEditUnedit(final IResource[] resources, final boolean recurse, final boolean notifyServer, final ICVSResourceVisitor editUneditVisitor, ISchedulingRule rule, IProgressMonitor monitor)560 	private void notifyEditUnedit(final IResource[] resources, final boolean recurse, final boolean notifyServer, final ICVSResourceVisitor editUneditVisitor, ISchedulingRule rule, IProgressMonitor monitor) throws CVSException {
561 		final CVSException[] exception = new CVSException[] { null };
562 		IWorkspaceRunnable workspaceRunnable = monitor1 -> {
563 			final ICVSResource[] cvsResources = getCVSArguments(resources);
564 
565 			// mark the files locally as being checked out
566 			try {
567 				for (int i = 0; i < cvsResources.length; i++) {
568 					cvsResources[i].accept(editUneditVisitor, recurse);
569 				}
570 			} catch (CVSException e2) {
571 				exception[0] = e2;
572 				return;
573 			}
574 
575 			// send the noop command to the server in order to deliver the notifications
576 			if (notifyServer) {
577 				monitor1.beginTask(null, 100);
578 				Session session = new Session(workspaceRoot.getRemoteLocation(), workspaceRoot.getLocalRoot(), true);
579 				try {
580 					try {
581 						session.open(Policy.subMonitorFor(monitor1, 10), true /* open for modification */);
582 					} catch (CVSException e1) {
583 						// If the connection cannot be opened, just exit normally.
584 						// The notifications will be sent when a connection can be made
585 						return;
586 					}
587 					Command.NOOP.execute(
588 						session,
589 						Command.NO_GLOBAL_OPTIONS,
590 						Command.NO_LOCAL_OPTIONS,
591 						cvsResources,
592 						null,
593 						Policy.subMonitorFor(monitor1, 90));
594 				} catch (CVSException e3) {
595 					exception[0] = e3;
596 				} finally {
597 					session.close();
598 					monitor1.done();
599 				}
600 			}
601 		};
602 		try {
603 			ResourcesPlugin.getWorkspace().run(workspaceRunnable, rule, 0, Policy.monitorFor(monitor));
604 		} catch (CoreException e) {
605 			if (exception[0] == null) {
606 				throw CVSException.wrapException(e);
607 			} else {
608 				CVSProviderPlugin.log(CVSException.wrapException(e));
609 			}
610 		}
611 		if (exception[0] != null) {
612 			throw exception[0];
613 		}
614 	}
615 
616 	/**
617 	 * Gets the etchAbsentDirectories.
618 	 * @return Returns a boolean
619 	 */
getFetchAbsentDirectories()620 	public boolean getFetchAbsentDirectories() throws CVSException {
621 		try {
622 			String property = getProject().getPersistentProperty(FETCH_ABSENT_DIRECTORIES_PROP_KEY);
623 			if (property == null) return CVSProviderPlugin.getPlugin().getFetchAbsentDirectories();
624 			return Boolean.valueOf(property).booleanValue();
625 		} catch (CoreException e) {
626 			throw new CVSException(new CVSStatus(IStatus.ERROR, CVSStatus.ERROR, NLS.bind(CVSMessages.CVSTeamProvider_errorGettingFetchProperty, new String[] { project.getName() }), e, project));
627 		}
628 	}
629 
630 	/**
631 	 * Sets the fetchAbsentDirectories.
632 	 * @param etchAbsentDirectories The etchAbsentDirectories to set
633 	 */
setFetchAbsentDirectories(boolean fetchAbsentDirectories)634 	public void setFetchAbsentDirectories(boolean fetchAbsentDirectories) throws CVSException {
635 		internalSetFetchAbsentDirectories(fetchAbsentDirectories ? Boolean.TRUE.toString() : Boolean.FALSE.toString());
636 	}
637 
internalSetFetchAbsentDirectories(String fetchAbsentDirectories)638 	public void internalSetFetchAbsentDirectories(String fetchAbsentDirectories) throws CVSException {
639 		try {
640 			getProject().setPersistentProperty(FETCH_ABSENT_DIRECTORIES_PROP_KEY, fetchAbsentDirectories);
641 		} catch (CoreException e) {
642 			IStatus status = new CVSStatus(IStatus.ERROR, CVSStatus.ERROR, NLS.bind(CVSMessages.CVSTeamProvider_errorSettingFetchProperty, new String[] { project.getName() }), e, project);
643 			throw new CVSException(status);
644 		}
645 	}
646 
647 	@Override
canHandleLinkedResources()648 	public boolean canHandleLinkedResources() {
649 		return true;
650 	}
651 
652 	@Override
canHandleLinkedResourceURI()653 	public boolean canHandleLinkedResourceURI() {
654 		return true;
655 	}
656 
657 	@Override
validateCreateLink(IResource resource, int updateFlags, IPath location)658 	public IStatus validateCreateLink(IResource resource, int updateFlags, IPath location) {
659 		return internalValidateCreateLink(resource);
660 	}
661 
internalValidateCreateLink(IResource resource)662 	private IStatus internalValidateCreateLink(IResource resource) {
663 		ICVSFolder cvsFolder = CVSWorkspaceRoot.getCVSFolderFor(resource.getParent().getFolder(new Path(resource.getName())));
664 		try {
665 			if (cvsFolder.isCVSFolder()) {
666 				// There is a remote folder that overlaps with the link so disallow
667 				return new CVSStatus(IStatus.ERROR, CVSStatus.ERROR, NLS.bind(CVSMessages.CVSTeamProvider_overlappingRemoteFolder, new String[] { resource.getFullPath().toString() }),resource);
668 			} else {
669 				ICVSFile cvsFile = CVSWorkspaceRoot.getCVSFileFor(resource.getParent().getFile(new Path(resource.getName())));
670 				if (cvsFile.isManaged()) {
671 					// there is an outgoing file deletion that overlaps the link so disallow
672 					return new CVSStatus(IStatus.ERROR, CVSStatus.ERROR, NLS.bind(CVSMessages.CVSTeamProvider_overlappingFileDeletion, new String[] { resource.getFullPath().toString() }),resource);
673 				}
674 			}
675 		} catch (CVSException e) {
676 			CVSProviderPlugin.log(e);
677 			return e.getStatus();
678 		}
679 		return Status.OK_STATUS;
680 	}
681 
682 	@Override
validateCreateLink(IResource resource, int updateFlags, URI location)683 	public IStatus validateCreateLink(IResource resource, int updateFlags, URI location) {
684 		return internalValidateCreateLink(resource);
685 	}
686 
687 	/**
688 	 * Get the editors of the resources by calling the <code>cvs editors</code> command.
689 	 *
690 	 * @author <a href="mailto:gregor.kohlwes@csc.com,kohlwes@gmx.net">Gregor Kohlwes</a>
691 	 * @param resources
692 	 * @param progress
693 	 * @return IEditorsInfo[]
694 	 * @throws CVSException
695 	 */
editors( IResource[] resources, IProgressMonitor progress)696 	public EditorsInfo[] editors(
697 		IResource[] resources,
698 		IProgressMonitor progress)
699 		throws CVSException {
700 
701 		// Build the local options
702 		LocalOption[] commandOptions = new LocalOption[] {
703 		};
704 		progress.worked(10);
705 		// Build the arguments list
706 		String[] arguments = getValidArguments(resources, commandOptions);
707 
708 		// Build the listener for the command
709 		EditorsListener listener = new EditorsListener();
710 
711 		// Check if canceled
712 		if (progress.isCanceled()) {
713 			return new EditorsInfo[0];
714 		}
715 		// Build the session
716 		Session session =
717 			new Session(
718 				workspaceRoot.getRemoteLocation(),
719 				workspaceRoot.getLocalRoot());
720 
721 		// Check if canceled
722 		if (progress.isCanceled()) {
723 			return new EditorsInfo[0];
724 		}
725 		progress.beginTask(null, 100);
726 		try {
727 			// Opening the session takes 20% of the time
728 			session.open(Policy.subMonitorFor(progress, 20), false /* read-only */);
729 
730 			if (!progress.isCanceled()) {
731 				// Execute the editors command
732 				Command.EDITORS.execute(
733 					session,
734 					Command.NO_GLOBAL_OPTIONS,
735 					commandOptions,
736 					arguments,
737 					listener,
738 					Policy.subMonitorFor(progress, 80));
739 			}
740 		} finally {
741 			session.close();
742 			progress.done();
743 		}
744 		// Return the infos about the editors
745 		return listener.getEditorsInfos();
746 	}
747 
748 	/**
749 	 * Return the commit comment template that was provided by the server.
750 	 *
751 	 * @return String
752 	 * @throws CVSException
753 	 */
getCommitTemplate()754 	public String getCommitTemplate() throws CVSException {
755 		ICVSFolder localFolder = getCVSWorkspaceRoot().getLocalRoot();
756 		ICVSFile templateFile = CVSWorkspaceRoot.getCVSFileFor(
757 			SyncFileWriter.getTemplateFile(
758 				(IContainer)localFolder.getIResource()));
759 		if (!templateFile.exists()) return null;
760 		InputStream in = new BufferedInputStream(templateFile.getContents());
761 		try {
762 			ByteArrayOutputStream out = new ByteArrayOutputStream();
763 			int b;
764 			do {
765 				b = in.read();
766 				if (b != -1)
767 					out.write((byte)b);
768 			} while (b != -1);
769 			out.close();
770 			return new String(out.toString());
771 		} catch (IOException e) {
772 			throw CVSException.wrapException(e);
773 		} finally {
774 			try {
775 				in.close();
776 			} catch (IOException e) {
777 				// Since we already have the contents, just log this exception
778 				CVSProviderPlugin.log(CVSException.wrapException(e));
779 			}
780 		}
781 	}
782 
783 	/**
784 	 * Return true if the project is configured to use watch/edit. A project will use
785 	 * watch/edit if it was checked out when the global preference to use watch/edit is
786 	 * turned on.
787 	 * @return boolean
788 	 */
isWatchEditEnabled()789 	public boolean isWatchEditEnabled() throws CVSException {
790 		IProject project = getProject();
791 		try {
792 			String property = (String)project.getSessionProperty(WATCH_EDIT_PROP_KEY);
793 			if (property == null) {
794 				property = project.getPersistentProperty(WATCH_EDIT_PROP_KEY);
795 				if (property == null) {
796 					// The persistant property for the project was never set (i.e. old project)
797 					// Use the global preference to determine if the project is using watch/edit
798 					return CVSProviderPlugin.getPlugin().isWatchEditEnabled();
799 				} else {
800 					project.setSessionProperty(WATCH_EDIT_PROP_KEY, property);
801 				}
802 			}
803 			return Boolean.valueOf(property).booleanValue();
804 		} catch (CoreException e) {
805 			if (project.isAccessible()) {
806 				// We only care if the project still exists
807 				IStatus status = new CVSStatus(IStatus.ERROR, CVSStatus.ERROR, NLS.bind(CVSMessages.CVSTeamProvider_errorGettingWatchEdit, new String[] { project.getName() }), e, project);
808 				throw new CVSException(status);
809 			}
810 		}
811 		return false;
812 	}
813 
setWatchEditEnabled(boolean enabled)814 	public void setWatchEditEnabled(boolean enabled) throws CVSException {
815 		internalSetWatchEditEnabled(enabled ? Boolean.TRUE.toString() : Boolean.FALSE.toString());
816 	}
817 
internalSetWatchEditEnabled(String enabled)818 	private void internalSetWatchEditEnabled(String enabled) throws CVSException {
819 		try {
820 			IProject project = getProject();
821 			project.setPersistentProperty(WATCH_EDIT_PROP_KEY, enabled);
822 			project.setSessionProperty(WATCH_EDIT_PROP_KEY, enabled);
823 		} catch (CoreException e) {
824 			IStatus status = new CVSStatus(IStatus.ERROR, CVSStatus.ERROR, NLS.bind(CVSMessages.CVSTeamProvider_errorSettingWatchEdit, new String[] { project.getName() }), e, project);
825 			throw new CVSException(status);
826 		}
827 	}
828 
829 	@Override
getRuleFactory()830 	public IResourceRuleFactory getRuleFactory() {
831 		return RESOURCE_RULE_FACTORY;
832 	}
833 
834 	@Override
getFileHistoryProvider()835 	public IFileHistoryProvider getFileHistoryProvider() {
836 			if (CVSTeamProvider.fileHistoryProvider == null) {
837 				CVSTeamProvider.fileHistoryProvider = new CVSFileHistoryProvider();
838 			}
839 			return CVSTeamProvider.fileHistoryProvider;
840 	}
841 }
842