org.eclipse.mylyn.internal.web.tasks.WebRepositoryConnector.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.mylyn.internal.web.tasks.WebRepositoryConnector.java

Source

/*******************************************************************************
 * Copyright (c) 2004, 2009 Eugene Kuleshov 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:
 *     Eugene Kuleshov - initial API and implementation
 *******************************************************************************/

package org.eclipse.mylyn.internal.web.tasks;

import static org.eclipse.mylyn.internal.web.tasks.Util.isPresent;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.codec.EncoderException;
import org.apache.commons.codec.net.URLCodec;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.lang.StringEscapeUtils;
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.commons.net.AbstractWebLocation;
import org.eclipse.mylyn.commons.net.AuthenticationCredentials;
import org.eclipse.mylyn.commons.net.AuthenticationType;
import org.eclipse.mylyn.commons.net.WebLocation;
import org.eclipse.mylyn.commons.net.WebUtil;
import org.eclipse.mylyn.tasks.core.AbstractRepositoryConnector;
import org.eclipse.mylyn.tasks.core.IRepositoryManager;
import org.eclipse.mylyn.tasks.core.IRepositoryQuery;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
import org.eclipse.mylyn.tasks.core.data.TaskAttributeMapper;
import org.eclipse.mylyn.tasks.core.data.TaskData;
import org.eclipse.mylyn.tasks.core.data.TaskDataCollector;
import org.eclipse.mylyn.tasks.core.data.TaskMapper;
import org.eclipse.mylyn.tasks.core.sync.ISynchronizationSession;
import org.eclipse.mylyn.tasks.ui.TaskRepositoryLocationUiFactory;
import org.eclipse.mylyn.tasks.ui.TasksUi;

import com.sun.syndication.feed.module.DCModule;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.XmlReader;

/**
 * Generic connector for web based issue tracking systems
 * 
 * @author Eugene Kuleshov
 */
public class WebRepositoryConnector extends AbstractRepositoryConnector {

    public static final String REPOSITORY_TYPE = "web"; //$NON-NLS-1$

    public static final String PROPERTY_TASK_CREATION_URL = "taskCreationUrl"; //$NON-NLS-1$

    public static final String PROPERTY_TASK_URL = "taskUrl"; //$NON-NLS-1$

    public static final String PROPERTY_QUERY_URL = "queryUrl"; //$NON-NLS-1$

    public static final String PROPERTY_QUERY_METHOD = "queryMethod"; //$NON-NLS-1$

    public static final String PROPERTY_QUERY_REGEXP = "queryPattern"; //$NON-NLS-1$

    public static final String PROPERTY_LOGIN_FORM_URL = "loginFormUrl"; //$NON-NLS-1$

    public static final String PROPERTY_LOGIN_TOKEN_REGEXP = "loginTokenPattern"; //$NON-NLS-1$

    public static final String PROPERTY_LOGIN_REQUEST_METHOD = "loginRequestMethod"; //$NON-NLS-1$

    public static final String PROPERTY_LOGIN_REQUEST_URL = "loginRequestUrl"; //$NON-NLS-1$

    public static final String PARAM_PREFIX = "param_"; //$NON-NLS-1$

    public static final String PARAM_SERVER_URL = "serverUrl"; //$NON-NLS-1$

    public static final String PARAM_USER_ID = "userId"; //$NON-NLS-1$

    public static final String PARAM_PASSWORD = "password"; //$NON-NLS-1$

    public static final String PARAM_LOGIN_TOKEN = "loginToken"; //$NON-NLS-1$

    public static final String REQUEST_POST = "POST"; //$NON-NLS-1$

    public static final String REQUEST_GET = "GET"; //$NON-NLS-1$

    private static final String COMPLETED_STATUSES = "completed|fixed|resolved|invalid|verified|deleted|closed|done"; //$NON-NLS-1$

    public static final String KEY_TASK_PREFIX = "taskPrefix"; //$NON-NLS-1$

