All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.taskadapter.redmineapi.internal.Transport Maven / Gradle / Ivy

Go to download

Free open-source Java API for Redmine and Chiliproject bug/task management systems. This project was originally a part of Task Adapter application (http://www.taskadapter.com) and then was open-sourced.

The newest version!
package com.taskadapter.redmineapi.internal;

import com.taskadapter.redmineapi.NotFoundException;
import com.taskadapter.redmineapi.RedmineAuthenticationException;
import com.taskadapter.redmineapi.RedmineException;
import com.taskadapter.redmineapi.RedmineFormatException;
import com.taskadapter.redmineapi.RedmineInternalError;
import com.taskadapter.redmineapi.RedmineManager;
import com.taskadapter.redmineapi.bean.Attachment;
import com.taskadapter.redmineapi.bean.CustomFieldDefinition;
import com.taskadapter.redmineapi.bean.FluentStyle;
import com.taskadapter.redmineapi.bean.Group;
import com.taskadapter.redmineapi.bean.Identifiable;
import com.taskadapter.redmineapi.bean.Issue;
import com.taskadapter.redmineapi.bean.IssueCategory;
import com.taskadapter.redmineapi.bean.IssuePriority;
import com.taskadapter.redmineapi.bean.IssueRelation;
import com.taskadapter.redmineapi.bean.IssueStatus;
import com.taskadapter.redmineapi.bean.Membership;
import com.taskadapter.redmineapi.bean.News;
import com.taskadapter.redmineapi.bean.Project;
import com.taskadapter.redmineapi.bean.Role;
import com.taskadapter.redmineapi.bean.SavedQuery;
import com.taskadapter.redmineapi.bean.TimeEntry;
import com.taskadapter.redmineapi.bean.TimeEntryActivity;
import com.taskadapter.redmineapi.bean.Tracker;
import com.taskadapter.redmineapi.bean.User;
import com.taskadapter.redmineapi.bean.Version;
import com.taskadapter.redmineapi.bean.Watcher;
import com.taskadapter.redmineapi.bean.WikiPage;
import com.taskadapter.redmineapi.bean.WikiPageDetail;
import com.taskadapter.redmineapi.internal.comm.BaseCommunicator;
import com.taskadapter.redmineapi.internal.comm.BasicHttpResponse;
import com.taskadapter.redmineapi.internal.comm.Communicator;
import com.taskadapter.redmineapi.internal.comm.Communicators;
import com.taskadapter.redmineapi.internal.comm.ContentHandler;
import com.taskadapter.redmineapi.internal.comm.SimpleCommunicator;
import com.taskadapter.redmineapi.internal.comm.redmine.RedmineAuthenticator;
import com.taskadapter.redmineapi.internal.comm.redmine.RedmineErrorHandler;
import com.taskadapter.redmineapi.internal.json.JsonInput;
import com.taskadapter.redmineapi.internal.json.JsonObjectParser;
import com.taskadapter.redmineapi.internal.json.JsonObjectWriter;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public final class Transport {
	private static final Map, EntityConfig> OBJECT_CONFIGS = new HashMap<>();
	private static final String CONTENT_TYPE = "application/json; charset=utf-8";
	private static final int DEFAULT_OBJECTS_PER_PAGE = 25;
	private static final String KEY_TOTAL_COUNT = "total_count";
	private static final String KEY_LIMIT = "limit";
	private static final String KEY_OFFSET = "offset";

	private final Logger logger = LoggerFactory.getLogger(RedmineManager.class);
	private final SimpleCommunicator communicator;
	private final Communicator errorCheckingCommunicator;
	private final RedmineAuthenticator authenticator;

    private String onBehalfOfUser = null;

    static {
		OBJECT_CONFIGS.put(
				Project.class,
				config("project", "projects",
						RedmineJSONBuilder::writeProject,
						RedmineJSONParser::parseProject));
		OBJECT_CONFIGS.put(
				Issue.class,
				config("issue", "issues", RedmineJSONBuilder::writeIssue,
						RedmineJSONParser::parseIssue));
		OBJECT_CONFIGS.put(
				User.class,
				config("user", "users", RedmineJSONBuilder::writeUser,
						RedmineJSONParser::parseUser));
		OBJECT_CONFIGS.put(
				Group.class,
				config("group", "groups", RedmineJSONBuilder::writeGroup,
						RedmineJSONParser::parseGroup));
		OBJECT_CONFIGS.put(
				IssueCategory.class,
				config("issue_category", "issue_categories",
						RedmineJSONBuilder::writeCategory,
						RedmineJSONParser::parseCategory));
		OBJECT_CONFIGS.put(
				Version.class,
				config("version", "versions",
						RedmineJSONBuilder::writeVersion,
						RedmineJSONParser::parseVersion));
		OBJECT_CONFIGS.put(
				TimeEntry.class,
				config("time_entry", "time_entries",
						RedmineJSONBuilder::writeTimeEntry,
						RedmineJSONParser::parseTimeEntry));
		OBJECT_CONFIGS.put(News.class,
				config("news", "news", null, RedmineJSONParser::parseNews));
		OBJECT_CONFIGS.put(
				IssueRelation.class,
				config("relation", "relations",
						RedmineJSONBuilder::writeRelation,
						RedmineJSONParser::parseRelation));
		OBJECT_CONFIGS.put(
				Tracker.class,
				config("tracker", "trackers", null,
						RedmineJSONParser::parseTracker));
		OBJECT_CONFIGS.put(
				IssueStatus.class,
				config("status", "issue_statuses", null,
						RedmineJSONParser::parseStatus));
		OBJECT_CONFIGS
				.put(SavedQuery.class,
						config("query", "queries", null,
								RedmineJSONParser::parseSavedQuery));
		OBJECT_CONFIGS.put(Role.class,
				config("role", "roles", null, RedmineJSONParser::parseRole));
		OBJECT_CONFIGS.put(
				Membership.class,
				config("membership", "memberships",
						RedmineJSONBuilder::writeMembership,
						RedmineJSONParser::parseMembership));
        OBJECT_CONFIGS.put(
                IssuePriority.class,
                config("issue_priority", "issue_priorities", null,
                        RedmineJSONParser::parseIssuePriority));
        OBJECT_CONFIGS.put(
                TimeEntryActivity.class,
                config("time_entry_activity", "time_entry_activities", null,
                        RedmineJSONParser::parseTimeEntryActivity));

        OBJECT_CONFIGS.put(
                Watcher.class,
                config("watcher", "watchers", null,
                        RedmineJSONParser::parseWatcher));

        OBJECT_CONFIGS.put(
                WikiPage.class,
                config("wiki_page", "wiki_pages", null, RedmineJSONParser::parseWikiPage)
        );

        OBJECT_CONFIGS.put(
                WikiPageDetail.class,
                config("wiki_page", null, RedmineJSONBuilder::writeWikiPageDetail, RedmineJSONParser::parseWikiPageDetail)
        );
        OBJECT_CONFIGS.put(
                CustomFieldDefinition.class,
                config("custom_field", "custom_fields", null,
                        RedmineJSONParser::parseCustomFieldDefinition));
		OBJECT_CONFIGS.put(
				Attachment.class,
				config("attachment", "attachments", null,
						RedmineJSONParser::parseAttachments));
    }

	private final URIConfigurator configurator;
	private String login;
	private String password;
	private int objectsPerPage = DEFAULT_OBJECTS_PER_PAGE;
	private static final String CHARSET = "UTF-8";

	public Transport(URIConfigurator configurator, HttpClient client) {
		this.configurator = configurator;
        final Communicator baseCommunicator = new BaseCommunicator(client);
		this.authenticator = new RedmineAuthenticator<>(
				baseCommunicator, CHARSET);
		final ContentHandler errorProcessor = new RedmineErrorHandler();
		errorCheckingCommunicator = Communicators.fmap(
				authenticator,
				Communicators.compose(errorProcessor,
						Communicators.transportDecoder()));
        Communicator coreCommunicator = Communicators.fmap(errorCheckingCommunicator,
            Communicators.contentReader());
		this.communicator = Communicators.simplify(coreCommunicator,
                Communicators.identityHandler());
	}

	public User getCurrentUser(RequestParam... params) throws RedmineException {
		URI uri = getURIConfigurator().createURI("users/current.json", params);
		HttpGet http = new HttpGet(uri);
		String response = send(http);
		return parseResponse(response, "user", RedmineJSONParser::parseUser);
	}

	/**
	 * Performs an "add object" request.
	 * 
	 * @param object
	 *            object to use.
	 * @param params
	 *            name params.
	 * @return object to use.
	 * @throws RedmineException
	 *             if something goes wrong.
	 */
	public  T addObject(T object, RequestParam... params)
			throws RedmineException {
		final EntityConfig config = getConfig(object.getClass());
        if (config.writer == null) {
            throw new RuntimeException("can't create object: writer is not implemented or is not registered in RedmineJSONBuilder for object " + object);
        }
		URI uri = getURIConfigurator().getObjectsURI(object.getClass(), params);
		HttpPost httpPost = new HttpPost(uri);
		String body = RedmineJSONBuilder.toSimpleJSON(config.singleObjectName, object, config.writer);
		setEntity(httpPost, body);
		String response = send(httpPost);
		logger.debug(response);
		return parseResponse(response, config.singleObjectName, config.parser);
	}

	/**
	 * Performs an "add child object" request.
	 * 
	 * @param parentClass
	 *            parent object id.
	 * @param object
	 *            object to use.
	 * @param params
	 *            name params.
	 * @return object to use.
	 * @throws RedmineException
	 *             if something goes wrong.
	 */
	public  T addChildEntry(Class parentClass, String parentId, T object,
			RequestParam... params) throws RedmineException {
		final EntityConfig config = getConfig(object.getClass());
		URI uri = getURIConfigurator().getChildObjectsURI(parentClass,
				parentId, object.getClass(), params);
		HttpPost httpPost = new HttpPost(uri);
		String body = RedmineJSONBuilder.toSimpleJSON(config.singleObjectName,
				object, config.writer);
		setEntity(httpPost, body);
		String response = send(httpPost);
		logger.debug(response);
		return parseResponse(response, config.singleObjectName, config.parser);
	}

	/*
	 * note: This method cannot return the updated object from Redmine because
	 * the server does not provide any XML in response.
	 * 
	 * @since 1.8.0
	 */
	public  void updateObject(T obj,
			RequestParam... params) throws RedmineException {
		final EntityConfig config = getConfig(obj.getClass());
		final Integer id = obj.getId();
		if (id == null) {
			throw new RuntimeException("'id' field cannot be NULL in the given object:" +
					" it is required to identify the object in the target system");
		}
		final URI uri = getURIConfigurator().getObjectURI(obj.getClass(),
				Integer.toString(id), params);
		final HttpPut http = new HttpPut(uri);
		final String body = RedmineJSONBuilder.toSimpleJSON(
				config.singleObjectName, obj, config.writer);
		setEntity(http, body);
		send(http);
	}

	/*
	 * note: This method cannot return the updated object from Redmine because
	 * the server does not provide anything in response.
	 */
	public  void updateChildEntry(Class parentClass, String parentId,
			T obj, String objId, RequestParam... params) throws RedmineException {
		final EntityConfig config = getConfig(obj.getClass());
		URI uri = getURIConfigurator().getChildIdURI(parentClass, parentId, obj.getClass(), objId, params);
		final HttpPut http = new HttpPut(uri);
		final String body = RedmineJSONBuilder.toSimpleJSON(config.singleObjectName, obj, config.writer);
		setEntity(http, body);
		send(http);
	}

	/**
	 * Performs "delete child Id" request.
	 * 
	 * @param parentClass
	 *            parent object id.
	 * @param object
	 *            object to use.
	 * @param value
	 *            child object id.
	 * @throws RedmineException
	 *             if something goes wrong.
	 */
    public  void deleteChildId(Class parentClass, String parentId, T object, Integer value) throws RedmineException {
        URI uri = getURIConfigurator().getChildIdURI(parentClass, parentId, object.getClass(), value);
        HttpDelete httpDelete = new HttpDelete(uri);
        String response = send(httpDelete);
        logger.debug(response);
    }

	/**
	 * Deletes an object.
	 * 
	 * @param classs
	 *            object class.
	 * @param id
	 *            object id.
	 * @throws RedmineException
	 *             if something goes wrong.
	 */
	public  void deleteObject(Class classs, String id)
			throws RedmineException {
		final URI uri = getURIConfigurator().getObjectURI(classs, id);
		final HttpDelete http = new HttpDelete(uri);
		send(http);
	}

	/**
	 * @param classs
	 *            target class
	 * @param key
	 *            item key
	 * @param params
	 *            extra arguments.
	 * @throws RedmineAuthenticationException
	 *             invalid or no API access key is used with the server, which
	 *             requires authorization. Check the constructor arguments.
	 * @throws NotFoundException
	 *             the object with the given key is not found
	 * @throws RedmineException
	 */
	public  T getObject(Class classs, String key, RequestParam... params)
			throws RedmineException {
		final EntityConfig config = getConfig(classs);
		final URI uri = getURIConfigurator().getObjectURI(classs, key, params);
		final HttpGet http = new HttpGet(uri);
		String response = send(http);
		logger.debug(response);
		return parseResponse(response, config.singleObjectName, config.parser);
	}

	/**
	 * Downloads redmine content.
	 * 
	 * @param uri
	 *            target uri.
	 * @param handler
	 *            content handler.
	 * @return handler result.
	 * @throws RedmineException
	 *             if something goes wrong.
	 */
	public  R download(String uri,
			ContentHandler handler)
			throws RedmineException {
		final URI requestUri = configurator.addAPIKey(uri);
		final HttpGet request = new HttpGet(requestUri);
        if (onBehalfOfUser != null) {
            request.addHeader("X-Redmine-Switch-User", onBehalfOfUser);
        }
        return errorCheckingCommunicator.sendRequest(request, handler);
    }

	/**
	 * Deprecated because Redmine server can return invalid string depending on its configuration.
	 * See https://github.com/taskadapter/redmine-java-api/issues/78 .
	 * 

Use {@link #upload(InputStream, long)} instead. * *

* Uploads content on a server. This method calls {@link #upload(InputStream, long)} with -1 as content length. * * @param content the content stream. * @return uploaded item token. * @throws RedmineException if something goes wrong. */ @Deprecated public String upload(InputStream content) throws RedmineException { return upload(content, -1); } /** * @param content the content * @param contentLength the length of the content in bytes. you can provide -1 but be aware that some * users reported Redmine configuration problems that prevent it from processing -1 correctly. * See https://github.com/taskadapter/redmine-java-api/issues/78 for details. * @return the string token of the uploaded item. see {@link Attachment#getToken()} */ public String upload(InputStream content, long contentLength) throws RedmineException { final URI uploadURI = getURIConfigurator().getUploadURI(); final HttpPost request = new HttpPost(uploadURI); final AbstractHttpEntity entity = new InputStreamEntity(content, contentLength); /* Content type required by a Redmine */ entity.setContentType("application/octet-stream"); request.setEntity(entity); final String result = send(request); return parseResponse(result, "upload", input -> JsonInput.getStringNotNull(input, "token")); } /** * @param classs * target class * @param key * item key * @param params * extra arguments. * @throws RedmineAuthenticationException * invalid or no API access key is used with the server, which * requires authorization. Check the constructor arguments. * @throws NotFoundException * the object with the given key is not found * @throws RedmineException */ public T getObject(Class classs, Integer key, RequestParam... params) throws RedmineException { return getObject(classs, key.toString(), params); } public List getObjectsList(Class objectClass, RequestParam... params) throws RedmineException { return getObjectsList(objectClass, Arrays.asList(params)); } /** * Returns all objects found using the provided parameters. * This method IGNORES "limit" and "offset" parameters and handles paging AUTOMATICALLY for you. * Please use getObjectsListNoPaging() method if you want to control paging yourself with "limit" and "offset" parameters. * * @return objects list, never NULL * * @see #getObjectsListNoPaging(Class, Collection) */ public List getObjectsList(Class objectClass, Collection params) throws RedmineException { final List result = new ArrayList<>(); int offset = 0; Integer totalObjectsFoundOnServer; do { final List newParams = new ArrayList<>(params); newParams.add(new RequestParam("limit", String.valueOf(objectsPerPage))); newParams.add(new RequestParam("offset", String.valueOf(offset))); final ResultsWrapper wrapper = getObjectsListNoPaging(objectClass, newParams); result.addAll(wrapper.getResults()); totalObjectsFoundOnServer = wrapper.getTotalFoundOnServer(); // Necessary for trackers. // TODO Alexey: is this still necessary for Redmine 2.x? if (totalObjectsFoundOnServer == null) { break; } if (!wrapper.hasSomeResults()) { break; } offset += wrapper.getResultsNumber(); } while (offset < totalObjectsFoundOnServer); return result; } /** * Returns an object list. Provide your own "limit" and "offset" parameters if you need those, otherwise * this method will return the first page of some default size only (this default is controlled by * your Redmine configuration). * * @return objects list, never NULL */ public ResultsWrapper getObjectsListNoPaging(Class objectClass, Collection params) throws RedmineException { final EntityConfig config = getConfig(objectClass); try { final JSONObject responseObject = getJsonResponseFromGet(objectClass, params); List results = JsonInput.getListOrNull(responseObject, config.multiObjectName, config.parser); Integer totalFoundOnServer = JsonInput.getIntOrNull(responseObject, KEY_TOTAL_COUNT); Integer limitOnServer = JsonInput.getIntOrNull(responseObject, KEY_LIMIT); Integer offsetOnServer = JsonInput.getIntOrNull(responseObject, KEY_OFFSET); return new ResultsWrapper<>(totalFoundOnServer, limitOnServer, offsetOnServer, results); } catch (JSONException e) { throw new RedmineFormatException(e); } } /** * Use this method if you need direct access to Json results.

	 Params params = new Params()
	   .add(...)
     getJsonResponseFromGet(Issue.class, params);
	 
*/ public JSONObject getJsonResponseFromGet(Class objectClass, Collection params) throws RedmineException, JSONException { final List newParams = new ArrayList<>(params); List paramsList = new ArrayList<>(newParams); final URI uri = getURIConfigurator().getObjectsURI(objectClass, paramsList); final HttpGet http = new HttpGet(uri); final String response = send(http); return RedmineJSONParser.getResponse(response); } public List getChildEntries(Class parentClass, int parentId, Class classs) throws RedmineException { return getChildEntries(parentClass, parentId + "", classs); } /** * Delivers a list of a child entries. * * @param classs * target class. */ public List getChildEntries(Class parentClass, String parentKey, Class classs) throws RedmineException { final EntityConfig config = getConfig(classs); final URI uri = getURIConfigurator().getChildObjectsURI(parentClass, parentKey, classs, new RequestParam("limit", String.valueOf(objectsPerPage))); HttpGet http = new HttpGet(uri); String response = send(http); final JSONObject responseObject; try { responseObject = RedmineJSONParser.getResponse(response); return JsonInput.getListNotNull(responseObject, config.multiObjectName, config.parser); } catch (JSONException e) { throw new RedmineFormatException("Bad categories response " + response, e); } } /** * Delivers a single child entry by its identifier. */ public T getChildEntry(Class parentClass, String parentId, Class classs, String childId, RequestParam... params) throws RedmineException { final EntityConfig config = getConfig(classs); final URI uri = getURIConfigurator().getChildIdURI(parentClass, parentId, classs, childId, params); HttpGet http = new HttpGet(uri); String response = send(http); return parseResponse(response, config.singleObjectName, config.parser); } /** * This number of objects (tasks, projects, users) will be requested from * Redmine server in 1 request. */ public void setObjectsPerPage(int pageSize) { if (pageSize <= 0) { throw new IllegalArgumentException("Page size must be >= 0. You provided: " + pageSize); } this.objectsPerPage = pageSize; } public void addUserToGroup(int userId, int groupId) throws RedmineException { logger.debug("adding user " + userId + " to group " + groupId + "..."); URI uri = getURIConfigurator().getChildObjectsURI(Group.class, Integer.toString(groupId), User.class); HttpPost httpPost = new HttpPost(uri); final StringWriter writer = new StringWriter(); final JSONWriter jsonWriter = new JSONWriter(writer); try { jsonWriter.object().key("user_id").value(userId).endObject(); } catch (JSONException e) { throw new RedmineInternalError("Unexpected exception", e); } String body = writer.toString(); setEntity(httpPost, body); String response = send(httpPost); logger.debug(response); } public void addWatcherToIssue(int watcherId, int issueId) throws RedmineException { logger.debug("adding watcher " + watcherId + " to issue " + issueId + "..."); URI uri = getURIConfigurator().getChildObjectsURI(Issue.class, Integer.toString(issueId), Watcher.class); HttpPost httpPost = new HttpPost(uri); final StringWriter writer = new StringWriter(); final JSONWriter jsonWriter = new JSONWriter(writer); try { jsonWriter.object().key("user_id").value(watcherId).endObject(); } catch (JSONException e) { throw new RedmineInternalError("Unexpected exception", e); } String body = writer.toString(); setEntity(httpPost, body); String response = send(httpPost); logger.debug(response); } private String send(HttpRequestBase http) throws RedmineException { if (onBehalfOfUser != null) { http.addHeader("X-Redmine-Switch-User", onBehalfOfUser); } return communicator.sendRequest(http); } private T parseResponse(String response, String tag, JsonObjectParser parser) throws RedmineFormatException { try { T parse = parser.parse(RedmineJSONParser.getResponseSingleObject(response, tag)); if (parse instanceof FluentStyle) { ((FluentStyle) parse).setTransport(this); } return parse; } catch (JSONException e) { throw new RedmineFormatException(e); } } private static void setEntity(HttpEntityEnclosingRequest request, String body) { setEntity(request, body, CONTENT_TYPE); } private static void setEntity(HttpEntityEnclosingRequest request, String body, String contentType) { StringEntity entity; try { entity = new StringEntity(body, CHARSET); } catch (UnsupportedCharsetException e) { throw new RedmineInternalError("Required charset " + CHARSET + " is not supported", e); } entity.setContentType(contentType); request.setEntity(entity); } @SuppressWarnings("unchecked") private EntityConfig getConfig(Class class1) { final EntityConfig guess = OBJECT_CONFIGS.get(class1); if (guess == null) throw new RedmineInternalError("Unsupported class " + class1); return (EntityConfig) guess; } private URIConfigurator getURIConfigurator() { return configurator; } private static EntityConfig config(String objectField, String urlPrefix, JsonObjectWriter writer, JsonObjectParser parser) { return new EntityConfig<>(objectField, urlPrefix, writer, parser); } public void setCredentials(String login, String password) { this.login = login; this.password = password; authenticator.setCredentials(login, password); } public void setPassword(String password) { setCredentials(login, password); } public void setLogin(String login) { setCredentials(login, password); } /** * This works only when the main authentication has led to Redmine Admin level user. * The given user name will be sent to the server in "X-Redmine-Switch-User" HTTP Header * to indicate that the action (create issue, delete issue, etc) must be done * on behalf of the given user name. * * @param loginName Redmine user login name to provide to the server * * @see Redmine issue 11755 */ public void setOnBehalfOfUser(String loginName) { this.onBehalfOfUser = loginName; } /** * Entity config. */ static class EntityConfig { final String singleObjectName; final String multiObjectName; final JsonObjectWriter writer; final JsonObjectParser parser; public EntityConfig(String objectField, String urlPrefix, JsonObjectWriter writer, JsonObjectParser parser) { super(); this.singleObjectName = objectField; this.multiObjectName = urlPrefix; this.writer = writer; this.parser = parser; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy