PageRenderTime 56ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/src/main/java/com/atlassian/jira/rest/client/internal/json/IssueJsonParser.java

https://bitbucket.org/sleberrigaud_atlassian/jira-rest-java-client
Java | 387 lines | 324 code | 45 blank | 18 comment | 46 complexity | 92de8358e8e1405d0f258992a690bb3d MD5 | raw file
  1. /*
  2. * Copyright (C) 2010 Atlassian
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.atlassian.jira.rest.client.internal.json;
  17. import com.atlassian.jira.rest.client.domain.Attachment;
  18. import com.atlassian.jira.rest.client.domain.BasicComponent;
  19. import com.atlassian.jira.rest.client.domain.BasicIssueType;
  20. import com.atlassian.jira.rest.client.domain.BasicPriority;
  21. import com.atlassian.jira.rest.client.domain.BasicProject;
  22. import com.atlassian.jira.rest.client.domain.BasicResolution;
  23. import com.atlassian.jira.rest.client.domain.BasicStatus;
  24. import com.atlassian.jira.rest.client.domain.BasicUser;
  25. import com.atlassian.jira.rest.client.domain.BasicVotes;
  26. import com.atlassian.jira.rest.client.domain.BasicWatchers;
  27. import com.atlassian.jira.rest.client.domain.ChangelogGroup;
  28. import com.atlassian.jira.rest.client.domain.Comment;
  29. import com.atlassian.jira.rest.client.domain.Field;
  30. import com.atlassian.jira.rest.client.domain.Issue;
  31. import com.atlassian.jira.rest.client.domain.IssueFieldId;
  32. import com.atlassian.jira.rest.client.domain.IssueLink;
  33. import com.atlassian.jira.rest.client.domain.Subtask;
  34. import com.atlassian.jira.rest.client.domain.TimeTracking;
  35. import com.atlassian.jira.rest.client.domain.Version;
  36. import com.atlassian.jira.rest.client.domain.Worklog;
  37. import com.google.common.base.Splitter;
  38. import com.google.common.collect.Iterables;
  39. import com.google.common.collect.Lists;
  40. import com.google.common.collect.Maps;
  41. import com.google.common.collect.Sets;
  42. import org.codehaus.jettison.json.JSONArray;
  43. import org.codehaus.jettison.json.JSONException;
  44. import org.codehaus.jettison.json.JSONObject;
  45. import org.joda.time.DateTime;
  46. import javax.annotation.Nullable;
  47. import java.util.ArrayList;
  48. import java.util.Collection;
  49. import java.util.Collections;
  50. import java.util.HashMap;
  51. import java.util.Iterator;
  52. import java.util.Map;
  53. import java.util.Set;
  54. import static com.atlassian.jira.rest.client.domain.IssueFieldId.AFFECTS_VERSIONS_FIELD;
  55. import static com.atlassian.jira.rest.client.domain.IssueFieldId.ASSIGNEE_FIELD;
  56. import static com.atlassian.jira.rest.client.domain.IssueFieldId.ATTACHMENT_FIELD;
  57. import static com.atlassian.jira.rest.client.domain.IssueFieldId.COMMENT_FIELD;
  58. import static com.atlassian.jira.rest.client.domain.IssueFieldId.COMPONENTS_FIELD;
  59. import static com.atlassian.jira.rest.client.domain.IssueFieldId.CREATED_FIELD;
  60. import static com.atlassian.jira.rest.client.domain.IssueFieldId.DESCRIPTION_FIELD;
  61. import static com.atlassian.jira.rest.client.domain.IssueFieldId.DUE_DATE_FIELD;
  62. import static com.atlassian.jira.rest.client.domain.IssueFieldId.FIX_VERSIONS_FIELD;
  63. import static com.atlassian.jira.rest.client.domain.IssueFieldId.ISSUE_TYPE_FIELD;
  64. import static com.atlassian.jira.rest.client.domain.IssueFieldId.LABELS_FIELD;
  65. import static com.atlassian.jira.rest.client.domain.IssueFieldId.LINKS_FIELD;
  66. import static com.atlassian.jira.rest.client.domain.IssueFieldId.LINKS_PRE_5_0_FIELD;
  67. import static com.atlassian.jira.rest.client.domain.IssueFieldId.PRIORITY_FIELD;
  68. import static com.atlassian.jira.rest.client.domain.IssueFieldId.PROJECT_FIELD;
  69. import static com.atlassian.jira.rest.client.domain.IssueFieldId.REPORTER_FIELD;
  70. import static com.atlassian.jira.rest.client.domain.IssueFieldId.RESOLUTION_FIELD;
  71. import static com.atlassian.jira.rest.client.domain.IssueFieldId.STATUS_FIELD;
  72. import static com.atlassian.jira.rest.client.domain.IssueFieldId.SUBTASKS_FIELD;
  73. import static com.atlassian.jira.rest.client.domain.IssueFieldId.SUMMARY_FIELD;
  74. import static com.atlassian.jira.rest.client.domain.IssueFieldId.TIMETRACKING_FIELD;
  75. import static com.atlassian.jira.rest.client.domain.IssueFieldId.TRANSITIONS_FIELD;
  76. import static com.atlassian.jira.rest.client.domain.IssueFieldId.UPDATED_FIELD;
  77. import static com.atlassian.jira.rest.client.domain.IssueFieldId.VOTES_FIELD;
  78. import static com.atlassian.jira.rest.client.domain.IssueFieldId.WATCHER_FIELD;
  79. import static com.atlassian.jira.rest.client.domain.IssueFieldId.WATCHER_PRE_5_0_FIELD;
  80. import static com.atlassian.jira.rest.client.domain.IssueFieldId.WORKLOGS_FIELD;
  81. import static com.atlassian.jira.rest.client.domain.IssueFieldId.WORKLOG_FIELD;
  82. import static com.atlassian.jira.rest.client.internal.json.JsonParseUtil.getStringKeys;
  83. public class IssueJsonParser implements JsonParser<Issue> {
  84. private static Set<String> SPECIAL_FIELDS = Sets.newHashSet(IssueFieldId.ids());
  85. public static final String SCHEMA_SECTION = "schema";
  86. public static final String NAMES_SECTION = "names";
  87. private final IssueLinkJsonParser issueLinkJsonParser = new IssueLinkJsonParser();
  88. private final IssueLinkJsonParserV5 issueLinkJsonParserV5 = new IssueLinkJsonParserV5();
  89. private final BasicVotesJsonParser votesJsonParser = new BasicVotesJsonParser();
  90. private final BasicStatusJsonParser statusJsonParser = new BasicStatusJsonParser();
  91. private final WorklogJsonParser worklogJsonParser = new WorklogJsonParser();
  92. private final JsonParser<BasicWatchers> watchersJsonParser
  93. = WatchersJsonParserBuilder.createBasicWatchersParser();
  94. private final VersionJsonParser versionJsonParser = new VersionJsonParser();
  95. private final BasicComponentJsonParser basicComponentJsonParser = new BasicComponentJsonParser();
  96. private final AttachmentJsonParser attachmentJsonParser = new AttachmentJsonParser();
  97. private final JsonFieldParser fieldParser = new JsonFieldParser();
  98. private final CommentJsonParser commentJsonParser = new CommentJsonParser();
  99. private final BasicIssueTypeJsonParser issueTypeJsonParser = new BasicIssueTypeJsonParser();
  100. private final BasicProjectJsonParser projectJsonParser = new BasicProjectJsonParser();
  101. private final BasicPriorityJsonParser priorityJsonParser = new BasicPriorityJsonParser();
  102. private final BasicResolutionJsonParser resolutionJsonParser = new BasicResolutionJsonParser();
  103. private final BasicUserJsonParser userJsonParser = new BasicUserJsonParser();
  104. private final SubtaskJsonParser subtaskJsonParser = new SubtaskJsonParser();
  105. private final ChangelogJsonParser changelogJsonParser = new ChangelogJsonParser();
  106. private final JsonWeakParserForString jsonWeakParserForString = new JsonWeakParserForString();
  107. private static final String FIELDS = "fields";
  108. private static final String VALUE_ATTR = "value";
  109. static Iterable<String> parseExpandos(JSONObject json) throws JSONException {
  110. final String expando = json.getString("expand");
  111. return Splitter.on(',').split(expando);
  112. }
  113. private <T> Collection<T> parseArray(JSONObject jsonObject, JsonWeakParser<T> jsonParser, String arrayAttribute)
  114. throws JSONException {
  115. // String type = jsonObject.getString("type");
  116. // final String name = jsonObject.getString("name");
  117. final JSONArray valueObject = jsonObject.optJSONArray(arrayAttribute);
  118. if (valueObject == null) {
  119. return new ArrayList<T>();
  120. }
  121. Collection<T> res = new ArrayList<T>(valueObject.length());
  122. for (int i = 0; i < valueObject.length(); i++) {
  123. res.add(jsonParser.parse(valueObject.get(i)));
  124. }
  125. return res;
  126. }
  127. private <T> Collection<T> parseOptionalArrayNotNullable(boolean shouldUseNestedValueJson, JSONObject json, JsonWeakParser<T> jsonParser, String... path)
  128. throws JSONException {
  129. Collection<T> res = parseOptionalArray(shouldUseNestedValueJson, json, jsonParser, path);
  130. return res == null ? Collections.<T>emptyList() : res;
  131. }
  132. @Nullable
  133. private <T> Collection<T> parseOptionalArray(boolean shouldUseNestedValueJson, JSONObject json, JsonWeakParser<T> jsonParser, String... path)
  134. throws JSONException {
  135. if (shouldUseNestedValueJson) {
  136. final JSONObject js = JsonParseUtil.getNestedOptionalObject(json, path);
  137. if (js == null) {
  138. return null;
  139. }
  140. return parseArray(js, jsonParser, VALUE_ATTR);
  141. } else {
  142. final JSONArray jsonArray = JsonParseUtil.getNestedOptionalArray(json, path);
  143. if (jsonArray == null) {
  144. return null;
  145. }
  146. final Collection<T> res = new ArrayList<T>(jsonArray.length());
  147. for (int i = 0; i < jsonArray.length(); i++) {
  148. res.add(jsonParser.parse(jsonArray.get(i)));
  149. }
  150. return res;
  151. }
  152. }
  153. private String getFieldStringValue(JSONObject json, String attributeName) throws JSONException {
  154. final JSONObject fieldsJson = json.getJSONObject(FIELDS);
  155. final Object summaryObject = fieldsJson.get(attributeName);
  156. if (summaryObject instanceof JSONObject) { // pre JIRA 5.0 way
  157. return ((JSONObject) summaryObject).getString(VALUE_ATTR);
  158. }
  159. if (summaryObject instanceof String) { // JIRA 5.0 way
  160. return (String) summaryObject;
  161. }
  162. throw new JSONException("Cannot parse [" + attributeName + "] from available fields");
  163. }
  164. private JSONObject getFieldUnisex(JSONObject json, String attributeName) throws JSONException {
  165. final JSONObject fieldsJson = json.getJSONObject(FIELDS);
  166. final JSONObject fieldJson = fieldsJson.getJSONObject(attributeName);
  167. if (fieldJson.has(VALUE_ATTR)) {
  168. return fieldJson.getJSONObject(VALUE_ATTR); // pre 5.0 way
  169. } else {
  170. return fieldJson; // JIRA 5.0 way
  171. }
  172. }
  173. @Nullable
  174. private String getOptionalFieldStringUnisex(boolean shouldUseNestedValueJson, JSONObject json, String attributeName)
  175. throws JSONException {
  176. final JSONObject fieldsJson = json.getJSONObject(FIELDS);
  177. if (shouldUseNestedValueJson) {
  178. final JSONObject fieldJson = fieldsJson.optJSONObject(attributeName);
  179. if (fieldJson != null) {
  180. return JsonParseUtil.getOptionalString(fieldJson, VALUE_ATTR); // pre 5.0 way
  181. } else {
  182. return null;
  183. }
  184. }
  185. return JsonParseUtil.getOptionalString(fieldsJson, attributeName);
  186. }
  187. private String getFieldStringUnisex(JSONObject json, String attributeName) throws JSONException {
  188. final JSONObject fieldsJson = json.getJSONObject(FIELDS);
  189. final Object fieldJson = fieldsJson.get(attributeName);
  190. if (fieldJson instanceof JSONObject) {
  191. return ((JSONObject) fieldJson).getString(VALUE_ATTR); // pre 5.0 way
  192. }
  193. return fieldJson.toString(); // JIRA 5.0 way
  194. }
  195. @Override
  196. public Issue parse(JSONObject s) throws JSONException {
  197. final Iterable<String> expandos = parseExpandos(s);
  198. final boolean isJira5x0OrNewer = Iterables.contains(expandos, SCHEMA_SECTION);
  199. final boolean shouldUseNestedValueAttribute = !isJira5x0OrNewer;
  200. final Collection<Comment> comments;
  201. if (isJira5x0OrNewer) {
  202. final JSONObject commentsJson = s.getJSONObject(FIELDS).getJSONObject(COMMENT_FIELD.id);
  203. comments = parseArray(commentsJson, new JsonWeakParserForJsonObject<Comment>(commentJsonParser), "comments");
  204. } else {
  205. final Collection<Comment> commentsTmp = parseOptionalArray(
  206. shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Comment>(commentJsonParser), FIELDS, COMMENT_FIELD.id);
  207. comments = commentsTmp != null ? commentsTmp : Lists.<Comment>newArrayList();
  208. }
  209. final String summary = getFieldStringValue(s, SUMMARY_FIELD.id);
  210. final String description = getOptionalFieldStringUnisex(shouldUseNestedValueAttribute, s, DESCRIPTION_FIELD.id);
  211. final Collection<Attachment> attachments = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Attachment>(attachmentJsonParser), FIELDS, ATTACHMENT_FIELD.id);
  212. final Collection<Field> fields = isJira5x0OrNewer ? parseFieldsJira5x0(s) : parseFields(s.getJSONObject(FIELDS));
  213. final BasicIssueType issueType = issueTypeJsonParser.parse(getFieldUnisex(s, ISSUE_TYPE_FIELD.id));
  214. final DateTime creationDate = JsonParseUtil.parseDateTime(getFieldStringUnisex(s, CREATED_FIELD.id));
  215. final DateTime updateDate = JsonParseUtil.parseDateTime(getFieldStringUnisex(s, UPDATED_FIELD.id));
  216. final String dueDateString = getOptionalFieldStringUnisex(shouldUseNestedValueAttribute, s, DUE_DATE_FIELD.id);
  217. final DateTime dueDate = dueDateString == null ? null : JsonParseUtil.parseDateTimeOrDate(dueDateString);
  218. final BasicPriority priority = getOptionalField(shouldUseNestedValueAttribute, s, PRIORITY_FIELD.id, priorityJsonParser);
  219. final BasicResolution resolution = getOptionalField(shouldUseNestedValueAttribute, s, RESOLUTION_FIELD.id, resolutionJsonParser);
  220. final BasicUser assignee = getOptionalField(shouldUseNestedValueAttribute, s, ASSIGNEE_FIELD.id, userJsonParser);
  221. final BasicUser reporter = getOptionalField(shouldUseNestedValueAttribute, s, REPORTER_FIELD.id, userJsonParser);
  222. final String transitionsUri = getOptionalFieldStringUnisex(shouldUseNestedValueAttribute, s, TRANSITIONS_FIELD.id);
  223. final BasicProject project = projectJsonParser.parse(getFieldUnisex(s, PROJECT_FIELD.id));
  224. final Collection<IssueLink> issueLinks;
  225. if (isJira5x0OrNewer) {
  226. issueLinks = parseOptionalArray(shouldUseNestedValueAttribute, s,
  227. new JsonWeakParserForJsonObject<IssueLink>(issueLinkJsonParserV5), FIELDS, LINKS_FIELD.id);
  228. } else {
  229. issueLinks = parseOptionalArray(shouldUseNestedValueAttribute, s,
  230. new JsonWeakParserForJsonObject<IssueLink>(issueLinkJsonParser), FIELDS, LINKS_PRE_5_0_FIELD.id);
  231. }
  232. Collection<Subtask> subtasks = null;
  233. if (isJira5x0OrNewer) {
  234. subtasks = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Subtask>(subtaskJsonParser), FIELDS, SUBTASKS_FIELD.id);
  235. }
  236. final BasicVotes votes = getOptionalField(shouldUseNestedValueAttribute, s, VOTES_FIELD.id, votesJsonParser);
  237. final BasicStatus status = statusJsonParser.parse(getFieldUnisex(s, STATUS_FIELD.id));
  238. final Collection<Version> fixVersions = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Version>(versionJsonParser), FIELDS, FIX_VERSIONS_FIELD.id);
  239. final Collection<Version> affectedVersions = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Version>(versionJsonParser), FIELDS, AFFECTS_VERSIONS_FIELD.id);
  240. final Collection<BasicComponent> components = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<BasicComponent>(basicComponentJsonParser), FIELDS, COMPONENTS_FIELD.id);
  241. final Collection<Worklog> worklogs;
  242. if (isJira5x0OrNewer) {
  243. if (JsonParseUtil.getNestedOptionalObject(s, FIELDS, WORKLOG_FIELD.id) != null) {
  244. worklogs = parseOptionalArray(shouldUseNestedValueAttribute, s,
  245. new JsonWeakParserForJsonObject<Worklog>(new WorklogJsonParserV5(JsonParseUtil.getSelfUri(s))),
  246. FIELDS, WORKLOG_FIELD.id, WORKLOGS_FIELD.id);
  247. } else {
  248. worklogs = Collections.emptyList();
  249. }
  250. } else {
  251. worklogs = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Worklog>(worklogJsonParser), FIELDS, WORKLOG_FIELD.id);
  252. }
  253. final BasicWatchers watchers = getOptionalField(shouldUseNestedValueAttribute, s,
  254. isJira5x0OrNewer ? WATCHER_FIELD.id : WATCHER_PRE_5_0_FIELD.id, watchersJsonParser);
  255. final TimeTracking timeTracking = getOptionalField(shouldUseNestedValueAttribute, s, TIMETRACKING_FIELD.id,
  256. isJira5x0OrNewer ? new TimeTrackingJsonParserV5() : new TimeTrackingJsonParser());
  257. final Set<String> labels = Sets.newHashSet(parseOptionalArrayNotNullable(shouldUseNestedValueAttribute, s,
  258. jsonWeakParserForString, FIELDS, LABELS_FIELD.id));
  259. final Collection<ChangelogGroup> changelog = parseOptionalArray(false, s, new JsonWeakParserForJsonObject<ChangelogGroup>(changelogJsonParser), "changelog", "histories");
  260. return new Issue(summary, JsonParseUtil.getSelfUri(s), s.getString("key"), project, issueType, status,
  261. description, priority, resolution, attachments, reporter, assignee, creationDate, updateDate,
  262. dueDate, affectedVersions, fixVersions, components, timeTracking, fields, comments,
  263. transitionsUri != null ? JsonParseUtil.parseURI(transitionsUri) : null, issueLinks,
  264. votes, worklogs, watchers, expandos, subtasks, changelog, labels);
  265. }
  266. @Nullable
  267. private <T> T getOptionalField(boolean shouldUseNestedValue, JSONObject s, final String fieldId, JsonParser<T> jsonParser)
  268. throws JSONException {
  269. final JSONObject fieldJson = JsonParseUtil.getNestedOptionalObject(s, FIELDS, fieldId);
  270. // for fields like assignee (when unassigned) value attribute may be missing completely
  271. if (fieldJson != null) {
  272. if (shouldUseNestedValue) {
  273. final JSONObject valueJsonObject = fieldJson.optJSONObject(VALUE_ATTR);
  274. if (valueJsonObject != null) {
  275. return jsonParser.parse(valueJsonObject);
  276. }
  277. } else {
  278. return jsonParser.parse(fieldJson);
  279. }
  280. }
  281. return null;
  282. }
  283. private Collection<Field> parseFieldsJira5x0(JSONObject issueJson) throws JSONException {
  284. final JSONObject names = issueJson.optJSONObject(NAMES_SECTION);
  285. final Map<String, String> namesMap = parseNames(names);
  286. final JSONObject types = issueJson.optJSONObject(SCHEMA_SECTION);
  287. final Map<String, String> typesMap = parseSchema(types);
  288. final JSONObject json = issueJson.getJSONObject(FIELDS);
  289. final ArrayList<Field> res = new ArrayList<Field>(json.length());
  290. @SuppressWarnings("unchecked")
  291. final Iterator<String> iterator = json.keys();
  292. while (iterator.hasNext()) {
  293. final String key = iterator.next();
  294. try {
  295. if (SPECIAL_FIELDS.contains(key)) {
  296. continue;
  297. }
  298. final Object value = json.opt(key);
  299. res.add(new Field(key, namesMap.get(key), typesMap.get("key"), value != JSONObject.NULL ? value : null));
  300. } catch (final Exception e) {
  301. throw new JSONException("Error while parsing [" + key + "] field: " + e.getMessage()) {
  302. @Override
  303. public Throwable getCause() {
  304. return e;
  305. }
  306. };
  307. }
  308. }
  309. return res;
  310. }
  311. private Map<String, String> parseSchema(JSONObject json) throws JSONException {
  312. final HashMap<String, String> res = Maps.newHashMap();
  313. final Iterator<String> it = getStringKeys(json);
  314. while (it.hasNext()) {
  315. final String fieldId = it.next();
  316. JSONObject fieldDefinition = json.getJSONObject(fieldId);
  317. res.put(fieldId, fieldDefinition.getString("type"));
  318. }
  319. return res;
  320. }
  321. private Map<String, String> parseNames(JSONObject json) throws JSONException {
  322. final HashMap<String, String> res = Maps.newHashMap();
  323. final Iterator<String> iterator = getStringKeys(json);
  324. while (iterator.hasNext()) {
  325. final String key = iterator.next();
  326. res.put(key, json.getString(key));
  327. }
  328. return res;
  329. }
  330. private Collection<Field> parseFields(JSONObject json) throws JSONException {
  331. ArrayList<Field> res = new ArrayList<Field>(json.length());
  332. final Iterator<String> iterator = getStringKeys(json);
  333. while (iterator.hasNext()) {
  334. final String key = iterator.next();
  335. if (SPECIAL_FIELDS.contains(key)) {
  336. continue;
  337. }
  338. res.add(fieldParser.parse(json.getJSONObject(key), key));
  339. }
  340. return res;
  341. }
  342. }