    public static final String KEY_QUERY_TEMPLATE = "UrlTemplate"; //$NON-NLS-1$

    public static final String KEY_QUERY_PATTERN = "Regexp"; //$NON-NLS-1$

    private static final String USER_AGENT = "WebTemplatesConnector"; //$NON-NLS-1$

    private final static Date DEFAULT_DATE = new Date(0);

    @Override
    public String getConnectorKind() {
        return REPOSITORY_TYPE;
    }

    @Override
    public String getLabel() {
        return Messages.WebRepositoryConnector_Web_Template_Advanced_;
    }

    @Override
    public boolean canCreateNewTask(TaskRepository repository) {
        return repository.hasProperty(PROPERTY_TASK_CREATION_URL);
    }

    @Override
    public boolean canCreateTaskFromKey(TaskRepository repository) {
        return repository.hasProperty(PROPERTY_TASK_URL);
    }

    @Override
    public boolean canSynchronizeTask(TaskRepository taskRepository, ITask task) {
        return false;
    }

    //   @Override
    //   public AbstractTask createTaskFromExistingId(TaskRepository repository, final String id, IProgressMonitor monitor)
    //         throws CoreException {
    //      if (REPOSITORY_TYPE.equals(repository.getConnectorKind())) {
    //         String taskPrefix = evaluateParams(repository.getProperty(PROPERTY_TASK_URL), repository);
    //
    //         final WebTask task = new WebTask(id, id, taskPrefix, repository.getRepositoryUrl(), REPOSITORY_TYPE);
    //
    //         RetrieveTitleFromUrlJob job = new RetrieveTitleFromUrlJob(taskPrefix + id) {
    //            @Override
    //            protected void setTitle(String pageTitle) {
    //               task.setSummary(pageTitle);
    //               TasksUiPlugin.getTaskList().notifyTaskChanged(task, false);
    //            }
    //         };
    //         job.schedule();
    //
    //         return task;
    //      }
    //
    //      return null;
    //   }

    @Override
    public TaskData getTaskData(TaskRepository repository, String taskId, IProgressMonitor monitor)
            throws CoreException {
        String taskPrefix = evaluateParams(repository.getProperty(PROPERTY_TASK_URL), repository);
        TaskData taskData = createTaskData(repository, taskId);
        TaskMapper mapper = new TaskMapper(taskData, true);
        mapper.setCreationDate(DEFAULT_DATE);
        mapper.setTaskUrl(taskPrefix + taskId);
        mapper.setValue(KEY_TASK_PREFIX, taskPrefix);
        // bug 300310: only update the summary on forced refreshes
        mapper.setSummary(taskId);
        try {
            String pageTitle = WebUtil.getTitleFromUrl(new WebLocation(taskPrefix + taskId), monitor);
            if (pageTitle != null) {
                mapper.setSummary(pageTitle);
            }
        } catch (IOException e) {
            // log to error log?
        }
        taskData.getRoot().getMappedAttribute(TaskAttribute.SUMMARY).getMetaData().putValue("forced",
                Boolean.TRUE.toString());
        return taskData;
    }

    @SuppressWarnings("restriction")
    @Override
    public String getRepositoryUrlFromTaskUrl(String url) {
        if (url == null) {
            return null;
        }

        // lookup repository using task prefix url
        IRepositoryManager repositoryManager = TasksUi.getRepositoryManager();
        for (TaskRepository repository : repositoryManager.getRepositories(getConnectorKind())) {
            String taskUrl = evaluateParams(repository.getProperty(PROPERTY_TASK_URL), repository);
            if (taskUrl != null && !taskUrl.equals("") && url.startsWith(taskUrl)) { //$NON-NLS-1$
                return repository.getRepositoryUrl();
            }
        }

        for (IRepositoryQuery query : org.eclipse.mylyn.internal.tasks.ui.util.TasksUiInternal.getTaskList()
                .getQueries()) {
            TaskRepository repository = repositoryManager.getRepository(query.getConnectorKind(),
                    query.getRepositoryUrl());
            if (repository != null) {
                String queryUrl = evaluateParams(query.getAttribute(KEY_TASK_PREFIX), //
                        getQueryParams(query), repository);
                if (queryUrl != null && !queryUrl.equals("") && url.startsWith(queryUrl)) { //$NON-NLS-1$
                    return query.getRepositoryUrl();
                }
            }
        }
        return null;
    }

