/* * Copyright 2001-2004 The Apache Software Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.ant.taskdefs.optional.starteam; import com.starbase.starteam.Folder; import com.starbase.starteam.Item; import com.starbase.starteam.Status; import com.starbase.starteam.View; import com.starbase.starteam.ViewConfiguration; import java.io.IOException; import java.io.File; import java.util.Enumeration; import java.util.Hashtable; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; /** * Checks out files from a StarTeam project. * It also creates all working directories on the * local directory if appropriate. Ant Usage: *
 * <taskdef name="starteamcheckout"
 * classname="org.apache.tools.ant.taskdefs.StarTeamCheckout"/>
 * <starteamcheckout username="BuildMaster" password="ant" starteamFolder="Source"
 * starteamurl="servername:portnum/project/view"
 * createworkingdirectories="true"/>
 * 
* * @version 1.1 * @see StarBase Web Site * * @ant.task name="stcheckout" category="scm" */ public class StarTeamCheckout extends TreeBasedTask { /** * holder for the createDirs attribute */ private boolean createDirs = true; /** * holder for the deleteUncontrolled attribute. If true, * all local files not in StarTeam will be deleted. */ private boolean deleteUncontrolled = true; /** * holder for the deleteUncontrolled attribute. If true, * (default) local non-binary files will be checked out using the local * platform's EOL convention. If false, checkouts will preserve the * server's EOL convention. */ private boolean convertEOL = true; /** * flag (defaults to true) to create all directories * that are in the Starteam repository even if they are empty. * * @param value the value to set the attribute to. */ public void setCreateWorkingDirs(boolean value) { this.createDirs = value; } /** * Whether or not all local files not in StarTeam should be deleted. * Optional, defaults to true. * @param value the value to set the attribute to. */ public void setDeleteUncontrolled(boolean value) { this.deleteUncontrolled = value; } /** * Set whether or not files should be checked out using the * local machine's EOL convention. * Optional, defaults to true. * @param value the value to set the attribute to. */ public void setConvertEOL(boolean value) { this.convertEOL = value; } /** * Sets the label StarTeam is to use for checkout; defaults to the most recent file. * The label must exist in starteam or an exception will be thrown. * @param label the label to be used */ public void setLabel(String label) { _setLabel(label); } /** * This attribute tells whether to do a locked checkout, an unlocked * checkout or to leave the checkout status alone (default). A locked * checkout locks all other users out from making changes. An unlocked * checkout reverts all local files to their previous repository status * and removes the lock. * @see #setLocked(boolean) * @see #setUnlocked(boolean) */ private int lockStatus = Item.LockType.UNCHANGED; /** * Set to do a locked checkout; optional default is false. * @param v True to do a locked checkout, false to checkout without * changing status/. * @exception BuildException if both locked and unlocked are set true */ public void setLocked(boolean v) throws BuildException { setLockStatus(v, Item.LockType.EXCLUSIVE); } /** * Set to do an unlocked checkout. Default is false; * @param v True to do an unlocked checkout, false to checkout without * changing status. * @exception BuildException if both locked and unlocked are set true */ public void setUnlocked(boolean v) throws BuildException { setLockStatus(v, Item.LockType.UNLOCKED); } private void setLockStatus(boolean v, int newStatus) throws BuildException { if (v) { if (this.lockStatus == Item.LockType.UNCHANGED) { this.lockStatus = newStatus; } else if (this.lockStatus != newStatus) { throw new BuildException( "Error: cannot set locked and unlocked both true."); } } } /** * should checked out files get the timestamp from the repository * or the time they are checked out. True means use the repository * timestamp. */ private boolean useRepositoryTimeStamp = false; /** * sets the useRepositoryTimestmp member. * * @param useRepositoryTimeStamp * true means checked out files will get the repository timestamp. * false means the checked out files will be timestamped at the time * of checkout. */ public void setUseRepositoryTimeStamp(boolean useRepositoryTimeStamp) { this.useRepositoryTimeStamp = useRepositoryTimeStamp; } /** * returns the value of the useRepositoryTimestamp member * * @return the value of the useRepositoryTimestamp member */ public boolean getUseRepositoryTimeStamp() { return this.useRepositoryTimeStamp; } /** * List files, dates, and statuses as of this date; optional. * If not specified, the most recent version of each file will be listed. * * @param asOfDateParam the date as of which the listing to be made * @since Ant 1.6 */ public void setAsOfDate(String asOfDateParam) { _setAsOfDate(asOfDateParam); } /** * Date Format with which asOfDate parameter to be parsed; optional. * Must be a SimpleDateFormat compatible string. * If not specified, and asOfDateParam is specified, parse will use ISO8601 * datetime and date formats. * * @param asOfDateFormat the SimpleDateFormat-compatible format string * @since Ant 1.6 */ public void setAsOfDateFormat(String asOfDateFormat) { _setAsOfDateFormat(asOfDateFormat); } /** * Override of base-class abstract function creates an * appropriately configured view for checkouts - either * the current view or a view from this.label or the raw * view itself in the case of a revision label. * * @param raw the unconfigured View * * @return the snapshot View appropriately configured. * @exception BuildException on error */ protected View createSnapshotView(View raw) throws BuildException { int labelID = getLabelID(raw); // if a label has been supplied and it is a view label, use it // to configure the view if (this.isUsingViewLabel()) { return new View(raw, ViewConfiguration.createFromLabel(labelID)); } // if a label has been supplied and it is a revision label, use the raw // the view as the snapshot else if (this.isUsingRevisionLabel()) { return raw; } // if a date has been supplied use a view configured to the date. View view = getViewConfiguredByDate(raw); if (view != null) { return view; } // otherwise, use this view configured as the tip. else { return new View(raw, ViewConfiguration.createTip()); } } /** * Implements base-class abstract function to define tests for * any preconditons required by the task. * * @exception BuildException thrown if both rootLocalFolder * and viewRootLocalFolder are defined */ protected void testPreconditions() throws BuildException { if (this.isUsingRevisionLabel() && this.createDirs) { log("Ignoring createworkingdirs while using a revision label." + " Folders will be created only as needed.", Project.MSG_WARN); this.createDirs = false; } if (lockStatus != Item.LockType.UNCHANGED) { boolean lockStatusBad = false; if (null != getLabel()) { log("Neither locked nor unlocked may be true" + " when checking out a labeled version.", Project.MSG_ERR); lockStatusBad = true; } else if (null != getAsOfDate()) { log("Neither locked nor unlocked may be true" + " when checking out by date.", Project.MSG_ERR); lockStatusBad = true; } if (lockStatusBad) { throw new BuildException( "Lock status may not be changed" + " when checking out a non-current version."); } } if (null != getLabel() && null != getAsOfDate()) { throw new BuildException( "Both label and asOfDate specified. " + "Unable to process request."); } } /** * extenders should emit to the log an entry describing the parameters * that will be used by this operation. * * @param starteamrootFolder * root folder in StarTeam for the operation * @param targetrootFolder * root local folder for the operation (whether specified * by the user or not. */ protected void logOperationDescription( Folder starteamrootFolder, java.io.File targetrootFolder) { log((this.isRecursive() ? "Recursive" : "Non-recursive") + " Checkout from: " + starteamrootFolder.getFolderHierarchy()); log(" Checking out to" + (null == getRootLocalFolder() ? "(default): " : ": ") + targetrootFolder.getAbsolutePath()); logLabel(); logAsOfDate(); logIncludes(); logExcludes(); if (this.lockStatus == Item.LockType.EXCLUSIVE) { log(" Items will be checked out with Exclusive locks."); } else if (this.lockStatus == Item.LockType.UNLOCKED) { log(" Items will be checked out unlocked " + "(even if presently locked)."); } else { log(" Items will be checked out with no change in lock status."); } log(" Items will be checked out with " + (this.useRepositoryTimeStamp ? "repository timestamps." : "the current timestamp.")); log(" Items will be checked out " + (this.isForced() ? "regardless of" : "in accordance with") + " repository status."); if (this.deleteUncontrolled) { log(" Local items not found in the repository will be deleted."); } log(" Items will be checked out " + (this.convertEOL ? "using the local machine's EOL convention" : "without changing the EOL convention used on the server")); log(" Directories will be created" + (this.createDirs ? " wherever they exist in the repository, even if empty." : " only where needed to check out files.")); } /** * Implements base-class abstract function to perform the checkout * operation on the files in each folder of the tree. * * @param starteamFolder the StarTeam folder from which files to be * checked out * @param targetFolder the local mapping of rootStarteamFolder * @exception BuildException if any error occurs */ protected void visit(Folder starteamFolder, java.io.File targetFolder) throws BuildException { try { if (null != getRootLocalFolder()) { starteamFolder.setAlternatePathFragment( targetFolder.getAbsolutePath()); } if (!targetFolder.exists()) { if (!this.isUsingRevisionLabel()) { if (this.createDirs) { if (targetFolder.mkdirs()) { log("Creating folder: " + targetFolder); } else { throw new BuildException( "Failed to create local folder " + targetFolder); } } } } Folder[] foldersList = starteamFolder.getSubFolders(); Item[] filesList = starteamFolder.getItems(getTypeNames().FILE); if (this.isUsingRevisionLabel()) { // prune away any files not belonging to the revision label // this is one ugly API from Starteam SDK Hashtable labelItems = new Hashtable(filesList.length); int s = filesList.length; int[] ids = new int[s]; for (int i = 0; i < s; i++) { ids[i] = filesList[i].getItemID(); labelItems.put(new Integer(ids[i]), new Integer(i)); } int[] foundIds = getLabelInUse().getLabeledItemIDs(ids); s = foundIds.length; Item[] labeledFiles = new Item[s]; for (int i = 0; i < s; i++) { Integer id = new Integer(foundIds[i]); labeledFiles[i] = filesList[((Integer) labelItems.get(id)).intValue()]; } filesList = labeledFiles; } // note, it's important to scan the items BEFORE we make the // Unmatched file map because that creates a bunch of NEW // folders and files (unattached to repository) and we // don't want to include those in our traversal. UnmatchedFileMap ufm = new CheckoutMap(). init(targetFolder.getAbsoluteFile(), starteamFolder); for (int i = 0; i < foldersList.length; i++) { Folder stFolder = foldersList[i]; java.io.File subfolder = new java.io.File(targetFolder, stFolder.getName()); ufm.removeControlledItem(subfolder); if (isRecursive()) { visit(stFolder, subfolder); } } for (int i = 0; i < filesList.length; i++) { com.starbase.starteam.File stFile = (com.starbase.starteam.File) filesList[i]; processFile(stFile, targetFolder); ufm.removeControlledItem( new java.io.File(targetFolder, stFile.getName())); } if (this.deleteUncontrolled) { ufm.processUncontrolledItems(); } } catch (IOException e) { throw new BuildException(e); } } /** * provides a string showing from and to full paths for logging * * @param remotefile the Star Team file being processed. * * @return a string showing from and to full paths */ private String describeCheckout(com.starbase.starteam.File remotefile, java.io.File localFile) { StringBuffer sb = new StringBuffer(); sb.append(getFullRepositoryPath(remotefile)) .append(" --> "); if (null == localFile) { sb.append(remotefile.getFullName()); } else { sb.append(localFile); } return sb.toString(); } private String describeCheckout(com.starbase.starteam.File remotefile) { return describeCheckout(remotefile, null); } /** * Processes (checks out) stFilesfiles from StarTeam folder. * * @param eachFile repository file to process * @param targetFolder a java.io.File (Folder) to work * @throws IOException when StarTeam API fails to work with files */ private void processFile(com.starbase.starteam.File eachFile, File targetFolder) throws IOException { String filename = eachFile.getName(); java.io.File localFile = new java.io.File(targetFolder, filename); // If the file doesn't pass the include/exclude tests, skip it. if (!shouldProcess(filename)) { log("Excluding " + getFullRepositoryPath(eachFile), Project.MSG_INFO); return; } if (this.isUsingRevisionLabel()) { if (!targetFolder.exists()) { if (targetFolder.mkdirs()) { log("Creating folder: " + targetFolder); } else { throw new BuildException( "Failed to create local folder " + targetFolder); } } boolean success = eachFile.checkoutByLabelID( localFile, getIDofLabelInUse(), this.lockStatus, !this.useRepositoryTimeStamp, true, false); if (success) { log("Checked out " + describeCheckout(eachFile, localFile)); } } else { boolean checkout = true; // Just a note: StarTeam has a status for NEW which implies // that there is an item on your local machine that is not // in the repository. These are the items that show up as // NOT IN VIEW in the Starteam GUI. // One would think that we would want to perhaps checkin the // NEW items (not in all cases! - Steve Cohen 15 Dec 2001) // Unfortunately, the sdk doesn't really work, and we can't // actually see anything with a status of NEW. That is why // we can just check out everything here without worrying // about losing anything. int fileStatus = (eachFile.getStatus()); // We try to update the status once to give StarTeam // another chance. if (fileStatus == Status.MERGE || fileStatus == Status.UNKNOWN) { eachFile.updateStatus(true, true); fileStatus = (eachFile.getStatus()); } log(eachFile.toString() + " has status of " + Status.name(fileStatus), Project.MSG_DEBUG); switch (fileStatus) { case Status.OUTOFDATE: case Status.MISSING: log("Checking out: " + describeCheckout(eachFile)); break; default: if (isForced()) { log("Forced checkout of " + describeCheckout(eachFile) + " over status " + Status.name(fileStatus)); } else { log("Skipping: " + getFullRepositoryPath(eachFile) + " - status: " + Status.name(fileStatus)); checkout = false; } } if (checkout) { if (!targetFolder.exists()) { if (targetFolder.mkdirs()) { log("Creating folder: " + targetFolder); } else { throw new BuildException( "Failed to create local folder " + targetFolder); } } eachFile.checkout(this.lockStatus, !this.useRepositoryTimeStamp, this.convertEOL, true); } } } /** * handles the deletion of uncontrolled items */ private class CheckoutMap extends UnmatchedFileMap { protected boolean isActive() { return StarTeamCheckout.this.deleteUncontrolled; } /** * override of the base class init. It can be much simpler, since * the action to be taken is simply to delete the local files. No * further interaction with the repository is necessary. * * @param localFolder * the local folder from which the mappings will be made. * @param remoteFolder * not used in this implementation */ UnmatchedFileMap init(java.io.File localFolder, Folder remoteFolder) { if (!localFolder.exists()) { return this; } String[] localFiles = localFolder.list(); for (int i = 0; i < localFiles.length; i++) { java.io.File localFile = new java.io.File(localFolder, localFiles[i]).getAbsoluteFile(); log("adding " + localFile + " to UnmatchedFileMap", Project.MSG_DEBUG); if (localFile.isDirectory()) { this.put(localFile, ""); } else { this.put(localFile, ""); } } return this; } /** * deletes uncontrolled items from the local tree. It is assumed * that this method will not be called until all the items in the * corresponding folder have been processed, and that the internal map * will contain only uncontrolled items. */ void processUncontrolledItems() throws BuildException { if (this.isActive()) { Enumeration e = this.keys(); while (e.hasMoreElements()) { java.io.File local = (java.io.File) e.nextElement(); delete(local); } } } /** * deletes all files and if the file is a folder recursively deletes * everything in it. * * @param local The local file or folder to be deleted. */ void delete(java.io.File local) { // once we find a folder that isn't in the repository, // anything below it can be deleted. if (local.isDirectory() && isRecursive()) { String[] contents = local.list(); for (int i = 0; i < contents.length; i++) { java.io.File file = new java.io.File(local, contents[i]); delete(file); } } local.delete(); log("Deleted uncontrolled item " + local.getAbsolutePath()); } } }