org.svenk.redmine.core.client.AbstractRedmineClient.java Source code

Java tutorial

Introduction

Here is the source code for org.svenk.redmine.core.client.AbstractRedmineClient.java

Source

/*******************************************************************************
 * Copyright (c) 2004, 2007 Mylyn project committers 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:
 *     Mylyn project committers
 *******************************************************************************/
/*******************************************************************************
 * Copyright (c) 2008 Sven Krzyzak
 * 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:
 *     Sven Krzyzak - adapted Trac implementation for Redmine
 *******************************************************************************/
package org.svenk.redmine.core.client;

import static org.svenk.redmine.core.IRedmineConstants.CLIENT_FIELD_ATTACHMENT_DESCRIPTION;
import static org.svenk.redmine.core.IRedmineConstants.CLIENT_FIELD_ATTACHMENT_FILE;
import static org.svenk.redmine.core.IRedmineConstants.CLIENT_FIELD_ATTACHMENT_NOTES;
import static org.svenk.redmine.core.IRedmineConstants.CLIENT_FIELD_CSRF_TOKEN;
import static org.svenk.redmine.core.IRedmineConstants.CLIENT_FIELD_NOTES;
import static org.svenk.redmine.core.IRedmineConstants.REDMINE_URL_ATTACHMENT_DOWNLOAD;
import static org.svenk.redmine.core.IRedmineConstants.REDMINE_URL_LOGIN;
import static org.svenk.redmine.core.IRedmineConstants.REDMINE_URL_LOGIN_CALLBACK;
import static org.svenk.redmine.core.IRedmineConstants.REDMINE_URL_TICKET_EDIT;
import static org.svenk.redmine.core.IRedmineConstants.REDMINE_URL_TICKET_NEW;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HeaderElement;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
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.AuthenticationType;
import org.eclipse.mylyn.commons.net.Policy;
import org.eclipse.mylyn.commons.net.UnsupportedRequestException;
import org.eclipse.mylyn.commons.net.WebUtil;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.data.AbstractTaskAttachmentSource;
import org.eclipse.mylyn.tasks.core.data.TaskAttachmentPartSource;
import org.svenk.redmine.core.IRedmineClient;
import org.svenk.redmine.core.RedmineCorePlugin;
import org.svenk.redmine.core.client.container.Version;
import org.svenk.redmine.core.client.container.Version.Release;
import org.svenk.redmine.core.exception.RedmineAuthenticationException;
import org.svenk.redmine.core.exception.RedmineErrorException;
import org.svenk.redmine.core.exception.RedmineException;
import org.svenk.redmine.core.exception.RedmineRemoteException;
import org.svenk.redmine.core.exception.RedmineStatusException;
import org.svenk.redmine.core.model.RedmineTicket;
import org.svenk.redmine.core.model.RedmineTicket.Key;

abstract public class AbstractRedmineClient implements IRedmineClient {

    protected final static String HEADER_STATUS = "status"; //$NON-NLS-1$

    protected final static String HEADER_REDIRECT = "location"; //$NON-NLS-1$

    protected final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; //$NON-NLS-1$

    protected final static String HEADER_WWW_AUTHENTICATE_REALM = "realm"; //$NON-NLS-1$

    protected final HttpClient httpClient;

    protected AbstractWebLocation location;

    protected RedmineClientData data;

    protected RedmineResponseReader responseReader;

    protected String characterEncoding;

    protected Version.Redmine vRedmine;

    protected RedmineTicket.Key attributeKeys[] = new RedmineTicket.Key[] { Key.ASSIGNED_TO, Key.PRIORITY,
            Key.VERSION, Key.CATEGORY, Key.STATUS, Key.TRACKER };

    protected TaskRepository repository;

    private IRedmineResponseParser<String> submitErrorParser;

    private IRedmineResponseParser<InputStream> attachmentParser;

    public AbstractRedmineClient(AbstractWebLocation location, RedmineClientData clientData,
            TaskRepository repository) {
        this.data = clientData;

        MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
        this.httpClient = new HttpClient(connectionManager);
        this.httpClient.getParams().setCookiePolicy(CookiePolicy.RFC_2109);

        refreshRepositorySettings(repository, location);

        createResponseParsers();
    }

    public void refreshRepositorySettings(TaskRepository repository, AbstractWebLocation location) {
        this.location = location;
        this.repository = repository;

        if (this.characterEncoding == null || !this.characterEncoding.equals(repository.getCharacterEncoding())) {
            this.characterEncoding = repository.getCharacterEncoding();
            this.httpClient.getParams().setContentCharset(characterEncoding);
        }

        if (!repository.getVersion().equals(TaskRepository.NO_VERSION_SPECIFIED)) {
            vRedmine = Version.Redmine.fromString(repository.getVersion());
        }
    }

