package com.perforce.api;
import java.io.*;
import java.util.*;
/*
* Copyright (c) 2001, Perforce Software, All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* Representation of a source control change. This class can be used to
* determine information for a particular p4 change. It can be constructed using
* the change number, but will not contain any additional change information
* until the <a href="#sync()">sync()</a> method is called.
*
* @author <a href="mailto:david@markley.cc">David Markley</a>
* @version $Date: 2002/08/05 $ $Revision: #10 $
*/
public final class Change extends SourceControlObject {
private int number = -1;
private User user = null;
private String client_name = "";
private String modtime_string = "";
private String description = "";
private int status = PENDING;
private static HashDecay changes = null;
/** Indicates that the Change is pending submission. */
public final static int PENDING = 1;
/** Indicates that the Change has been submitted. */
public final static int SUBMITTED = 2;
/**
* Default no-argument constructor.
*/
public Change() {
super();
getCache();
}
public Change(Env environ) {
this();
this.setEnv(environ);
}
/**
* Constructor that accepts the change number. This change is not populated
* with the correct information until the sync() method is called on it.
*
* @param number
* Change number
*/
public Change(int number) {
this();
this.number = number;
}
public Change(String number) {
this();
this.number = Integer.valueOf(number).intValue();
}
private static HashDecay setCache() {
if(null == changes) {
changes = new HashDecay(300000);
changes.start();
}
return changes;
}
public HashDecay getCache() {
return setCache();
}
public static Change getChange(String number) {
return getChange(null, number, true);
}
public static Change getChange(String number, boolean force) {
return getChange(null, number, force);
}
public static Change getChange(Env env, String number, boolean force) {
return getChange(env, (Integer.valueOf(number)).intValue(), force);
}
public static Change getChange(int number) {
return getChange(null, number, true);
}
public static Change getChange(int number, boolean force) {
return getChange(null, number, force);
}
public static Change getChange(Env env, int number, boolean force) {
Change c;
if(null == (c = (Change) setCache().get(new Integer(number)))) {
c = new Change(number);
force = true;
}
if(null != env)
c.setEnv(env);
if(force)
c.sync();
changes.put(new Integer(number), c);
return c;
}
public String getClientName() {
return client_name;
}
public void setClientName(String name) {
this.client_name = name;
}
public String getModtimeString() {
return modtime_string;
}
public void setModtimeString(String modtime) {
this.modtime_string = modtime;
}
/**
* Sets the change number for the Change. This invalidates all the other
* data for the Change.
*
* @param number
* Change number
*/
public void setNumber(int number) {
this.number = number;
user = null;
description = "";
status = PENDING;
}
/**
* Returns the number of this Change.
*/
public int getNumber() {
return number;
}
/**
* Sets the User that owns this Change.
*
* @param user
* Owning user.
*/
public void setUser(User user) {
this.user = user;
}
/**
* Returns the User that owns this Change.
*/
public User getUser() {
return user;
}
/**
* Sets the description for the change.
*/
public void setDescription(String description) {
String l;
try {
StringBuffer sb = new StringBuffer();
BufferedReader b = new BufferedReader(new StringReader(description));
while(null != (l = b.readLine())) {
sb.append('\t');
sb.append(l.trim());
sb.append('\n');
}
this.description = sb.toString();
} catch(IOException ex) {
this.description = description;
}
}
/**
* Returns the description for the Change. This description includes not
* only the textual description provided by the user, but also the list of
* affected files and how they were affected.
*
* The String returned includes newline characters.
*/
public String getDescription() {
return description;
}
public String getShortDescription() {
return getShortDescription(false);
}
public String getShortDescription(boolean blurb) {
StringBuffer sb = new StringBuffer();
String l;
try {
BufferedReader b = new BufferedReader(new StringReader(getDescription()));
while(null != (l = b.readLine())) {
if(blurb && l.startsWith("Change"))
continue;
if(blurb && l.startsWith("Jobs fixed"))
break;
if(l.startsWith("Affected file")) {
break;
} else {
sb.append(l);
sb.append('\n');
}
}
} catch(IOException ex) {
}
return sb.toString();
}
/**
* Returns a Vector filled with the files (including revision numbers) that
* were affected by this change. What was done to each file as a result of
* this Change is stripped off.
*
* This method uses the value of the Change's description to determine the
* files that are affected.
*
* @return <code>Vector</code> of <code>String</code>s of files
* affected.
*/
public Vector getFiles() {
Vector v = new Vector();
try {
BufferedReader b = new BufferedReader(new StringReader(getDescription()));
String l, t;
int pos;
while(null != (l = b.readLine())) {
t = l.trim();
// if (t.startsWith("... ")) {
if(t.startsWith("//")) {
if(-1 != (pos = t.lastIndexOf(" "))) {
v.addElement(t.substring(0, pos).trim());
}
}
}
} catch(IOException e) {
}
return v;
}
/**
* Returns a Vector filled with the files (including revision numbers) that
* were affected by this change. What was done to each file as a result of
* this Change is stripped off.
*
* This method uses the value of the Change's description to determine the
* files that are affected.
*
* @return <code>Vector</code> of <code>FileEntry</code> objects of
* files affected.
*/
public Vector getFileEntries() {
if(PENDING == getStatus() && 0 < getNumber()) {
return FileEntry.getOpened(getEnv(), false, false, getNumber(), null);
}
Vector v = new Vector();
FileEntry fent;
try {
BufferedReader b = new BufferedReader(new StringReader(getDescription()));
String l, t;
int beg, end;
while(null != (l = b.readLine())) {
t = l.trim();
if(t.startsWith("//")) {
fent = new FileEntry();
fent.setEnv(getEnv());
beg = 0;
end = 4;
if(-1 != (end = t.indexOf('#', beg))) {
fent.setDepotPath(t.substring(beg, end));
beg = end + 1;
if(-1 != (end = t.indexOf(' ', beg))) {
fent.setHeadRev(Integer.valueOf(t.substring(beg, end).trim()).intValue());
fent.setHeadAction(t.substring(end + 1));
}
} else {
fent.setDepotPath(t);
}
v.addElement(fent);
}
}
} catch(IOException e) {
}
return v;
}
/**
* Adds the given <code>FileEntry</code> to the changelist. If the
* changelist has not been committed to the server, that is done first.
*
* @param fent
* file entry to be added.
*/
public void addFile(FileEntry fent) throws PerforceException {
if(-1 == number)
commit();
fent.reopen(null, this);
}
/**
* Resolves this file. If the force flag is false, and auto-resolve is
* attempted (p4 resolve -am). If the force flag is true, an "accept theirs"
* resolve is completed (p4 resolve -at).
*
* @see FileEntry#resolve(boolean)
* @param force
* Indicates whether the resolve should be forced.
*/
public String resolve(boolean force) throws PerforceException {
StringBuffer sb = new StringBuffer();
Enumeration en = getFileEntries().elements();
try {
while(en.hasMoreElements()) {
sb.append(((FileEntry) en.nextElement()).resolve(force));
}
} catch(Exception ex) {
throw new PerforceException(ex.getMessage());
}
return sb.toString();
}
/**
* Sets status for the Change. This can be either PENDING or SUBMITTED.
*/
public void setStatus(int status) {
this.status = status;
}
/**
* Returns the status for the Change. This can be either PENDING or
* SUBMITTED.
*/
public int getStatus() {
return status;
}
/**
* Submits the change, if it is pending.
*
* @throws SubmitException
* If the submit fails.
*/
public String submit() throws SubmitException {
String l;
StringBuffer sb = new StringBuffer();
if(PENDING != status) {
throw new SubmitException("Change already submitted.");
}
String[] cmd = { "p4", "submit", "-c", String.valueOf(getNumber()) };
try {
P4Process p = new P4Process(getEnv());
p.exec(cmd);
while(null != (l = p.readLine())) {
sb.append(l);
sb.append('\n');
}
p.close();
} catch(Exception ex) {
throw new SubmitException(ex.getMessage() + "\n\n" + sb.toString());
}
return sb.toString();
}
/**
* Updates the change or creates a pending change.
*
* @deprecated Use {@link #commit() commit()} instead.
*/
public void store() throws CommitException {
this.commit();
}
public void commit() throws CommitException {
Enumeration en;
Vector fents = getFileEntries();
StringBuffer sb = new StringBuffer();
String[] cmd = { "p4", "change", "-i" };
String l;
int pos;
boolean store_failed = false;
try {
P4Process p = new P4Process(getEnv());
p.exec(cmd);
try {
Thread.sleep(1000);
} catch(InterruptedException intex) { /* Ignoring Exception */
}
if(0 > number) {
p.println("Change: new");
} else {
p.println("Change: " + getNumber());
}
p.println("Client: " + getClientName());
if(null == getUser() && null != getEnv()) {
p.println("User: " + getEnv().getUser());
} else {
p.println("User: " + user.getId());
}
p.println("Description: ");
p.println(getDescription());
if(null != fents && 0 < fents.size()) {
FileEntry fent;
p.println("Files: ");
en = fents.elements();
while(en.hasMoreElements()) {
fent = (FileEntry) en.nextElement();
p.println("\t" + fent.getDepotPath());
}
}
if(Utils.isWindows()) {
p.println("\032\n\032");
}
p.flush();
p.outClose();
Debug.notify("Change.store(): Wrote change info.");
while(null != (l = p.readLine())) {
Debug.notify("READ: " + l);
if(l.startsWith("Change ") && (-1 != (pos = l.indexOf("created")))) {
setNumber(Integer.valueOf(l.substring(7, pos - 1).trim()).intValue());
}
if(l.startsWith("Error"))
store_failed = true;
sb.append(l);
sb.append('\n');
}
p.close();
Debug.notify("Change.store(): All done reading.");
} catch(Exception ex) {
throw new CommitException(ex.getMessage());
}
if(store_failed || 0 > getNumber()) {
throw new CommitException(sb.toString());
}
}
/**
* Synchronizes the Change with the correct information from P4, using
* whatever change number has already been set in the Change. After this
* method is called, all the information in the Change is valid.
*/
public void sync() {
sync(number);
}
/**
* Sycnhronizes the Change with the correct information from P4. After this
* method is called, all the information in the Change is valid.
*
* @param number
* Change number
*/
public void sync(int number) {
if(SUBMITTED == status && !outOfSync(60000))
return;
this.number = number;
String l, tstr;
String[] cmd = { "p4", "describe", "-s", "number" };
cmd[3] = String.valueOf(number);
boolean wasFound = false;
try {
P4Process p = new P4Process(getEnv());
p.exec(cmd);
while(null != (l = p.readLine())) {
if(!wasFound && l.startsWith("Change")) {
tstr = l.substring(l.indexOf("by") + 3).trim();
tstr = tstr.substring(0, tstr.indexOf("@"));
user = User.getUser(getEnv(), tstr);
modtime_string = l.substring(l.indexOf(" on ") + 4).trim();
if(-1 == l.indexOf("pending")) {
status = SUBMITTED;
}
description = l;
wasFound = true;
} else {
description += l.trim() + "\n";
}
}
p.close();
inSync();
} catch(IOException ex) {
Debug.out(Debug.ERROR, ex);
}
}
/**
* Reverts all the files associated with a pending changelist.
*/
public void revert() throws PerforceException {
if(PENDING != status) {
throw new PerforceException("Change already submitted.");
}
Enumeration en = getFileEntries().elements();
try {
while(en.hasMoreElements()) {
((FileEntry) en.nextElement()).revert();
}
} catch(Exception ex) {
throw new PerforceException(ex.getMessage());
}
}
/**
* Delete the pending changelist. This method will revert any open files
* associated with the changelist and then delete it.
*
* @return log of delete command.
*/
public String delete() throws PerforceException {
this.revert();
return this.deleteEmptyChange();
}
/**
* Deletes the Changelist if it is empty.
*
* @deprecated Use <code>delete</code> method instead.
* @return String Contents of the information returned by P4 as a result of
* the delete call.
*/
public String deleteEmptyChange() throws PerforceException {
String l;
StringBuffer sb = new StringBuffer();
if(PENDING != status) {
throw new PerforceException("Change already submitted.");
}
String[] cmd = { "p4", "change", "-d", String.valueOf(getNumber()) };
try {
P4Process p = new P4Process(getEnv());
p.exec(cmd);
while(null != (l = p.readLine())) {
sb.append(l);
sb.append('\n');
}
p.close();
} catch(Exception ex) {
throw new PerforceException(ex.getMessage() + "\n\n" + sb.toString());
}
return sb.toString();
}
/**
* Overrides the default toString() method.
*/
public String toString() {
StringBuffer sb = new StringBuffer("Change: ");
sb.append(number);
sb.append("\nUser: ");
sb.append(user);
sb.append("\nDescription:\n");
sb.append(description);
return sb.toString();
}
public static Change[] getChanges(Env env, String path) throws PerforceException {
return getChanges(env, path, 100, null, null, false, null);
}
public static Change[] getChanges(String path) throws PerforceException {
return getChanges(null, path, 100, null, null, false, null);
}
public static Change[] getChanges(Env env, String path, int max, String start, String end, boolean use_integs,
String ufilter) throws PerforceException {
int cmdlen = 8;
String[] cmd;
String tpath = path;
if(use_integs)
cmdlen++;
if(null == tpath)
tpath = "";
if(null != start && !start.trim().equals("")) {
tpath += "@" + start;
if(null != end && !end.trim().equals("")) {
tpath += "," + end;
}
}
if(tpath.trim().equals(""))
cmdlen--;
cmd = new String[cmdlen];
if(!tpath.trim().equals(""))
cmd[cmdlen - 1] = tpath;
cmd[0] = "p4";
cmd[1] = "changes";
cmd[2] = "-m";
cmd[3] = String.valueOf(max);
cmd[4] = "-l";
cmd[5] = "-s";
cmd[6] = "submitted";
if(use_integs)
cmd[7] = "-i";
Vector v = new Vector();
Change[] chngs;
StringTokenizer st;
int num;
String l, id, description = "";
User user;
Change c = null;
String modtime, client_name;
try {
P4Process p = new P4Process(env);
p.setRawMode(true);
p.exec(cmd);
while(null != (l = p.readLine())) {
if(l.startsWith("info: Change")) {
l = l.substring(6).trim();
st = new StringTokenizer(l);
if(!st.nextToken().equals("Change"))
continue;
try {
num = Integer.parseInt(st.nextToken());
} catch(Exception ex) {
throw new PerforceException("Could not parse change number from line: " + l);
}
if(!st.nextToken().equals("on"))
continue;
modtime = st.nextToken();
if(!st.nextToken().equals("by"))
continue;
id = st.nextToken();
int pos = id.indexOf("@");
client_name = id.substring(pos + 1);
id = id.substring(0, pos);
user = User.getUser(env, id);
if(null != c) {
c.setDescription(description);
}
description = "";
c = new Change(num);
c.setEnv(env);
c.setUser(user);
c.setClientName(client_name);
c.setModtimeString(modtime);
if(null == ufilter || id.equals(ufilter)) {
v.addElement(c);
}
} else {
l = l.substring(5).trim();
description += l + "\n";
}
}
if(null != c) {
c.setDescription(description);
}
p.close();
} catch(IOException ex) {
Debug.out(Debug.ERROR, ex);
}
chngs = new Change[v.size()];
for(int i = 0; i < v.size(); i++) {
chngs[i] = (Change) v.elementAt(i);
changes.put(new Integer(chngs[i].getNumber()), chngs[i]);
}
return chngs;
}
public String toXML() {
StringBuffer sb = new StringBuffer("<change number=\"");
sb.append(getNumber());
sb.append("\" user=\"");
sb.append(getUser());
sb.append("\" client=\"");
sb.append(getClientName());
sb.append("\" status=\"");
sb.append(getStatus());
sb.append("\" modtime=\"");
sb.append(getModtimeString());
sb.append("\">");
sb.append("<description>");
sb.append(getDescription());
sb.append("</description>");
Vector v = getFileEntries();
FileEntry fent = null;
if(null != v && 0 != v.size()) {
sb.append("<files>");
for(int i = 0; i < v.size(); i++) {
fent = (FileEntry) v.elementAt(i);
sb.append("<file path=\"");
sb.append(fent.getDepotPath());
sb.append("\" rev=\"");
sb.append(fent.getHeadRev());
sb.append("\"/>");
}
sb.append("</files>");
}
sb.append("</change>");
return sb.toString();
}
/**
* Determine the users that review this changelist. This method returns an
* array of Users that need to be informed.
*/
public User[] reviews() throws PerforceException {
Vector users = new Vector();
User[] usrs = new User[0];
User chng;
User usr;
String uid;
String email;
String name, t;
StringTokenizer st;
String l;
String[] cmd = { "p4", "reviews", "-c", String.valueOf(this.number) };
try {
P4Process p = new P4Process(getEnv());
p.exec(cmd);
while(null != (l = p.readLine())) {
if(l.length() < 4)
continue;
st = new StringTokenizer(l);
uid = st.nextToken();
email = st.nextToken("<> \t");
name = st.nextToken("<> ()\t");
try {
while(null != (t = st.nextToken("<> ()\t"))) {
name += (" " + t);
}
} catch(NoSuchElementException ex) {
}
// System.out.print("U: "+uid+", E: "+email+", N: "+name+"\n");
usr = new User(uid);
usr.setEnv(getEnv());
usr.setEmail(email);
usr.setFullName(name);
users.addElement(usr);
}
p.close();
} catch(Exception ex) {
throw new PerforceException(ex.getMessage());
}
if(0 == users.size())
return null;
return (User[]) users.toArray(usrs);
}
/**
* Used for testing.
*
* @deprecated Actually in use, but this keeps it out of the docs.
*/
public static void main(String[] args) {
String propfile = "/etc/p4.conf";
Env environ = null;
Debug.setDebugLevel(Debug.VERBOSE);
if(0 < args.length)
propfile = args[0];
try {
environ = new Env(propfile);
} catch(PerforceException ex) {
System.out.println("Could not load properties from " + propfile + ": " + ex);
System.exit(-1);
}
System.out.println(environ);
Change chng = new Change(environ);
chng.setDescription("This is a test changelist.");
try {
chng.commit();
} catch(CommitException e) {
System.err.println("Unable to store new change.");
e.printStackTrace(System.err);
System.exit(-1);
}
System.out.println("New Changelist Generated: " + chng.getNumber());
Utils.cleanUp();
}
}