/******************************************************************************* * 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; } }