    public static Map<String, String> getQueryParams(IRepositoryQuery query) {
        Map<String, String> params = new LinkedHashMap<String, String>();
        Map<String, String> attributes = query.getAttributes();
        for (String name : attributes.keySet()) {
            if (name.startsWith(WebRepositoryConnector.PARAM_PREFIX)) {
                params.put(name, attributes.get(name));
            }
        }
        return params;
    }

    @Override
    public String getTaskIdFromTaskUrl(String url) {
        if (url == null) {
            return null;
        }

        IRepositoryManager repositoryManager = TasksUi.getRepositoryManager();
        for (TaskRepository repository : repositoryManager.getRepositories(getConnectorKind())) {
            String start = evaluateParams(repository.getProperty(PROPERTY_TASK_URL), repository);
            if (start != null && url.startsWith(start)) {
                return url.substring(start.length());
            }
        }
        return null;
    }

    @Override
    public String getTaskUrl(String repositoryUrl, String taskId) {
        IRepositoryManager repositoryManager = TasksUi.getRepositoryManager();
        TaskRepository repository = repositoryManager.getRepository(getConnectorKind(), repositoryUrl);
        if (repository != null) {
            String prefix = evaluateParams(repository.getProperty(PROPERTY_TASK_URL), repository);
            return prefix + taskId;
        }
        return null;
    }

    @Override
    public IStatus performQuery(TaskRepository repository, IRepositoryQuery query,
            TaskDataCollector resultCollector, ISynchronizationSession session, IProgressMonitor monitor) {
        Map<String, String> queryParameters = getQueryParams(query);
        String queryUrl = evaluateParams(query.getUrl(), queryParameters, repository);
        try {
            String content = fetchResource(queryUrl, queryParameters, repository);

            String taskPrefixAttribute = query.getAttribute(KEY_TASK_PREFIX);
            if (!Util.isPresent(taskPrefixAttribute)) {
                return performRssQuery(content, monitor, resultCollector, repository);
            } else {
                String taskPrefix = evaluateParams(taskPrefixAttribute, queryParameters, repository);
                String queryPattern = evaluateParams(query.getAttribute(KEY_QUERY_PATTERN), queryParameters,
                        repository);
                return performQuery(content, queryPattern, taskPrefix, monitor, resultCollector, repository);
            }
        } catch (IOException e) {
            String msg = e.getMessage() == null ? e.toString() : e.getMessage();
            return new Status(IStatus.ERROR, TasksWebPlugin.ID_PLUGIN, IStatus.ERROR, //
                    Messages.WebRepositoryConnector_Could_not_fetch_resource + queryUrl + "\n" + msg, e); //$NON-NLS-1$ 
        }
    }

    @Override
    public boolean isRepositoryConfigurationStale(TaskRepository repository, IProgressMonitor monitor)
            throws CoreException {
        return false;
    }

    @Override
    public void updateRepositoryConfiguration(TaskRepository repository, IProgressMonitor monitor)
            throws CoreException {
        // ignore
    }

    @Override
    public void updateTaskFromTaskData(TaskRepository repository, ITask task, TaskData taskData) {
        preProcessTaskData(task, taskData);

        TaskMapper mapper = new TaskMapper(taskData);
        if (Util.isPresent(mapper.getValue(KEY_TASK_PREFIX))) {
            task.setAttribute(KEY_TASK_PREFIX, mapper.getValue(KEY_TASK_PREFIX));
            task.setTaskKey(task.getTaskId());
        } else {
            // do not show task id for RSS items
            task.setTaskKey(null);
        }
        mapper.applyTo(task);
    }

