| /******************************************************************************* |
| * Copyright (c) 2006, 2010 Steffen Pingel 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: |
| * Steffen Pingel - initial API and implementation |
| *******************************************************************************/ |
| |
| package org.eclipse.mylyn.internal.trac.core.client; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.StreamTokenizer; |
| import java.io.StringReader; |
| import java.net.HttpURLConnection; |
| import java.text.ParseException; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.StringTokenizer; |
| |
| import javax.swing.text.html.HTML.Tag; |
| |
| import org.apache.commons.httpclient.Credentials; |
| import org.apache.commons.httpclient.HostConfiguration; |
| import org.apache.commons.httpclient.HttpClient; |
| import org.apache.commons.httpclient.HttpStatus; |
| import org.apache.commons.httpclient.auth.AuthScope; |
| import org.apache.commons.httpclient.methods.GetMethod; |
| import org.apache.commons.lang.StringEscapeUtils; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.mylyn.commons.core.StatusHandler; |
| import org.eclipse.mylyn.commons.net.AbstractWebLocation; |
| import org.eclipse.mylyn.commons.net.AuthenticationCredentials; |
| import org.eclipse.mylyn.commons.net.AuthenticationType; |
| import org.eclipse.mylyn.commons.net.HtmlStreamTokenizer; |
| import org.eclipse.mylyn.commons.net.HtmlStreamTokenizer.Token; |
| import org.eclipse.mylyn.commons.net.HtmlTag; |
| import org.eclipse.mylyn.commons.net.Policy; |
| import org.eclipse.mylyn.commons.net.SslCertificateException; |
| import org.eclipse.mylyn.commons.net.UnsupportedRequestException; |
| import org.eclipse.mylyn.commons.net.WebUtil; |
| import org.eclipse.mylyn.internal.trac.core.TracCorePlugin; |
| import org.eclipse.mylyn.internal.trac.core.model.TracComment; |
| import org.eclipse.mylyn.internal.trac.core.model.TracComponent; |
| import org.eclipse.mylyn.internal.trac.core.model.TracMilestone; |
| import org.eclipse.mylyn.internal.trac.core.model.TracPriority; |
| import org.eclipse.mylyn.internal.trac.core.model.TracRepositoryInfo; |
| import org.eclipse.mylyn.internal.trac.core.model.TracSearch; |
| import org.eclipse.mylyn.internal.trac.core.model.TracSearchFilter; |
| import org.eclipse.mylyn.internal.trac.core.model.TracSearchFilter.CompareOperator; |
| import org.eclipse.mylyn.internal.trac.core.model.TracSeverity; |
| import org.eclipse.mylyn.internal.trac.core.model.TracTicket; |
| import org.eclipse.mylyn.internal.trac.core.model.TracTicket.Key; |
| import org.eclipse.mylyn.internal.trac.core.model.TracTicketResolution; |
| import org.eclipse.mylyn.internal.trac.core.model.TracTicketStatus; |
| import org.eclipse.mylyn.internal.trac.core.model.TracTicketType; |
| import org.eclipse.mylyn.internal.trac.core.model.TracVersion; |
| import org.eclipse.mylyn.internal.trac.core.util.TracHttpClientTransportFactory.TracHttpException; |
| import org.eclipse.mylyn.internal.trac.core.util.TracUtil; |
| |
| import com.google.gson.Gson; |
| import com.google.gson.GsonBuilder; |
| import com.google.gson.JsonSyntaxException; |
| import com.google.gson.reflect.TypeToken; |
| |
| /** |
| * Represents a Trac repository that is accessed through the Trac's query script and web interface. |
| * |
| * @author Steffen Pingel |
| */ |
| public class TracWebClient extends AbstractTracClient { |
| |
| private interface AttributeFactory { |
| |
| void initialize(); |
| |
| void addAttribute(String value); |
| |
| } |
| |
| private static class TracConfiguration { |
| |
| private final Map<String, AttributeFactory> factoryByField = new HashMap<String, AttributeFactory>(); |
| |
| public TracConfiguration(final TracClientData data) { |
| AttributeFactory attributeFactory = new AttributeFactory() { |
| public void addAttribute(String value) { |
| data.components.add(new TracComponent(value)); |
| } |
| |
| public void initialize() { |
| data.components = new ArrayList<TracComponent>(); |
| } |
| }; |
| factoryByField.put("component", attributeFactory); //$NON-NLS-1$ |
| |
| attributeFactory = new AttributeFactory() { |
| public void addAttribute(String value) { |
| data.milestones.add(new TracMilestone(value)); |
| } |
| |
| public void initialize() { |
| data.milestones = new ArrayList<TracMilestone>(); |
| } |
| }; |
| factoryByField.put("milestone", attributeFactory); //$NON-NLS-1$ |
| |
| attributeFactory = new AttributeFactory() { |
| public void addAttribute(String value) { |
| data.priorities.add(new TracPriority(value, data.priorities.size() + 1)); |
| } |
| |
| public void initialize() { |
| data.priorities = new ArrayList<TracPriority>(); |
| } |
| }; |
| factoryByField.put("priority", attributeFactory); //$NON-NLS-1$ |
| |
| attributeFactory = new AttributeFactory() { |
| public void addAttribute(String value) { |
| data.ticketResolutions.add(new TracTicketResolution(value, data.ticketResolutions.size() + 1)); |
| } |
| |
| public void initialize() { |
| data.ticketResolutions = new ArrayList<TracTicketResolution>(); |
| } |
| }; |
| factoryByField.put("resolution", attributeFactory); //$NON-NLS-1$ |
| |
| attributeFactory = new AttributeFactory() { |
| public void addAttribute(String value) { |
| data.severities.add(new TracSeverity(value, data.severities.size() + 1)); |
| } |
| |
| public void initialize() { |
| data.severities = new ArrayList<TracSeverity>(); |
| } |
| }; |
| factoryByField.put("severity", attributeFactory); //$NON-NLS-1$ |
| |
| attributeFactory = new AttributeFactory() { |
| public void addAttribute(String value) { |
| data.ticketStatus.add(new TracTicketStatus(value, data.ticketStatus.size() + 1)); |
| } |
| |
| public void initialize() { |
| data.ticketStatus = new ArrayList<TracTicketStatus>(); |
| } |
| }; |
| factoryByField.put("status", attributeFactory); //$NON-NLS-1$ |
| |
| attributeFactory = new AttributeFactory() { |
| public void addAttribute(String value) { |
| data.ticketTypes.add(new TracTicketType(value, data.ticketTypes.size() + 1)); |
| } |
| |
| public void initialize() { |
| data.ticketTypes = new ArrayList<TracTicketType>(); |
| } |
| }; |
| factoryByField.put("type", attributeFactory); //$NON-NLS-1$ |
| |
| attributeFactory = new AttributeFactory() { |
| public void addAttribute(String value) { |
| data.versions.add(new TracVersion(value)); |
| } |
| |
| public void initialize() { |
| data.versions = new ArrayList<TracVersion>(); |
| } |
| }; |
| factoryByField.put("version", attributeFactory); //$NON-NLS-1$ |
| } |
| |
| public AttributeFactory getFactoryByField(String field) { |
| return factoryByField.get(field); |
| } |
| |
| } |
| |
| private static class TracConfigurationField { |
| |
| @SuppressWarnings("unused") |
| String label; |
| |
| @SuppressWarnings("unused") |
| String type; |
| |
| List<String> options; |
| |
| List<TracConfigurationOptGroup> optgroups; |
| |
| } |
| |
| private static class TracConfigurationOptGroup { |
| |
| @SuppressWarnings("unused") |
| String label; |
| |
| List<String> options; |
| |
| } |
| |
| private class Request { |
| |
| private final String url; |
| |
| private HostConfiguration hostConfiguration; |
| |
| public Request(String url) { |
| this.url = url; |
| } |
| |
| public GetMethod execute(IProgressMonitor monitor) throws TracLoginException, IOException, TracHttpException { |
| hostConfiguration = WebUtil.createHostConfiguration(httpClient, location, monitor); |
| |
| for (int attempt = 0; attempt < 2; attempt++) { |
| // force authentication |
| if (!authenticated) { |
| AuthenticationCredentials credentials = location.getCredentials(AuthenticationType.REPOSITORY); |
| if (credentialsValid(credentials)) { |
| try { |
| authenticate(monitor); |
| } catch (TracLoginException e) { |
| // re-try once, see bug 302792 |
| authenticate(monitor); |
| } |
| } |
| } |
| |
| GetMethod method = new GetMethod(WebUtil.getRequestPath(url)); |
| int code; |
| try { |
| code = WebUtil.execute(httpClient, hostConfiguration, method, monitor); |
| } catch (IOException e) { |
| WebUtil.releaseConnection(method, monitor); |
| throw e; |
| } catch (RuntimeException e) { |
| WebUtil.releaseConnection(method, monitor); |
| throw e; |
| } |
| |
| if (code == HttpURLConnection.HTTP_OK) { |
| return method; |
| } else { |
| WebUtil.releaseConnection(method, monitor); |
| if (code == HttpURLConnection.HTTP_UNAUTHORIZED || code == HttpURLConnection.HTTP_FORBIDDEN) { |
| // login or re-authenticate due to an expired session |
| authenticated = false; |
| authenticate(monitor); |
| } else { |
| throw new TracHttpException(code); |
| } |
| } |
| } |
| |
| throw new TracLoginException(); |
| } |
| |
| private void authenticate(IProgressMonitor monitor) throws TracLoginException, IOException { |
| while (true) { |
| AuthenticationCredentials credentials = location.getCredentials(AuthenticationType.REPOSITORY); |
| if (!credentialsValid(credentials)) { |
| throw new TracLoginException(); |
| } |
| |
| // try standard basic/digest/ntlm authentication first |
| AuthScope authScope = new AuthScope(WebUtil.getHost(repositoryUrl), WebUtil.getPort(repositoryUrl), |
| null, AuthScope.ANY_SCHEME); |
| Credentials httpCredentials = WebUtil.getHttpClientCredentials(credentials, |
| WebUtil.getHost(repositoryUrl)); |
| httpClient.getState().setCredentials(authScope, httpCredentials); |
| // if (CoreUtil.TEST_MODE) { |
| // System.err.println(" Setting credentials: " + httpCredentials); //$NON-NLS-1$ |
| // } |
| |
| GetMethod method = new GetMethod(WebUtil.getRequestPath(repositoryUrl + LOGIN_URL)); |
| method.setFollowRedirects(false); |
| int code; |
| try { |
| code = WebUtil.execute(httpClient, hostConfiguration, method, monitor); |
| if (needsReauthentication(code, monitor)) { |
| continue; |
| } |
| } catch (SslCertificateException e) { |
| if (needsReauthentication(SC_CERT_AUTH_FAILED, monitor)) { |
| continue; |
| } |
| throw e; |
| } finally { |
| WebUtil.releaseConnection(method, monitor); |
| } |
| |
| // the expected return code is a redirect, anything else is suspicious |
| if (code == HttpURLConnection.HTTP_OK) { |
| // try form-based authentication via AccountManagerPlugin as a |
| // fall-back |
| authenticateAccountManager(httpClient, hostConfiguration, credentials, monitor); |
| } |
| |
| validateAuthenticationState(httpClient); |
| |
| // success since no exception was thrown |
| authenticated = true; |
| break; |
| } |
| } |
| |
| private boolean needsReauthentication(int code, IProgressMonitor monitor) throws IOException, |
| TracLoginException { |
| final AuthenticationType authenticationType; |
| if (code == HttpStatus.SC_UNAUTHORIZED || code == HttpStatus.SC_FORBIDDEN) { |
| authenticationType = AuthenticationType.REPOSITORY; |
| } else if (code == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) { |
| authenticationType = AuthenticationType.PROXY; |
| } else if (code == SC_CERT_AUTH_FAILED) { |
| authenticationType = AuthenticationType.CERTIFICATE; |
| } else { |
| return false; |
| } |
| |
| try { |
| location.requestCredentials(authenticationType, null, monitor); |
| } catch (UnsupportedRequestException e) { |
| throw new TracLoginException(); |
| } |
| |
| hostConfiguration = WebUtil.createHostConfiguration(httpClient, location, monitor); |
| return true; |
| } |
| |
| } |
| |
| private final HttpClient httpClient; |
| |
| private boolean authenticated; |
| |
| public TracWebClient(AbstractWebLocation location, Version version) { |
| super(location, version); |
| this.httpClient = createHttpClient(); |
| } |
| |
| private synchronized GetMethod connect(String requestUrl, IProgressMonitor monitor) throws TracException { |
| monitor = Policy.monitorFor(monitor); |
| try { |
| Request request = new Request(requestUrl); |
| return request.execute(monitor); |
| } catch (TracException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new TracException(e); |
| } |
| } |
| |
| /** |
| * Fetches the web site of a single ticket and returns the Trac ticket. |
| * |
| * @param id |
| * Trac id of ticket |
| */ |
| public TracTicket getTicket(int id, IProgressMonitor monitor) throws TracException { |
| GetMethod method = connect(repositoryUrl + ITracClient.TICKET_URL + id, monitor); |
| try { |
| TracTicket ticket = new TracTicket(id); |
| |
| InputStream in = WebUtil.getResponseBodyAsStream(method, monitor); |
| try { |
| BufferedReader reader = new BufferedReader(new InputStreamReader(in, method.getResponseCharSet())); |
| HtmlStreamTokenizer tokenizer = new HtmlStreamTokenizer(reader, null); |
| for (Token token = tokenizer.nextToken(); token.getType() != Token.EOF; token = tokenizer.nextToken()) { |
| if (token.getType() == Token.TAG) { |
| HtmlTag tag = (HtmlTag) token.getValue(); |
| if (tag.getTagType() == Tag.TD) { |
| String headers = tag.getAttribute("headers"); //$NON-NLS-1$ |
| if ("h_component".equals(headers)) { //$NON-NLS-1$ |
| ticket.putBuiltinValue(Key.COMPONENT, getText(tokenizer)); |
| } else if ("h_milestone".equals(headers)) { //$NON-NLS-1$ |
| ticket.putBuiltinValue(Key.MILESTONE, getText(tokenizer)); |
| } else if ("h_priority".equals(headers)) { //$NON-NLS-1$ |
| ticket.putBuiltinValue(Key.PRIORITY, getText(tokenizer)); |
| } else if ("h_severity".equals(headers)) { //$NON-NLS-1$ |
| ticket.putBuiltinValue(Key.SEVERITY, getText(tokenizer)); |
| } else if ("h_version".equals(headers)) { //$NON-NLS-1$ |
| ticket.putBuiltinValue(Key.VERSION, getText(tokenizer)); |
| } else if ("h_keywords".equals(headers)) { //$NON-NLS-1$ |
| ticket.putBuiltinValue(Key.KEYWORDS, getText(tokenizer)); |
| } else if ("h_cc".equals(headers)) { //$NON-NLS-1$ |
| ticket.putBuiltinValue(Key.CC, getText(tokenizer)); |
| } else if ("h_owner".equals(headers)) { //$NON-NLS-1$ |
| ticket.putBuiltinValue(Key.OWNER, getText(tokenizer)); |
| } else if ("h_reporter".equals(headers)) { //$NON-NLS-1$ |
| ticket.putBuiltinValue(Key.REPORTER, getText(tokenizer)); |
| } |
| // TODO handle custom fields |
| } else if ((tag.getTagType() == Tag.H2 && ("summary".equals(tag.getAttribute("class")) || "summary searchable".equals(tag.getAttribute("class")))) |
| || tag.getTagType() == Tag.SPAN && ("summary".equals(tag.getAttribute("class")))) { //$NON-NLS-1$ //$NON-NLS-2$ |
| ticket.putBuiltinValue(Key.SUMMARY, getText(tokenizer)); |
| } else if (tag.getTagType() == Tag.H3 && "status".equals(tag.getAttribute("class"))) { //$NON-NLS-1$ //$NON-NLS-2$ |
| String text = getStrongText(tokenizer); |
| if (text.length() > 0) { |
| // Trac 0.9 format: status / status (resolution) |
| int i = text.indexOf(" ("); //$NON-NLS-1$ |
| if (i != -1) { |
| ticket.putBuiltinValue(Key.STATUS, text.substring(0, i)); |
| ticket.putBuiltinValue(Key.RESOLUTION, text.substring(i + 2, text.length() - 1)); |
| } else { |
| ticket.putBuiltinValue(Key.STATUS, text); |
| } |
| } |
| } else if (tag.getTagType() == Tag.SPAN) { |
| String clazz = tag.getAttribute("class"); //$NON-NLS-1$ |
| if ("status".equals(clazz)) { //$NON-NLS-1$ |
| // Trac 0.10 format: (status type) / (status type: resolution) |
| String text = getText(tokenizer); |
| if (text.startsWith("(") && text.endsWith(")")) { //$NON-NLS-1$ //$NON-NLS-2$ |
| StringTokenizer t = new StringTokenizer(text.substring(1, text.length() - 1), " :"); //$NON-NLS-1$ |
| if (t.hasMoreTokens()) { |
| ticket.putBuiltinValue(Key.STATUS, t.nextToken()); |
| } |
| if (t.hasMoreTokens()) { |
| ticket.putBuiltinValue(Key.TYPE, t.nextToken()); |
| } |
| if (t.hasMoreTokens()) { |
| ticket.putBuiltinValue(Key.RESOLUTION, t.nextToken()); |
| } |
| } |
| } else if ("trac-status".equals(clazz)) { //$NON-NLS-1$ |
| ticket.putBuiltinValue(Key.STATUS, getText(tokenizer)); |
| } else if ("trac-type".equals(clazz)) { //$NON-NLS-1$ |
| ticket.putBuiltinValue(Key.TYPE, getText(tokenizer)); |
| } else if ("trac-resolution".equals(clazz)) { //$NON-NLS-1$ |
| String text = getText(tokenizer); |
| if (text.startsWith("(") && text.endsWith(")")) { //$NON-NLS-1$ //$NON-NLS-2$ |
| ticket.putBuiltinValue(Key.RESOLUTION, text.substring(1, text.length() - 1).trim()); |
| } else { |
| ticket.putBuiltinValue(Key.RESOLUTION, text); |
| } |
| } |
| |
| } |
| // TODO parse description |
| } |
| } |
| } finally { |
| in.close(); |
| } |
| |
| if (ticket.isValid() && ticket.getValue(Key.SUMMARY) != null) { |
| return ticket; |
| } |
| |
| throw new InvalidTicketException(); |
| } catch (IOException e) { |
| throw new TracException(e); |
| } catch (ParseException e) { |
| throw new TracException(e); |
| } finally { |
| WebUtil.releaseConnection(method, monitor); |
| } |
| } |
| |
| public void searchForTicketIds(TracSearch query, List<Integer> result, IProgressMonitor monitor) |
| throws TracException { |
| List<TracTicket> ticketResult = new ArrayList<TracTicket>(); |
| search(query, ticketResult, monitor); |
| for (TracTicket tracTicket : ticketResult) { |
| result.add(tracTicket.getId()); |
| } |
| } |
| |
| public void search(TracSearch query, List<TracTicket> tickets, IProgressMonitor monitor) throws TracException { |
| GetMethod method = connect(repositoryUrl + ITracClient.QUERY_URL + query.toUrl(), monitor); |
| try { |
| InputStream in = WebUtil.getResponseBodyAsStream(method, monitor); |
| try { |
| BufferedReader reader = new BufferedReader(new InputStreamReader(in, method.getResponseCharSet())); |
| String line; |
| |
| Map<String, String> constantValues = getExactMatchValues(query); |
| |
| // first line contains names of returned ticket fields |
| line = reader.readLine(); |
| if (line == null) { |
| throw new InvalidTicketException(); |
| } |
| // the utf-8 output in Trac 1.0 starts with a byte-order mark which |
| // is passed to the tokenizer since it would otherwise end up in the first token |
| StringTokenizer t = new StringTokenizer(line, "\ufeff\t"); //$NON-NLS-1$ |
| Key[] fields = new Key[t.countTokens()]; |
| for (int i = 0; i < fields.length; i++) { |
| fields[i] = Key.fromKey(t.nextToken()); |
| } |
| |
| // create a ticket for each following line of output |
| while ((line = reader.readLine()) != null) { |
| t = new StringTokenizer(line, "\t"); //$NON-NLS-1$ |
| TracTicket ticket = new TracTicket(); |
| for (int i = 0; i < fields.length && t.hasMoreTokens(); i++) { |
| if (fields[i] != null) { |
| try { |
| if (fields[i] == Key.ID) { |
| ticket.setId(Integer.parseInt(t.nextToken())); |
| } else if (fields[i] == Key.TIME) { |
| ticket.setCreated(TracUtil.parseDate(Integer.parseInt(t.nextToken()))); |
| } else if (fields[i] == Key.CHANGE_TIME) { |
| ticket.setLastChanged(TracUtil.parseDate(Integer.parseInt(t.nextToken()))); |
| } else { |
| ticket.putBuiltinValue(fields[i], parseTicketValue(t.nextToken())); |
| } |
| } catch (NumberFormatException e) { |
| StatusHandler.log(new Status(IStatus.WARNING, TracCorePlugin.ID_PLUGIN, |
| "Error parsing response: '" + line + "'", e)); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| } |
| } |
| |
| if (ticket.isValid()) { |
| for (String key : constantValues.keySet()) { |
| ticket.putValue(key, parseTicketValue(constantValues.get(key))); |
| } |
| |
| tickets.add(ticket); |
| } |
| } |
| } finally { |
| in.close(); |
| } |
| } catch (IOException e) { |
| throw new TracException(e); |
| } finally { |
| WebUtil.releaseConnection(method, monitor); |
| } |
| } |
| |
| /** |
| * Trac has sepcial encoding rules for the returned output: None is represented by "--". |
| */ |
| private String parseTicketValue(String value) { |
| if ("--".equals(value)) { //$NON-NLS-1$ |
| return ""; //$NON-NLS-1$ |
| } |
| return value; |
| } |
| |
| /** |
| * Extracts constant values from <code>query</code>. The Trac query script does not return fields that matched |
| * exactly againt a single value. |
| */ |
| private Map<String, String> getExactMatchValues(TracSearch query) { |
| Map<String, String> values = new HashMap<String, String>(); |
| List<TracSearchFilter> filters = query.getFilters(); |
| for (TracSearchFilter filter : filters) { |
| if (filter.getOperator() == CompareOperator.IS && filter.getValues().size() == 1) { |
| values.put(filter.getFieldName(), filter.getValues().get(0)); |
| } |
| } |
| return values; |
| } |
| |
| public TracRepositoryInfo validate(IProgressMonitor monitor) throws TracException { |
| GetMethod method = connect(repositoryUrl + "/", monitor); //$NON-NLS-1$ |
| try { |
| return new TracRepositoryInfo(); |
| } finally { |
| WebUtil.releaseConnection(method, monitor); |
| } |
| } |
| |
| @Override |
| public void updateAttributes(IProgressMonitor monitor) throws TracException { |
| monitor.beginTask(Messages.TracWebClient_Updating_attributes, IProgressMonitor.UNKNOWN); |
| |
| GetMethod method = connect(repositoryUrl + ITracClient.CUSTOM_QUERY_URL, monitor); |
| try { |
| InputStream in = WebUtil.getResponseBodyAsStream(method, monitor); |
| try { |
| BufferedReader reader = new BufferedReader(new InputStreamReader(in, method.getResponseCharSet())); |
| HtmlStreamTokenizer tokenizer = new HtmlStreamTokenizer(reader, null); |
| for (Token token = tokenizer.nextToken(); token.getType() != Token.EOF; token = tokenizer.nextToken()) { |
| if (monitor.isCanceled()) { |
| throw new OperationCanceledException(); |
| } |
| |
| if (token.getType() == Token.TAG) { |
| HtmlTag tag = (HtmlTag) token.getValue(); |
| if (tag.getTagType() == Tag.SCRIPT) { |
| String text = getText(tokenizer).trim(); |
| int i = text.indexOf("var properties="); |
| if (i != -1) { |
| if (!parseAttributesJSon(text.substring(i))) { |
| // fall back |
| parseAttributesTokenizer(text.substring(i)); |
| } |
| } |
| } |
| } |
| } |
| |
| addResolutionAndStatus(); |
| } finally { |
| in.close(); |
| } |
| } catch (IOException e) { |
| throw new TracException(e); |
| } catch (ParseException e) { |
| throw new TracException(e); |
| } finally { |
| WebUtil.releaseConnection(method, monitor); |
| } |
| } |
| |
| enum AttributeState { |
| INIT, IN_LIST, IN_ATTRIBUTE_KEY, IN_ATTRIBUTE_VALUE, IN_ATTRIBUTE_VALUE_LIST |
| }; |
| |
| private boolean parseAttributesJSon(String text) { |
| // remove surrounding JavaScript |
| if (text.startsWith("var properties=")) { //$NON-NLS-1$ |
| text = text.substring("var properties=".length()); //$NON-NLS-1$ |
| } |
| int i = text.indexOf("};"); //$NON-NLS-1$ |
| if (i != -1) { |
| text = text.substring(0, i + 1); |
| } |
| |
| // parse JSon stream |
| GsonBuilder builder = new GsonBuilder(); |
| Gson gson = builder.create(); |
| TypeToken<Map<String, TracConfigurationField>> type = new TypeToken<Map<String, TracConfigurationField>>() { |
| }; |
| Map<String, TracConfigurationField> fieldByName; |
| try { |
| fieldByName = gson.fromJson(text, type.getType()); |
| if (fieldByName == null) { |
| return false; |
| } |
| } catch (JsonSyntaxException e) { |
| return false; |
| } |
| |
| // copy parsed JSon objects in to client data |
| TracConfiguration configuration = new TracConfiguration(data); |
| for (Map.Entry<String, TracConfigurationField> entry : fieldByName.entrySet()) { |
| AttributeFactory factory = configuration.getFactoryByField(entry.getKey()); |
| if (factory != null) { |
| factory.initialize(); |
| |
| TracConfigurationField field = entry.getValue(); |
| if (field.options != null && field.options.size() > 0) { |
| for (String option : field.options) { |
| factory.addAttribute(option); |
| } |
| } else if (field.optgroups != null && field.optgroups.size() > 0) { |
| // milestones in Trac 0.13 support groups for labeling related options: ignore groups but extract options |
| for (TracConfigurationOptGroup group : field.optgroups) { |
| if (group.options != null) { |
| for (String option : group.options) { |
| factory.addAttribute(option); |
| } |
| } |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Parses the JavaScript code from the query page to extract repository configuration. |
| */ |
| private void parseAttributesTokenizer(String text) throws IOException { |
| StreamTokenizer t = new StreamTokenizer(new StringReader(text)); |
| t.quoteChar('"'); |
| |
| TracConfiguration configuration = new TracConfiguration(data); |
| AttributeFactory attributeFactory = null; |
| String attributeType = null; |
| |
| AttributeState state = AttributeState.INIT; |
| int tokenType; |
| while ((tokenType = t.nextToken()) != StreamTokenizer.TT_EOF) { |
| switch (tokenType) { |
| case StreamTokenizer.TT_WORD: |
| case '"': |
| if (state == AttributeState.IN_LIST) { |
| attributeFactory = configuration.getFactoryByField(t.sval); |
| if (attributeFactory != null) { |
| attributeFactory.initialize(); |
| } |
| } else if (state == AttributeState.IN_ATTRIBUTE_KEY) { |
| attributeType = t.sval; |
| } else if (state == AttributeState.IN_ATTRIBUTE_VALUE_LIST && "options".equals(attributeType)) { //$NON-NLS-1$ |
| if (attributeFactory != null) { |
| attributeFactory.addAttribute(t.sval); |
| } |
| } |
| break; |
| case ':': |
| if (state == AttributeState.IN_ATTRIBUTE_KEY) { |
| state = AttributeState.IN_ATTRIBUTE_VALUE; |
| } |
| break; |
| case ',': |
| if (state == AttributeState.IN_ATTRIBUTE_VALUE) { |
| state = AttributeState.IN_ATTRIBUTE_KEY; |
| } |
| break; |
| case '[': |
| if (state == AttributeState.IN_ATTRIBUTE_VALUE) { |
| state = AttributeState.IN_ATTRIBUTE_VALUE_LIST; |
| } |
| break; |
| case ']': |
| if (state == AttributeState.IN_ATTRIBUTE_VALUE_LIST) { |
| state = AttributeState.IN_ATTRIBUTE_VALUE; |
| } |
| break; |
| case '{': |
| if (state == AttributeState.INIT) { |
| state = AttributeState.IN_LIST; |
| } else if (state == AttributeState.IN_LIST) { |
| state = AttributeState.IN_ATTRIBUTE_KEY; |
| } else { |
| throw new IOException("Error parsing attributes: unexpected token '{'"); //$NON-NLS-1$ |
| } |
| break; |
| case '}': |
| if (state == AttributeState.IN_ATTRIBUTE_KEY || state == AttributeState.IN_ATTRIBUTE_VALUE) { |
| state = AttributeState.IN_LIST; |
| } else if (state == AttributeState.IN_LIST) { |
| state = AttributeState.INIT; |
| } else { |
| throw new IOException("Error parsing attributes: unexpected token '}'"); //$NON-NLS-1$ |
| } |
| break; |
| } |
| } |
| } |
| |
| public void updateAttributesNewTicketPage(IProgressMonitor monitor) throws TracException { |
| monitor.beginTask(Messages.TracWebClient_Updating_attributes, IProgressMonitor.UNKNOWN); |
| |
| GetMethod method = connect(repositoryUrl + ITracClient.NEW_TICKET_URL, monitor); |
| try { |
| InputStream in = WebUtil.getResponseBodyAsStream(method, monitor); |
| try { |
| BufferedReader reader = new BufferedReader(new InputStreamReader(in, method.getResponseCharSet())); |
| HtmlStreamTokenizer tokenizer = new HtmlStreamTokenizer(reader, null); |
| for (Token token = tokenizer.nextToken(); token.getType() != Token.EOF; token = tokenizer.nextToken()) { |
| if (monitor.isCanceled()) { |
| throw new OperationCanceledException(); |
| } |
| |
| if (token.getType() == Token.TAG) { |
| HtmlTag tag = (HtmlTag) token.getValue(); |
| if (tag.getTagType() == Tag.SELECT) { |
| String name = tag.getAttribute("id"); //$NON-NLS-1$ |
| if ("component".equals(name)) { //$NON-NLS-1$ |
| List<String> values = getOptionValues(tokenizer); |
| data.components = new ArrayList<TracComponent>(values.size()); |
| for (String value : values) { |
| data.components.add(new TracComponent(value)); |
| } |
| } else if ("milestone".equals(name)) { //$NON-NLS-1$ |
| List<String> values = getOptionValues(tokenizer); |
| data.milestones = new ArrayList<TracMilestone>(values.size()); |
| for (String value : values) { |
| data.milestones.add(new TracMilestone(value)); |
| } |
| } else if ("priority".equals(name)) { //$NON-NLS-1$ |
| List<String> values = getOptionValues(tokenizer); |
| data.priorities = new ArrayList<TracPriority>(values.size()); |
| for (int i = 0; i < values.size(); i++) { |
| data.priorities.add(new TracPriority(values.get(i), i + 1)); |
| } |
| } else if ("severity".equals(name)) { //$NON-NLS-1$ |
| List<String> values = getOptionValues(tokenizer); |
| data.severities = new ArrayList<TracSeverity>(values.size()); |
| for (int i = 0; i < values.size(); i++) { |
| data.severities.add(new TracSeverity(values.get(i), i + 1)); |
| } |
| } else if ("type".equals(name)) { //$NON-NLS-1$ |
| List<String> values = getOptionValues(tokenizer); |
| data.ticketTypes = new ArrayList<TracTicketType>(values.size()); |
| for (int i = 0; i < values.size(); i++) { |
| data.ticketTypes.add(new TracTicketType(values.get(i), i + 1)); |
| } |
| } else if ("version".equals(name)) { //$NON-NLS-1$ |
| List<String> values = getOptionValues(tokenizer); |
| data.versions = new ArrayList<TracVersion>(values.size()); |
| for (String value : values) { |
| data.versions.add(new TracVersion(value)); |
| } |
| } |
| } |
| } |
| } |
| |
| addResolutionAndStatus(); |
| } finally { |
| in.close(); |
| } |
| } catch (IOException e) { |
| throw new TracException(e); |
| } catch (ParseException e) { |
| throw new TracException(e); |
| } finally { |
| WebUtil.releaseConnection(method, monitor); |
| } |
| } |
| |
| private void addResolutionAndStatus() { |
| if (data.ticketResolutions == null || data.ticketResolutions.isEmpty()) { |
| data.ticketResolutions = new ArrayList<TracTicketResolution>(5); |
| data.ticketResolutions.add(new TracTicketResolution("fixed", 1)); //$NON-NLS-1$ |
| data.ticketResolutions.add(new TracTicketResolution("invalid", 2)); //$NON-NLS-1$ |
| data.ticketResolutions.add(new TracTicketResolution("wontfix", 3)); //$NON-NLS-1$ |
| data.ticketResolutions.add(new TracTicketResolution("duplicate", 4)); //$NON-NLS-1$ |
| data.ticketResolutions.add(new TracTicketResolution("worksforme", 5)); //$NON-NLS-1$ |
| } |
| |
| if (data.ticketStatus == null || data.ticketStatus.isEmpty()) { |
| data.ticketStatus = new ArrayList<TracTicketStatus>(4); |
| data.ticketStatus.add(new TracTicketStatus("new", 1)); //$NON-NLS-1$ |
| data.ticketStatus.add(new TracTicketStatus("assigned", 2)); //$NON-NLS-1$ |
| data.ticketStatus.add(new TracTicketStatus("reopened", 3)); //$NON-NLS-1$ |
| data.ticketStatus.add(new TracTicketStatus("closed", 4)); //$NON-NLS-1$ |
| } |
| } |
| |
| private List<String> getOptionValues(HtmlStreamTokenizer tokenizer) throws IOException, ParseException { |
| List<String> values = new ArrayList<String>(); |
| for (Token token = tokenizer.nextToken(); token.getType() != Token.EOF; token = tokenizer.nextToken()) { |
| if (token.getType() == Token.TAG) { |
| HtmlTag tag = (HtmlTag) token.getValue(); |
| if (tag.getTagType() == Tag.OPTION && !tag.isEndTag()) { |
| String value = getText(tokenizer).trim(); |
| if (value.length() > 0) { |
| values.add(value); |
| } |
| } else { |
| return values; |
| } |
| } |
| } |
| return values; |
| } |
| |
| private String getText(HtmlStreamTokenizer tokenizer) throws IOException, ParseException { |
| StringBuilder sb = new StringBuilder(); |
| for (Token token = tokenizer.nextToken(); token.getType() != Token.EOF; token = tokenizer.nextToken()) { |
| if (token.getType() == Token.TEXT) { |
| sb.append(token.toString().trim()); |
| sb.append(" "); //$NON-NLS-1$ |
| } else if (token.getType() == Token.COMMENT) { |
| // ignore |
| } else if (token.getType() == Token.TAG && ((HtmlTag) token.getValue()).getTagType() == Tag.A) { |
| // ignore, Trac 0.11 wraps milestone values in links |
| } else { |
| break; |
| } |
| } |
| return StringEscapeUtils.unescapeHtml(sb.toString().trim()); |
| } |
| |
| /** |
| * Looks for a <code>strong</code> tag and returns the text enclosed by the tag. |
| */ |
| private String getStrongText(HtmlStreamTokenizer tokenizer) throws IOException, ParseException { |
| for (Token token = tokenizer.nextToken(); token.getType() != Token.EOF; token = tokenizer.nextToken()) { |
| if (token.getType() == Token.TAG && ((HtmlTag) token.getValue()).getTagType() == Tag.STRONG) { |
| return getText(tokenizer); |
| } else if (token.getType() == Token.COMMENT) { |
| // ignore |
| } else if (token.getType() == Token.TEXT) { |
| // ignore |
| } else { |
| break; |
| } |
| } |
| return ""; //$NON-NLS-1$ |
| } |
| |
| public InputStream getAttachmentData(int id, String filename, IProgressMonitor monitor) throws TracException { |
| GetMethod method = connect(repositoryUrl + ITracClient.ATTACHMENT_URL + id + "/" + filename + "?format=raw", //$NON-NLS-1$ //$NON-NLS-2$ |
| monitor); |
| try { |
| // the receiver is responsible for closing the stream which will |
| // release the connection |
| return method.getResponseBodyAsStream(); |
| } catch (IOException e) { |
| WebUtil.releaseConnection(method, monitor); |
| throw new TracException(e); |
| } |
| } |
| |
| public void putAttachmentData(int id, String name, String description, InputStream in, IProgressMonitor monitor, |
| boolean replace) throws TracException { |
| throw new TracException("Unsupported operation"); //$NON-NLS-1$ |
| } |
| |
| public void deleteAttachment(int ticketId, String filename, IProgressMonitor monitor) throws TracException { |
| throw new TracException("Unsupported operation"); //$NON-NLS-1$ |
| } |
| |
| public int createTicket(TracTicket ticket, IProgressMonitor monitor) throws TracException { |
| throw new TracException("Unsupported operation"); //$NON-NLS-1$ |
| } |
| |
| public void updateTicket(TracTicket ticket, String comment, IProgressMonitor monitor) throws TracException { |
| throw new TracException("Unsupported operation"); //$NON-NLS-1$ |
| } |
| |
| public Set<Integer> getChangedTickets(Date since, IProgressMonitor monitor) throws TracException { |
| return null; |
| } |
| |
| public Date getTicketLastChanged(Integer id, IProgressMonitor monitor) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public void deleteTicket(int ticketId, IProgressMonitor monitor) throws TracException { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public List<TracComment> getComments(int id, IProgressMonitor monitor) throws TracException { |
| throw new UnsupportedOperationException(); |
| } |
| |
| } |