Java tutorial
// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See License.txt in the repository root. package com.microsoft.tfs.client.clc.commands; import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; import java.net.MalformedURLException; import java.net.URI; import java.rmi.activation.ActivationException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import com.microsoft.tfs.client.clc.AcceptedOptionSet; import com.microsoft.tfs.client.clc.CLCConnectionAdvisor; import com.microsoft.tfs.client.clc.CommandsMap; import com.microsoft.tfs.client.clc.EnvironmentVariables; import com.microsoft.tfs.client.clc.ExitCode; import com.microsoft.tfs.client.clc.Messages; import com.microsoft.tfs.client.clc.OptionsMap; import com.microsoft.tfs.client.clc.exceptions.ArgumentException; import com.microsoft.tfs.client.clc.exceptions.CLCException; import com.microsoft.tfs.client.clc.exceptions.CannotFindWorkspaceException; import com.microsoft.tfs.client.clc.exceptions.InvalidFreeArgumentException; import com.microsoft.tfs.client.clc.exceptions.InvalidOptionException; import com.microsoft.tfs.client.clc.exceptions.InvalidOptionValueException; import com.microsoft.tfs.client.clc.exceptions.LicenseException; import com.microsoft.tfs.client.clc.exceptions.LicenseException.LicenseExceptionType; import com.microsoft.tfs.client.clc.exceptions.MissingRequiredOptionException; import com.microsoft.tfs.client.clc.options.Option; import com.microsoft.tfs.client.clc.options.shared.OptionCollection; import com.microsoft.tfs.client.clc.options.shared.OptionLogin; import com.microsoft.tfs.client.clc.options.shared.OptionServer; import com.microsoft.tfs.client.clc.prompt.Prompt; import com.microsoft.tfs.client.clc.vc.CLCPathWatcherFactory; import com.microsoft.tfs.client.clc.vc.Main; import com.microsoft.tfs.client.clc.vc.MatchedFileArgument; import com.microsoft.tfs.client.clc.vc.QualifiedItem; import com.microsoft.tfs.client.clc.vc.options.OptionNoPrompt; import com.microsoft.tfs.client.clc.vc.options.OptionWorkspace; import com.microsoft.tfs.client.clc.xml.CommonXMLNames; import com.microsoft.tfs.client.clc.xml.SimpleXMLWriter; import com.microsoft.tfs.client.common.license.LicenseManager; import com.microsoft.tfs.console.display.Display; import com.microsoft.tfs.console.display.NullDisplay; import com.microsoft.tfs.console.input.Input; import com.microsoft.tfs.console.input.NullInput; import com.microsoft.tfs.core.TFSTeamProjectCollection; import com.microsoft.tfs.core.clients.versioncontrol.GetStatus; import com.microsoft.tfs.core.clients.versioncontrol.OperationStatus; import com.microsoft.tfs.core.clients.versioncontrol.VersionControlClient; import com.microsoft.tfs.core.clients.versioncontrol.VersionControlConstants; import com.microsoft.tfs.core.clients.versioncontrol.Workstation; import com.microsoft.tfs.core.clients.versioncontrol.conflicts.resolutions.ConflictResolutionHelper; import com.microsoft.tfs.core.clients.versioncontrol.events.BeforeCheckinListener; import com.microsoft.tfs.core.clients.versioncontrol.events.BeforeShelveListener; import com.microsoft.tfs.core.clients.versioncontrol.events.CheckinEvent; import com.microsoft.tfs.core.clients.versioncontrol.events.CheckinListener; import com.microsoft.tfs.core.clients.versioncontrol.events.ConflictEvent; import com.microsoft.tfs.core.clients.versioncontrol.events.ConflictListener; import com.microsoft.tfs.core.clients.versioncontrol.events.ConflictResolvedEvent; import com.microsoft.tfs.core.clients.versioncontrol.events.ConflictResolvedListener; import com.microsoft.tfs.core.clients.versioncontrol.events.DestroyEvent; import com.microsoft.tfs.core.clients.versioncontrol.events.DestroyListener; import com.microsoft.tfs.core.clients.versioncontrol.events.GetEvent; import com.microsoft.tfs.core.clients.versioncontrol.events.GetListener; import com.microsoft.tfs.core.clients.versioncontrol.events.MergingEvent; import com.microsoft.tfs.core.clients.versioncontrol.events.MergingListener; import com.microsoft.tfs.core.clients.versioncontrol.events.NewPendingChangeListener; import com.microsoft.tfs.core.clients.versioncontrol.events.NonFatalErrorEvent; import com.microsoft.tfs.core.clients.versioncontrol.events.NonFatalErrorListener; import com.microsoft.tfs.core.clients.versioncontrol.events.PendingChangeEvent; import com.microsoft.tfs.core.clients.versioncontrol.events.UndonePendingChangeListener; import com.microsoft.tfs.core.clients.versioncontrol.events.VersionControlEventEngine; import com.microsoft.tfs.core.clients.versioncontrol.exceptions.DownloadProxyException; import com.microsoft.tfs.core.clients.versioncontrol.exceptions.ServerPathFormatException; import com.microsoft.tfs.core.clients.versioncontrol.internal.localworkspace.NullPathWatcherFactory; import com.microsoft.tfs.core.clients.versioncontrol.path.ItemPath; import com.microsoft.tfs.core.clients.versioncontrol.path.LocalPath; import com.microsoft.tfs.core.clients.versioncontrol.path.ServerPath; import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.ChangeType; import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.Item; import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.ItemType; import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.PendingChange; import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.PendingSet; import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.SeverityType; import com.microsoft.tfs.core.clients.versioncontrol.soapextensions.Workspace; import com.microsoft.tfs.core.clients.versioncontrol.specs.LabelSpecParseException; import com.microsoft.tfs.core.clients.versioncontrol.specs.VersionedFileSpec; import com.microsoft.tfs.core.clients.versioncontrol.specs.WorkspaceSpec; import com.microsoft.tfs.core.clients.versioncontrol.specs.WorkspaceSpecParseException; import com.microsoft.tfs.core.clients.versioncontrol.specs.version.LatestVersionSpec; import com.microsoft.tfs.core.clients.versioncontrol.specs.version.VersionSpec; import com.microsoft.tfs.core.clients.versioncontrol.specs.version.VersionSpecParseException; import com.microsoft.tfs.core.clients.versioncontrol.specs.version.WorkspaceVersionSpec; import com.microsoft.tfs.core.clients.versioncontrol.workspacecache.WorkspaceInfo; import com.microsoft.tfs.core.config.persistence.DefaultPersistenceStoreProvider; import com.microsoft.tfs.core.credentials.CachedCredentials; import com.microsoft.tfs.core.credentials.CredentialsManager; import com.microsoft.tfs.core.credentials.CredentialsManagerFactory; import com.microsoft.tfs.core.httpclient.Credentials; import com.microsoft.tfs.core.httpclient.DefaultNTCredentials; import com.microsoft.tfs.core.httpclient.JwtCredentials; import com.microsoft.tfs.core.httpclient.UsernamePasswordCredentials; import com.microsoft.tfs.core.pendingcheckin.CheckinNoteFailure; import com.microsoft.tfs.core.util.CredentialsUtils; import com.microsoft.tfs.core.util.FileEncoding; import com.microsoft.tfs.core.util.ServerURIUtils; import com.microsoft.tfs.core.util.URIUtils; import com.microsoft.tfs.core.util.notifications.MessageWindowNotificationManager; import com.microsoft.tfs.jni.NTLMEngine; import com.microsoft.tfs.jni.NegotiateEngine; import com.microsoft.tfs.jni.PlatformMiscUtils; import com.microsoft.tfs.util.Check; import com.microsoft.tfs.util.Closable; import com.microsoft.tfs.util.NewlineUtils; import com.microsoft.tfs.util.Platform; import com.microsoft.tfs.util.StringUtil; /** * The base class for other command classes in the command-line client. * * This class is NOT guaranteed thread-safe. */ public abstract class Command implements BeforeCheckinListener, CheckinListener, NonFatalErrorListener, UndonePendingChangeListener, NewPendingChangeListener, BeforeShelveListener, DestroyListener, ConflictListener, GetListener, MergingListener, ConflictResolvedListener, Closable { private static final int SUMMARY_THRESHOLD = 10; protected static final Log log = LogFactory.getLog(Command.class); private TFSTeamProjectCollection collection = null; public final TFSTeamProjectCollection getCollection() { return collection; } public static final DefaultPersistenceStoreProvider CLC_PERSISTENCE_PROVIDER = DefaultPersistenceStoreProvider.INSTANCE; /** * This command's canonical name. */ private String canonicalName = ""; //$NON-NLS-1$ /** * The alias under which this option was invoked. */ private String alias; /** * We save this so we can reconstruct command-lines exactly as the user * typed them. */ private String userText; private Option[] options; private String[] freeArguments; /** * The display object this command will use to relay output to the user. */ private Display display = new NullDisplay(); /** * The input object this command will use to read input from the user. */ private Input input = new NullInput(); /** * The exit code for this command. If never set with * {@link #setExitCode(int)} while the command runs, * {@link ExitCode#UNKNOWN} is converted to {@link ExitCode#SUCCESS}. */ private int exitCode = ExitCode.UNKNOWN; /** * Used when printing out pending changes, so we can keep track of the last * directory we printed and only print file names until the directory * changes. */ private String lastPendingChangeDirectory; /** * When non-null, XML is written to the display instead of the normal format * for things like pending changes. */ private SimpleXMLWriter xmlWriter = null; /** * We use this to keep track of the last server or local path that was part * of an onGet() event, so we can print short file names. */ private String lastGetDirectory = null; private final List<GetEvent> getWarningList = new ArrayList<GetEvent>(); private final List<MergingEvent> mergeWarningList = new ArrayList<MergingEvent>(); /* * Non-fatal events come in two flavors, warnings and errors. We need to * keep track of the counts separately for use in the summary output. */ private final List<NonFatalErrorMessage> nonFatalMessageList = new ArrayList<NonFatalErrorMessage>(); private int numNonFatalErrors; private int numNonFatalWarnings; /* * Cache connections to the servers by URI and credentials; in batch mode, * we can reuse these connections to avoid spinning up new ones. We maintain * a map of URIs to all the connections built for that URI so that we can * match to the credentials. We cannot simply map the credentials because * they can change over time, after map insertion. */ private static final Map<URI, List<TFSTeamProjectCollection>> connections = new HashMap<URI, List<TFSTeamProjectCollection>>(); class NonFatalErrorMessage { private final boolean error; private final String message; public NonFatalErrorMessage(final boolean error, final String message) { this.error = error; this.message = message; } public boolean isError() { return error; } public String getMessage() { return message; } }; /** * NonFatalError events may be fired from threads other than the main thread * and may be fired concurrently. We use this lock to protect the list and * counters related to NonFatalError handling. */ private final Object nonFatalErrorLock = new Object(); /** * Set by {@link #initializeClient(VersionControlClient)} on platforms where * cross-process notifications are supported. */ private MessageWindowNotificationManager notificationManager; /** * Set by {@link #initializeClient(VersionControlClient)} because * {@link #close()} needs it to do its work properly. */ private VersionControlClient client; private Workstation workstation; /** * Zero argument constructor so we can create new instances of this class * dynamically. */ public Command() { } /** * Execute the command. Be sure to call setOptions() and setFreeArguments() * (if any were supplied) or this method will probably fail. * <p> * The method may call {@link #setExitCode(int)} during its execution. * * @throws InvalidFreeArgumentException * if a required argument is missing. */ public abstract void run() throws ArgumentException, MalformedURLException, CLCException, LicenseException; /** * Gets the option profiles this command supports. Options in the global * option set of the {@link CommandsMap} used by ths application (vc, wit, * etc.) do not need to be listed by extending classes. * * @return an array of option profiles that this command class supports (not * including global options), an empty array if no options are * supported. */ public abstract AcceptedOptionSet[] getSupportedOptionSets(); /** * Gets the free-form text help for this command, each string representing a * single paragraph (without explicit line breaks). * * @return the free form text help for this command, each string a paragraph * without any line breaks. */ public abstract String[] getCommandHelpText(); /** * @param userText * the string the user typed to invoke this command (not necessarily * the canonical name). Not null or empty. */ public void setUserText(final String userText) { Check.notNullOrEmpty(userText, "userText"); //$NON-NLS-1$ this.userText = userText; } /** * @return the string the user typed to invoke this command (not necessarily * the canonical name). */ public final String getUserText() { return userText; } /** * @param alias * the alias that was matched to create this command (not null or * empty). */ public final void setMatchedAlias(final String alias) { Check.notNullOrEmpty(alias, "alias"); //$NON-NLS-1$ this.alias = alias; } /** * @return the alias that was matched to create this command. */ public final String getMatchedAlias() { return alias; } /** * @param canonicalName * this command's canonical name (not null or empty). */ public final void setCanonicalName(final String canonicalName) { Check.notNullOrEmpty(canonicalName, "name"); //$NON-NLS-1$ this.canonicalName = canonicalName; } /** * @return this command's canonical name. */ public final String getCanonicalName() { return canonicalName; } /** * Call this method for every command you create, or the command defaults to * a {@link NullDisplay} which the user won't appreciate. * * @param display * the display to use for all output for this command (not null). */ public final void setDisplay(final Display display) { Check.notNull(display, "display"); //$NON-NLS-1$ this.display = display; } /** * @return the display that this command uses for all output. */ public final Display getDisplay() { return display; } /** * Call this method for every command you create, or the command defaults to * a {@link NullInput} which the user won't appreciate. * * @param input * the class to use for all input for this command (not null). */ public final void setInput(final Input input) { Check.notNull(input, "input"); //$NON-NLS-1$ this.input = input; } /** * @return the class that this command uses for all input. */ public final Input getInput() { return input; } /** * Sets the options that the user wants set for this command instance. * * @param setOptions * the options to set for this command (not null). * @param validGlobalOptions * the valid global options to accept for this command (not null). * @throws InvalidOptionException * if one of the given options is not appropriate for this command. */ public final void setOptions(final Option[] setOptions, final Class[] validGlobalOptions) throws InvalidOptionException { Check.notNull(setOptions, "options"); //$NON-NLS-1$ Check.notNull(validGlobalOptions, "globalOptions"); //$NON-NLS-1$ final ArrayList allValidOptions = new ArrayList(); final AcceptedOptionSet[] profiles = getSupportedOptionSets(); for (int i = 0; i < profiles.length; i++) { allValidOptions.addAll(Arrays.asList(profiles[i].getOptionalOptions())); allValidOptions.addAll(Arrays.asList(profiles[i].getRequiredOptions())); } allValidOptions.addAll(Arrays.asList(validGlobalOptions)); /* * Make sure each of the set options is in the array of all valid * options. */ for (int i = 0; i < setOptions.length; i++) { boolean foundInValidOptions = false; for (int j = 0; j < allValidOptions.size(); j++) { if (allValidOptions.get(j).equals(setOptions[i].getClass())) { foundInValidOptions = true; break; } } if (!foundInValidOptions) { final String messageFormat = Messages.getString("Command.CommandDoesNotSupportOptionFormat"); //$NON-NLS-1$ final String message = MessageFormat.format(messageFormat, getMatchedAlias(), setOptions[i].toString()); throw new InvalidOptionException(message); } } options = setOptions; } /** * @return the options that the user wants set for this command instance. */ public Option[] getOptions() { return options; } /** * @param args * the free arguments the user supplied for this command (not null). */ public void setFreeArguments(final String[] args) { Check.notNull(args, "args"); //$NON-NLS-1$ freeArguments = args; } /** * @return the free arguments the user supplied for this command. */ public String[] getFreeArguments() { return freeArguments; } public int getExitCode() { return exitCode; } /** * Set the exit code for the commands. Setting the exit code multiple times * composes in interesting ways here. If all exitCodes match, the result is * the value. If the new setting is greater, keep the new one (that way if * 100 happens at the end, it wins). Otherwise, if different, the result is * {@link ExitCode#PARTIAL_SUCCESS}. * * @param exitCode * Exit code to set */ public void setExitCode(final int exitCode) { if (this.exitCode == ExitCode.UNKNOWN || exitCode > this.exitCode) { this.exitCode = exitCode; } else if (exitCode != this.exitCode) { this.exitCode = ExitCode.PARTIAL_SUCCESS; } } /** * Gets the subset of the free arguments that begin with the given index. * * @param firstLocalPathArgumentIndex * the index of the first free argument that will be included in the * returned set (must be >= 0 and <= to the last index of the free * arguments). * @return the subset of the free arguments in the original order starting * with the given index. */ public String[] getLastFreeArguments(final int firstLocalPathArgumentIndex) { final String[] allFreeArguments = getFreeArguments(); final List subList = Arrays.asList(allFreeArguments).subList(firstLocalPathArgumentIndex, allFreeArguments.length); return (String[]) subList.toArray(new String[subList.size()]); } /** * Finds all options that the user set for this command that match the given * class. * * @param optionClass * the class to search for option types that match (not null). * @return the array of options the user set on this command that have the * given type. */ public Option[] findAllOptionTypes(final Class optionClass) { Check.notNull(optionClass, "optionClass"); //$NON-NLS-1$ return findAllOptionTypes(new Class[] { optionClass }); } /** * Finds all options that the user set for this command that match the given * classes * * @param optionClass * the classes to search for option types that match (not null). * @return the array of options the user set on this command that have the * given type. */ public Option[] findAllOptionTypes(final Class[] optionClasses) { Check.notNull(optionClasses, "optionClasses"); //$NON-NLS-1$ /* Store in a List to ensure that we get them back in the given order */ final Set<Option> optionsSet = new HashSet<Option>(); final List<Option> optionsList = new ArrayList<Option>(); for (int i = 0; i < options.length; i++) { for (int j = 0; j < optionClasses.length; j++) { if (options[i].getClass().equals(optionClasses[j])) { optionsSet.add(options[i]); optionsList.add(options[i]); break; } } } return optionsList.toArray(new Option[optionsList.size()]); } /** * Finds the first option that the user set for this command that matches * the given class. * * @param optionClass * the class to search for an option type that matches (not null). * @return the first option the user set on this command that has the given * type, or null if no matching option was found */ public Option findOptionType(final Class optionClass) { Check.notNull(optionClass, "optionClass"); //$NON-NLS-1$ for (int i = 0; i < options.length; i++) { if (options[i].getClass().equals(optionClass)) { return options[i]; } } return null; } /* * (non-Javadoc) * * @see java.lang.Object#toString() */ @Override public String toString() { return getMatchedAlias(); } /** * Get the syntax helper string for each option profile supported by this * command. The syntax strings do NOT include this command's name. * * @param optionsMap * the options map for this Command (not null). * @return a command-line help syntax string for each option profile * supported by this command, not including the command name in the * strings. */ public final String[] getSyntaxStrings(final OptionsMap optionsMap) { Check.notNull(optionsMap, "optionsMap"); //$NON-NLS-1$ final ArrayList<String> ret = new ArrayList<String>(); final AcceptedOptionSet[] profiles = getSupportedOptionSets(); for (int i = 0; i < profiles.length; i++) { final AcceptedOptionSet profile = profiles[i]; if (profile == null) { continue; } final StringBuffer sb = new StringBuffer(); final Class[] optionalOptions = profile.getOptionalOptions(); final Class[] requiredOptions = profile.getRequiredOptions(); // Add the required options first, without the brackets. for (int j = 0; j < requiredOptions.length; j++) { if (sb.length() > 0) { sb.append(" "); //$NON-NLS-1$ } if (requiredOptions[j] == null) { final String messageFormat = "parsed null required option at index {0} , skipping"; //$NON-NLS-1$ final String message = MessageFormat.format(messageFormat, Integer.toString(j)); log.error(message); continue; } final Option o = instantiateOptionForSyntaxString(requiredOptions[j], optionsMap); if (o != null) { // Get the command-bound string first String s = o.getSyntaxString(getClass()); if (s == null) { // Fallback to the non-command-bound string s = o.getSyntaxString(); } sb.append(s); } } // Now do the optional options. for (int j = 0; j < optionalOptions.length; j++) { if (sb.length() > 0) { sb.append(" "); //$NON-NLS-1$ } if (optionalOptions[j] == null) { final String messageFormat = "parsed null optional option at index {0} , skipping"; //$NON-NLS-1$ final String message = MessageFormat.format(messageFormat, Integer.toString(j)); log.error(message); continue; } final Option o = instantiateOptionForSyntaxString(optionalOptions[j], optionsMap); if (o != null) { sb.append("["); //$NON-NLS-1$ sb.append(o.getSyntaxString()); sb.append("]"); //$NON-NLS-1$ } } // Do free arguments. final String freeArgumentsSyntax = profile.getFreeArgumentsSyntax(); if (!StringUtil.isNullOrEmpty(freeArgumentsSyntax)) { if (sb.length() > 0) { sb.append(" "); //$NON-NLS-1$ } sb.append(freeArgumentsSyntax); } // Add the string buffer's contents to the return list. ret.add(sb.toString()); } return ret.toArray(new String[ret.size()]); } /** * Creates an instance of an option of the given type that must exist in the * given options map. * * @param optionClass * the class of the option type to create (not null). * @param optionsMap * the map that contains the option (not null). * @return the instance of the option requested. */ private Option instantiateOptionForSyntaxString(final Class optionClass, final OptionsMap optionsMap) { Check.notNull(optionClass, "optionClass"); //$NON-NLS-1$ Check.notNull(optionsMap, "optionsMap"); //$NON-NLS-1$ // Look up the canonical name of the option by the class. final String canonicalName = (String) optionsMap.getOptionsToCanonicalNamesMap().get(optionClass); if (canonicalName == null) { final String messageFormat = Messages.getString("Command.OptionNotRegisteredFormat"); //$NON-NLS-1$ final String message = MessageFormat.format(messageFormat, optionClass.toString(), Main.VENDOR_NAME); log.error(message); throw new RuntimeException(message); } final Option option = optionsMap.instantiateOption(canonicalName); option.setMatchedAlias(canonicalName); return option; } /** * Finds the single locally cached workspace that contains the given list of * items. Only workspaces on this computer can match. * * @param items * the list of items that have to belong to a single cached workspace * (not null). * @return the workspace found that contains the all the given items. * @throws CannotFindWorkspaceException * if the local client cache's lock file could not be created. * InvalidFreeArgumentException if the specified items do not belong * to a single workspace */ protected final WorkspaceInfo findSingleCachedWorkspace(final QualifiedItem[] items) throws InvalidFreeArgumentException, CannotFindWorkspaceException { Check.notNull(items, "items"); //$NON-NLS-1$ final String[] canonicalPaths = new String[items.length]; int idx = 0; for (final QualifiedItem item : items) { canonicalPaths[idx++] = item.getPath(); } return findSingleCachedWorkspaceImpl(canonicalPaths); } /** * Finds the single locally cached workspace that contains the given list of * paths. Only workspaces on this computer can match. * * @param paths * the list of path that have to belong to a single cached workspace * (not null). * @return the workspace found that contains the all the given paths. * @throws CannotFindWorkspaceException * if the local client cache's lock file could not be created. * InvalidFreeArgumentException if the specified paths do not belong * to a single workspace */ protected final WorkspaceInfo findSingleCachedWorkspace(final String[] paths) throws InvalidFreeArgumentException, CannotFindWorkspaceException { Check.notNull(paths, "paths"); //$NON-NLS-1$ final String[] canonicalPaths = new String[paths.length]; int idx = 0; for (final String path : paths) { if (ServerPath.isServerPath(path)) { canonicalPaths[idx++] = ServerPath.canonicalize(path); } else { canonicalPaths[idx++] = LocalPath.canonicalize(path); } } return findSingleCachedWorkspaceImpl(canonicalPaths); } private final WorkspaceInfo findSingleCachedWorkspaceImpl(final String[] paths) throws InvalidFreeArgumentException, CannotFindWorkspaceException { Check.notNull(paths, "paths"); //$NON-NLS-1$ WorkspaceInfo singleWorkspace = null; WorkspaceInfo mappedWorkspaceForCurrentWorkingDirectory = null; for (final String path : paths) { Check.notNull(path, "path"); //$NON-NLS-1$ final WorkspaceInfo mappedWorkspace; if (ServerPath.isServerPath(path)) { if (mappedWorkspaceForCurrentWorkingDirectory == null) { /* * See if the current directory is mapped in a locally * cached workspace. */ final File currentDirectory = new File( LocalPath.canonicalize(LocalPath.getCurrentWorkingDirectory())); mappedWorkspaceForCurrentWorkingDirectory = findCachedWorkspaceForPath( currentDirectory.getAbsolutePath()); } mappedWorkspace = mappedWorkspaceForCurrentWorkingDirectory; } else { /* * See if any locally cached workspace contains this path. */ mappedWorkspace = findCachedWorkspaceForPath(path); } if (mappedWorkspace != null) { /* * Save this workspace to a method variable to we can ensure all * of the arguments the user supplied are in the same workspace. */ if (singleWorkspace == null) { singleWorkspace = mappedWorkspace; } else if (!singleWorkspace.equals(mappedWorkspace)) { throw new InvalidFreeArgumentException( Messages.getString("Command.AllItemsMustResideSingleWorkspace")); //$NON-NLS-1$ } } else { /* * TODO Implement this condition. We should search all mapped * workspaces for sub-folders that intersect with the recursive * expansion of the item supplied by the user, and get those * folders. */ throw new InvalidFreeArgumentException( MessageFormat.format(Messages.getString("Command.UnableToDetermineWorkspaceFormat"), //$NON-NLS-1$ OptionsMap.getPreferredOptionPrefix())); } } return singleWorkspace; } /** * Finds the locally cached workspace that contains the given local path. * Only workspaces on this computer can match. null is returned if no * matching workspace is found. * * @param localPath * the local path (file or folder) to find in all locally cached * workspaces (not null). * @return the first workspace found that contains the given path, null if * no matching workspace is found. * @throws CannotFindWorkspaceException * if the local client cache's lock file could not be created. */ protected final WorkspaceInfo findCachedWorkspaceForPath(final String localPath) throws CannotFindWorkspaceException { if (localPath != null) { if (!ServerPath.isServerPath(localPath) && Workstation.getCurrent(CLC_PERSISTENCE_PROVIDER).isMapped(localPath)) { return Workstation.getCurrent(CLC_PERSISTENCE_PROVIDER).getLocalWorkspaceInfo(localPath); } } return null; } /** * Determines which cached workspace (if any) matches the workspace command * line option, or the free arguments, or the current directory (in that * order). Throws if no match is found. * <p> * All free arguments are considered local paths and can cause a cached * workspace that has matching working folder mappings to be returned. Call * {@link #determineCachedWorkspace(String[])} or * {@link #determineCachedWorkspace(String[], boolean)} and supply your own * local path arguments if you desire finer control. * * @return the cached workspace that matches the command line options or * current working directory. * @throws CannotFindWorkspaceException * if the workspace specified on the command line cannot be found in * the local client cache, or if no cached workspaces map to the * current working directory. * @throws InvalidOptionValueException * if the server option was given but the option value can't be * parsed as a URI. * @throws InvalidOptionException * if the server and collection options are both specified */ protected final WorkspaceInfo determineCachedWorkspace() throws CannotFindWorkspaceException, InvalidOptionValueException, InvalidOptionException { return determineCachedWorkspace(null, false); } /** * Determines which cached workspace (if any) matches the workspace command * line option, or the free arguments, or the current directory (in that * order). Throws if no match is found. * * @param pathFreeArguments * an array of all the free arguments that are local paths that can * matched against cached workspace working folder mappings to find a * matching cached workspace. If null or empty, no path-based search * for a cached workspace is performed. * * @return the cached workspace that matches the command line options or * current working directory. * @throws CannotFindWorkspaceException * if the workspace specified on the command line cannot be found in * the local client cache, or if no cached workspaces map to the * current working directory. * @throws InvalidOptionValueException * if the server option was given but the option value can't be * parsed as a URI. * @throws InvalidOptionException * if the server and collection options are both specified */ protected final WorkspaceInfo determineCachedWorkspace(final String[] pathFreeArguments) throws CannotFindWorkspaceException, InvalidOptionValueException, InvalidOptionException { return determineCachedWorkspace(pathFreeArguments, false); } /** * Determines which cached workspace (if any) matches the workspace command * line option, or the free arguments, or the current directory (in that * order). Throws if no match is found. Only cached workspaces that reside * on the current computer can match. * * @param pathFreeArguments * an array of all the free arguments that are local paths that can * matched against cached workspace working folder mappings to find a * matching cached workspace. If null or empty, no path-based search * for a cached workspace is performed. * @param ignoreWorkspaceOptionValue * if true, the any workspace option supplied on the command-line is * ignored, and only the free arguments and current directory are * checked to determine the cached workspace. This is probably only * useful for the CLC for commands where the /workspace option is * used to set the workspace name to query on, but should not cause a * cached workspace to be present for it. * @return the cached workspace that matches the command line options or * current working directory. * @throws CannotFindWorkspaceException * if the workspace specified on the command line cannot be found in * the local client cache, or if no cached workspaces map to the * current working directory. * @throws InvalidOptionValueException * if the server option was given but the option value can't be * parsed as a URI. * @throws InvalidOptionException * if the server and collection options are both specified */ protected final WorkspaceInfo determineCachedWorkspace(final String[] pathFreeArguments, final boolean ignoreWorkspaceOptionValue) throws CannotFindWorkspaceException, InvalidOptionValueException, InvalidOptionException { WorkspaceInfo cachedWorkspace = null; /* * See if the user specified it on the command line. */ final OptionWorkspace optionWorkspace = (OptionWorkspace) findOptionType(OptionWorkspace.class); if (!ignoreWorkspaceOptionValue && optionWorkspace != null) { final WorkspaceSpec spec; try { spec = WorkspaceSpec.parse(optionWorkspace.getValue(), null); } catch (final WorkspaceSpecParseException e) { throw new CannotFindWorkspaceException(e.getMessage()); } /* * The user may have specified the server option to qualify an * ambiguous workspace (same name, owner, domain, different server). */ final URI serverURI; final OptionCollection collectionOption = getCollectionOption(); if (collectionOption != null) { serverURI = collectionOption.getURI(); } else { serverURI = null; } final WorkspaceInfo[] all = findLocalWorkspaces(serverURI, spec.getName(), spec.getOwner()); if (all == null || all.length == 0) { final String messageFormat = Messages.getString("Command.WorkspaceNotFoundInCacheFormat"); //$NON-NLS-1$ final String message = MessageFormat.format(messageFormat, spec.toString()); throw new CannotFindWorkspaceException(message); } else if (all.length > 1 && spec.getOwner() == null) { final String messageFormat = Messages.getString("Command.WorkspaceNameMatchesMoreThanOneFormat"); //$NON-NLS-1$ final String message = MessageFormat.format(messageFormat, spec.toString()); throw new CannotFindWorkspaceException(message); } else if (all.length > 1) { // Workspace owner was specified, still ambiguous. final String messageFormat = Messages .getString("Command.WorkspaceNameAndOwnerMatchesMoreThanOneFormat"); //$NON-NLS-1$ final String message = MessageFormat.format(messageFormat, spec.getName(), spec.getOwner()); throw new CannotFindWorkspaceException(message); } cachedWorkspace = all[0]; } else { /* * No workspace option was specified, or the parameter was set so we * ignored it, so look at the free arguments for a local file name, * and try to find that mapped in some locally cached workspace. In * this mode of operation (no workspace option given), the CLC will * never hit the server to find a workspace. It must already be in * the cache. */ /* * If the pathFreeArguments option is null, treat all free arguments * as paths. */ final String[] pathsToSearch = (pathFreeArguments == null) ? getFreeArguments() : pathFreeArguments; for (int i = 0; i < pathsToSearch.length; i++) { String path = pathsToSearch[i]; /* * A path can be a version spec, which is a path with a version * appended. Run the string through the version spec class to * get only the file part. */ try { final VersionedFileSpec spec = VersionedFileSpec.parse(path, VersionControlConstants.AUTHENTICATED_USER, true); if (spec.getItem() != null) { path = spec.getItem(); } } catch (final VersionSpecParseException e) { // Ignore. } catch (final LabelSpecParseException e) { // Ignore. } /* * Canonicalize the path so relative local paths can be used. * It's possible a free argument is not a path, so we just skip * those if there's not cached workspace that matched it as a * path. */ path = ItemPath.canonicalize(path); /* * We can only reliably search for mapped local paths, because a * server path is likely to be mapped multiple places. */ if (!ServerPath.isServerPath(path)) { cachedWorkspace = findCachedWorkspaceForPath(path); if (cachedWorkspace != null) { break; } } } if (cachedWorkspace == null) { /* * We couldn't find it using the free arguments (maybe they * weren't full paths). Try the current directory. */ final File currentDirectory = new File(LocalPath.getCurrentWorkingDirectory()); cachedWorkspace = findCachedWorkspaceForPath(currentDirectory.getAbsolutePath()); } if (cachedWorkspace == null) { throw new CannotFindWorkspaceException(Messages.getString("Command.WorkspaceCouldNotBeDetermined")); //$NON-NLS-1$ } } Check.notNull(cachedWorkspace, "cachedWorkspace"); //$NON-NLS-1$ return cachedWorkspace; } /** * Searches the local workspace cache for workspaces that match. * * @param serverURI * the server {@link URI} to match (<code>null</code> matches all) * @param name * the name to match (must not be <code>null</code>) * @param owner * the owner to match (<code>null</code> matches all) * @return the workspaces found, possibly an empty list */ private WorkspaceInfo[] findLocalWorkspaces(final URI serverURI, final String name, final String owner) { Check.notNull(name, "name"); //$NON-NLS-1$ final WorkspaceInfo[] infos = Workstation.getCurrent(CLC_PERSISTENCE_PROVIDER).getAllLocalWorkspaceInfo(); if (infos == null || infos.length == 0) { return new WorkspaceInfo[0]; } final List<WorkspaceInfo> matches = new ArrayList<WorkspaceInfo>(); for (final WorkspaceInfo info : infos) { if ((serverURI == null || Workspace.matchServerURI(serverURI, info.getServerURI())) && Workspace.matchName(name, info.getName()) && (owner == null || info.ownerNameMatches(owner))) { matches.add(info); } } return matches.toArray(new WorkspaceInfo[matches.size()]); } /** * Throw an exception if the the specified array of paths contain a local * file path that does not have a workspace mapping. * * @param pathFreeArguments * an array of server and/or local path names * @throws CannotFindWorkspaceException * thrown by findCacheWorkspaceForPath * @throws CLCException * if a local path does not have a workspace mapping. */ protected void throwIfContainsUnmappedLocalPath(final String[] pathFreeArguments) throws CannotFindWorkspaceException, CLCException { if (pathFreeArguments != null) { for (final String path : pathFreeArguments) { if (!ServerPath.isServerPath(path)) { if (findCachedWorkspaceForPath(path) == null) { final String messageFormat = Messages.getString("CommandDir.NoWorkingFolderMappingFormat"); //$NON-NLS-1$ throw new CLCException(MessageFormat.format(messageFormat, path)); } } } } } /** * Gets the {@link OptionCollection} (possibly an instance of * {@link OptionServer}) the user specified, or null if neither was * specified. If both were specified, reports an error. * * @return the {@link OptionCollection} (possibly an instance of * {@link OptionServer}) specified by the user, or null if neither * was specified * @throws InvalidOptionException * if {@link OptionCollection} and {@link OptionServer} were both * specified */ protected OptionCollection getCollectionOption() throws InvalidOptionException { final OptionServer serverOption = (OptionServer) findOptionType(OptionServer.class); final OptionCollection collectionOption = (OptionCollection) findOptionType(OptionCollection.class); if (serverOption != null && collectionOption != null) { final String messageFormat = Messages.getString("Command.OptionCannotBeUsedWithOptionFormat"); //$NON-NLS-1$ final String message = MessageFormat.format(messageFormat, serverOption.getMatchedAlias(), collectionOption.getMatchedAlias()); throw new InvalidOptionException(message); } if (serverOption != null) { return serverOption; } return collectionOption; } /** * Creates a new {@link TFSTeamProjectCollection} object and initialize it * with the given arguments and options given to this command. * <p> * When searching for a cached workspace to use for the connection, this * method treats all free arguments as local paths. Use a method that takes * a list of local paths for finer control. * * @return a new TFSConnection object, initialized for the user's arguments, * and already connected to the server. * @throws TFSException * if the server returned an error when the repository properties * were refreshed. * @throws MissingRequiredOptionException * when a required option is missing (TODO remove the capability to * throw this exception type when the options are really optional, * and not required). * @throws CannotFindWorkspaceException * when the workspace was not specified as an option, and cannot be * determined from any argument paths. * @throws MalformedURLException * if a valid URL could not be constructed for this TFS object. * @throws CLCException * when the login credentials could not be found in the cache and * were not supplied as an option. * @throws ActivationException */ protected final TFSTeamProjectCollection createConnection() throws ArgumentException, MalformedURLException, CLCException, LicenseException { return createConnection(null, null, false, false); } /** * Creates a new {@link TFSTeamProjectCollection} object and initialize it * with the given arguments and options given to this command. * <p> * When searching for a cached workspace to use for the connection, this * method treats all free arguments as local paths. Use a method that takes * a list of local paths for finer control. * * @param ignoreWorkspaceDetectionFailure * if true, this method will not throw an exception when no workspace * is detected, but the server option is required, or an exception is * thrown. If false, an exception is thrown if a workspace is not * detected. * @return a new TFSConnection object, initialized for the user's arguments, * and already connected to the server. * @throws CannotFindWorkspaceException * when the workspace was not specified as an option, and cannot be * determined from any argument paths. * @throws MalformedURLException * if a valid URL could not be constructed for this TFS object. * @throws CLCException * when the login credentials could not be found in the cache and * were not supplied as an option. * @throws ActivationException */ protected final TFSTeamProjectCollection createConnection(final boolean ignoreWorkspaceDetectionFailure) throws ArgumentException, MalformedURLException, CLCException, LicenseException { return createConnection(null, null, ignoreWorkspaceDetectionFailure, false); } /** * Creates a new {@link TFSTeamProjectCollection} object and initialize it * with the given arguments and options given to this command. * * @param pathFreeArguments * an array of all the free arguments that are local paths for this * command, so they can be searched if no profile or workspace option * was given (or they are not checked because of other parameter * values). If null, all free arguments are matched as local paths. * If an empty array, no path-based search for a cached workspace is * performed. * * @return a new TFSConnection object, initialized for the user's arguments, * and already connected to the server. * @throws TFSException * if the server returned an error when the repository properties * were refreshed. * @throws MissingRequiredOptionException * when a required option is missing (TODO remove the capability to * throw this exception type when the options are really optional, * and not required). * @throws CannotFindWorkspaceException * when the workspace was not specified as an option, and cannot be * determined from any argument paths. * @throws MalformedURLException * if a valid URL could not be constructed for this TFS object. * @throws CLCException * when the login credentials could not be found in the cache and * were not supplied as an option. * @throws ActivationException */ protected final TFSTeamProjectCollection createConnection(final String[] pathFreeArguments) throws ArgumentException, MalformedURLException, CLCException, LicenseException { return createConnection(null, pathFreeArguments, false, false); } /** * Creates a new {@link TFSTeamProjectCollection} object and initialize it * with the given arguments and options given to this command. * * @param pathFreeArguments * an array of all the free arguments that are local paths for this * command, so they can be searched if no profile or workspace option * was given (or they are not checked because of other parameter * values). If null, all free arguments are matched as local paths. * If an empty array, no path-based search for a cached workspace is * performed. * @param ignoreWorkspaceDetectionFailure * if true, this method will not throw an exception when no workspace * is detected, but the server option is required, or an exception is * thrown. If false, an exception is thrown if a workspace is not * detected. * @return a new TFSConnection object, initialized for the user's arguments, * and already connected to the server. * @throws CannotFindWorkspaceException * when the workspace was not specified as an option, and cannot be * determined from any argument paths. * @throws MalformedURLException * if a valid URL could not be constructed for this TFS object. * @throws CLCException * when the login credentials could not be found in the cache and * were not supplied as an option. * @throws ActivationException */ protected final TFSTeamProjectCollection createConnection(final String[] pathFreeArguments, final boolean ignoreWorkspaceDetectionFailure) throws ArgumentException, MalformedURLException, CLCException, LicenseException { return createConnection(null, pathFreeArguments, ignoreWorkspaceDetectionFailure, false); } /** * Creates a new {@link TFSTeamProjectCollection} object and initialize it * with the given arguments and options given to this command. * <p> * The first param (ignoreWorkspaceAutoDetectionFailure) is a little * complicated. The option controls whether the exception is thrown if and * only if an explicit workspace option is not given by the user and the * automatic detection fails. The exception is always thrown if the * workspace option value was given and it can't be used. This option is set * for commands like history, label, properties, status, workspace, etc. * These commands don't (always) require a local workspace. * <p> * When searching for a cached workspace to use for the connection, this * method treats all free arguments as local paths. Use a method that takes * a list of local paths for finer control. * * @param ignoreWorkspaceAutoDetectionFailure * if true, this method will not throw an exception when no workspace * is detected and no workspace option was given, but the server * option is required, or an exception is thrown. If false, an * exception is thrown if a workspace is not detected. Useful for * commands that don't require a workspace. * @param ignoreWorkspaceOptionValue * if true, the any workspace option supplied on the command-line is * ignored, and only the free arguments and current directory are * checked to determine the cached workspace. This is probably only * useful for the CLC for commands where the /workspace option is * used to set the workspace name to query on, but should not cause a * cached workspace to be present for it. * @return a new TFSConnection object, initialized for the user's arguments, * and already connected to the server. * @throws CannotFindWorkspaceException * when the workspace was not specified as an option, or the * workspace option matched multiple or no workspace, or the * workspace cannot be determined from any argument paths. * @throws MalformedURLException * if a valid URL could not be constructed for this TFS object. * @throws CLCException * when the login credentials could not be found in the cache and * were not supplied as an option. * @throws ActivationException */ protected final TFSTeamProjectCollection createConnection(final boolean ignoreWorkspaceAutoDetectionFailure, final boolean ignoreWorkspaceOptionValue) throws ArgumentException, MalformedURLException, CLCException, LicenseException { return createConnection(null, null, ignoreWorkspaceAutoDetectionFailure, ignoreWorkspaceOptionValue); } /** * Creates a new {@link TFSTeamProjectCollection} object and initialize it * with the given arguments and options given to this command. * <p> * The first param (ignoreWorkspaceAutoDetectionFailure) is a little * complicated. The option controls whether the exception is thrown if and * only if an explicit workspace option is not given by the user and the * automatic detection fails. The exception is always thrown if the * workspace option value was given and it can't be used. This option is set * for commands like history, label, properties, status, workspace, etc. * These commands don't (always) require a local workspace. * * @param pathFreeArguments * an array of all the free arguments that are local paths for this * command, so they can be searched if no profile or workspace option * was given (or they are not checked because of other parameter * values). If null, all free arguments are matched as local paths. * If an empty array, no path-based search for a cached workspace is * performed. * @param ignoreWorkspaceAutoDetectionFailure * if true, this method will not throw an exception when no workspace * is detected and no workspace option was given, but the server * option is required, or an exception is thrown. If false, an * exception is thrown if a workspace is not detected. Useful for * commands that don't require a workspace. * @param ignoreWorkspaceOptionValue * if true, the any workspace option supplied on the command-line is * ignored, and only the free arguments and current directory are * checked to determine the cached workspace. This is probably only * useful for the CLC for commands where the /workspace option is * used to set the workspace name to query on, but should not cause a * cached workspace to be present for it. * @return a new TFSConnection object, initialized for the user's arguments, * and already connected to the server. * @throws CannotFindWorkspaceException * when the workspace was not specified as an option, or the * workspace option matched multiple or no workspace, or the * workspace cannot be determined from any argument paths. * @throws MalformedURLException * if a valid URL could not be constructed for this TFS object. * @throws CLCException * when the login credentials could not be found in the cache and * were not supplied as an option. * @throws ActivationException */ protected final TFSTeamProjectCollection createConnection(final String[] pathFreeArguments, final boolean ignoreWorkspaceAutoDetectionFailure, final boolean ignoreWorkspaceOptionValue) throws ArgumentException, MalformedURLException, CLCException, LicenseException { return createConnection(null, pathFreeArguments, ignoreWorkspaceAutoDetectionFailure, ignoreWorkspaceOptionValue); } /** * Creates a new {@link TFSTeamProjectCollection} object and initialize it * with the given arguments and options given to this command. * <p> * The ignoreWorkspaceAutoDetectionFailure parameter is a little * complicated. The option controls whether the exception is thrown if and * only if an explicit workspace option is not given by the user and the * automatic detection fails. The exception is always thrown if the * workspace option value was given and it can't be used. This option is set * for commands like history, label, properties, status, workspace, etc. * These commands don't (always) require a local workspace. * * @param uri * the connection uri to use. If null, one is built from the * environment and options provided by the user. If non-null, the * profile is cloned and the clone is modified by the method. * @param pathFreeArguments * an array of all the free arguments that are local paths for this * command, so they can be searched if no profile or workspace option * was given (or they are not checked because of other parameter * values). If null or empty, no path-based search for a cached * workspace is performed. * @param ignoreWorkspaceAutoDetectionFailure * if true, this method will not throw an exception when no workspace * is detected and no workspace option was given, but the server * option is required, or an exception is thrown. If false, an * exception is thrown if a workspace is not detected. Useful for * commands that don't require a workspace. * @param ignoreWorkspaceOptionValue * if true, the any workspace option supplied on the command-line is * ignored, and only the free arguments and current directory are * checked to determine the cached workspace. This is probably only * useful for the CLC for commands where the /workspace option is * used to set the workspace name to query on, but should not cause a * cached workspace to be present for it. * @return a new TFSConnection object, initialized for the user's arguments, * and already connected to the server. * @throws CannotFindWorkspaceException * when the workspace was not specified as an option, or the * workspace option matched multiple or no workspace, or the * workspace cannot be determined from any argument paths. * @throws MalformedURLException * if a valid URL could not be constructed for this TFS object. * @throws CLCException * when the login credentials could not be found in the cache and * were not supplied as an option. * @throws LicenseException * when a licensing error occurs (the EULA is not accepted or a * product id is not installed) */ protected final TFSTeamProjectCollection createConnection(URI serverURI, final String[] pathFreeArguments, final boolean ignoreWorkspaceAutoDetectionFailure, final boolean ignoreWorkspaceOptionValue) throws ArgumentException, MalformedURLException, CLCException, LicenseException { Option o = null; /* Test for EULA acceptance / product id installation */ testLicense(); /* * If we don't yet have a profile (no profile option), find a cached * workspace and use its profile. */ if (serverURI == null) { WorkspaceInfo cachedWorkspace = null; try { cachedWorkspace = determineCachedWorkspace(pathFreeArguments, ignoreWorkspaceOptionValue); } catch (final CannotFindWorkspaceException e) { /* * Since we had an error loading the cached workspace, throw if * any of: * * 1. The option was given and failed to load. * * 2. The option wasn't given but the user doesn't want to * ignore auto detect failures. */ if (findOptionType(OptionWorkspace.class) != null || !ignoreWorkspaceAutoDetectionFailure) { throw e; } } if (cachedWorkspace != null) { serverURI = cachedWorkspace.getServerURI(); } } /* * Now override any cached settings with properties from other * command-line options. These are not transient properties, they should * be saved in the workspace cache for future sessions. Transient * properties would not be saved at all, leaving missing attributes in * the cached workspaces. */ if ((o = getCollectionOption()) != null) { serverURI = getCollectionOption().getURI(); } /* * If we have no server URL by now, we should error. This check should * happen before the credentials are validated so we don't prompt the * user for data, then throw for lack of server URL. */ if (serverURI == null) { throw new CLCException(Messages.getString("Command.CouldNotDetermineCollectionURL")); //$NON-NLS-1$ } Credentials credentials = null; final CredentialsManager credentialsManager = CredentialsManagerFactory .getCredentialsManager(CLC_PERSISTENCE_PROVIDER, usePersistanceCredentialsManager()); final CachedCredentials cachedCredentials = credentialsManager.getCredentials(serverURI); if ((o = findOptionType(OptionLogin.class)) != null) { credentials = ((OptionLogin) o).getType() == OptionLogin.LoginType.USERNAME_PASSWORD ? new UsernamePasswordCredentials(((OptionLogin) o).getUsername(), ((OptionLogin) o).getPassword()) : new JwtCredentials(((OptionLogin) o).getToken()); } /* * If the user has saved a username (regardless of whether they have a * saved password), then use that information. */ else if (cachedCredentials != null && cachedCredentials.getUsername() != null && cachedCredentials.getUsername().length() > 0) { credentials = new UsernamePasswordCredentials(cachedCredentials.getUsername(), cachedCredentials.getPassword()); } else { /* * For TFS instance, validate will test whether Kerberos is * available and the user has a ticket and prompt for * username/pass/domain if not. * * For VSTS instance, do interactive auth if allow prompt */ credentials = ServerURIUtils.isHosted(serverURI) ? null : new DefaultNTCredentials(); } /* * This method checks the credentials we've read already, prompts if * allowed for missing ones, or lets defaults kick in. */ log.debug("credentialsManager.canWrite(): " + credentialsManager.canWrite()); //$NON-NLS-1$ log.debug("persistCredentials(): " + persistCredentials()); //$NON-NLS-1$ credentials = validateCredentials(serverURI, credentials, credentialsManager.canWrite() && persistCredentials()); /* * The CLCConnectionAdvisor looks for the "proxy" option to configure * the TF download proxy. */ final TFSTeamProjectCollection connection = getConnection(serverURI, credentials); return connection; } private TFSTeamProjectCollection getConnection(URI serverURI, final Credentials credentials) { List<TFSTeamProjectCollection> connectionList; serverURI = URIUtils.ensurePathHasTrailingSlash(serverURI); serverURI = URIUtils.toLowerCase(serverURI); if (connections.containsKey(serverURI)) { connectionList = connections.get(serverURI); } else { connectionList = new ArrayList<TFSTeamProjectCollection>(); connections.put(serverURI, connectionList); } for (final TFSTeamProjectCollection connection : connectionList) { if (credentials.equals(connection.getCredentials())) { return connection; } } collection = new TFSTeamProjectCollection(serverURI, credentials, new CLCConnectionAdvisor(this)); connectionList.add(collection); return collection; } /** * Tests for EULA acceptance and product id installation. * * @throws LicenseException * if there is a licensing error. */ private void testLicense() throws LicenseException { if (!LicenseManager.getInstance().isEULAAccepted()) { throw new LicenseException(LicenseExceptionType.EULA, Messages.getString("Command.YouMustAcceptEULA")); //$NON-NLS-1$ } } /** * Validates the credentials given, prompting for any missing bits (if * allowed), or filling in with default credentials (if available and * allowed). Throws on error, returns the updated credentials on success. * * @param credentials * the credentials to validate (or <code>null</code> if no * credentials are known) * @param persistCredentials * true to save credentials, false otherwise * @throws CLCException * if the credentials were not valid and couldn't be fixed. */ private Credentials validateCredentials(final URI serverURI, Credentials credentials, final boolean persistCredentials) throws CLCException { /* * Disable UI prompts for encrypted password storage (eg, unlock * keychains) when in noprompt mode. */ final boolean prompt = (findOptionType(OptionNoPrompt.class) == null); // We'll prompt (or throw) for any of these still invalid at end of // method String username = null; String password = null; // No credentials specified if (credentials == null) { /* * Defensively create a DefaultNTCredentials so we do not throw NPE */ credentials = new DefaultNTCredentials(); if (Prompt.interactiveLoginAllowed() && ServerURIUtils.isHosted(serverURI) && prompt) { log.debug( "Request against VisualStudio Team Services and credential is not available, try OAuth2 flow."); //$NON-NLS-1$ /* * Interactive auth for team services when there is no cred * presented */ final UsernamePasswordCredentials cred = Prompt.getCredentialsInteractively(serverURI, display, persistCredentials); if (cred != null) { log.debug("Retrieved a credential interactively from OAuth2 flow."); //$NON-NLS-1$ credentials = cred; } else { log.debug( "Failed to retrieve any credential, did user close the browser or JavaFx failed to load?"); //$NON-NLS-1$ } } } if (credentials instanceof JwtCredentials) { return credentials; } else if (credentials instanceof UsernamePasswordCredentials) { username = ((UsernamePasswordCredentials) credentials).getUsername(); password = ((UsernamePasswordCredentials) credentials).getPassword(); } else if (credentials instanceof DefaultNTCredentials) { // Validate default credentials are available if (!NegotiateEngine.getInstance().isAvailable() && !NTLMEngine.getInstance().isAvailable()) { log.debug( "Default credentials are unavailable (library load failure or other initialization problem)"); //$NON-NLS-1$ getDisplay().printLine(Messages.getString("Command.DefaultCredsUnavailableLibraryLoadFailure")); //$NON-NLS-1$ } else if (!NegotiateEngine.getInstance().supportsCredentialsDefault() && !NTLMEngine.getInstance().supportsCredentialsDefault()) { log.debug("Default credentials are unavailable (no ticket or token)"); //$NON-NLS-1$ getDisplay().printLine(Messages.getString("Command.DefaultCredsUnavailableNoTicket")); //$NON-NLS-1$ } // Tests again that the libraries loaded and the user has a ticket if (CredentialsUtils.supportsDefaultCredentials()) { log.debug("Using default credentials (supported and available)"); //$NON-NLS-1$ return credentials; } /* * Can't load Kerberos libs or no ticket available. */ } else { /* Unknown credential type. */ throwForInsufficientCredentials(); } // Incomplete if (!(username != null && username.length() > 0 && password != null)) { if (!prompt) { throwForInsufficientCredentials(); } credentials = Prompt.getCredentials(getDisplay(), getInput(), username, password); // Null if IO error or not enough info to create credentials if (credentials == null) { throwForInsufficientCredentials(); } } if (persistCredentials) { CredentialsManagerFactory .getCredentialsManager(CLC_PERSISTENCE_PROVIDER, usePersistanceCredentialsManager()) .setCredentials(new CachedCredentials(serverURI, credentials)); } return credentials; } /** * @return <code>true</code> if credentials may be saved (possibly * insecurely), <code>false</code> if they may not be saved */ public boolean persistCredentials() { return (PlatformMiscUtils.getInstance() .getEnvironmentVariable(EnvironmentVariables.AUTO_SAVE_CREDENTIALS) != null); } /** * @return <code>true</code> if PersistanceCredentialsManager may be used * */ public boolean usePersistanceCredentialsManager() { return !EnvironmentVariables.getBoolean(EnvironmentVariables.USE_KEYCHAIN, true) || !EnvironmentVariables.getBoolean(EnvironmentVariables.USE_SYSTEM_CREDENTIAL_MANAGER, true); } private String throwForInsufficientCredentials() throws CLCException { throw new CLCException(Messages.getString("Command.AutoCredentialsNotAvailable")); //$NON-NLS-1$ } /** * Just like {@link WorkspaceInfo#getWorkspace(TFSTeamProjectCollection)} * except it throws if no workspace could be found, which is useful for CLC * command classes. * * @param cachedWorkspace * the cached workspace to realize (not null). * @param client * a {@link VersionControlClient} initialized for this connection * (not null). * @return the {@link Workspace} object that matches this cached workspace. * @throws CannotFindWorkspaceException * if an {@link Workspace} could not be found that matches the * cached workspace (it may have been deleted, renamed, permissions * changed, etc.). */ public final Workspace realizeCachedWorkspace(final WorkspaceInfo cachedWorkspace, final VersionControlClient client) throws CannotFindWorkspaceException { Check.notNull(cachedWorkspace, "cachedWorkspace"); //$NON-NLS-1$ Check.notNull(client, "client"); //$NON-NLS-1$ final Workspace ret = cachedWorkspace.getWorkspace(client.getConnection()); if (ret == null) { final String workspaceSpec = new WorkspaceSpec(cachedWorkspace.getName(), cachedWorkspace.getOwnerDisplayName()).toString(); final String messageFormat = Messages.getString("Command.WorkspaceNoLongerExistsFormat"); //$NON-NLS-1$ final String message = MessageFormat.format(messageFormat, workspaceSpec); throw new CannotFindWorkspaceException(message); } return ret; } /** * Hooks up event listeners and configures a local workspace path watcher * factory for the given client. * * @param client * the client to initialize (must not be <code>null</code>) */ protected void initializeClient(final VersionControlClient client) { Check.notNull(client, "client"); //$NON-NLS-1$ final VersionControlEventEngine ee = client.getEventEngine(); /* * Hook up all the events we care about. */ ee.addNewPendingChangeListener(this); ee.addBeforeCheckinListener(this); ee.addBeforeShelveListener(this); ee.addCheckinListener(this); ee.addNonFatalErrorListener(this); ee.addUndonePendingChangeListener(this); ee.addDestroyListener(this); ee.addConflictListener(this); ee.addMergingListener(this); client.setPathWatcherFactory(new CLCPathWatcherFactory()); // Store this for close() this.client = client; workstation = Workstation.getCurrent(client.getConnection().getPersistenceStoreProvider()); if (Platform.isCurrentPlatform(Platform.WINDOWS)) { try { notificationManager = new MessageWindowNotificationManager(); workstation.setNotificationManager(notificationManager); } catch (final Exception e) { // Might fail for non-interactive process, ignore } } } @Override public void close() { // Flush any remaining notifications and remove the manager if (notificationManager != null) { notificationManager.close(); workstation.setNotificationManager(null); notificationManager = null; workstation = null; } // Update client event engine if (client != null) { client.setPathWatcherFactory(new NullPathWatcherFactory()); final VersionControlEventEngine ee = client.getEventEngine(); ee.clear(); client = null; } } @Override public void onCheckin(final CheckinEvent e) { /* * Diplay any changes that were undone by the server (not committed in * the changeset). */ if (e.getUndoneChanges().length > 0) { getDisplay().printLine(""); //$NON-NLS-1$ getDisplay().printLine(Messages.getString("Command.ChangesNotCheckedInBecauseNotModified")); //$NON-NLS-1$ for (final PendingChange pc : e.getUndoneChanges()) { String localOrServerPath = pc.getLocalItem(); if (StringUtil.isNullOrEmpty(localOrServerPath)) { localOrServerPath = pc.getServerItem(); } getDisplay().printLine(MessageFormat.format(Messages.getString("Command.UndoingUndoneFormat"), //$NON-NLS-1$ pc.getChangeType().toUIString(false, pc), localOrServerPath)); } } if (e.getChangesetID() == 0) { getDisplay().printErrorLine(""); //$NON-NLS-1$ getDisplay().printErrorLine(Messages.getString("Command.NoChangesLeftToCheckin")); //$NON-NLS-1$ } else if (e.getChangesetID() > 0) { getDisplay().printLine(""); //$NON-NLS-1$ getDisplay() .printLine(MessageFormat.format(Messages.getString("Command.ChangesetNumberCheckedInFormat"), //$NON-NLS-1$ Integer.toString(e.getChangesetID()))); } } @Override public void onNonFatalError(final NonFatalErrorEvent e) { boolean showError = true; String s = null; synchronized (nonFatalErrorLock) { if (e.getThrowable() != null) { numNonFatalErrors++; if (e.getThrowable() == null || !(e.getThrowable() instanceof DownloadProxyException)) { setExitCode(ExitCode.PARTIAL_SUCCESS); } if (e.getThrowable().getLocalizedMessage() != null) { s = e.getThrowable().getLocalizedMessage(); } else { final StringWriter sw = new StringWriter(); e.getThrowable().printStackTrace(new PrintWriter(sw)); s = sw.toString(); } } if (e.getFailure() != null) { if (e.getFailure().getSeverity() == SeverityType.ERROR) { numNonFatalErrors++; setExitCode(ExitCode.PARTIAL_SUCCESS); } else if (e.getFailure().getSeverity() == SeverityType.WARNING) { showError = false; numNonFatalWarnings++; } else { log.error("Unexpected severity: " + e.getFailure().getSeverity()); //$NON-NLS-1$ } if (e.getFailure().getWarnings() != null && e.getFailure().getWarnings().length > 0) { /* * For warnings, separating the output with a blank line * makes them easier to read since they consist of multiple * lines. */ s = NewlineUtils.PLATFORM_NEWLINE + e.getFailure().getFormattedMessage(); } else { s = e.getFailure().getFormattedMessage(); } } // Record it in the list of errors and warnings. nonFatalMessageList.add(new NonFatalErrorMessage(showError, s)); } // Display the error. (TODO if we get a warning stream, print // non-showError messages there) getDisplay().printErrorLine(s); } @Override public void onConflict(final ConflictEvent e) { getDisplay().printLine(e.getMessage()); } /* * Note: this is for conflicts that are automatically resolved (in dev 11). * Commands must hook this manually, it is not hooked in initializeClient(). * This is to avoid printing the automatic conflict resolution message when * running CommandResolve. */ @Override public void onConflictResolved(final ConflictResolvedEvent e) { getDisplay().printLine(MessageFormat.format(Messages.getString("Command.AutoResolvedConflictFormat"), //$NON-NLS-1$ e.getConflict().getDetailedMessage(false), ConflictResolutionHelper.getResolutionString(e.getConflict().getResolution()))); } @Override public void onMerging(final MergingEvent e) { // Keep a list of unsuccessful operations for displaying in the summary. if (e.getStatus() != OperationStatus.GETTING && e.getStatus() != OperationStatus.DELETING && e.getStatus() != OperationStatus.REPLACING) { mergeWarningList.add(e); } displayMergingEvent(e); } /** * Displays a merge event. * * @param e * the event to display */ private void displayMergingEvent(final MergingEvent e) { final AtomicReference<String> errorHolder = new AtomicReference<String>(); final String message = e.getMessage(errorHolder); // Display the merge info. if (e.getStatus() == OperationStatus.CONFLICT) { getDisplay().printErrorLine(message); setExitCode(ExitCode.PARTIAL_SUCCESS); } else { getDisplay().printLine(message); } if (e.getStatus() == OperationStatus.GETTING || e.getStatus() == OperationStatus.REPLACING || e.getStatus() == OperationStatus.DELETING || e.getStatus() == OperationStatus.CONFLICT) { // Nothing to do } else if (e.getStatus() == OperationStatus.SOURCE_DIRECTORY_NOT_EMPTY || e.getStatus() == OperationStatus.SOURCE_WRITABLE || e.getStatus() == OperationStatus.TARGET_IS_DIRECTORY || e.getStatus() == OperationStatus.TARGET_LOCAL_PENDING || e.getStatus() == OperationStatus.TARGET_WRITABLE) { getDisplay().printErrorLine(errorHolder.get()); setExitCode(ExitCode.PARTIAL_SUCCESS); } } @Override public void onGet(final GetEvent e) { // Get a short version of the target path if it's not null. String shortTargetName = null; if (e.getTargetLocalItem() != null) { // First check for an item that doesn't have a parent, such as c:\. // Then extract the parent directory name from the path. if (LocalPath.getParent(e.getTargetLocalItem()) == null || e.getTargetLocalItem().equals(LocalPath.getParent(e.getTargetLocalItem()))) { shortTargetName = e.getTargetLocalItem(); } else { shortTargetName = LocalPath.getFileName(e.getTargetLocalItem()); final String folder = LocalPath.getParent(e.getTargetLocalItem()); // Write a header if necessary. if (lastGetDirectory == null || !LocalPath.equals(lastGetDirectory, folder)) { if (lastGetDirectory != null) { getDisplay().printLine(""); //$NON-NLS-1$ } getDisplay() .printLine(MessageFormat.format(Messages.getString("Command.AColonFormat"), folder)); //$NON-NLS-1$ // Update the folder of the last item displayed. lastGetDirectory = folder; } } } if (e.getStatus() != OperationStatus.GETTING && e.getStatus() != OperationStatus.DELETING && e.getStatus() != OperationStatus.REPLACING) { getWarningList.add(e); } displayGetEvent(e, shortTargetName); } /** * Displays a {@link GetEvent}. * * @param e * the event to display (must not be <code>null</code>) * @param targetName * the target name to use (may be <code>null</code> to use an item * name from the event) */ private void displayGetEvent(final GetEvent e, final String targetName) { final AtomicReference<String> errorHolder = new AtomicReference<String>(); final String message = e.getMessage(targetName, errorHolder); if (e.getStatus() == OperationStatus.CONFLICT || e.getStatus() == OperationStatus.SOURCE_WRITABLE || e.getStatus() == OperationStatus.TARGET_LOCAL_PENDING || e.getStatus() == OperationStatus.TARGET_WRITABLE || e.getStatus() == OperationStatus.SOURCE_DIRECTORY_NOT_EMPTY || e.getStatus() == OperationStatus.TARGET_IS_DIRECTORY || e.getStatus() == OperationStatus.UNABLE_TO_REFRESH) { getDisplay().printErrorLine(errorHolder.get()); setExitCode(ExitCode.PARTIAL_SUCCESS); } else if (e.getStatus() == OperationStatus.GETTING || e.getStatus() == OperationStatus.REPLACING || e.getStatus() == OperationStatus.DELETING) { getDisplay().printLine(message); } } /** * Displays the summary information for a merge. * * @param status * get status info * @param showConflicts * If false, only warnings and errors will be shown. */ protected void displayMergeSummary(final GetStatus status, final boolean showConflicts) { if (!shouldDisplaySummary(status)) { return; } displayGetStatus(status); if (status.getNumConflicts() > 0 && !showConflicts) { // Conflict display is suppressed: inform the user. getDisplay().printErrorLine( MessageFormat.format(Messages.getString("Command.MergeConflictsSuppressedFormat"), //$NON-NLS-1$ Integer.toString(status.getNumConflicts()), OptionsMap.getPreferredOptionPrefix())); setExitCode(ExitCode.PARTIAL_SUCCESS); } for (final MergingEvent e : mergeWarningList) { if ((e.getStatus() != OperationStatus.CONFLICT) || showConflicts) { displayMergingEvent(e); } } displayErrors(); } /** * Displays the summary information for a get. * * @param get * status info */ protected void displayGetSummary(final GetStatus status) { if (!shouldDisplaySummary(status)) { return; } displayGetStatus(status); for (final GetEvent e : getWarningList) { displayGetEvent(e, e.getTargetLocalItem()); } displayErrors(); } /** * Returns true if the get/merge summary should be displayed. */ private boolean shouldDisplaySummary(final GetStatus status) { final int numNonFatalErrorsAndWarnings = numNonFatalErrors + numNonFatalWarnings; final String noSummaryValue = PlatformMiscUtils.getInstance() .getEnvironmentVariable(EnvironmentVariables.NO_SUMMARY); /* * Since the conflicts and warnings are accounted for by NumOperations, * add NumFailures to determine whether the threshold has been crossed. */ return numNonFatalErrorsAndWarnings + status.getNumConflicts() + status.getNumWarnings() != 0 && status.getNumOperations() >= SUMMARY_THRESHOLD && (noSummaryValue == null || noSummaryValue.length() == 0); } /** * Display the totals from GetStatus. * * @param status * the status information to display */ protected void displayGetStatus(final GetStatus status) { getDisplay().printLine(""); //$NON-NLS-1$ getDisplay().printLine(MessageFormat.format(Messages.getString("Command.GetStatusSummaryFormat"), //$NON-NLS-1$ Integer.toString(status.getNumConflicts()), Integer.toString(status.getNumWarnings() + numNonFatalWarnings), Integer.toString(numNonFatalErrors))); } protected boolean displayCheckinNoteFailures(final CheckinNoteFailure[] noteFailures) { if (noteFailures != null && noteFailures.length > 0) { getDisplay().printLine(""); //$NON-NLS-1$ if (noteFailures.length > 1) { getDisplay().printErrorLine(Messages.getString("Command.CheckInNotesDoNotPassRequirement")); //$NON-NLS-1$ } else { getDisplay().printErrorLine(Messages.getString("Command.CheckinNoteDoesNotPassRequirement")); //$NON-NLS-1$ } for (final CheckinNoteFailure failure : noteFailures) { final String messageFormat = Messages.getString("Command.CheckinNoteFailureOutputFormat"); //$NON-NLS-1$ final String message = MessageFormat.format(messageFormat, failure.getDefinition().getName(), failure.getMessage()); getDisplay().printErrorLine(message); } return true; } return false; } /** * Display the non-fatal errors captured during execution of the command. */ private void displayErrors() { synchronized (nonFatalErrorLock) { for (final NonFatalErrorMessage nfem : nonFatalMessageList) { // TODO if we ever get stdwarn in Unix, print non-isError() // items there :) getDisplay().printErrorLine(nfem.getMessage()); } } } @Override public void onBeforeCheckin(final PendingChangeEvent e) { Check.notNull(e, "e"); //$NON-NLS-1$ Check.notNull(e.getPendingChange(), "e.getPendingChange()"); //$NON-NLS-1$ displayPendingChange(e.getPendingChange(), Messages.getString("Command.OnCheckingInFormat")); //$NON-NLS-1$ } @Override public void onBeforeShelve(final PendingChangeEvent e) { Check.notNull(e, "e"); //$NON-NLS-1$ Check.notNull(e.getPendingChange(), "e.getPendingChange()"); //$NON-NLS-1$ displayPendingChange(e.getPendingChange(), Messages.getString("Command.OnShelvingFormat")); //$NON-NLS-1$ } @Override public void onUndonePendingChange(final PendingChangeEvent e) { Check.notNull(e, "e"); //$NON-NLS-1$ Check.notNull(e.getPendingChange(), "e.getPendingChange()"); //$NON-NLS-1$ displayPendingChange(e.getPendingChange(), Messages.getString("Command.OnUndoingFormat")); //$NON-NLS-1$ } @Override public void onNewPendingChange(final PendingChangeEvent e) { Check.notNull(e, "e"); //$NON-NLS-1$ Check.notNull(e.getPendingChange(), "e.getPendingChange()"); //$NON-NLS-1$ displayPendingChange(e.getPendingChange(), Messages.getString("Command.OnNewPendingChangeFormatSKIPVALIDATE")); //$NON-NLS-1$ } @Override public void onDestroy(final DestroyEvent event) { final Item destroyedItem = event.getDestroyedItem(); String itemString = destroyedItem.getServerItem(); if (destroyedItem.getDeletionID() != 0) { itemString += ";X" + destroyedItem.getDeletionID(); //$NON-NLS-1$ } getDisplay().printLine(MessageFormat.format(Messages.getString("Command.DestroyedFormat"), itemString)); //$NON-NLS-1$ } /** * Displays the pending change information to the user. Unifies code that * should run during onNewPendingChange, onUndonePendingChange, etc. * * @param change * the change to display (if null, ignored). * @param message * a format string for MessageFormat.format(), correct for the null, * the change's item name is printed. The message will be built with * the following parameters: {0} = operation name, {1} = item name. */ protected void displayPendingChange(final PendingChange change, final String messageFormat) { if (change != null) { if (xmlWriter != null) { try { displayPendingChangeAsXML(change); } catch (final SAXException e) { log.error("XML exception", e); //$NON-NLS-1$ e.printStackTrace(); } return; } String folder; String item; String localItem = change.getLocalItem(); String serverItem = change.getServerItem(); /* * If the change is an undone rename (and the change type is not * undelete), we need to swap the source and destination for them to * make sense to the user. */ if (change.isUndone() && change.getChangeType().contains(ChangeType.RENAME) && !change.getChangeType().contains(ChangeType.UNDELETE)) { localItem = change.getSourceLocalItem(); serverItem = change.getSourceServerItem(); } if (localItem != null) { folder = LocalPath.getDirectory(localItem); item = LocalPath.getFileName(localItem); /* * Microsoft's client prints things relative to the current * directory. */ String shortFolder = LocalPath.makeRelative(folder, LocalPath.getCurrentWorkingDirectory()); if (shortFolder.length() == 0) { shortFolder = LocalPath.getCurrentWorkingDirectory(); } /* * If this is the first change we're printing or we've changed * directories, we need to print the new directory. If the * directory is the same as the current directory, don't print * it at all. */ if ((lastPendingChangeDirectory == null && !LocalPath.equals(folder, LocalPath.getCurrentWorkingDirectory())) || (lastPendingChangeDirectory != null && (ServerPath.isServerPath(lastPendingChangeDirectory) || !LocalPath.equals(lastPendingChangeDirectory, folder)))) { if (lastPendingChangeDirectory != null) { getDisplay().printLine(""); //$NON-NLS-1$ } getDisplay().printLine(shortFolder + ":"); //$NON-NLS-1$ } } else if (serverItem != null) { folder = ServerPath.getParent(serverItem); item = ServerPath.getFileName(serverItem); /* * If this is the first change we're printing or we've changed * server directories, we need to print the new directory. */ if (lastPendingChangeDirectory == null || !ServerPath.isServerPath(folder) || !ServerPath.equals(lastPendingChangeDirectory, folder)) { if (lastPendingChangeDirectory != null) { getDisplay().printLine(""); //$NON-NLS-1$ } getDisplay().printLine(folder + ":"); //$NON-NLS-1$ } } else { /* * Microsoft's implementation mentions this case happening for * odd rename change undo operations. */ if (change.getLocalItem() != null) { item = LocalPath.makeRelative(change.getLocalItem(), LocalPath.getCurrentWorkingDirectory()); if (item.length() == 0) { item = LocalPath.getCurrentWorkingDirectory(); } } else if (change.getSourceServerItem() != null) { item = change.getSourceServerItem(); } else { /* * This is all we have. */ item = change.getServerItem(); } folder = null; // TODO How does this affect the item string? // item = Resources.Format(Resources.UndoMovedTo, item); } lastPendingChangeDirectory = folder; if (messageFormat != null) { getDisplay().printLine(MessageFormat.format(messageFormat, new Object[] { change.getChangeType().toUIString(false, change), item })); } else { // Print something, at least the item name. getDisplay().printLine(item); } } } private void displayPendingChangeAsXML(final PendingChange change) throws SAXException { Check.notNull(change, "change"); //$NON-NLS-1$ Check.notNull(xmlWriter, "this.xmlWriter"); //$NON-NLS-1$ final AttributesImpl changeAttributes = new AttributesImpl(); final String localItem; final String serverItem; /* * If the change is an undone rename (and the change type is not * undelete), we need to swap the source and destination for them to * make sense to the user. */ if (change.isUndone() && change.getChangeType().contains(ChangeType.RENAME) && !change.getChangeType().contains(ChangeType.UNDELETE)) { localItem = change.getSourceLocalItem(); serverItem = change.getSourceServerItem(); } else { localItem = change.getLocalItem(); serverItem = change.getServerItem(); } changeAttributes.addAttribute("", "", CommonXMLNames.SERVER_ITEM, "CDATA", serverItem); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ if (localItem != null) { changeAttributes.addAttribute("", "", CommonXMLNames.LOCAL_ITEM, "CDATA", localItem); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } changeAttributes.addAttribute("", "", CommonXMLNames.VERSION, "CDATA", //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$ Integer.toString(change.getVersion())); changeAttributes.addAttribute("", //$NON-NLS-1$ "", //$NON-NLS-1$ CommonXMLNames.DATE, "CDATA", //$NON-NLS-1$ SimpleXMLWriter.ISO_DATE_FORMAT.format(change.getCreationDate().getTime())); changeAttributes.addAttribute("", "", CommonXMLNames.LOCK, "CDATA", change.getLockLevelName()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ changeAttributes.addAttribute("", //$NON-NLS-1$ "", //$NON-NLS-1$ CommonXMLNames.CHANGE_TYPE, "CDATA", //$NON-NLS-1$ change.getChangeType().toUIString(false, change)); /* * If the source server item and server item paths differ, we have a * rename, move, etc., and want to print the source item. */ if (change.getSourceServerItem() != null && !ServerPath.equals(change.getSourceServerItem(), change.getServerItem())) { changeAttributes.addAttribute("", //$NON-NLS-1$ "", //$NON-NLS-1$ CommonXMLNames.SOURCE_SERVER_ITEM, "CDATA", //$NON-NLS-1$ change.getSourceServerItem()); } if (change.getItemType() == ItemType.FILE && change.getEncoding() != VersionControlConstants.ENCODING_UNCHANGED) { changeAttributes.addAttribute("", //$NON-NLS-1$ "", //$NON-NLS-1$ CommonXMLNames.FILE_TYPE, "CDATA", //$NON-NLS-1$ new FileEncoding(change.getEncoding()).getName()); } if (change.getDeletionID() != 0) { changeAttributes.addAttribute("", //$NON-NLS-1$ "", //$NON-NLS-1$ CommonXMLNames.DELETION_ID, "CDATA", //$NON-NLS-1$ Integer.toString(change.getDeletionID())); } xmlWriter.startElement("", "", CommonXMLNames.PENDING_CHANGE, changeAttributes); //$NON-NLS-1$ //$NON-NLS-2$ xmlWriter.endElement("", "", CommonXMLNames.PENDING_CHANGE); //$NON-NLS-1$ //$NON-NLS-2$ } /** * Parses qualified items (item specs with optional versions, version * ranges, or deletion specifiers) from the free arguments. * * @param defaultVersion * the default version spec to create the new QualifiedItems with if * the free argument does not specify a version (if null, null is set * on the qualified items). * @param allowVersionRange * if true, allow the free arguments to specify a version range * instead of a single version. * @param startIndex * the free argument array index (0 is the first element) at which to * start parsing as qualified items. Useful to skip some arguments * that may have already been parsed as non-qualified items (e.g. * label name). * * @return an array of qualified items parsed from the free arguments. */ protected QualifiedItem[] parseQualifiedItems(final VersionSpec defaultVersion, final boolean allowVersionRange, final int startIndex) { return parseQualifiedItems(getFreeArguments(), defaultVersion, allowVersionRange, startIndex); } /** * Parses qualified items (item specs with optional versions, version * ranges, or deletion specifiers) from the given strings. * * @param arguments * the arguments to parse as {@link QualifiedItem}s (not null). * @param defaultVersion * the default version spec to create the new {@link QualifiedItem}s * with if the argument does not specify a version (if null, null is * set on the qualified items). * @param allowVersionRange * if true, allow the free arguments to specify a version range * instead of a single version. * @param startIndex * the argument array index (0 is the first element) at which to * start parsing as qualified items. Useful to skip some arguments * that may have already been parsed as non-qualified items (e.g. * label name). * * @return an array of qualified items parsed from the given argument * strings. */ protected QualifiedItem[] parseQualifiedItems(final String[] arguments, final VersionSpec defaultVersion, final boolean allowVersionRange, final int startIndex) { Check.notNull(arguments, "arguments"); //$NON-NLS-1$ final List<QualifiedItem> items = new ArrayList<QualifiedItem>(arguments.length); for (int i = startIndex; i < arguments.length; i++) { final String arg = arguments[i]; if (!StringUtil.isNullOrEmpty(arg)) { try { /* * The constructor for QualifiedItem will canonialize the * local path we supply (the arg). */ final QualifiedItem qi = new QualifiedItem(arg, VersionControlConstants.AUTHENTICATED_USER, defaultVersion, allowVersionRange); items.add(qi); } catch (final VersionSpecParseException e) { reportWrongArgument(arg, e); } catch (final LabelSpecParseException e) { reportWrongArgument(arg, e); } } } return items.toArray(new QualifiedItem[items.size()]); } private void reportWrongArgument(final String arg, final Exception e) { final String messageFormat = Messages.getString("Command.ArgumentSkippedFormat"); //$NON-NLS-1$ final String message = MessageFormat.format(messageFormat, arg, e.getLocalizedMessage()); getDisplay().printErrorLine(message); setExitCode(ExitCode.PARTIAL_SUCCESS); } /** * Gets the pending changes that match the free arguments given for this * command. Warnings are printed for each free argument which fails to match * a pending change. If any non-wildcard argument fails to match a change, * an empty array is returned (in addition to the warnings printed). * * @param workspace * the workspace to use when matching pending changes. * @param recursive * whether the free arguments should match changes recursively. * @return an array of matched changes, an empty array if any argument * failed to match a path. * @throws ServerPathFormatException * when the format of one of the free arguments is detected to be a * server path, but is in an invalid format. */ protected PendingChange[] getPendingChangesMatchingFreeArguments(final Workspace workspace, final boolean recursive) throws ServerPathFormatException { return getPendingChangesMatchingLocalPaths(workspace, recursive, getFreeArguments()); } /** * Gets the pending changes that match the given local paths. Warnings are * printed for each free argument which fails to match a pending change. If * any non-wildcard argument fails to match a change, an empty array is * returned (in addition to the warnings printed). * * @param workspace * the workspace to use when matching pending changes. * @param recursive * whether the free arguments should match changes recursively. * @param localPaths * the local paths to find changes for. * @return an array of matched changes, an empty array if any argument * failed to match a path. * @throws TFSException * @throws ServerPathFormatException * when the format of one of the free arguments is detected to be a * server path, but is in an invalid format. */ protected PendingChange[] getPendingChangesMatchingLocalPaths(final Workspace workspace, final boolean recursive, final String[] localPaths) throws ServerPathFormatException { final PendingSet pendingChangeSet = workspace.getPendingChanges(); if (pendingChangeSet == null) { return null; } final PendingChange[] pendingChanges = pendingChangeSet.getPendingChanges(); if (pendingChanges == null || pendingChanges.length == 0) { return null; } // The matched arguments (results of matching below). final MatchedFileArgument[] matchedArguments = new MatchedFileArgument[localPaths.length]; final ArrayList<PendingChange> matchedChanges = new ArrayList<PendingChange>(); for (int i = 0; i < pendingChanges.length; i++) { final PendingChange change = pendingChanges[i]; if (change == null) { continue; } for (int j = 0; j < localPaths.length; j++) { /* * Step 1: Figure out which local path from the change we should * use. */ final String changePath; if (ServerPath.isServerPath(localPaths[j])) { changePath = change.getServerItem(); } else if (change.getLocalItem() != null) { changePath = change.getLocalItem(); } else if (change.getSourceLocalItem() != null) { changePath = change.getSourceLocalItem(); } else { /* * This change doesn't have any paths associated with it, so * we can't match it. Not an error. */ continue; } /* * Step 2: Allocate a matched argument class (if needed). * * Since we may evaluate a free argument multiple times (against * multiple changes), we only allocate an argument match class * for this item if one does not already exist. */ if (matchedArguments[j] == null) { matchedArguments[j] = new MatchedFileArgument(); matchedArguments[j].exactString = localPaths[j]; /* * Transform a server path into a MatchedFileArgument. */ if (ServerPath.isServerPath(matchedArguments[j].exactString)) { matchedArguments[j].isServerItem = true; /* * Always break a server path into file and folder * parts. We'll search for the item both as a file and a * folder below, because we can't know what type the * user is referring to (without hitting the server). */ matchedArguments[j].folderPart = ServerPath.getParent(matchedArguments[j].exactString); matchedArguments[j].filePart = ServerPath.getFileName(matchedArguments[j].exactString); matchedArguments[j].fullPath = ServerPath.canonicalize(matchedArguments[j].exactString); } /* * Transform a local path into a MatchedFileArgument. */ if (!ServerPath.isServerPath(matchedArguments[j].exactString)) { if (LocalPath.equals(changePath, LocalPath.canonicalize(matchedArguments[j].exactString))) { /* * In the local path case, we may have a pending * change where a change describes a directory that * doesn't yet exist (the target of a branch, for * example), but a checkin with that (not yet * created) target as a free argument should match, * so we handle that here. */ /* * We can ask the change directly if it's a file or * folder (since the disk item doesn't exist, we * couldn't test it that way if we wanted to). */ if (change.getItemType() == ItemType.FOLDER) { matchedArguments[j].folderPart = change.getLocalItem(); } else { // It's a file. matchedArguments[j].filePart = LocalPath .getLastComponent(LocalPath.canonicalize(localPaths[j])); matchedArguments[j].folderPart = LocalPath .getDirectory(LocalPath.canonicalize(localPaths[j])); } } else if (new File(matchedArguments[j].exactString).exists() && new File(matchedArguments[j].exactString).isDirectory()) { // The argument is a local directory. matchedArguments[j].filePart = null; matchedArguments[j].folderPart = LocalPath .canonicalize(matchedArguments[j].exactString); } else { // The argument is a local file. matchedArguments[j].filePart = LocalPath .getLastComponent(LocalPath.canonicalize(localPaths[j])); matchedArguments[j].folderPart = LocalPath .getDirectory(LocalPath.canonicalize(localPaths[j])); } matchedArguments[j].fullPath = LocalPath.canonicalize(matchedArguments[j].exactString); } } /* * Step 3: Test for a match between the change and the argument. * * If this argument is a server path, we have to test it in both * direct path match (possibly with recursion) and path/wildcard * pattern combination (also possibly with recursion) so we can * be sure to match every change. For local paths, we always do * the match respecting wildcards. * * This is the behavior of Microsoft's client, and we should * match it. */ boolean matchesAsServerItem = false; boolean matchesAsServerWildcard = false; boolean matchesAsLocalWildcard = false; if (matchedArguments[j].isServerItem) { matchesAsServerItem = ServerPath.matchesWildcard(changePath, matchedArguments[j].fullPath, null, recursive); matchesAsServerWildcard = ServerPath.matchesWildcard(changePath, matchedArguments[j].folderPart, matchedArguments[j].filePart, recursive); } else { matchesAsLocalWildcard = LocalPath.matchesWildcard(changePath, matchedArguments[j].folderPart, matchedArguments[j].filePart, recursive); } if ((matchedArguments[j].isServerItem && (matchesAsServerItem || matchesAsServerWildcard)) || (!matchedArguments[j].isServerItem && matchesAsLocalWildcard)) { // Only add if we didn't just add it (input list was // sorted). if (matchedChanges.size() == 0 || matchedChanges.get(matchedChanges.size() - 1) != change) { matchedChanges.add(change); } // Mark it matched. matchedArguments[j].isMatched = true; } /* * Don't break so we can continue to catch other matches. */ } } // Make sure each argument matched something. boolean failedToMatchAll = false; for (int i = 0; i < matchedArguments.length; i++) { if (!matchedArguments[i].isMatched) { if (!ItemPath.isWildcard(matchedArguments[i].filePart)) { final String messageFormat = Messages.getString("Command.ArgumentFailMatchNoWildcardFormat"); //$NON-NLS-1$ final String message = MessageFormat.format(messageFormat, matchedArguments[i].exactString); getDisplay().printErrorLine(message); failedToMatchAll = true; } else { // Just warn about the wildcards. final String messageFormat = Messages.getString("Command.ArgumentFailMatchWildcardFormat"); //$NON-NLS-1$ final String message = MessageFormat.format(messageFormat, localPaths[i]); getDisplay().printErrorLine(message); } } } if (failedToMatchAll) { return new PendingChange[0]; } return matchedChanges.toArray(new PendingChange[0]); } /** * Gets a version spec appropriate for the given path string. If the path is * a local path, a workspace spec is constructed using the given workspace * name and owner. If the path is a server path, an ALatestVersionSpec is * returned. * * @param itemPath * the item path to check (not null or empty). * @param workspaceName * the workspace name to use if the path is local (not null or * empty). * @param workspaceOwner * the workspace owner to use if the path is local (not null or * empty). * @param workspaceOwnerDisplayName * the display name for a workspace owner to use if the path is local * (not null or empty) * @return an AWorkspaceVersionSpec if the given item path is local, an * ALatestVersionSpec if the path was a server path. */ public VersionSpec getVersionSpecForPath(final String itemPath, final String workspaceName, final String workspaceOwner, final String workspaceOwnerDisplayName) { Check.notNullOrEmpty(itemPath, "itemPath"); //$NON-NLS-1$ Check.notNullOrEmpty(workspaceName, "workspaceName"); //$NON-NLS-1$ Check.notNullOrEmpty(workspaceOwner, "workspaceOwner"); //$NON-NLS-1$ Check.notNullOrEmpty(workspaceOwnerDisplayName, "workspaceOwnerDisplayName"); //$NON-NLS-1$ if (!ServerPath.isServerPath(itemPath)) { return new WorkspaceVersionSpec(workspaceName, workspaceOwner, workspaceOwnerDisplayName); } else { return LatestVersionSpec.INSTANCE; } } /** * Sets the {@link SimpleXMLWriter} to use when writing XML to the display. * Set a null writer to print normal text (the default). * * @param xmlWriter * the {@link SimpleXMLWriter} to use when writing XML to the * display, or null to print normal text. */ public void setXMLWriter(final SimpleXMLWriter xmlWriter) { this.xmlWriter = xmlWriter; } protected void reportBadOptionCombinationIfPresent(final Class<? extends Option> firstClass, final Class<? extends Option> secondClass) throws InvalidOptionException { final Option firstOption = findOptionType(firstClass); final Option secondOption = findOptionType(secondClass); if (firstOption != null && secondOption != null) { throw new InvalidOptionException( MessageFormat.format(Messages.getString("Command.InvalidOptionCombinationFormat"), //$NON-NLS-1$ OptionsMap.getPreferredOptionPrefix(), firstOption.getMatchedAlias(), OptionsMap.getPreferredOptionPrefix(), secondOption.getMatchedAlias())); } } }