    private void preProcessTaskData(ITask task, TaskData taskData) {
        if (task.getSummary() != null && task.getSummary().length() > 0) {
            // bug 300310: if task already has a summary, keep it 
            TaskAttribute summaryAttribute = taskData.getRoot().getMappedAttribute(TaskAttribute.SUMMARY);
            if (summaryAttribute != null
                    && Boolean.parseBoolean(summaryAttribute.getMetaData().getValue("forced"))) {
                summaryAttribute.setValue(task.getSummary());
            }
        }
    }

    public static IStatus performQuery(String resource, String regexp, String taskPrefix, IProgressMonitor monitor,
            TaskDataCollector resultCollector, TaskRepository repository) {
        NamedPattern p = new NamedPattern(regexp, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL
                | Pattern.UNICODE_CASE | Pattern.CANON_EQ);

        Matcher matcher = p.matcher(resource);

        if (!matcher.find()) {
            return Status.OK_STATUS;
        } else {
            boolean isCorrect = true;
            do {
                if (p.getGroups().isEmpty()) {
                    // "classic" mode, no named patterns
                    if (matcher.groupCount() < 2) {
                        isCorrect = false;
                    }
                    if (matcher.groupCount() >= 1) {
                        String id = matcher.group(1);
                        String description = matcher.groupCount() > 1 ? cleanup(matcher.group(2), repository)
                                : null;
                        description = unescapeHtml(description);

                        TaskData data = createTaskData(repository, id);
                        TaskMapper mapper = new TaskMapper(data, true);
                        mapper.setCreationDate(DEFAULT_DATE);
                        mapper.setTaskUrl(taskPrefix + id);
                        mapper.setSummary(description);
                        mapper.setValue(KEY_TASK_PREFIX, taskPrefix);
                        resultCollector.accept(data);
                    }
                } else {
                    String id = p.group("Id", matcher); //$NON-NLS-1$
                    String description = p.group("Description", matcher); //$NON-NLS-1$
                    if (id == null || description == null) {
                        isCorrect = false;
                    }
                    if (id != null) {
                        description = unescapeHtml(description);

                        String owner = unescapeHtml(cleanup(p.group("Owner", matcher), repository)); //$NON-NLS-1$
                        String type = unescapeHtml(cleanup(p.group("Type", matcher), repository)); //$NON-NLS-1$

                        TaskData data = createTaskData(repository, id);
                        TaskMapper mapper = new TaskMapper(data, true);
                        mapper.setCreationDate(DEFAULT_DATE);
                        mapper.setTaskUrl(taskPrefix + id);
                        mapper.setSummary(description);
                        mapper.setValue(KEY_TASK_PREFIX, taskPrefix);
                        mapper.setOwner(owner);
                        mapper.setTaskKind(type);

                        String status = p.group("Status", matcher); //$NON-NLS-1$
                        if (status != null) {
                            if (COMPLETED_STATUSES.contains(status.toLowerCase())) {
                                // TODO set actual completion date here
                                mapper.setCompletionDate(DEFAULT_DATE);
                            }
                        }

                        resultCollector.accept(data);
                    }
                }
            } while (matcher.find() && !monitor.isCanceled());

            if (isCorrect) {
                return Status.OK_STATUS;
            } else {
                return new Status(IStatus.ERROR, TasksWebPlugin.ID_PLUGIN, IStatus.ERROR,
                        Messages.WebRepositoryConnector_Require_two_matching_groups, null);
            }
        }
    }

    private static TaskData createTaskData(TaskRepository taskRepository, String id) {
        TaskData data = new TaskData(new TaskAttributeMapper(taskRepository),
                WebRepositoryConnector.REPOSITORY_TYPE, taskRepository.getRepositoryUrl(), id);
        data.setPartial(true);
        return data;
    }

