1 /*******************************************************************************
2  * Copyright (c) 2000, 2007 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  *     Matt McCutchen <hashproduct+eclipse@gmail.com> - Bug 179174 CVS client sets timestamps back when replacing
14  *******************************************************************************/
15 package org.eclipse.team.internal.ccvs.core.client;
16 
17 import java.util.*;
18 
19 import org.eclipse.core.resources.IFile;
20 import org.eclipse.core.resources.IResource;
21 import org.eclipse.core.runtime.*;
22 import org.eclipse.osgi.util.NLS;
23 import org.eclipse.team.internal.ccvs.core.*;
24 import org.eclipse.team.internal.ccvs.core.client.listeners.ICommandOutputListener;
25 
26 /**
27  * Abstract base class for command requests.
28  * Provides a framework for implementing command execution.
29  */
30 public abstract class Command extends Request {
31 	/*** Command singleton instances ***/
32 	public final static Add ADD = new Add();
33 	public final static Admin ADMIN = new Admin();
34 	public final static Annotate ANNOTATE = new Annotate();
35 	public final static Checkout CHECKOUT = new CheckoutWithOverwrite();
36 	public final static Commit COMMIT = new Commit();
37 	public final static Diff DIFF = new Diff();
38 	public final static RDiff RDIFF = new RDiff();
39 	public final static Editors EDITORS = new Editors();
40 	public final static Import IMPORT = new Import();
41 	public final static Log LOG = new Log();
42 	public final static Remove REMOVE = new Remove();
43 	public final static Status STATUS = new Status();
44 	public final static Tag TAG = new Tag();
45 	// The CUSTOM_TAG command has special handling for added and removed resources.
46 	// This behavior supports branching with local changes in the workspace
47 	public final static Tag CUSTOM_TAG = new Tag(true);
48 	public final static RTag RTAG = new RTag();
49 	public final static Update UPDATE = new Update();
50 	public final static Update REPLACE = new Replace();
51 	public final static SyncUpdate SYNCUPDATE = new SyncUpdate();
52 	public final static Version VERSION = new Version();
53 	public final static NOOPCommand NOOP = new NOOPCommand();
54 
55 	// Empty argument array
56 	public final static String[] NO_ARGUMENTS = new String[0];
57 
58 	/*** Global options ***/
59 	// Empty global option array
60 	public static final GlobalOption[] NO_GLOBAL_OPTIONS = new GlobalOption[0];
61 	// Do not change file contents
62 	public static final GlobalOption DO_NOT_CHANGE = new GlobalOption("-n");  //$NON-NLS-1$
63 	// Do not record this operation into CVS command history
64 	public static final GlobalOption DO_NOT_LOG = new GlobalOption("-l");  //$NON-NLS-1$
65 	// Make new working files read-only
66 	public static final GlobalOption MAKE_READ_ONLY = new GlobalOption("-r"); //$NON-NLS-1$
67 	// Trace command execution
68 	public static final GlobalOption TRACE_EXECUTION = new GlobalOption("-t"); //$NON-NLS-1$
69 
70 	/*** Global options: quietness ***/
71 	// Don't be quiet (normal verbosity)
72 	public static final QuietOption VERBOSE = new QuietOption(""); //$NON-NLS-1$
73 	// Be somewhat quiet (suppress informational messages)
74 	public static final QuietOption PARTLY_QUIET = new QuietOption("-q"); //$NON-NLS-1$
75 	// Be really quiet (silent but for serious problems)
76 	public static final QuietOption SILENT = new QuietOption("-Q"); //$NON-NLS-1$
77 
78 	/*** Local options: common to many commands ***/
79 	// Empty local option array
80 	public static final LocalOption[] NO_LOCAL_OPTIONS = new LocalOption[0];
81 	// valid for: annotate checkout commit diff export log rdiff remove rtag status tag update
82 	public static final LocalOption RECURSE = new LocalOption("-R"); //$NON-NLS-1$
83 	public static final LocalOption DO_NOT_RECURSE = new LocalOption("-l"); //$NON-NLS-1$
84 	// valid for: checkout export update
85 	public static final LocalOption PRUNE_EMPTY_DIRECTORIES = new LocalOption("-P"); //$NON-NLS-1$
86 	// valid for: checkout export update
87 	public static final LocalOption MESSAGE_OPTION = new LocalOption("-m"); //$NON-NLS-1$
88 
89 	/*** Local options: keyword substitution mode ***/
90 	// valid for: add admin checkout export import update
91 	private static final Map<String, KSubstOption> ksubstOptionMap = new HashMap<>();
92 	public static final KSubstOption KSUBST_BINARY = new KSubstOption("-kb"); //$NON-NLS-1$
93 	public static final KSubstOption KSUBST_TEXT = new KSubstOption("-ko"); //$NON-NLS-1$
94 	public static final KSubstOption KSUBST_TEXT_EXPAND = new KSubstOption("-kkv"); //$NON-NLS-1$
95 	public static final KSubstOption KSUBST_TEXT_EXPAND_LOCKER = new KSubstOption("-kkvl"); //$NON-NLS-1$
96 	public static final KSubstOption KSUBST_TEXT_VALUES_ONLY = new KSubstOption("-kv"); //$NON-NLS-1$
97 	public static final KSubstOption KSUBST_TEXT_KEYWORDS_ONLY = new KSubstOption("-kk"); //$NON-NLS-1$
98 
99 	/*** Default command output listener ***/
100 	protected static final ICommandOutputListener DEFAULT_OUTPUT_LISTENER = new CommandOutputListener();
101 
102 	/**
103 	 * Prevents client code from instantiating us.
104 	 */
Command()105 	protected Command() { }
106 
107 	/**
108 	 * Provides the default command output listener which is used to accumulate errors.
109 	 *
110 	 * Subclasses can override this method in order to properly interpret information
111 	 * received from the server.
112 	 */
getDefaultCommandOutputListener()113 	protected ICommandOutputListener getDefaultCommandOutputListener() {
114 		return DEFAULT_OUTPUT_LISTENER;
115 	}
116 
117 	/**
118 	 * Sends the command's arguments to the server.
119 	 * [template method]
120 	 * <p>
121 	 * The default implementation sends all arguments.  Subclasses may override
122 	 * this method to provide alternate behaviour.
123 	 * </p>
124 	 *
125 	 * @param session the CVS session
126 	 * @param arguments the arguments that were supplied by the caller of execute()
127 	 */
sendArguments(Session session, String[] arguments)128 	protected void sendArguments(Session session, String[] arguments) throws CVSException {
129 		for (int i = 0; i < arguments.length; ++i) {
130 			session.sendArgument(arguments[i]);
131 		}
132 	}
133 
134 	/**
135 	 * Describes the local resource state to the server prior to command execution.
136 	 * [template method]
137 	 * <p>
138 	 * Commands must override this method to inform the server about the state of
139 	 * local resources using the Entries, Modified, Unchanged, and Questionable
140 	 * requests as needed.
141 	 * </p>
142 	 * <p>
143 	 * This method should return the resources that are of interest to the
144 	 * <code>Command#commandFinished()</code> method. In most cases, it
145 	 * is the same resources that are provided but in some cases (e.g. Commit)
146 	 * the resources to be passed to the above method are different.
147 	 * </p>
148 	 *
149 	 * @param session the CVS session
150 	 * @param globalOptions the global options for the command
151 	 * @param localOptions the local options for the command
152 	 * @param resources the resource arguments for the command
153 	 * @param monitor the progress monitor
154 	 * @return ICVSResource[]
155 	 */
sendLocalResourceState(Session session, GlobalOption[] globalOptions, LocalOption[] localOptions, ICVSResource[] resources, IProgressMonitor monitor)156 	protected abstract ICVSResource[] sendLocalResourceState(Session session, GlobalOption[] globalOptions,
157 		LocalOption[] localOptions, ICVSResource[] resources, IProgressMonitor monitor)
158 		throws CVSException;
159 
160 	/**
161 	 * Cleans up after command execution.
162 	 * [template method]
163 	 * <p>
164 	 * The default implementation is a no-op.  Subclasses may override this
165 	 * method to follow up command execution on the server with clean up
166 	 * operations on local resources.
167 	 * </p>
168 	 *
169 	 * @param session the CVS session
170 	 * @param globalOptions the global options for the command
171 	 * @param localOptions the local options for the command
172 	 * @param resources the resource arguments for the command
173 	 * @param monitor the progress monitor
174 	 * @param status the status accumulated so far. If the code == CVSStatus.SERVER_ERROR
175 	 *    then the command failed
176 	 * @return status the status past in plus any additional status accumulated during the finish
177 	 */
commandFinished(Session session, GlobalOption[] globalOptions, LocalOption[] localOptions, ICVSResource[] resources, IProgressMonitor monitor, IStatus status)178 	protected IStatus commandFinished(Session session, GlobalOption[] globalOptions,
179 		LocalOption[] localOptions, ICVSResource[] resources, IProgressMonitor monitor,
180 		IStatus status) throws CVSException {
181 			return status;
182 	}
183 
184 	/**
185 	 * Sends the local working directory path prior to command execution.
186 	 * [template method]
187 	 * <p>
188 	 * The default implementation sends the paths of local root directory
189 	 * (assuming it exists).  Subclasses may override this method to provide
190 	 * alternate behaviour.
191 	 * </p>
192 	 *
193 	 * @param session the CVS session
194 	 */
sendLocalWorkingDirectory(Session session)195 	protected void sendLocalWorkingDirectory(Session session) throws CVSException {
196 		ICVSFolder localRoot = session.getLocalRoot();
197 		if (localRoot.isCVSFolder()) {
198 			session.sendLocalRootDirectory();
199 		} else {
200 			session.sendConstructedRootDirectory();
201 		}
202 	}
203 
204 	/**
205 	 * Computes an array of ICVSResources corresponding to command arguments.
206 	 * [template method]
207 	 * <p>
208 	 * The default implementation assumes that all arguments supplied to the
209 	 * command represent resources in the local root that are to be manipulated.
210 	 * Subclasses must override this method if this assumption does not hold.
211 	 * </p>
212 	 * @param session the CVS session
213 	 * @param localOptions the command local options
214 	 * @param arguments the command arguments
215 	 * @return the resource arguments for the command
216 	 */
computeWorkResources(Session session, LocalOption[] localOptions, String[] arguments)217 	protected ICVSResource[] computeWorkResources(Session session,
218 		LocalOption[] localOptions, String[] arguments) throws CVSException {
219 		ICVSFolder localRoot = session.getLocalRoot();
220 
221 		if (arguments.length == 0) {
222 			// As a convenience, passing no arguments to the CVS command
223 			// implies the command will operate on the local root folder.
224 			return new ICVSResource[] { localRoot };
225 		} else {
226 			// Assume all arguments represent resources that are descendants
227 			// of the local root folder.
228 			ICVSResource[] resources = new ICVSResource[arguments.length];
229 			for (int i = 0; i < arguments.length; i++) {
230 				ICVSResource resource = localRoot.getChild(arguments[i]);
231 				// file does not exist, it could have been deleted. It doesn't matter
232 				// which type we return since only the name of the resource is used
233 				// and sent to the server.
234 				if(resource==null) {
235 					if(localRoot.getName().length()==0) {
236 						// Return a folder because it is the safest choice when
237 						// localRoot is a handle to the IWorkspaceRoot!
238 						resource = localRoot.getFolder(arguments[i]);
239 					} else {
240 						resource = localRoot.getFile(arguments[i]);
241 					}
242 				}
243 				resources[i] = resource;
244 			}
245 			return resources;
246 		}
247 	}
248 
249 	/**
250 	 * Send an array of Resources.
251 	 * @param localOptions
252 	 *
253 	 * @see Command#sendFileStructure(ICVSResource,IProgressMonitor,boolean,boolean,boolean)
254 	 */
sendFileStructure(Session session, ICVSResource[] resources, LocalOption[] localOptions, boolean emptyFolders, IProgressMonitor monitor)255 	protected void sendFileStructure(Session session, ICVSResource[] resources,
256 		LocalOption[] localOptions, boolean emptyFolders, IProgressMonitor monitor) throws CVSException {
257 		checkResourcesManaged(session, resources);
258 
259 		new FileStructureVisitor(session, localOptions, emptyFolders, true).visit(session, resources, monitor);
260 	}
261 
262 	/**
263 	 * Checks that all work resources are managed.
264 	 * @param session TODO
265 	 * @param resources the resource arguments for the command
266 	 *
267 	 * @throws CVSException if some resources are not managed
268 	 */
checkResourcesManaged(Session session, ICVSResource[] resources)269 	protected void checkResourcesManaged(Session session, ICVSResource[] resources) throws CVSException {
270 		for (int i = 0; i < resources.length; ++i) {
271 			ICVSFolder folder;
272 			if (resources[i].isFolder()) {
273 				folder = (ICVSFolder) resources[i];
274 			}
275 			else {
276 				folder = resources[i].getParent();
277 			}
278 			if (!folder.isCVSFolder() && folder.exists()) {
279 				IStatus status = new CVSStatus(IStatus.ERROR,CVSStatus.ERROR,NLS.bind(CVSMessages.Command_argumentNotManaged, new String[] { folder.getName() }),session.getLocalRoot());
280 				throw new CVSException(status);
281 			}
282 		}
283 	}
284 
285 	/**
286 	 * Executes a CVS command.
287 	 * <p>
288 	 * Dispatches the commands, retrieves the results, and determines whether or
289 	 * not an error occurred.  A listener may be supplied to capture message text
290 	 * that would normally be written to the standard error and standard output
291 	 * streams of a command line CVS client.
292 	 * </p>
293 	 * @param session the open CVS session
294 	 * @param globalOptions the array of global options, or NO_GLOBAL_OPTIONS
295 	 * @param localOptions the array of local options, or NO_LOCAL_OPTIONS
296 	 * @param arguments the array of arguments (usually filenames relative to localRoot), or NO_ARGUMENTS
297 	 * @param listener the command output listener, or null to discard all messages
298 	 * @param monitor the progress monitor
299 	 * @return a status code indicating success or failure of the operation
300 	 * @throws CVSException if a fatal error occurs (e.g. connection timeout)
301 	 */
execute(final Session session, final GlobalOption[] globalOptions, final LocalOption[] localOptions, final String[] arguments, final ICommandOutputListener listener, IProgressMonitor pm)302 	public final IStatus execute(final Session session, final GlobalOption[] globalOptions,
303 		final LocalOption[] localOptions, final String[] arguments, final ICommandOutputListener listener,
304 		IProgressMonitor pm) throws CVSException {
305 		final IStatus[] status = new IStatus[1];
306 		ICVSRunnable job = monitor -> {
307 			// update the global and local options
308 			GlobalOption[] gOptions = filterGlobalOptions(session, globalOptions);
309 			LocalOption[] lOptions = filterLocalOptions(session, gOptions, localOptions);
310 
311 			// print the invocation string to the console
312 			if (session.isOutputToConsole() || Policy.isDebugProtocol()) {
313 				IPath commandRootPath;
314 				IResource resource = session.getLocalRoot().getIResource();
315 				if (resource == null) {
316 					commandRootPath = Path.EMPTY;
317 				} else {
318 					commandRootPath = resource.getFullPath();
319 				}
320 				String line = constructCommandInvocationString(commandRootPath, gOptions, lOptions, arguments);
321 				ConsoleListeners.getInstance().commandInvoked(session, line);
322 				if (Policy.isDebugProtocol()) Policy.printProtocolLine("CMD> " + line); //$NON-NLS-1$
323 			}
324 
325 			// run the command
326 			try {
327 				session.setCurrentCommand(Command.this);
328 				status[0] = doExecute(session, gOptions, lOptions, arguments, listener, monitor);
329 				notifyConsoleOnCompletion(session, status[0], null);
330 			} catch (CVSException e1) {
331 				notifyConsoleOnCompletion(session, null, e1);
332 				throw e1;
333 			} catch (RuntimeException e2) {
334 				notifyConsoleOnCompletion(session, null, e2);
335 				throw e2;
336 			}
337 		};
338 		if (isWorkspaceModification()) {
339 			session.getLocalRoot().run(job, pm);
340 		} else {
341 			job.run(pm);
342 		}
343 		return status[0];
344 	}
345 
346 	/**
347 	 * Return whether this command modifies the workspace.
348 	 * If <code>true</code> is returned, a scheduling rule on
349 	 * the session local root is obtained. Otherwise, no
350 	 * scheduling rule is obtained. By default, <code>true</code>
351 	 * is returned
352 	 * @return whether this command modifies the workspace
353 	 */
isWorkspaceModification()354 	protected boolean isWorkspaceModification() {
355 		return true;
356 	}
357 
notifyConsoleOnCompletion(Session session, IStatus status, Exception exception)358 	private void notifyConsoleOnCompletion(Session session, IStatus status, Exception exception) {
359 		ConsoleListeners.getInstance().commandCompleted(session, status, exception);
360 		if (Policy.isDebugProtocol()) {
361 			if (status != null) Policy.printProtocolLine("RESULT> " + status.toString()); //$NON-NLS-1$
362 			else Policy.printProtocolLine("RESULT> " + exception.toString()); //$NON-NLS-1$
363 		}
364 	}
365 
doExecute(Session session, GlobalOption[] globalOptions, LocalOption[] localOptions, String[] arguments, ICommandOutputListener listener, IProgressMonitor monitor)366 	protected IStatus doExecute(Session session, GlobalOption[] globalOptions,
367 		LocalOption[] localOptions, String[] arguments, ICommandOutputListener listener,
368 		IProgressMonitor monitor) throws CVSException {
369 		ICVSResource[] resources = null;
370 		/*** setup progress monitor ***/
371 		monitor = Policy.monitorFor(monitor);
372 		monitor.beginTask(null, 100);
373 		Policy.checkCanceled(monitor);
374 		try {
375 			/*** prepare for command ***/
376 			// clear stale command state from previous runs
377 			session.setNoLocalChanges(DO_NOT_CHANGE.isElementOf(globalOptions));
378 			session.setModTime(null);
379 
380 			/*** initiate command ***/
381 			// send global options
382 			for (GlobalOption globalOption : globalOptions) {
383 				globalOption.send(session);
384 			}
385 			Policy.checkCanceled(monitor);
386 			// send local options
387 			for (LocalOption localOption : localOptions) {
388 				localOption.send(session);
389 			}
390 			Policy.checkCanceled(monitor);
391 			// compute the work resources
392 			resources = computeWorkResources(session, localOptions, arguments);
393 			Policy.checkCanceled(monitor);
394 			// send local working directory state contributes 48% of work
395 			resources = sendLocalResourceState(session, globalOptions, localOptions,
396 					resources, Policy.infiniteSubMonitorFor(monitor, 48));
397 			Policy.checkCanceled(monitor);
398 			// escape file names, see bug 149683
399 			for(int i = 0; i < arguments.length; i++){
400 				if(arguments[i].startsWith("-")){ //$NON-NLS-1$
401 					arguments[i] = "./" + arguments[i]; //$NON-NLS-1$
402 				}
403 			}
404 			// send arguments
405 			sendArguments(session, arguments);
406 			// send local working directory path
407 			sendLocalWorkingDirectory(session);
408 
409 			// if no listener was provided, use the command's default in order to get error reporting
410 			if (listener == null) listener = getDefaultCommandOutputListener();
411 
412 			/*** execute command and process responses ***/
413 			// Processing responses contributes 50% of work.
414 			IStatus status = executeRequest(session, listener, Policy.subMonitorFor(monitor, 50));
415 
416 			// Finished adds last 2% of work.
417 			status = commandFinished(session, globalOptions, localOptions, resources, Policy.subMonitorFor(monitor, 2),
418 				status);
419 			return status;
420 		} finally {
421 			monitor.done();
422 		}
423 	}
424 
425 	/**
426 	 * Constucts the CVS command invocation string corresponding to the arguments.
427 	 *
428 	 * @param globalOptions the global options
429 	 * @param localOption the local options
430 	 * @param arguments the arguments
431 	 * @return the command invocation string
432 	 */
constructCommandInvocationString(IPath commandRootPath, GlobalOption[] globalOptions, LocalOption[] localOptions, String[] arguments)433 	private String constructCommandInvocationString(IPath commandRootPath, GlobalOption[] globalOptions,
434 		LocalOption[] localOptions, String[] arguments) {
435 		StringBuilder commandLine = new StringBuilder("cvs"); //$NON-NLS-1$
436 		for (int i = 0; i < globalOptions.length; ++i) {
437 			String option = globalOptions[i].toString();
438 			if (option.length() == 0) continue;
439 			commandLine.append(' ');
440 			commandLine.append(option);
441 		}
442 		commandLine.append(' ');
443 		commandLine.append(getRequestId());
444 		for (int i = 0; i < localOptions.length; ++i) {
445 			String option = localOptions[i].toString();
446 			if (option.length() == 0) continue;
447 			commandLine.append(' ');
448 			commandLine.append(option);
449 		}
450 		for (int i = 0; i < arguments.length; ++i) {
451 			if (arguments[i].length() == 0) continue;
452 			commandLine.append(" \""); //$NON-NLS-1$
453 			IPath completePath = commandRootPath;
454 			if (!arguments[i].equals(Session.CURRENT_LOCAL_FOLDER)) {
455 				completePath = completePath.append(arguments[i]);
456 			}
457 			commandLine.append(completePath.toString());
458 			commandLine.append('"');
459 		}
460 		return commandLine.toString();
461 	}
462 
463 	/**
464 	 * Superclass for all CVS command options
465 	 */
466 	protected static abstract class Option {
467 		protected String option, argument;
Option(String option, String argument)468 		protected Option(String option, String argument) {
469 			this.option = option;
470 			this.argument = argument;
471 		}
472 		/**
473 		 * Determines if this option is an element of an array of options
474 		 * @param array the array of options
475 		 * @return true iff the array contains this option
476 		 */
isElementOf(Option[] array)477 		public boolean isElementOf(Option[] array) {
478 			return findOption(array, option) != null;
479 		}
480 		/**
481 		 * Returns the option part of the option
482 		 */
getOption()483 		String getOption() {
484 			return option;
485 		}
486 		/**
487 		 * Compares two options for equality.
488 		 * @param other the other option
489 		 */
equals(Object other)490 		public boolean equals(Object other) {
491 			if (this == other) return true;
492 			if (other instanceof Option) {
493 				Option otherOption = (Option) other;
494 				return option.equals(otherOption.option);
495 			}
496 			return false;
497 		}
498 		/**
499 		 * Sends the option to a CVS server
500 		 * @param session the CVS session
501 		 */
send(Session session)502 		public abstract void send(Session session) throws CVSException;
503 		/*
504 		 * To make debugging a tad easier.
505 		 */
toString()506 		public String toString() {
507 			if (argument != null && argument.length() != 0) {
508 				return option + " \"" + argument + '"'; //$NON-NLS-1$
509 			} else {
510 				return option;
511 			}
512 		}
513 	}
514 	/**
515 	 * Option subtype for global options that are common to all commands.
516 	 */
517 	public static class GlobalOption extends Option {
GlobalOption(String option)518 		protected GlobalOption(String option) {
519 			super(option, null);
520 		}
send(Session session)521 		public void send(Session session) throws CVSException {
522 			session.sendGlobalOption(option);
523 		}
524 		/**
525 		 * Add the given global option to the end of the provided list
526 		 *
527 		 * @param newOption
528 		 * @param options
529 		 * @return GlobalOption[]
530 		 */
addToEnd(GlobalOption[] options)531 		protected GlobalOption[] addToEnd(GlobalOption[] options) {
532 			GlobalOption[] globalOptions = new GlobalOption[options.length + 1];
533 			System.arraycopy(options, 0, globalOptions, 0, options.length);
534 			globalOptions[globalOptions.length - 1] = this;
535 			return globalOptions;
536 		}
537 	}
538 	/**
539 	 * Option subtype for global quietness options.
540 	 */
541 	public static final class QuietOption extends GlobalOption {
QuietOption(String option)542 		private QuietOption(String option) {
543 			super(option);
544 		}
send(Session session)545 		public void send(Session session) throws CVSException {
546 			if (option.length() != 0) super.send(session);
547 		}
548 	}
549 	/**
550 	 * Option subtype for local options that vary from command to command.
551 	 */
552 	public static class LocalOption extends Option {
LocalOption(String option)553 		protected LocalOption(String option) {
554 			super(option, null);
555 		}
LocalOption(String option, String argument)556 		protected LocalOption(String option, String argument) {
557 			super(option, argument);
558 		}
send(Session session)559 		public void send(Session session) throws CVSException {
560 			session.sendArgument(option);
561 			if (argument != null) session.sendArgument(argument);
562 		}
addTo(LocalOption[] options)563 		public LocalOption[] addTo(LocalOption[] options) {
564 			if (this.isElementOf(options)) {
565 				return options;
566 			}
567 			LocalOption[] newOptions = new LocalOption[options.length + 1];
568 			System.arraycopy(options, 0, newOptions, 0, options.length);
569 			newOptions[options.length] = this;
570 			return newOptions;
571 		}
removeFrom(LocalOption[] options)572 		public LocalOption[] removeFrom(LocalOption[] options) {
573 			if (!this.isElementOf(options)) {
574 				return options;
575 			}
576 			List<LocalOption> result = new ArrayList<>();
577 			for (LocalOption option : options) {
578 				if (!option.equals(this)) {
579 					result.add(option);
580 				}
581 			}
582 			return result.toArray(new LocalOption[result.size()]);
583 		}
584 	}
585 	/**
586 	 * Options subtype for keyword substitution options.
587 	 */
588 	public static class KSubstOption extends LocalOption {
589 		private boolean isUnknownMode;
KSubstOption(String option)590 		private KSubstOption(String option) {
591 			this(option, false);
592 		}
KSubstOption(String option, boolean isUnknownMode)593 		private KSubstOption(String option, boolean isUnknownMode) {
594 			super(option);
595 			this.isUnknownMode = isUnknownMode;
596 			ksubstOptionMap.put(option, this);
597 		}
598 		/**
599 		 * Gets the KSubstOption instance for the specified mode.
600 		 *
601 		 * @param mode the mode, e.g. -kb
602 		 * @return an instance for that mode
603 		 */
fromMode(String mode)604 		public static KSubstOption fromMode(String mode) {
605 			if (mode.length() == 0) mode = "-kkv"; // use default //$NON-NLS-1$
606 			KSubstOption option = ksubstOptionMap.get(mode);
607 			if (option == null) option = new KSubstOption(mode, true);
608 			return option;
609 		}
610 		/**
611 		 * Gets the KSubstOption instance for the specified file.
612 		 *
613 		 * @param file the file to get the option for
614 		 * @return an instance for that mode
615 		 */
fromFile(IFile file)616 		public static KSubstOption fromFile(IFile file) {
617 			if (CVSProviderPlugin.isText(file))
618 				return getDefaultTextMode();
619 			return KSUBST_BINARY;
620 		}
621 		/**
622 		 * Returns an array of all valid modes.
623 		 */
getAllKSubstOptions()624 		public static KSubstOption[] getAllKSubstOptions() {
625 			return ksubstOptionMap.values().toArray(new KSubstOption[ksubstOptionMap.size()]);
626 		}
627 		/**
628 		 * Returns the entry line mode string for this instance. Note that it might return blank strings
629 		 * for certain options. For UI, use {@link #toMode()} which will always return the a string
630 		 * containing the keyword substitution.
631 		 */
toEntryLineMode()632 		public String toEntryLineMode() {
633 			if (KSUBST_TEXT_EXPAND.equals(this)) return ""; //$NON-NLS-1$
634 			return getOption();
635 		}
636 
637 		/**
638 		 * Returns the entry line mode string for this instance.
639 		 */
toMode()640 		public String toMode(){
641 			return getOption();
642 		}
643 
644 		/**
645 		 * Returns true if the substitution mode requires no data translation
646 		 * during file transfer.
647 		 */
isBinary()648 		public boolean isBinary() {
649 			return KSUBST_BINARY.equals(this);
650 		}
651 		/**
652 		 * Returns a short localized text string describing this mode.
653 		 */
getShortDisplayText()654 		public String getShortDisplayText() {
655 			if (isUnknownMode)
656 				return NLS.bind(CVSMessages.KSubstOption_unknown_short, new String[] { option });
657 			if (option.equals("-kb")) //$NON-NLS-1$
658 				return CVSMessages.KSubstOption__kb_short;
659 			if (option.equals("-kkv")) //$NON-NLS-1$
660 				return CVSMessages.KSubstOption__kkv_short;
661 			if (option.equals("-ko")) //$NON-NLS-1$
662 				return CVSMessages.KSubstOption__ko_short;
663 			if (option.equals("-kk")) //$NON-NLS-1$
664 				return CVSMessages.KSubstOption__kk_short;
665 			if (option.equals("-kv")) //$NON-NLS-1$
666 				return CVSMessages.KSubstOption__kv_short;
667 			if (option.equals("-kkvl")) //$NON-NLS-1$
668 				return CVSMessages.KSubstOption__kkvl_short;
669 			return NLS.bind(CVSMessages.KSubstOption_unknown_short, new String[] { option });
670 		}
671 		/**
672 		 * Returns a long localized text string describing this mode.
673 		 */
getLongDisplayText()674 		public String getLongDisplayText() {
675 			if (isUnknownMode)
676 				return NLS.bind(CVSMessages.KSubstOption_unknown_long, new String[] { option });
677 			if (option.equals("-kb")) //$NON-NLS-1$
678 				return CVSMessages.KSubstOption__kb_long;
679 			if (option.equals("-kkv")) //$NON-NLS-1$
680 				return CVSMessages.KSubstOption__kkv_long;
681 			if (option.equals("-ko")) //$NON-NLS-1$
682 				return CVSMessages.KSubstOption__ko_long;
683 			if (option.equals("-kk")) //$NON-NLS-1$
684 				return CVSMessages.KSubstOption__kk_long;
685 			if (option.equals("-kv")) //$NON-NLS-1$
686 				return CVSMessages.KSubstOption__kv_long;
687 			if (option.equals("-kkvl")) //$NON-NLS-1$
688 				return CVSMessages.KSubstOption__kkvl_long;
689 			return NLS.bind(CVSMessages.KSubstOption_unknown_long, new String[] { option });
690 		}
691 		/**
692 		 * Return the text mode that will be used by default
693 		 */
getDefaultTextMode()694 		public static KSubstOption getDefaultTextMode() {
695 			return CVSProviderPlugin.getPlugin().getDefaultTextKSubstOption();
696 		}
697 	}
698 
699 	/**
700 	 * Makes a -m log message option.
701 	 * Valid for: add commit import
702 	 */
makeArgumentOption(LocalOption option, String argument)703 	public static LocalOption makeArgumentOption(LocalOption option, String argument) {
704 		if(argument == null) {
705 			argument = ""; //$NON-NLS-1$
706 		}
707 		return new LocalOption(option.getOption(), argument);
708 	}
709 
710 	/**
711 	 * Makes a -r or -D option for a tag.
712 	 * Valid for: checkout export history rdiff update
713 	 */
makeTagOption(CVSTag tag)714 	public static LocalOption makeTagOption(CVSTag tag) {
715 		int type = tag.getType();
716 		switch (type) {
717 			case CVSTag.BRANCH:
718 			case CVSTag.VERSION:
719 				return new LocalOption("-r", tag.getName()); //$NON-NLS-1$
720 			case CVSTag.DATE:
721 				return new LocalOption("-D", tag.getName()); //$NON-NLS-1$
722 			default:
723 				// tag must not be HEAD
724 				throw new IllegalArgumentException(CVSMessages.Command_invalidTag);
725 		}
726 	}
727 
728 	/**
729 	 * Find a specific option in an array of options
730 	 * @param array the array of options
731 	 * @param option the option string to search for
732 	 * @return the first element matching the option string, or null if none
733 	 */
findOption(Option[] array, String option)734 	public static Option findOption(Option[] array, String option) {
735 		for (int i = 0; i < array.length; ++i) {
736 			if (array[i].getOption().equals(option)) return array[i];
737 		}
738 		return null;
739 	}
740 
741 	/**
742 	 * Collect all arguments of a specific option from an array of options
743 	 * @param array the array of options
744 	 * @param option the option string to search for
745 	 * @return an array of all arguments of belonging to matching options
746 	 */
collectOptionArguments(Option[] array, String option)747 	protected static String[] collectOptionArguments(Option[] array, String option) {
748 		List<String> list = new ArrayList<>();
749 		for (int i = 0; i < array.length; ++i) {
750 			if (array[i].getOption().equals(option)) {
751 				list.add(array[i].argument);
752 			}
753 		}
754 		return list.toArray(new String[list.size()]);
755 	}
756 
757 	/**
758 	 * Allows commands to filter the set of global options to be sent.
759 	 * This method invokes the method of the same name on the session
760 	 * itself in order to get any session wide or globally set options.
761 	 * Subclasses that override this method should call the superclass.
762 	 *
763 	 * @param session the session
764 	 * @param globalOptions the global options, read-only
765 	 * @return the filtered global options
766 	 */
filterGlobalOptions(Session session, GlobalOption[] globalOptions)767 	protected GlobalOption[] filterGlobalOptions(Session session, GlobalOption[] globalOptions) {
768 		return session.filterGlobalOptions(globalOptions);
769 	}
770 
771 	/**
772 	 * Allows commands to filter the set of local options to be sent.
773 	 * Subclasses that override this method should call the superclass.
774 	 *
775 	 * @param session the session
776 	 * @param globalOptions the global options, read-only
777 	 * @param localOptions the local options, read-only
778 	 * @return the filtered local options
779 	 */
filterLocalOptions(Session session, GlobalOption[] globalOptions, LocalOption[] localOptions)780 	protected LocalOption[] filterLocalOptions(Session session, GlobalOption[] globalOptions, LocalOption[] localOptions) {
781 		return localOptions;
782 	}
783 
784 	/**
785 	 * Execute a CVS command on an array of ICVSResource. This method simply converts
786 	 * the ICVSResource to String paths relative to the local root of the session and
787 	 * invokes <code>execute(Session, GlobalOption[], LocalOption[], String[], ICommandOutputListener, IProgressMonitor)</code>.
788 	 *
789 	 * @param session the open CVS session
790 	 * @param globalOptions the array of global options, or NO_GLOBAL_OPTIONS
791 	 * @param localOptions the array of local options, or NO_LOCAL_OPTIONS
792 	 * @param arguments the array of ICVSResource to be operated on
793 	 * @param listener the command output listener, or null to discard all messages
794 	 * @param monitor the progress monitor
795 	 * @return a status code indicating success or failure of the operation
796 	 * @throws CVSException if a fatal error occurs (e.g. connection timeout)
797 	 *
798 	 * @see Command#execute(Session, GlobalOption[], LocalOption[], String[], ICommandOutputListener, IProgressMonitor)
799 	 */
execute(Session session, GlobalOption[] globalOptions, LocalOption[] localOptions, ICVSResource[] arguments, ICommandOutputListener listener, IProgressMonitor pm)800 	public final IStatus execute(Session session, GlobalOption[] globalOptions, LocalOption[] localOptions, ICVSResource[] arguments,
801 		ICommandOutputListener listener, IProgressMonitor pm) throws CVSException {
802 
803 		String[] stringArguments = convertArgumentsForOpenSession(arguments, session);
804 		return execute(session, globalOptions, localOptions, stringArguments, listener, pm);
805 	}
806 
convertArgumentsForOpenSession(ICVSResource[] arguments, Session openSession)807 	protected String[] convertArgumentsForOpenSession(ICVSResource[] arguments, Session openSession) throws CVSException {
808 		// Convert arguments
809 		List<String> stringArguments = new ArrayList<>(arguments.length);
810 		for (ICVSResource argument : arguments) {
811 			stringArguments.add(argument.getRelativePath(openSession.getLocalRoot()));
812 		}
813 		return stringArguments.toArray(new String[stringArguments.size()]);
814 	}
815 
816 	/**
817 	 * Method mergeStatus.
818 	 * @param status
819 	 * @param cVSStatus
820 	 * @return IStatus
821 	 */
mergeStatus(IStatus accumulatedStatus, IStatus newStatus)822 	protected IStatus mergeStatus(IStatus accumulatedStatus, IStatus newStatus) {
823 		if (accumulatedStatus.isMultiStatus()) {
824 			((MultiStatus)accumulatedStatus).merge(newStatus);
825 			return accumulatedStatus;
826 		}
827 		if (accumulatedStatus.isOK()) return newStatus;
828 		if (newStatus.isOK()) return accumulatedStatus;
829 		MultiStatus result = new MultiStatus(CVSProviderPlugin.ID, IStatus.INFO,
830 				new IStatus[] {accumulatedStatus, newStatus},
831 				NLS.bind(CVSMessages.Command_warnings, new String[] { getDisplayText() }), null);
832 		return result;
833 	}
834 }
835