blob: 1f4f68f0da097cfff49f020538c3381d4c3fcb8e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010, 2014 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.internal.bugzilla.core;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.apache.xmlrpc.XmlRpcException;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.mylyn.internal.bugzilla.core.service.BugzillaXmlRpcClient;
import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
/**
* @author Charley Wang
*/
public class CustomTransitionManager implements Serializable {
private static final long serialVersionUID = 3340305752692674487L;
private static final String DEFAULT_DUPLICATE_STATUS = "RESOLVED"; //$NON-NLS-1$
/** Default start status -- this uses the Mylyn default from before bug #317729 */
public static final String DEFAULT_START_STATUS = "NEW"; //$NON-NLS-1$
/*
* Create the default Bugzilla operations
*/
//Indexed by the current status
private final HashMap<String, List<AbstractBugzillaOperation>> operationMapByCurrentStatus;
//Indexed by the status to be transitioned into
private final HashMap<String, List<AbstractBugzillaOperation>> operationMapByEndStatus;
//Statuses that are not marked as is_open by Bugzilla
private final ArrayList<String> closedStatuses;
//Whether or not customized names were used in Bugzilla
private boolean customNames = false;
private boolean valid = false;
private String filePath;
private String duplicateStatus;
private String startStatus;
public CustomTransitionManager() {
operationMapByCurrentStatus = new HashMap<String, List<AbstractBugzillaOperation>>();
operationMapByEndStatus = new HashMap<String, List<AbstractBugzillaOperation>>();
closedStatuses = new ArrayList<String>();
this.valid = false;
this.filePath = ""; //$NON-NLS-1$
duplicateStatus = DEFAULT_DUPLICATE_STATUS;
startStatus = DEFAULT_START_STATUS;
}
/**
* Searches for a valid transition description file. Returns true if a file exists and was sucessfully parsed, false
* otherwise
*
* @param filePath
* @return true if anything was changed, false otherwise.
* @throws CoreException
*/
public boolean parse(String filePath) throws CoreException {
if (filePath == null || filePath.length() < 1) {
setValid(false);
return false;
} else if (filePath.equals(this.filePath)) {
//Do nothing, already parsed this file
return false;
}
this.filePath = filePath;
setValid(true);
operationMapByCurrentStatus.clear();
operationMapByEndStatus.clear();
closedStatuses.clear();
File file = new File(filePath);
if (!file.exists() || !file.canRead()) {
setValid(false);
return isValid();
}
BufferedReader br = null;
try {
String s;
boolean checkOptions = true;
br = new BufferedReader(new FileReader(file));
while ((s = br.readLine()) != null && isValid()) {
if (s.equals("<transitions>")) { //$NON-NLS-1$
checkOptions = false;
defaultNames();
continue;
}
if (checkOptions) {
parseOptions(s);
} else {
parseTransitions(s);
}
}
} catch (IOException e) {
setValid(false);
throw new CoreException(new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN, 1,
"Error parsing transition description file.\n\n" + e.getMessage(), e)); //$NON-NLS-1$
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
}
}
}
return valid;
}
private void parseOptions(String s) throws IOException {
String[] pieces = s.split("="); //$NON-NLS-1$
if (pieces.length != 2) {
throw new IOException(Messages.CustomTransitionManager_InvalidBugzillaOption + s);
}
String name = pieces[0];
String value = pieces[1];
if (name.equals("CustomStatusNames")) { //$NON-NLS-1$
if (value.equals("true")) { //$NON-NLS-1$
customNames = true;
} else {
customNames = false;
}
} else if (name.equals("DuplicateStatus")) { //$NON-NLS-1$
duplicateStatus = value;
} else if (name.equals("ClosedCustomStatus")) { //$NON-NLS-1$
closedStatuses.add(value);
} else if (name.equals("StartStatus")) {//$NON-NLS-1$
startStatus = value;
}
}
private void parseTransitions(String s) throws IOException {
String[] pieces = s.split(":"); //$NON-NLS-1$
if (pieces.length < 4) {
throw new IOException(Messages.CustomTransitionManager_InvalidBugzillaTransition + s);
}
String status = pieces[1];
String[] endStatuses = pieces[3].split(","); //$NON-NLS-1$
parse(status, endStatuses);
}
private void defaultNames() {
/*
* Add the default operations if we are not using custom names
*/
//Step 1: Make custom operations for the default operations
//This is necessary so we can add verifiers to these operations later
if (operationMapByEndStatus.size() == 0) {
ArrayList<AbstractBugzillaOperation> list = new ArrayList<AbstractBugzillaOperation>();
//NONE is currently not used from the map, included for completeness
list.add(BugzillaOperation.none);
operationMapByEndStatus.put("NONE", list); //$NON-NLS-1$
list = new ArrayList<AbstractBugzillaOperation>();
list.add(BugzillaOperation.accept);
operationMapByEndStatus.put("ASSIGNED", list); //$NON-NLS-1$
list = new ArrayList<AbstractBugzillaOperation>();
list.add(BugzillaOperation.resolve);
if (duplicateStatus.equals("RESOLVED")) { //$NON-NLS-1$
list.add(BugzillaOperation.duplicate);
}
operationMapByEndStatus.put("RESOLVED", list); //$NON-NLS-1$
list = new ArrayList<AbstractBugzillaOperation>();
list.add(BugzillaOperation.reopen);
operationMapByEndStatus.put("REOPENED", list); //$NON-NLS-1$
list = new ArrayList<AbstractBugzillaOperation>();
list.add(BugzillaOperation.unconfirmed);
operationMapByEndStatus.put("UNCONFIRMED", list); //$NON-NLS-1$
list = new ArrayList<AbstractBugzillaOperation>();
list.add(BugzillaOperation.verify);
if (duplicateStatus.equals("VERIFIED")) { //$NON-NLS-1$
list.add(BugzillaOperation.duplicate);
}
operationMapByEndStatus.put("VERIFIED", list); //$NON-NLS-1$
list = new ArrayList<AbstractBugzillaOperation>();
list.add(BugzillaOperation.close);
list.add(BugzillaOperation.close_with_resolution);
if (duplicateStatus.equals("CLOSED")) { //$NON-NLS-1$
list.add(BugzillaOperation.duplicate);
}
operationMapByEndStatus.put("CLOSED", list); //$NON-NLS-1$
list = new ArrayList<AbstractBugzillaOperation>();
list.add(BugzillaOperation.markNew);
operationMapByEndStatus.put("NEW", list); //$NON-NLS-1$
}
}
public List<AbstractBugzillaOperation> getOperation(String id) {
return operationMapByEndStatus.get(id);
}
public List<AbstractBugzillaOperation> getValidTransitions(String key) {
return operationMapByCurrentStatus.get(key);
}
/**
* Sets whether or not this class is valid. If set to false, the filePath will be set to "" so that subsequent calls
* to parse(filePath) will work.
*
* @param val
*/
public void setValid(boolean val) {
if (val == false) {
this.filePath = ""; //$NON-NLS-1$
}
this.valid = val;
}
public boolean isValid() {
return this.valid;
}
/**
* Returns the duplicate status. Standard Bugzilla installations will have a duplicate status of RESOLVED, VERIFIED
* or CLOSED. <br>
* By default, the duplicate status will be RESOLVED.
*
* @return The status to send if a bug is set to Duplicate
*/
public String getDuplicateStatus() {
if (duplicateStatus == null || duplicateStatus.length() == 0) {
duplicateStatus = DEFAULT_DUPLICATE_STATUS;
}
return duplicateStatus;
}
/**
* Returns the start status. Standard Bugzilla installations have 2 available start statuses, UNCONFIRMED and NEW.
* Each user has a permissions setting that determines whether their new bugs start as UNCONFIRMED or NEW. This
* permissions setting currently cannot be accessed, so this function does not try to guess and returns either the
* default status (NEW) or whichever status was set by a transition file. <br>
* Fix for bug #318128, set startStatus to NEW if startStatus is somehow null at this point.
*
* @return The valid start status. Default value is NEW.
*/
public String getStartStatus() {
if (startStatus == null || startStatus.length() == 0) {
startStatus = DEFAULT_START_STATUS;
}
return startStatus;
}
private void addTransition(String start, String endStatus) {
if (!customNames
&& (start.equals("REOPENED") && (endStatus.equals("UNCONFIRMED") || endStatus.equals("REOPENED")))) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
//REOPENED should not retransition to REOPENED
return;
}
List<AbstractBugzillaOperation> list = operationMapByCurrentStatus.get(start);
if (list == null) {
list = new ArrayList<AbstractBugzillaOperation>();
}
list.addAll(operationMapByEndStatus.get(endStatus));
operationMapByCurrentStatus.put(start, list);
}
private void parse(String start, Object[] transitions) {
addNewStatus(start);
//Find valid transitions
for (Object o : transitions) {
if ((o instanceof HashMap<?, ?>)) {
//Used for XMLRPC
HashMap<?, ?> tran = (HashMap<?, ?>) o;
String endStatus = (String) tran.get("name"); //$NON-NLS-1$
addNewStatus(endStatus);
if (!endStatus.equals(start)) {
addTransition(start, endStatus);
}
} else if (o instanceof String) {
//Used for files
String endStatus = (String) o;
addNewStatus(endStatus);
if (!endStatus.equals(start)) {
addTransition(start, endStatus);
}
}
}
}
public void parse(IProgressMonitor monitor, BugzillaXmlRpcClient xmlClient) throws CoreException {
this.filePath = ""; //$NON-NLS-1$
operationMapByCurrentStatus.clear();
operationMapByEndStatus.clear();
closedStatuses.clear();
defaultNames();
setValid(false);
//Assume custom names, we have no way to check
customNames = true;
try {
if (xmlClient.getUserID() == -1) {
xmlClient.login(monitor);
}
String[] fields = new String[1];
fields[0] = "bug_status"; //$NON-NLS-1$
for (Object raw : xmlClient.getFieldsWithNames(monitor, fields)) {
if (raw instanceof HashMap<?, ?>) {
Object[] values = (Object[]) ((HashMap<?, ?>) raw).get("values"); //$NON-NLS-1$
if (values == null) {
continue;
}
for (Object status : values) {
if (status instanceof HashMap<?, ?>) {
//Get name
HashMap<?, ?> map = (HashMap<?, ?>) status;
String start = (String) map.get("name"); //$NON-NLS-1$
//Get is_open
Object is_open = map.get("is_open"); //$NON-NLS-1$
if (is_open.toString().equals("false")) { //$NON-NLS-1$
closedStatuses.add(start);
}
parse(start, (Object[]) map.get("can_change_to")); //$NON-NLS-1$
}
}
} else {
throw new XmlRpcException(Messages.CustomTransitionManager_UnexpectedResponse);
}
}
//Should check if there are conditions we can use to terminate early
if (operationMapByCurrentStatus.size() == 0) {
throw new XmlRpcException(Messages.CustomTransitionManager_UnexpectedResponse);
}
setValid(true);
} catch (XmlRpcException e) {
setValid(false);
String message = e.linkedException == null ? e.getMessage() : e.getMessage() + ">" //$NON-NLS-1$
+ e.linkedException.getMessage();
throw new CoreException(new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN, 1,
"Error parsing xmlrpc response.\n\n" + message, e)); //$NON-NLS-1$
}
}
/**
* Creates a new status with a single operation with the same name as the status itself. Does nothing if a status of
* that name already exists.
*
* @param status
*/
private void addNewStatus(String status) {
List<AbstractBugzillaOperation> list = operationMapByEndStatus.get(status);
if (list == null) {
list = new ArrayList<AbstractBugzillaOperation>();
} else {
return;
}
if (!closedStatuses.contains(status)) {
list.add(new BugzillaOperation(AbstractBugzillaOperation.DEFAULT_LABEL_PREFIX + status));
} else {
list.add(new BugzillaOperation(AbstractBugzillaOperation.DEFAULT_LABEL_PREFIX + status, "resolution", //$NON-NLS-1$
TaskAttribute.TYPE_SINGLE_SELECT, status));
}
operationMapByEndStatus.put(status, list);
}
public ArrayList<String> getClosedStatuses() {
return closedStatuses;
}
}