    private static String unescapeHtml(String text) {
        if (text == null) {
            return ""; //$NON-NLS-1$
        }
        return StringEscapeUtils.unescapeHtml(text);
    }

    private static String cleanup(String text, TaskRepository repository) {
        if (text == null) {
            return null;
        }

        // Has to disable this for now. See bug 166737 and bug 166936
        // try {
        // text = URLDecoder.decode(text, repository.getCharacterEncoding());
        // } catch (UnsupportedEncodingException ex) {
        // // ignore
        // }

        text = text.replaceAll("<!--.+?-->", ""); //$NON-NLS-1$ //$NON-NLS-2$

        String[] tokens = text.split(" |\\t|\\n|\\r"); //$NON-NLS-1$
        StringBuilder sb = new StringBuilder();
        String sep = ""; //$NON-NLS-1$
        for (String token : tokens) {
            if (token.length() > 0) {
                sb.append(sep).append(token);
                sep = " "; //$NON-NLS-1$
            }
        }

        return sb.toString();
    }

    public static IStatus performRssQuery(String content, IProgressMonitor monitor,
            TaskDataCollector resultCollector, TaskRepository repository) {
        SyndFeedInput input = new SyndFeedInput();
        try {
            SyndFeed feed = input.build(new XmlReader(new ByteArrayInputStream(content.getBytes())));

            SimpleDateFormat df = new SimpleDateFormat("yy-MM-dd HH:mm"); //$NON-NLS-1$

            Iterator<?> it;
            for (it = feed.getEntries().iterator(); it.hasNext();) {
                SyndEntry entry = (SyndEntry) it.next();

                String author = entry.getAuthor();
                if (author == null) {
                    DCModule module = (DCModule) entry.getModule("http://purl.org/dc/elements/1.1/"); //$NON-NLS-1$
                    author = module.getCreator();
                }

                Date date = entry.getUpdatedDate();
                if (date == null) {
                    date = entry.getPublishedDate();
                }
                if (date == null) {
                    DCModule module = (DCModule) entry.getModule("http://purl.org/dc/elements/1.1/"); //$NON-NLS-1$
                    date = module.getDate();
                }

                String entryUri = entry.getLink();
                if (entryUri == null) {
                    entryUri = entry.getUri();
                }

                String entrTitle = entry.getTitle();

                TaskData data = createTaskData(repository, entryUri.replaceAll("-", "%2D")); //$NON-NLS-1$ //$NON-NLS-2$
                TaskMapper schema = new TaskMapper(data, true);
                schema.setSummary(((date == null ? "" : df.format(date) + " - ") + entrTitle)); //$NON-NLS-1$ //$NON-NLS-2$
                schema.setCreationDate(date);
                schema.setOwner(author);
                schema.setTaskUrl(entryUri);
                resultCollector.accept(data);
            }
            return Status.OK_STATUS;
        } catch (Exception e) {
            String msg = e.getMessage() == null ? e.toString() : e.getMessage();
            return new Status(IStatus.ERROR, TasksWebPlugin.ID_PLUGIN, IStatus.ERROR, //
                    Messages.WebRepositoryConnector_Failed_to_parse_RSS_feed + "\"" + msg + "\"", e); //$NON-NLS-1$ //$NON-NLS-2$ 
        }
    }

    public static String fetchResource(String url, Map<String, String> params, TaskRepository repository)
            throws IOException {
        HttpClient client = new HttpClient();
        WebUtil.configureHttpClient(client, USER_AGENT);
        AbstractWebLocation location = new TaskRepositoryLocationUiFactory().createWebLocation(repository);
        HostConfiguration hostConfiguration = WebUtil.createHostConfiguration(client, location, null);

        loginRequestIfNeeded(client, hostConfiguration, params, repository);

        GetMethod method = new GetMethod(url);
        // method.setFollowRedirects(false);
        return requestResource(url, client, hostConfiguration, method);
    }

