PageRenderTime 50ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

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

https://bitbucket.org/mrdon/jira-rest-client-p3
Java | 388 lines | 325 code | 45 blank | 18 comment | 46 complexity | 7e96eee30ba0619e46290fb79a59305a 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 JsonObjectParser<Issue>
  84. {
  85. private static Set<String> SPECIAL_FIELDS = Sets.newHashSet(IssueFieldId.ids());
  86. public static final String SCHEMA_SECTION = "schema";
  87. public static final String NAMES_SECTION = "names";
  88. private final IssueLinkJsonParser issueLinkJsonParser = new IssueLinkJsonParser();
  89. private final IssueLinkJsonParserV5 issueLinkJsonParserV5 = new IssueLinkJsonParserV5();
  90. private final BasicVotesJsonParser votesJsonParser = new BasicVotesJsonParser();
  91. private final BasicStatusJsonParser statusJsonParser = new BasicStatusJsonParser();
  92. private final WorklogJsonParser worklogJsonParser = new WorklogJsonParser();
  93. private final JsonObjectParser<BasicWatchers> watchersJsonParser
  94. = WatchersJsonParserBuilder.createBasicWatchersParser();
  95. private final VersionJsonParser versionJsonParser = new VersionJsonParser();
  96. private final BasicComponentJsonParser basicComponentJsonParser = new BasicComponentJsonParser();
  97. private final AttachmentJsonParser attachmentJsonParser = new AttachmentJsonParser();
  98. private final JsonFieldParser fieldParser = new JsonFieldParser();
  99. private final CommentJsonParser commentJsonParser = new CommentJsonParser();
  100. private final BasicIssueTypeJsonParser issueTypeJsonParser = new BasicIssueTypeJsonParser();
  101. private final BasicProjectJsonParser projectJsonParser = new BasicProjectJsonParser();
  102. private final BasicPriorityJsonParser priorityJsonParser = new BasicPriorityJsonParser();
  103. private final BasicResolutionJsonParser resolutionJsonParser = new BasicResolutionJsonParser();
  104. private final BasicUserJsonParser userJsonParser = new BasicUserJsonParser();
  105. private final SubtaskJsonParser subtaskJsonParser = new SubtaskJsonParser();
  106. private final ChangelogJsonParser changelogJsonParser = new ChangelogJsonParser();
  107. private final JsonWeakParserForString jsonWeakParserForString = new JsonWeakParserForString();
  108. private static final String FIELDS = "fields";
  109. private static final String VALUE_ATTR = "value";
  110. static Iterable<String> parseExpandos(JSONObject json) throws JSONException {
  111. final String expando = json.getString("expand");
  112. return Splitter.on(',').split(expando);
  113. }
  114. private <T> Collection<T> parseArray(JSONObject jsonObject, JsonWeakParser<T> jsonParser, String arrayAttribute)
  115. throws JSONException {
  116. // String type = jsonObject.getString("type");
  117. // final String name = jsonObject.getString("name");
  118. final JSONArray valueObject = jsonObject.optJSONArray(arrayAttribute);
  119. if (valueObject == null) {
  120. return new ArrayList<T>();
  121. }
  122. Collection<T> res = new ArrayList<T>(valueObject.length());
  123. for (int i = 0; i < valueObject.length(); i++) {
  124. res.add(jsonParser.parse(valueObject.get(i)));
  125. }
  126. return res;
  127. }
  128. private <T> Collection<T> parseOptionalArrayNotNullable(boolean shouldUseNestedValueJson, JSONObject json, JsonWeakParser<T> jsonParser, String... path)
  129. throws JSONException {
  130. Collection<T> res = parseOptionalArray(shouldUseNestedValueJson, json, jsonParser, path);
  131. return res == null ? Collections.<T>emptyList() : res;
  132. }
  133. @Nullable
  134. private <T> Collection<T> parseOptionalArray(boolean shouldUseNestedValueJson, JSONObject json, JsonWeakParser<T> jsonParser, String... path)
  135. throws JSONException {
  136. if (shouldUseNestedValueJson) {
  137. final JSONObject js = JsonParseUtil.getNestedOptionalObject(json, path);
  138. if (js == null) {
  139. return null;
  140. }
  141. return parseArray(js, jsonParser, VALUE_ATTR);
  142. } else {
  143. final JSONArray jsonArray = JsonParseUtil.getNestedOptionalArray(json, path);
  144. if (jsonArray == null) {
  145. return null;
  146. }
  147. final Collection<T> res = new ArrayList<T>(jsonArray.length());
  148. for (int i = 0; i < jsonArray.length(); i++) {
  149. res.add(jsonParser.parse(jsonArray.get(i)));
  150. }
  151. return res;
  152. }
  153. }
  154. private String getFieldStringValue(JSONObject json, String attributeName) throws JSONException {
  155. final JSONObject fieldsJson = json.getJSONObject(FIELDS);
  156. final Object summaryObject = fieldsJson.get(attributeName);
  157. if (summaryObject instanceof JSONObject) { // pre JIRA 5.0 way
  158. return ((JSONObject) summaryObject).getString(VALUE_ATTR);
  159. }
  160. if (summaryObject instanceof String) { // JIRA 5.0 way
  161. return (String) summaryObject;
  162. }
  163. throw new JSONException("Cannot parse [" + attributeName + "] from available fields");
  164. }
  165. private JSONObject getFieldUnisex(JSONObject json, String attributeName) throws JSONException {
  166. final JSONObject fieldsJson = json.getJSONObject(FIELDS);
  167. final JSONObject fieldJson = fieldsJson.getJSONObject(attributeName);
  168. if (fieldJson.has(VALUE_ATTR)) {
  169. return fieldJson.getJSONObject(VALUE_ATTR); // pre 5.0 way
  170. } else {
  171. return fieldJson; // JIRA 5.0 way
  172. }
  173. }
  174. @Nullable
  175. private String getOptionalFieldStringUnisex(boolean shouldUseNestedValueJson, JSONObject json, String attributeName)
  176. throws JSONException {
  177. final JSONObject fieldsJson = json.getJSONObject(FIELDS);
  178. if (shouldUseNestedValueJson) {
  179. final JSONObject fieldJson = fieldsJson.optJSONObject(attributeName);
  180. if (fieldJson != null) {
  181. return JsonParseUtil.getOptionalString(fieldJson, VALUE_ATTR); // pre 5.0 way
  182. } else {
  183. return null;
  184. }
  185. }
  186. return JsonParseUtil.getOptionalString(fieldsJson, attributeName);
  187. }
  188. private String getFieldStringUnisex(JSONObject json, String attributeName) throws JSONException {
  189. final JSONObject fieldsJson = json.getJSONObject(FIELDS);
  190. final Object fieldJson = fieldsJson.get(attributeName);
  191. if (fieldJson instanceof JSONObject) {
  192. return ((JSONObject) fieldJson).getString(VALUE_ATTR); // pre 5.0 way
  193. }
  194. return fieldJson.toString(); // JIRA 5.0 way
  195. }
  196. @Override
  197. public Issue parse(JSONObject s) throws JSONException {
  198. final Iterable<String> expandos = parseExpandos(s);
  199. final boolean isJira5x0OrNewer = Iterables.contains(expandos, SCHEMA_SECTION);
  200. final boolean shouldUseNestedValueAttribute = !isJira5x0OrNewer;
  201. final Collection<Comment> comments;
  202. if (isJira5x0OrNewer) {
  203. final JSONObject commentsJson = s.getJSONObject(FIELDS).getJSONObject(COMMENT_FIELD.id);
  204. comments = parseArray(commentsJson, new JsonWeakParserForJsonObject<Comment>(commentJsonParser), "comments");
  205. } else {
  206. final Collection<Comment> commentsTmp = parseOptionalArray(
  207. shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Comment>(commentJsonParser), FIELDS, COMMENT_FIELD.id);
  208. comments = commentsTmp != null ? commentsTmp : Lists.<Comment>newArrayList();
  209. }
  210. final String summary = getFieldStringValue(s, SUMMARY_FIELD.id);
  211. final String description = getOptionalFieldStringUnisex(shouldUseNestedValueAttribute, s, DESCRIPTION_FIELD.id);
  212. final Collection<Attachment> attachments = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Attachment>(attachmentJsonParser), FIELDS, ATTACHMENT_FIELD.id);
  213. final Collection<Field> fields = isJira5x0OrNewer ? parseFieldsJira5x0(s) : parseFields(s.getJSONObject(FIELDS));
  214. final BasicIssueType issueType = issueTypeJsonParser.parse(getFieldUnisex(s, ISSUE_TYPE_FIELD.id));
  215. final DateTime creationDate = JsonParseUtil.parseDateTime(getFieldStringUnisex(s, CREATED_FIELD.id));
  216. final DateTime updateDate = JsonParseUtil.parseDateTime(getFieldStringUnisex(s, UPDATED_FIELD.id));
  217. final String dueDateString = getOptionalFieldStringUnisex(shouldUseNestedValueAttribute, s, DUE_DATE_FIELD.id);
  218. final DateTime dueDate = dueDateString == null ? null : JsonParseUtil.parseDateTimeOrDate(dueDateString);
  219. final BasicPriority priority = getOptionalField(shouldUseNestedValueAttribute, s, PRIORITY_FIELD.id, priorityJsonParser);
  220. final BasicResolution resolution = getOptionalField(shouldUseNestedValueAttribute, s, RESOLUTION_FIELD.id, resolutionJsonParser);
  221. final BasicUser assignee = getOptionalField(shouldUseNestedValueAttribute, s, ASSIGNEE_FIELD.id, userJsonParser);
  222. final BasicUser reporter = getOptionalField(shouldUseNestedValueAttribute, s, REPORTER_FIELD.id, userJsonParser);
  223. final String transitionsUri = getOptionalFieldStringUnisex(shouldUseNestedValueAttribute, s, TRANSITIONS_FIELD.id);
  224. final BasicProject project = projectJsonParser.parse(getFieldUnisex(s, PROJECT_FIELD.id));
  225. final Collection<IssueLink> issueLinks;
  226. if (isJira5x0OrNewer) {
  227. issueLinks = parseOptionalArray(shouldUseNestedValueAttribute, s,
  228. new JsonWeakParserForJsonObject<IssueLink>(issueLinkJsonParserV5), FIELDS, LINKS_FIELD.id);
  229. } else {
  230. issueLinks = parseOptionalArray(shouldUseNestedValueAttribute, s,
  231. new JsonWeakParserForJsonObject<IssueLink>(issueLinkJsonParser), FIELDS, LINKS_PRE_5_0_FIELD.id);
  232. }
  233. Collection<Subtask> subtasks = null;
  234. if (isJira5x0OrNewer) {
  235. subtasks = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Subtask>(subtaskJsonParser), FIELDS, SUBTASKS_FIELD.id);
  236. }
  237. final BasicVotes votes = getOptionalField(shouldUseNestedValueAttribute, s, VOTES_FIELD.id, votesJsonParser);
  238. final BasicStatus status = statusJsonParser.parse(getFieldUnisex(s, STATUS_FIELD.id));
  239. final Collection<Version> fixVersions = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Version>(versionJsonParser), FIELDS, FIX_VERSIONS_FIELD.id);
  240. final Collection<Version> affectedVersions = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Version>(versionJsonParser), FIELDS, AFFECTS_VERSIONS_FIELD.id);
  241. final Collection<BasicComponent> components = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<BasicComponent>(basicComponentJsonParser), FIELDS, COMPONENTS_FIELD.id);
  242. final Collection<Worklog> worklogs;
  243. if (isJira5x0OrNewer) {
  244. if (JsonParseUtil.getNestedOptionalObject(s, FIELDS, WORKLOG_FIELD.id) != null) {
  245. worklogs = parseOptionalArray(shouldUseNestedValueAttribute, s,
  246. new JsonWeakParserForJsonObject<Worklog>(new WorklogJsonParserV5(JsonParseUtil.getSelfUri(s))),
  247. FIELDS, WORKLOG_FIELD.id, WORKLOGS_FIELD.id);
  248. } else {
  249. worklogs = Collections.emptyList();
  250. }
  251. } else {
  252. worklogs = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Worklog>(worklogJsonParser), FIELDS, WORKLOG_FIELD.id);
  253. }
  254. final BasicWatchers watchers = getOptionalField(shouldUseNestedValueAttribute, s,
  255. isJira5x0OrNewer ? WATCHER_FIELD.id : WATCHER_PRE_5_0_FIELD.id, watchersJsonParser);
  256. final TimeTracking timeTracking = getOptionalField(shouldUseNestedValueAttribute, s, TIMETRACKING_FIELD.id,
  257. isJira5x0OrNewer ? new TimeTrackingJsonParserV5() : new TimeTrackingJsonParser());
  258. final Set<String> labels = Sets.newHashSet(parseOptionalArrayNotNullable(shouldUseNestedValueAttribute, s,
  259. jsonWeakParserForString, FIELDS, LABELS_FIELD.id));
  260. final Collection<ChangelogGroup> changelog = parseOptionalArray(false, s, new JsonWeakParserForJsonObject<ChangelogGroup>(changelogJsonParser), "changelog", "histories");
  261. return new Issue(summary, JsonParseUtil.getSelfUri(s), s.getString("key"), project, issueType, status,
  262. description, priority, resolution, attachments, reporter, assignee, creationDate, updateDate,
  263. dueDate, affectedVersions, fixVersions, components, timeTracking, fields, comments,
  264. transitionsUri != null ? JsonParseUtil.parseURI(transitionsUri) : null, issueLinks,
  265. votes, worklogs, watchers, expandos, subtasks, changelog, labels);
  266. }
  267. @Nullable
  268. private <T> T getOptionalField(boolean shouldUseNestedValue, JSONObject s, final String fieldId, JsonObjectParser<T> jsonParser)
  269. throws JSONException {
  270. final JSONObject fieldJson = JsonParseUtil.getNestedOptionalObject(s, FIELDS, fieldId);
  271. // for fields like assignee (when unassigned) value attribute may be missing completely
  272. if (fieldJson != null) {
  273. if (shouldUseNestedValue) {
  274. final JSONObject valueJsonObject = fieldJson.optJSONObject(VALUE_ATTR);
  275. if (valueJsonObject != null) {
  276. return jsonParser.parse(valueJsonObject);
  277. }
  278. } else {
  279. return jsonParser.parse(fieldJson);
  280. }
  281. }
  282. return null;
  283. }
  284. private Collection<Field> parseFieldsJira5x0(JSONObject issueJson) throws JSONException {
  285. final JSONObject names = issueJson.optJSONObject(NAMES_SECTION);
  286. final Map<String, String> namesMap = parseNames(names);
  287. final JSONObject types = issueJson.optJSONObject(SCHEMA_SECTION);
  288. final Map<String, String> typesMap = parseSchema(types);
  289. final JSONObject json = issueJson.getJSONObject(FIELDS);
  290. final ArrayList<Field> res = new ArrayList<Field>(json.length());
  291. @SuppressWarnings("unchecked")
  292. final Iterator<String> iterator = json.keys();
  293. while (iterator.hasNext()) {
  294. final String key = iterator.next();
  295. try {
  296. if (SPECIAL_FIELDS.contains(key)) {
  297. continue;
  298. }
  299. final Object value = json.opt(key);
  300. res.add(new Field(key, namesMap.get(key), typesMap.get("key"), value != JSONObject.NULL ? value : null));
  301. } catch (final Exception e) {
  302. throw new JSONException("Error while parsing [" + key + "] field: " + e.getMessage()) {
  303. @Override
  304. public Throwable getCause() {
  305. return e;
  306. }
  307. };
  308. }
  309. }
  310. return res;
  311. }
  312. private Map<String, String> parseSchema(JSONObject json) throws JSONException {
  313. final HashMap<String, String> res = Maps.newHashMap();
  314. final Iterator<String> it = getStringKeys(json);
  315. while (it.hasNext()) {
  316. final String fieldId = it.next();
  317. JSONObject fieldDefinition = json.getJSONObject(fieldId);
  318. res.put(fieldId, fieldDefinition.getString("type"));
  319. }
  320. return res;
  321. }
  322. private Map<String, String> parseNames(JSONObject json) throws JSONException {
  323. final HashMap<String, String> res = Maps.newHashMap();
  324. final Iterator<String> iterator = getStringKeys(json);
  325. while (iterator.hasNext()) {
  326. final String key = iterator.next();
  327. res.put(key, json.getString(key));
  328. }
  329. return res;
  330. }
  331. private Collection<Field> parseFields(JSONObject json) throws JSONException {
  332. ArrayList<Field> res = new ArrayList<Field>(json.length());
  333. final Iterator<String> iterator = getStringKeys(json);
  334. while (iterator.hasNext()) {
  335. final String key = iterator.next();
  336. if (SPECIAL_FIELDS.contains(key)) {
  337. continue;
  338. }
  339. res.add(fieldParser.parse(json.getJSONObject(key), key));
  340. }
  341. return res;
  342. }
  343. }