/*******************************************************************************
* Copyright (c) 2004, 2005
* Thomas Hallgren, Kenneth Olwing, Mitch Sonies
* Pontus Rydin, Nils Unden, Peer Torngren
* The code, documentation and other materials contained herein have been
* licensed under the Eclipse Public License - v 1.0 by the individual
* copyright holders listed above, as Initial Contributors under such license.
* The text of such license is available at www.eclipse.org.
*******************************************************************************/
package org.eclipse.buckminster.p4.internal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.eclipse.buckminster.p4.Messages;
import org.eclipse.buckminster.runtime.BuckminsterException;
import org.eclipse.buckminster.runtime.Trivial;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.osgi.util.NLS;
/**
* The P4 Client specification.
*
* @author thhal
*/
public class ClientSpec extends DepotObject
{
public enum LineEnd
{
/**
* Use mode native to the client (default).
*/
local,
/**
* Macintosh-style: <code>CR</code> only.
*/
mac,
/**
* Shared mode: Line endings are <code>LF</code> with any <code>CR/LF</code> pairs translated to <code>LF</code>
* -only style before storage or syncing with the depot.<br/>
* When you sync your client workspace, line endings will be <code>LF</code>. If you edit the file on a Windows
* machine, and your editor inserts <code>CR</code>s before each <code>LF</code>, the extra <code>CR</code>s
* will not appear in the archive file.<br/>
* The most common use of the <code>share</code> option is for users of Windows workstations who have UNIX home
* directories mounted as network drives; if they sync files from UNIX, but edit the files on the Windows
* machine, the <code>share</code> option eliminates any problems caused by Windows-based editors' insertion of
* extra carriage return characters at line endings.
*/
share,
/**
* UNIX-style line endings: <code>LF</code> only.
*/
unix,
/**
* Windows-style: <code>CR, LF</code>.
*/
win
}
private boolean m_dirty;
/**
* Create a ClientSpec based on <code>info</code> obtained from <code>conn</code>.
*
* @param conn
* The P4 Connection
* @param info
* Information stemming from a <code>p4 client</code> command.
* @throws BuckminsterException
*/
public ClientSpec(Connection conn, Map<String, String> info) throws BuckminsterException
{
super(conn, info);
m_dirty = false;
}
/**
* Guarantee that the mapping is included in the view. Create a new mapping if needed.
*
* @param depotPath
* The path in the depot in UNC format
* @param localPath
* The absolute location on disk for the local path.
* @return <code>true</code> if the location was added to the view, <code>false</code> if it was already present.
*/
public boolean addLocation(IPath depotPath, IPath localPath) throws CoreException
{
if(entriesContainsMapping(getView(), depotPath, localPath))
return false;
ViewEntry[] entries = getView();
List<ViewEntry> newEntries = new ArrayList<ViewEntry>(entries.length + 1);
for(ViewEntry entry : entries)
{
if(DepotURI.pathEquals(depotPath, entry.getDepotPath()))
throw BuckminsterException.fromMessage(NLS.bind(
Messages.depot_path_0_is_already_mapped_to_1_in_client_2, new Object[] { depotPath,
entry.getLocalPath(), getClient() }));
newEntries.add(entry);
}
IPath root = getRoot();
IPath clientRoot = new Path("//" + getClient()); //$NON-NLS-1$
IPath clientPath = null;
if(root == null)
clientPath = clientRoot.append(localPath.makeRelative());
else
{
if(root.isPrefixOf(localPath))
clientPath = clientRoot.append(localPath.removeFirstSegments(root.segmentCount()));
else
{
for(IPath altPath : getAltRoots())
{
if(altPath.isPrefixOf(localPath))
{
clientPath = clientRoot.append(localPath.removeFirstSegments(altPath.segmentCount()));
break;
}
}
}
}
if(clientPath == null)
throw BuckminsterException.fromMessage(NLS.bind(Messages.local_path_0_is_not_a_root_or_altroot_of_client_1,
localPath, getClient()));
newEntries.add(new ViewEntry(depotPath.append("..."), clientPath.append("..."))); //$NON-NLS-1$ //$NON-NLS-2$
setView(newEntries.toArray(new ViewEntry[newEntries.size()]));
return true;
}
/**
* Check if this spec has any pending changes and commit them if that is the case.
*
* @throws BuckminsterException
*/
public synchronized void commitChanges() throws CoreException
{
if(m_dirty)
{
getConnection().setClientSpec(getInfo());
m_dirty = false;
}
}
/**
* Returns true if this view is mapping <code>depotPath</code> to <code>localPath</code>.
*
* @param depotPath
* The path in the depot in UNC format and without trailing "..."
* @param localPath
* The absolute location on disk for the local path.
* @return
*/
public boolean containsMapping(IPath depotPath, IPath localPath)
{
return entriesContainsMapping(getView(), depotPath, localPath);
}
/**
* The date and time that any part of the client workspace specification was last accessed by any Perforce command.
*/
public Date getAccess() throws CoreException
{
return getParsedDate("Access"); //$NON-NLS-1$
}
/**
* Returns up to two alternate client workspace roots.
*
* @return The alternate roots. This array may have a lenght of zero.
*/
public IPath[] getAltRoots()
{
IPath[] altRoots;
String ars = get("AltRoots"); //$NON-NLS-1$
if(ars == null)
return Trivial.EMPTY_PATH_ARRAY;
String[] paths = splitMultiPaths(ars);
int top = paths.length;
altRoots = new IPath[top];
for(int idx = 0; idx < top; ++idx)
altRoots[idx] = new Path(paths[idx]);
return altRoots;
}
/**
* The client workspace name, as specified in the <code>P4CLIENT</code> environment variable or its equivalents.
*
* @return The name of the client workspace.
*/
public String getClient()
{
return get("Client"); //$NON-NLS-1$
}
/**
* A textual description of the client workspace. The default text is Created by owner.
*
* @return The description or <code>null</code> if no description exists.
*/
public String getDescription()
{
return get("Description"); //$NON-NLS-1$
}
/**
* The name of the host machine on which this client workspace resides. If included, operations on this client
* workspace can be run only from this host.
*
* @return The name of the <code>host</code> or null when no host is set.
*/
public String getHost()
{
return get("Host"); //$NON-NLS-1$
}
/**
* An option that control carriage-return/linefeed (CR/LF) conversion.
*
* @return The setting of the <code>LineEnd</code> option.
*/
public LineEnd getLineEnd()
{
return LineEnd.valueOf(get("LineEnd")); //$NON-NLS-1$
}
/**
* Convert the <code>clientPath</code> into possible local directories using the <code>AltRoots</code>.
*
* @param clientPath
* @return An array, possibly empty but never <code>null</code>, of local directories.
*/
public IPath[] getLocalAltRoots(IPath clientPath)
{
IPath[] altRoots = getAltRoots();
int top = altRoots.length;
for(int idx = 0; idx < top; ++idx)
altRoots[idx] = resolveClientPath(clientPath, altRoots[idx]);
return altRoots;
}
/**
* Convert the <code>clientPath</code> into a local directory using the <code>Root</code>.
*
* @param clientPath
* @return The local directory.
*/
public IPath getLocalRoot(IPath clientPath)
{
return resolveClientPath(clientPath, getRoot());
}
/**
* The Perforce user name of the user who owns the client workspace.
*
* @return The owner of the client workspace.
*/
public String getOwner()
{
return get("Owner"); //$NON-NLS-1$
}
/**
* The directory (on the local host) relative to which all the files in the <code>view</code> are specified. The
* default is the current working directory.
*
* @return The local root directory.
*/
public IPath getRoot()
{
String root = get("Root"); //$NON-NLS-1$
return (root == null || root.equals("null")) //$NON-NLS-1$
? null
: new Path(root);
}
/**
* The date the client workspace specification was last modified.
*/
public Date getUpdate() throws CoreException
{
return getParsedDate("Update"); //$NON-NLS-1$
}
/**
* Gets the mappings between files in the depot and files in the client workspace.
*
* @return An array of mappings.
*/
public ViewEntry[] getView()
{
return getViewSpec();
}
/**
* Obtains the setting of the <code>allwrite</code> option.
*
* @return The current setting of the option.
* @see #setAllWrite(boolean flag)
*/
public boolean isAllWrite()
{
return isOption("allwrite"); //$NON-NLS-1$
}
/**
* Obtains the setting of the <code>clobber</code> option.
*
* @return The current setting of the option.
* @see #setClobber(boolean flag)
*/
public boolean isClobber()
{
return isOption("clobber"); //$NON-NLS-1$
}
/**
* Obtains the setting of the <code>compress</code> option.
*
* @return The current setting of the option.
* @see #setCompress(boolean flag)
*/
public boolean isCompress()
{
return isOption("compress"); //$NON-NLS-1$
}
/**
* Obtains the setting of the <code>locked</code> option.
*
* @return The current setting of the option.
* @see #setLocked(boolean flag)
*/
public boolean isLocked()
{
return isOption("locked"); //$NON-NLS-1$
}
/**
* Obtains the setting of the <code>modtime</code> option.
*
* @return The current setting of the option.
* @see #setModTime(boolean flag)
*/
public boolean isModTime()
{
return isOption("modtime"); //$NON-NLS-1$
}
/**
* Obtains the setting of the <code>rmdir</code> option.
*
* @return The current setting of the option.
* @see #setRmDir(boolean flag)
*/
public boolean isRmDir()
{
return isOption("rmdir"); //$NON-NLS-1$
}
/**
* If set, unopened files on the client are left writable.
*
* @param flag
* <code>true</code> if this option should be set.
*/
public void setAllWrite(boolean flag)
{
setOption("allwrite", "noallwrite", flag); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Up to two optional alternate client workspace roots. Perforce client programs use the first of the main and
* alternate roots to match the client program's current working directory. This enables users to use the same
* Perforce client specification on multiple platforms with different directory naming conventions. If you are using
* a Windows directory in any of your client roots, you must specify the Windows directory as your main client root
* and specify your other client root directories in the AltRoots: field. For example, an engineer building products
* on multiple platforms might specify a main client root of C:\Projects\Build for Windows builds, and an alternate
* root of /staff/userid/projects/build for any work on UNIX builds.
*
* @param altRoots
* Up to two alternative roots.
*/
public synchronized void setAltRoots(IPath[] altRoots)
{
if(altRoots == null || altRoots.length == 0)
{
m_dirty = (remove("AltRoots") != null); //$NON-NLS-1$
return;
}
if(altRoots.length > 2)
throw new IllegalArgumentException(Messages.max_2_paths_allowed_for_AltRoots);
StringBuilder bld = new StringBuilder();
boolean first = true;
for(IPath altRoot : altRoots)
{
if(first)
first = false;
else
bld.append(' ');
String path = altRoot.toString();
if(path.indexOf(' ') >= 0)
{
bld.append('"');
bld.append(path);
bld.append('"');
}
else
bld.append(path);
}
String newRoots = bld.toString();
m_dirty = !newRoots.equals(put("AltRoots", newRoots)); //$NON-NLS-1$
}
/**
* If set, a <code>p4 sync</code> overwrites ("clobbers") writable-but-unopened files in the client that
* have the same name as the newly-synced files
*
* @param flag
* <code>true</code> if this option should be set.
*/
public void setClobber(boolean flag)
{
setOption("clobber", "noclobber", flag); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* If set, the data stream between the client and the server is compressed.
*
* @param flag
* <code>true</code> if this option should be set.
*/
public void setCompress(boolean flag)
{
setOption("compress", "nocompress", flag); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* A textual description of the client workspace. The default text is Created by owner.
*
* @param host
* The description. Might be <code>null</code>.
*/
public synchronized void setDescription(String description)
{
if(description == null || description.length() == 0)
m_dirty = (remove("Description") != null); //$NON-NLS-1$
else
m_dirty = !description.equals(put("Description", description)); //$NON-NLS-1$
}
/**
* The hostname must be provided exactly as it appears in the output of p4 info when run from that host.<br/>
* This field is meant to prevent accidental misuse of client workspaces on the wrong machine. It doesn't provide
* security, since the actual value of the host name can be overridden with the -H flag to any p4 command, or with
* the P4HOST environment variable. For a similar mechanism that does provide security, use the IP address
* restriction feature of p4 protect.
*
* @param host
* The name of the host
*/
public synchronized void setHost(String host)
{
if(host == null || host.length() == 0)
m_dirty = (remove("Host") != null); //$NON-NLS-1$
else
m_dirty = !host.equals(put("Host", host)); //$NON-NLS-1$
}
/**
* Set the option that control carriage-return/linefeed (CR/LF) conversion.
*
* @param lineEnd
*/
public synchronized void setLineEnd(LineEnd lineEnd)
{
if(lineEnd == null)
lineEnd = LineEnd.local;
m_dirty = !lineEnd.name().equals(put("LineEnd", lineEnd.name())); //$NON-NLS-1$
}
/**
* Grant or deny other users permission to edit the client specification (To make a locked client specification
* truly effective, you should also set a the client's owner's password with p4 passwd.)<br/>
* If locked, only the owner is able to use, edit, or delete the client spec. Perforce administrators can override
* the lock by using the -f (force) flag with p4 client.
*
* @param flag
* <code>true</code> if this option should be set.
*/
public void setLocked(boolean flag)
{
setOption("locked", "unlocked", flag); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* For files without the +m (modtime) file type modifier
* <ul>
* <li>If modtime is set, the modification date (on the local filesystem) of a newly synced file is the datestamp on
* the file when the file was last modified.</li>
* <li>If nomodtime is set, the modification date is the date and time of sync.</li>
* </ul>
* For files with the +m (modtime) file type modifier, the modification date (on the local filesystem) of a newly
* synced file is the datestamp on the file when the file was submitted to the depot, regardless of the setting of
* modtime or nomodtime on the client.
*
* @param flag
* <code>true</code> if this option should be set.
*/
public void setModTime(boolean flag)
{
setOption("modtime", "nomodtime", flag); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Sets the Perforce user name of the user who owns the client workspace. The default is the user who created the
* client workspace.
*/
public synchronized void setOwner(String owner)
{
if(owner == null || owner.length() == 0)
m_dirty = (remove("Owner") != null); //$NON-NLS-1$
else
m_dirty = !owner.equals(put("Owner", owner)); //$NON-NLS-1$
}
/**
* If set, p4 sync deletes empty directories in a client if all files in the directory have been removed.
*
* @param flag
* <code>true</code> if this option should be set.
*/
public void setRmDir(boolean flag)
{
setOption("rmdir", "normdir", flag); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Sets the root directory on the local host. The path must be absolute.
*
* @param root
* The new root.
*/
public synchronized void setRoot(IPath root) throws BuckminsterException
{
if(root == null || !root.isAbsolute())
throw new IllegalArgumentException(Messages.root_cannot_be_null_or_relative);
String osName = root.toOSString();
m_dirty = !osName.equals(put("Root", osName)); //$NON-NLS-1$
}
/**
* Specifies the mappings between files in the depot and files in the client workspace.
*
* @param view
* An array of mappings.
*/
public synchronized void setView(ViewEntry[] view)
{
int top = view.length;
int idx = 0;
while(idx < top)
{
String newView = view[idx].toString();
String oldView = put("View" + Integer.toString(idx), newView); //$NON-NLS-1$
if(!m_dirty && !newView.equals(oldView))
m_dirty = true;
++idx;
}
while(remove("View" + Integer.toString(idx)) != null) //$NON-NLS-1$
{
m_dirty = true;
++idx;
}
}
private boolean entriesContainsMapping(ViewEntry[] entries, IPath depotPath, IPath localPath)
{
for(ViewEntry entry : entries)
{
if(DepotURI.pathEquals(depotPath, entry.getDepotPath()))
{
if(DepotURI.pathEquals(getLocalRoot(entry.getLocalPath()), localPath))
return true;
// Try AltRoots also.
//
for(IPath altPath : getLocalAltRoots(entry.getLocalPath()))
if(DepotURI.pathEquals(altPath, localPath))
return true;
}
}
return false;
}
private String[] getSplitOptions()
{
return get("Options").split("\\s+"); //$NON-NLS-1$ //$NON-NLS-2$
}
private boolean isOption(String enabled)
{
for(String option : getSplitOptions())
if(enabled.equals(option))
return true;
return false;
}
private IPath resolveClientPath(IPath clientPath, IPath root)
{
int numSegs = clientPath.segmentCount();
if(!clientPath.isUNC() && numSegs >= 2)
//
// This is not a client path.
//
return null;
if(!clientPath.segment(0).equals(getClient()))
//
// Client path does belong to this client.
//
return null;
clientPath = clientPath.removeFirstSegments(1);
if(root == null)
clientPath.makeAbsolute();
else
clientPath = root.append(clientPath);
return clientPath;
}
private synchronized void setOption(String enabled, String disabled, boolean flag)
{
boolean found = false;
boolean changed = false;
String[] options = getSplitOptions();
int top = options.length;
for(int idx = 0; idx < top; ++idx)
{
String option = options[idx];
if(option.equals(enabled))
{
found = true;
if(!flag)
{
changed = true;
options[idx] = disabled;
}
break;
}
if(option.equals(disabled))
{
found = true;
if(flag)
{
changed = true;
options[idx] = enabled;
}
break;
}
}
if(!found)
throw new IllegalArgumentException(NLS.bind(Messages.no_such_option_0, enabled));
if(changed)
{
StringBuilder bld = new StringBuilder();
bld.append(options[0]);
for(int idx = 1; idx < top; ++idx)
{
bld.append(' ');
bld.append(options[idx]);
}
put("Options", bld.toString()); //$NON-NLS-1$
}
m_dirty = changed;
}
}