    private static void loginRequestIfNeeded(HttpClient client, HostConfiguration hostConfiguration,
            Map<String, String> params, TaskRepository repository) throws HttpException, IOException {
        if (repository.getCredentials(AuthenticationType.REPOSITORY) == null
                || !isPresent(repository.getProperty(PROPERTY_LOGIN_REQUEST_URL))) {
            return;
        }

        String loginFormUrl = evaluateParams(repository.getProperty(PROPERTY_LOGIN_FORM_URL), params, repository);
        String loginToken = evaluateParams(repository.getProperty(PROPERTY_LOGIN_TOKEN_REGEXP), params, repository);
        if (isPresent(loginFormUrl) || isPresent(loginToken)) {
            GetMethod method = new GetMethod(loginFormUrl);
            // method.setFollowRedirects(false);
            String loginFormPage = requestResource(loginFormUrl, client, hostConfiguration, method);
            if (loginFormPage != null) {
                Pattern p = Pattern.compile(loginToken);
                Matcher m = p.matcher(loginFormPage);
                if (m.find()) {
                    params.put(PARAM_PREFIX + PARAM_LOGIN_TOKEN, m.group(1));
                }
            }
        }

        String loginRequestUrl = evaluateParams(repository.getProperty(PROPERTY_LOGIN_REQUEST_URL), params,
                repository);
        requestResource(loginRequestUrl, client, hostConfiguration, getLoginMethod(params, repository));
    }

    public static HttpMethod getLoginMethod(Map<String, String> params, TaskRepository repository) {
        String requestMethod = repository.getProperty(PROPERTY_LOGIN_REQUEST_METHOD);
        String requestTemplate = repository.getProperty(PROPERTY_LOGIN_REQUEST_URL);
        String requestUrl = evaluateParams(requestTemplate, params, repository);

        if (REQUEST_GET.equals(requestMethod)) {
            return new GetMethod(requestUrl);
            // method.setFollowRedirects(false);
        }

        int n = requestUrl.indexOf('?');
        if (n == -1) {
            return new PostMethod(requestUrl);
        }

        PostMethod postMethod = new PostMethod(requestUrl.substring(0, n));
        // TODO this does not take into account escaped values
        n = requestTemplate.indexOf('?');
        String[] requestParams = requestTemplate.substring(n + 1).split("&"); //$NON-NLS-1$
        for (String requestParam : requestParams) {
            String[] nv = requestParam.split("="); //$NON-NLS-1$
            if (nv.length == 1) {
                postMethod.addParameter(nv[0], ""); //$NON-NLS-1$
            } else {
                String value = evaluateParams(nv[1], getParams(repository, params), false);
                postMethod.addParameter(nv[0], value);
            }
        }
        return postMethod;
    }

    private static String requestResource(String url, HttpClient client, HostConfiguration hostConfiguration,
            HttpMethod method) throws IOException, HttpException {
        String refreshUrl = null;
        try {
            client.executeMethod(hostConfiguration, method);
            //          int statusCode = client.executeMethod(method);
            //         if (statusCode == 300 || statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
            //            Header location = method.getResponseHeader("Location");
            //            if (location != null) {
            //               refreshUrl = location.getValue();
            //               if (!refreshUrl.startsWith("/")) {
            //                  refreshUrl = "/" + refreshUrl;
            //               }
            //            }
            //         }

            refreshUrl = getRefreshUrl(url, method);
            if (refreshUrl == null) {
                return method.getResponseBodyAsString();
            }
        } finally {
            method.releaseConnection();
        }

        method = new GetMethod(refreshUrl);
        try {
            client.executeMethod(hostConfiguration, method);
            return method.getResponseBodyAsString();
        } finally {
            method.releaseConnection();
        }
    }