    public Version checkClientConnection(IProgressMonitor monitor) throws RedmineException {
        return checkClientVersion(monitor);
    }

    abstract protected Version checkClientVersion(IProgressMonitor monitor) throws RedmineException;

    public boolean supportStartDueDate() {
        return false;
    }

    public boolean supportTimeEntries() {
        return false;
    }

    public boolean supportTrackerChange() {
        return vRedmine != null && vRedmine.compareTo(Release.REDMINE_0_9) >= 0;
    }

    public InputStream getAttachmentContent(int attachmentId, IProgressMonitor monitor) throws RedmineException {
        GetMethod method = new GetMethod(REDMINE_URL_ATTACHMENT_DOWNLOAD + attachmentId);
        return executeMethod(method, attachmentParser, monitor);
    }

    public void uploadAttachment(int ticketId, String fileName, String comment, String description,
            AbstractTaskAttachmentSource source, IProgressMonitor monitor) throws RedmineException {
        PostMethod method = new PostMethod(REDMINE_URL_TICKET_EDIT + ticketId);

        //assigned by RedmineAuthenticityTokenAspect
        NameValuePair tokenValue = method.getParameter(CLIENT_FIELD_CSRF_TOKEN);

        List<Part> parts = new ArrayList<Part>(4);
        parts.add(new StringPart(CLIENT_FIELD_ATTACHMENT_DESCRIPTION, description, characterEncoding));
        parts.add(new StringPart(CLIENT_FIELD_ATTACHMENT_NOTES, comment, characterEncoding));
        if (tokenValue != null) {
            parts.add(new StringPart(CLIENT_FIELD_CSRF_TOKEN, tokenValue == null ? "" : tokenValue.getValue(), //$NON-NLS-1$
                    characterEncoding));
        }

        //Workaround: http://rack.lighthouseapp.com/projects/22435/tickets/79-multipart-handling-incorrectly-assuming-file-upload
        for (Part part : parts) {
            ((StringPart) part).setContentType(null);
        }

        parts.add(new FilePart(CLIENT_FIELD_ATTACHMENT_FILE, new TaskAttachmentPartSource(source, fileName),
                source.getContentType(), this.httpClient.getParams().getContentCharset()));
        method.setRequestEntity(
                new MultipartRequestEntity(parts.toArray(new Part[parts.size()]), method.getParams()));

        String errorMsg = executeMethod(method, submitErrorParser, monitor, HttpStatus.SC_OK,
                HttpStatus.SC_MOVED_TEMPORARILY);
        if (errorMsg != null) {
            throw new RedmineStatusException(IStatus.INFO, errorMsg);
        }
    }

    public int createTicket(String project, Map<String, String> postValues, IProgressMonitor monitor)
            throws RedmineException {
        PostMethod method = new PostMethod(String.format(REDMINE_URL_TICKET_NEW, project));

        List<NameValuePair> values = this.ticket2HttpData(postValues);
        method.addParameters(values.toArray(new NameValuePair[values.size()]));

        String errorMsg = executeMethod(method, submitErrorParser, monitor, HttpStatus.SC_OK,
                HttpStatus.SC_MOVED_TEMPORARILY);
        if (errorMsg == null) {
            //TODO PRFEN !!!
            Header respHeader = method.getResponseHeader(HEADER_REDIRECT);
            if (respHeader != null) {
                String location = respHeader.getValue();

                Matcher m = Pattern.compile("(\\d+)$").matcher(location); //$NON-NLS-1$
                if (m.find()) {
                    try {
                        return Integer.parseInt(m.group(1));
                    } catch (NumberFormatException e) {
                        throw new RedmineException(Messages.AbstractRedmineClient_INVALID_TASK_ID);
                    }
                } else {
                    throw new RedmineException(Messages.AbstractRedmineClient_MISSING_TASK_ID_IN_RESPONSE);
                }
            }
        } else {
            throw new RedmineStatusException(IStatus.INFO, errorMsg);
        }

        throw new RedmineException(Messages.AbstractRedmineClient_UNHANDLED_SUBMIT_ERROR);
    }

