package com.taskadapter.redmineapi.internal; import com.taskadapter.redmineapi.LoggerFactory; 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.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 java.io.InputStream; import java.io.StringWriter; import java.io.UnsupportedEncodingException; 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 com.taskadapter.redmineapi.Logger; public final class Transport { private static final Map<Class<?>, EntityConfig<?>> OBJECT_CONFIGS = new HashMap<Class<?>, EntityConfig<?>>(); 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 final Logger logger = LoggerFactory.getLogger(RedmineManager.class); private final SimpleCommunicator<String> communicator; private final Communicator<BasicHttpResponse> errorCheckingCommunicator; private final RedmineAuthenticator<HttpResponse> authenticator; private String onBehalfOfUser = null; static { OBJECT_CONFIGS.put( Project.class, config("project", "projects", RedmineJSONBuilder.PROJECT_WRITER, RedmineJSONParser.PROJECT_PARSER)); OBJECT_CONFIGS.put( Issue.class, config("issue", "issues", RedmineJSONBuilder.ISSUE_WRITER, RedmineJSONParser.ISSUE_PARSER)); OBJECT_CONFIGS.put( User.class, config("user", "users", RedmineJSONBuilder.USER_WRITER, RedmineJSONParser.USER_PARSER)); OBJECT_CONFIGS.put( Group.class, config("group", "groups", RedmineJSONBuilder.GROUP_WRITER, RedmineJSONParser.GROUP_PARSER)); OBJECT_CONFIGS.put( IssueCategory.class, config("issue_category", "issue_categories", RedmineJSONBuilder.CATEGORY_WRITER, RedmineJSONParser.CATEGORY_PARSER)); OBJECT_CONFIGS.put( Version.class, config("version", "versions", RedmineJSONBuilder.VERSION_WRITER, RedmineJSONParser.VERSION_PARSER)); OBJECT_CONFIGS.put( TimeEntry.class, config("time_entry", "time_entries", RedmineJSONBuilder.TIME_ENTRY_WRITER, RedmineJSONParser.TIME_ENTRY_PARSER)); OBJECT_CONFIGS.put(News.class, config("news", "news", null, RedmineJSONParser.NEWS_PARSER)); OBJECT_CONFIGS.put( IssueRelation.class, config("relation", "relations", RedmineJSONBuilder.RELATION_WRITER, RedmineJSONParser.RELATION_PARSER)); OBJECT_CONFIGS.put( Tracker.class, config("tracker", "trackers", null, RedmineJSONParser.TRACKER_PARSER)); OBJECT_CONFIGS.put( IssueStatus.class, config("status", "issue_statuses", null, RedmineJSONParser.STATUS_PARSER)); OBJECT_CONFIGS .put(SavedQuery.class, config("query", "queries", null, RedmineJSONParser.QUERY_PARSER)); OBJECT_CONFIGS.put(Role.class, config("role", "roles", null, RedmineJSONParser.ROLE_PARSER)); OBJECT_CONFIGS.put( Membership.class, config("membership", "memberships", RedmineJSONBuilder.MEMBERSHIP_WRITER, RedmineJSONParser.MEMBERSHIP_PARSER)); OBJECT_CONFIGS.put( IssuePriority.class, config("issue_priority", "issue_priorities", null, RedmineJSONParser.ISSUE_PRIORITY_PARSER)); OBJECT_CONFIGS.put( TimeEntryActivity.class, config("time_entry_activity", "time_entry_activities", null, RedmineJSONParser.TIME_ENTRY_ACTIVITY_PARSER)); OBJECT_CONFIGS.put( Watcher.class, config("watcher", "watchers", null, RedmineJSONParser.WATCHER_PARSER)); OBJECT_CONFIGS.put( WikiPage.class, config("wiki_page", "wiki_pages", null, RedmineJSONParser.WIKI_PAGE_PARSER) ); OBJECT_CONFIGS.put( WikiPageDetail.class, config("wiki_page", null, null, RedmineJSONParser.WIKI_PAGE_DETAIL_PARSER) ); OBJECT_CONFIGS.put( CustomFieldDefinition.class, config("custom_field", "custom_fields", null, RedmineJSONParser.CUSTOM_FIELD_DEFINITION_PARSER)); OBJECT_CONFIGS.put(Attachment.class, config("attachment","attachments",null,RedmineJSONParser.ATTACHMENT_PARSER)); } 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<HttpResponse> baseCommunicator = new BaseCommunicator(client); this.authenticator = new RedmineAuthenticator<HttpResponse>( baseCommunicator, CHARSET); final ContentHandler<BasicHttpResponse, BasicHttpResponse> errorProcessor = new RedmineErrorHandler(); errorCheckingCommunicator = Communicators.fmap( authenticator, Communicators.compose(errorProcessor, Communicators.transportDecoder())); Communicator<String> coreCommunicator = Communicators.fmap(errorCheckingCommunicator, Communicators.contentReader()); this.communicator = Communicators.simplify(coreCommunicator, Communicators.<String>identityHandler()); } public User getCurrentUser(NameValuePair... 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.USER_PARSER); } /** * 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> T addObject(T object, NameValuePair... params) throws RedmineException { final EntityConfig<T> 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> T addChildEntry(Class<?> parentClass, String parentId, T object, NameValuePair... params) throws RedmineException { final EntityConfig<T> 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 <T extends Identifiable> void updateObject(T obj, NameValuePair... params) throws RedmineException { final EntityConfig<T> config = getConfig(obj.getClass()); final URI uri = getURIConfigurator().getObjectURI(obj.getClass(), Integer.toString(obj.getId())); 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 <T> 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 <T extends Identifiable> void deleteObject(Class<T> 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 args * 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> T getObject(Class<T> classs, String key, NameValuePair... args) throws RedmineException { final EntityConfig<T> config = getConfig(classs); final URI uri = getURIConfigurator().getObjectURI(classs, key, args); 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> R download(String uri, ContentHandler<BasicHttpResponse, R> handler) throws RedmineException { final HttpGet request = new HttpGet(uri); if (onBehalfOfUser != null) { request.addHeader("X-Redmine-Switch-User", onBehalfOfUser); } return errorCheckingCommunicator.sendRequest(request, handler); } /** * UPloads content on a server. * * @param content * content stream. * @return uploaded item token. * @throws RedmineException * if something goes wrong. */ public String upload(InputStream content) throws RedmineException { final URI uploadURI = getURIConfigurator().getUploadURI(); final HttpPost request = new HttpPost(uploadURI); final AbstractHttpEntity entity = new InputStreamEntity(content, -1); /* Content type required by a Redmine */ entity.setContentType("application/octet-stream"); request.setEntity(entity); final String result = send(request); return parseResponse(result, "upload", RedmineJSONParser.UPLOAD_TOKEN_PARSER); } /** * @param classs * target class * @param key * item key * @param args * 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> T getObject(Class<T> classs, Integer key, NameValuePair... args) throws RedmineException { return getObject(classs, key.toString(), args); } public <T> List<T> getObjectsList(Class<T> objectClass, NameValuePair... params) throws RedmineException { return getObjectsList(objectClass, Arrays.asList(params)); } /** * Returns an object list. * * @return objects list, never NULL */ public <T> List<T> getObjectsList(Class<T> objectClass, Collection<? extends NameValuePair> params) throws RedmineException { final EntityConfig<T> config = getConfig(objectClass); final List<T> result = new ArrayList<T>(); final List<NameValuePair> newParams = new ArrayList<NameValuePair>(params); newParams.add(new BasicNameValuePair("limit", String.valueOf(objectsPerPage))); int offset = 0; int totalObjectsFoundOnServer; do { List<NameValuePair> paramsList = new ArrayList<NameValuePair>(newParams); paramsList.add(new BasicNameValuePair("offset", String.valueOf(offset))); final URI uri = getURIConfigurator().getObjectsURI(objectClass, paramsList); logger.debug(uri.toString()); final HttpGet http = new HttpGet(uri); final String response = send(http); logger.debug("received: " + response); final List<T> foundItems; try { final JSONObject responseObject = RedmineJSONParser.getResponse(response); foundItems = JsonInput.getListOrNull(responseObject,config.multiObjectName, config.parser); result.addAll(foundItems); /* Necessary for trackers */ if (!responseObject.has(KEY_TOTAL_COUNT)) { break; } totalObjectsFoundOnServer = JsonInput.getInt(responseObject,KEY_TOTAL_COUNT); } catch (JSONException e) { throw new RedmineFormatException(e); } if (foundItems.size() == 0) { break; } offset += foundItems.size(); } while (offset < totalObjectsFoundOnServer); return result; } /** * This number of objects (tasks, projects, users) will be requested from * Redmine server in 1 request. */ public int getObjectsPerPage() { return objectsPerPage; } public <T> List<T> getChildEntries(Class<?> parentClass, int parentId, Class<T> classs) throws RedmineException { return getChildEntries(parentClass, parentId + "", classs); } /** * Delivers a list of a child entries. * * @param classs * target class. */ public <T> List<T> getChildEntries(Class<?> parentClass, String parentKey, Class<T> classs) throws RedmineException { final EntityConfig<T> config = getConfig(classs); final URI uri = getURIConfigurator().getChildObjectsURI(parentClass, parentKey, classs, new BasicNameValuePair("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> T getChildEntry(Class<?> parentClass, String parentId, Class<T> classs, String childId, NameValuePair... params) throws RedmineException { final EntityConfig<T> 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 static <T> T parseResponse(String response, String tag, JsonObjectParser<T> parser) throws RedmineFormatException { try { return parser.parse(RedmineJSONParser.getResponseSingleObject(response, tag)); } 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 <T> EntityConfig<T> getConfig(Class<?> class1) { final EntityConfig<?> guess = OBJECT_CONFIGS.get(class1); if (guess == null) throw new RedmineInternalError("Unsupported class " + class1); return (EntityConfig<T>) guess; } private URIConfigurator getURIConfigurator() { return configurator; } private static <T> EntityConfig<T> config(String objectField, String urlPrefix, JsonObjectWriter<T> writer, JsonObjectParser<T> parser) { return new EntityConfig<T>(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 <a href="http://www.redmine.org/issues/11755">Redmine issue 11755</a> */ public void setOnBehalfOfUser(String loginName) { this.onBehalfOfUser = loginName; } /** * Entity config. */ static class EntityConfig<T> { final String singleObjectName; final String multiObjectName; final JsonObjectWriter<T> writer; final JsonObjectParser<T> parser; public EntityConfig(String objectField, String urlPrefix, JsonObjectWriter<T> writer, JsonObjectParser<T> parser) { super(); this.singleObjectName = objectField; this.multiObjectName = urlPrefix; this.writer = writer; this.parser = parser; } } }