    private static String getRefreshUrl(String url, HttpMethod method) {
        Header refreshHeader = method.getResponseHeader("Refresh"); //$NON-NLS-1$
        if (refreshHeader == null) {
            return null;
        }
        String value = refreshHeader.getValue();
        int n = value.indexOf(";url="); //$NON-NLS-1$
        if (n == -1) {
            return null;
        }
        value = value.substring(n + 5);
        int requestPath;
        if (value.charAt(0) == '/') {
            int colonSlashSlash = url.indexOf("://"); //$NON-NLS-1$
            requestPath = url.indexOf('/', colonSlashSlash + 3);
        } else {
            requestPath = url.lastIndexOf('/');
        }

        String refreshUrl;
        if (requestPath == -1) {
            refreshUrl = url + "/" + value; //$NON-NLS-1$
        } else {
            refreshUrl = url.substring(0, requestPath + 1) + value;
        }
        return refreshUrl;
    }

    public static String evaluateParams(String value, Map<String, String> params, TaskRepository repository) {
        return evaluateParams(value, getParams(repository, params), true);
    }

    public static String evaluateParams(String value, TaskRepository repository) {
        return evaluateParams(value, getParams(repository, null), true);
    }

    private static String evaluateParams(String value, Map<String, String> params, boolean encode) {
        if (value == null || value.indexOf("${") == -1) { //$NON-NLS-1$
            return value;
        }

        int n = 0;
        int n1 = value.indexOf("${"); //$NON-NLS-1$
        StringBuilder evaluatedValue = new StringBuilder(value.length());
        while (n1 > -1) {
            evaluatedValue.append(value.substring(n, n1));
            int n2 = value.indexOf("}", n1); //$NON-NLS-1$
            if (n2 > -1) {
                String key = value.substring(n1 + 2, n2);
                if (PARAM_SERVER_URL.equals(key) || PARAM_USER_ID.equals(key) || PARAM_PASSWORD.equals(key)) {
                    evaluatedValue.append(evaluateParams(params.get(key), params, false));
                } else {
                    String val = evaluateParams(params.get(PARAM_PREFIX + key), params, false);
                    evaluatedValue.append(encode ? encode(val) : val);
                }
            }
            n = n2 + 1;
            n1 = value.indexOf("${", n2); //$NON-NLS-1$
        }
        if (n > -1) {
            evaluatedValue.append(value.substring(n));
        }
        return evaluatedValue.toString();
    }

    private static Map<String, String> getParams(TaskRepository repository, Map<String, String> params) {
        Map<String, String> mergedParams = new LinkedHashMap<String, String>(repository.getProperties());
        mergedParams.put(PARAM_SERVER_URL, repository.getRepositoryUrl());
        AuthenticationCredentials credentials = repository.getCredentials(AuthenticationType.REPOSITORY);
        if (credentials != null) {
            mergedParams.put(PARAM_USER_ID, credentials.getUserName());
            mergedParams.put(PARAM_PASSWORD, credentials.getPassword());
        }
        if (params != null) {
            mergedParams.putAll(params);
        }
        return mergedParams;
    }

    private static String encode(String value) {
        try {
            return new URLCodec().encode(value);
        } catch (EncoderException ex) {
            return value;
        }
    }

    public static List<String> getTemplateVariables(String value) {
        if (value == null) {
            return Collections.emptyList();
        }

        List<String> vars = new ArrayList<String>();
        Matcher m = Pattern.compile("\\$\\{(.+?)\\}").matcher(value); //$NON-NLS-1$
        while (m.find()) {
            vars.add(m.group(1));
        }
        return vars;
    }

    @Override
    public boolean hasLocalCompletionState(TaskRepository taskRepository, ITask task) {
        return true;
    }

    @Override
    public boolean hasTaskChanged(TaskRepository taskRepository, ITask task, TaskData taskData) {
        preProcessTaskData(task, taskData);
        return new TaskMapper(taskData).hasChanges(task);
    }

}