    public void updateTicket(int ticketId, Map<String, String> postValues, String comment, IProgressMonitor monitor)
            throws RedmineException {
        PostMethod method = new PostMethod(REDMINE_URL_TICKET_EDIT + ticketId);

        List<NameValuePair> values = this.ticket2HttpData(postValues);
        values.add(new NameValuePair(CLIENT_FIELD_NOTES, comment));

        method.addParameters(values.toArray(new NameValuePair[values.size()]));

        String errorMsg = executeMethod(method, submitErrorParser, monitor, HttpStatus.SC_OK,
                HttpStatus.SC_MOVED_TEMPORARILY);
        if (errorMsg != null) {
            throw new RedmineStatusException(IStatus.INFO, errorMsg);
        }
    }

    protected <T extends Object> T executeMethod(HttpMethodBase method, IRedmineResponseParser<T> parser,
            IProgressMonitor monitor) throws RedmineException {
        return executeMethod(method, parser, monitor, HttpStatus.SC_OK);
    }

    protected <T extends Object> T executeMethod(HttpMethodBase method, IRedmineResponseParser<T> parser,
            IProgressMonitor monitor, int... expectedSC) throws RedmineException {
        monitor = Policy.monitorFor(monitor);
        method.setFollowRedirects(false);
        HostConfiguration hostConfiguration = WebUtil.createHostConfiguration(httpClient, location, monitor);

        T response = null;
        try {
            int sc = executeMethod(method, hostConfiguration, monitor);

            if (parser != null && expectedSC != null) {
                boolean found = false;
                for (int i : expectedSC) {
                    if (i == sc) {
                        InputStream input = WebUtil.getResponseBodyAsStream(method, monitor);
                        try {
                            found = true;
                            response = parser.parseResponse(input, sc);
                        } finally {
                            input.close();
                        }
                        break;
                    }
                }
                if (!found) {
                    String msg = Messages.AbstractRedmineClient_UNEXPECTED_RESPONSE_CODE;
                    msg = String.format(msg, sc, method.getPath(), method.getName());
                    IStatus status = new Status(IStatus.ERROR, RedmineCorePlugin.PLUGIN_ID, msg);
                    StatusHandler.fail(status);
                    throw new RedmineStatusException(status);
                }
            }
        } catch (RedmineErrorException e) {
            IStatus status = RedmineCorePlugin.toStatus(e, null);
            StatusHandler.fail(status);
            throw new RedmineStatusException(status);
        } catch (IOException e) {
            IStatus status = RedmineCorePlugin.toStatus(e, null);
            StatusHandler.log(status);
            throw new RedmineStatusException(status);
        } finally {
            method.releaseConnection();
        }

        return response;
    }

    /**
     * Execute the given method - handle authentication concerns.
     * 
     * @param method
     * @param hostConfiguration
     * @param monitor
     * @param authenticated
     * @return
     * @throws RedmineException
     */
    protected int executeMethod(HttpMethod method, HostConfiguration hostConfiguration, IProgressMonitor monitor)
            throws RedmineException {
        monitor = Policy.monitorFor(monitor);

        int statusCode = performExecuteMethod(method, hostConfiguration, monitor);

        if (statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
            Header statusHeader = method.getResponseHeader(HEADER_STATUS);
            String msg = Messages.AbstractRedmineClient_SERVER_ERROR;
            if (statusHeader != null) {
                msg += " : " + statusHeader.getValue().replace("" + HttpStatus.SC_INTERNAL_SERVER_ERROR, "").trim(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            }

            throw new RedmineRemoteException(msg);
        }

        //TODO testen, sollte ohne gehen
        //      if (statusCode==HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
        //         hostConfiguration = refreshCredentials(AuthenticationType.PROXY, method, monitor);
        //         return executeMethod(method, hostConfiguration, monitor, authenticated);
        //      }
        //      
        //      if(statusCode==HttpStatus.SC_UNAUTHORIZED && supportAdditionalHttpAuth()) {
        //         hostConfiguration = refreshCredentials(AuthenticationType.HTTP, method, monitor);
        //         return executeMethod(method, hostConfiguration, monitor, authenticated);
        //      }
        //
        //      if (statusCode>=400 && statusCode<=599) {
        //         throw new RedmineRemoteException(method.getStatusLine().toString());
        //      }

        Header respHeader = method.getResponseHeader(HEADER_REDIRECT);
        if (respHeader != null && (respHeader.getValue().endsWith(REDMINE_URL_LOGIN)
                || respHeader.getValue().indexOf(REDMINE_URL_LOGIN_CALLBACK) >= 0)) {
            throw new RedmineException(Messages.AbstractRedmineClient_LOGIN_FORMALY_INEFFECTIVE);
        }

        return statusCode;
    }

    synchronized protected int performExecuteMethod(HttpMethod method, HostConfiguration hostConfiguration,
            IProgressMonitor monitor) throws RedmineException {
        try {
            //complete URL
            String baseUrl = new URL(location.getUrl()).getPath();
            if (!method.getPath().startsWith(baseUrl)) {
                method.setPath(baseUrl + method.getPath());
            }

            return WebUtil.execute(httpClient, hostConfiguration, method, monitor);
        } catch (OperationCanceledException e) {
            monitor.setCanceled(true);
            throw new RedmineException(e.getMessage(), e);
        } catch (RuntimeException e) {
            IStatus status = RedmineCorePlugin.toStatus(e, null,
                    Messages.AbstractRedmineClient_UNHANDLED_RUNTIME_EXCEPTION);
            StatusHandler.fail(status);
            throw new RedmineStatusException(status);
        } catch (IOException e) {
            IStatus status = RedmineCorePlugin.toStatus(e, null);
            StatusHandler.log(status);
            throw new RedmineStatusException(status);
        }
    }

    /**
     * Ask user for name and password.
     * 
     * @param authenticationType
     * @param method
     * @param monitor
     * @return
     * @throws RedmineException
     */
    protected HostConfiguration refreshCredentials(AuthenticationType authenticationType, HttpMethod method,
            IProgressMonitor monitor) throws RedmineException {
        if (Policy.isBackgroundMonitor(monitor)) {
            throw new RedmineAuthenticationException(method.getStatusCode(),
                    Messages.AbstractRedmineClient_MISSING_CREDENTIALS_MANUALLY_SYNC_REQUIRED);
        }

        try {
            String message = Messages.AbstractRedmineClient_AUTHENTICATION_REQUIRED;
            if (authenticationType.equals(AuthenticationType.HTTP)) {
                Header authHeader = method.getResponseHeader(HEADER_WWW_AUTHENTICATE);
                if (authHeader != null) {
                    for (HeaderElement headerElem : authHeader.getElements()) {
                        if (headerElem.getName().contains(HEADER_WWW_AUTHENTICATE_REALM)) {
                            message += ": " + headerElem.getValue(); //$NON-NLS-1$
                            break;
                        }
                    }
                }
            }
            location.requestCredentials(authenticationType, message, monitor);

            return WebUtil.createHostConfiguration(httpClient, location, monitor);
        } catch (UnsupportedRequestException e) {
            IStatus status = RedmineCorePlugin.toStatus(e, null,
                    Messages.AbstractRedmineClient_CREDENTIALS_REQUEST_FAILED);
            StatusHandler.log(status);
            throw new RedmineStatusException(status);
        } catch (OperationCanceledException e) {
            monitor.setCanceled(true);
            throw new RedmineException(Messages.AbstractRedmineClient_AUTHENTICATION_CANCELED);
        }
    }

    protected List<NameValuePair> ticket2HttpData(Map<String, String> postValues) {
        List<NameValuePair> nameValuePair = new ArrayList<NameValuePair>(postValues.size());
        for (Entry<String, String> entry : postValues.entrySet()) {
            nameValuePair.add(new NameValuePair(entry.getKey(), entry.getValue()));
        }
        return nameValuePair;
    }

    private RedmineResponseReader getResponseReader() {
        if (responseReader == null) {
            responseReader = new RedmineResponseReader();
        }
        return responseReader;
    }

    private void createResponseParsers() {
        submitErrorParser = new IRedmineResponseParser<String>() {
            public String parseResponse(InputStream input, int sc) throws RedmineException {
                if (sc == HttpStatus.SC_OK) {
                    Collection<String> messages = getResponseReader().readErrors(input);
                    if (messages != null) {
                        StringBuilder sb = new StringBuilder();
                        for (Iterator<String> iterator = messages.iterator(); iterator.hasNext();) {
                            sb.append(iterator.next());
                            sb.append(" "); //$NON-NLS-1$
                        }
                        return sb.toString().trim();
                    }
                }
                return null;
            }
        };

        attachmentParser = new IRedmineResponseParser<InputStream>() {
            public InputStream parseResponse(InputStream input, int sc) throws RedmineException {
                InputStream response = null;
                try {
                    ByteArrayOutputStream output = new ByteArrayOutputStream(input.available());
                    try {
                        byte[] buffer = new byte[4096];
                        int len = 0;
                        while ((len = input.read(buffer)) > 0) {
                            output.write(buffer, 0, len);
                        }
                        response = new ByteArrayInputStream(output.toByteArray());
                    } finally {
                        output.close();
                    }
                } catch (IOException e) {
                    throw new RedmineException(e.getMessage(), e);
                }

                return response;
            }
        };
    }

}