1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.atlassian.jira.rest.client.internal.json;
18
19 import com.atlassian.jira.rest.client.domain.Attachment;
20 import com.atlassian.jira.rest.client.domain.BasicComponent;
21 import com.atlassian.jira.rest.client.domain.BasicIssueType;
22 import com.atlassian.jira.rest.client.domain.BasicPriority;
23 import com.atlassian.jira.rest.client.domain.BasicProject;
24 import com.atlassian.jira.rest.client.domain.BasicResolution;
25 import com.atlassian.jira.rest.client.domain.BasicStatus;
26 import com.atlassian.jira.rest.client.domain.BasicUser;
27 import com.atlassian.jira.rest.client.domain.BasicVotes;
28 import com.atlassian.jira.rest.client.domain.BasicWatchers;
29 import com.atlassian.jira.rest.client.domain.ChangelogGroup;
30 import com.atlassian.jira.rest.client.domain.Comment;
31 import com.atlassian.jira.rest.client.domain.Field;
32 import com.atlassian.jira.rest.client.domain.Issue;
33 import com.atlassian.jira.rest.client.domain.IssueLink;
34 import com.atlassian.jira.rest.client.domain.Subtask;
35 import com.atlassian.jira.rest.client.domain.TimeTracking;
36 import com.atlassian.jira.rest.client.domain.Version;
37 import com.atlassian.jira.rest.client.domain.Worklog;
38 import com.google.common.base.Splitter;
39 import com.google.common.collect.Iterables;
40 import com.google.common.collect.Lists;
41 import com.google.common.collect.Maps;
42 import com.google.common.collect.Sets;
43 import org.codehaus.jettison.json.JSONArray;
44 import org.codehaus.jettison.json.JSONException;
45 import org.codehaus.jettison.json.JSONObject;
46 import org.joda.time.DateTime;
47
48 import javax.annotation.Nullable;
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 import java.util.Collection;
52 import java.util.Collections;
53 import java.util.HashMap;
54 import java.util.HashSet;
55 import java.util.Iterator;
56 import java.util.Map;
57 import java.util.Set;
58
59 public class IssueJsonParser implements JsonParser<Issue> {
60 private static final String UPDATED_ATTR = "updated";
61 private static final String CREATED_ATTR = "created";
62 private static final String AFFECTS_VERSIONS_ATTR = "versions";
63 private static final String FIX_VERSIONS_ATTR = "fixVersions";
64 private static final String COMPONENTS_ATTR = "components";
65 private static final String LINKS_ATTR = "links";
66 private static final String LINKS_ATTR_5_0 = "issuelinks";
67 private static final String ISSUE_TYPE_ATTR = "issuetype";
68 private static final String VOTES_ATTR = "votes";
69 private static final String WORKLOG_ATTR = "worklog";
70 private static final String WORKLOGS_ATTR = "worklogs";
71 private static final String WATCHER_ATTR = "watcher";
72 private static final String WATCHER_ATTR_5_0 = "watches";
73 private static final String PROJECT_ATTR = "project";
74 private static final String STATUS_ATTR = "status";
75 private static final String COMMENT_ATTR = "comment";
76 private static final String PRIORITY_ATTR = "priority";
77 private static final String ATTACHMENT_ATTR = "attachment";
78 private static final String RESOLUTION_ATTR = "resolution";
79 private static final String ASSIGNEE_ATTR = "assignee";
80 private static final String REPORTER_ATTR = "reporter";
81 private static final String SUMMARY_ATTR = "summary";
82 private static final String DESCRIPTION_ATTR = "description";
83 private static final String TIMETRACKING_ATTR = "timetracking";
84 private static final String TRANSITIONS_ATTR = "transitions";
85 private static final String SUBTASKS_ATTR = "subtasks";
86 private static final String LABELS_ATTR = "labels";
87
88 private static Set<String> SPECIAL_FIELDS = new HashSet<String>(Arrays.asList(SUMMARY_ATTR, UPDATED_ATTR, CREATED_ATTR,
89 AFFECTS_VERSIONS_ATTR, FIX_VERSIONS_ATTR, COMPONENTS_ATTR, LINKS_ATTR, LINKS_ATTR_5_0, ISSUE_TYPE_ATTR, VOTES_ATTR,
90 WORKLOG_ATTR, WATCHER_ATTR, PROJECT_ATTR, STATUS_ATTR, COMMENT_ATTR, ATTACHMENT_ATTR, SUMMARY_ATTR, DESCRIPTION_ATTR,
91 PRIORITY_ATTR, RESOLUTION_ATTR, ASSIGNEE_ATTR, REPORTER_ATTR, TIMETRACKING_ATTR, LABELS_ATTR));
92 public static final String SCHEMA_SECTION = "schema";
93 public static final String NAMES_SECTION = "names";
94 public static final String TRANSITIONS_SECTION = "transitions";
95
96 private final IssueLinkJsonParser issueLinkJsonParser = new IssueLinkJsonParser();
97 private final IssueLinkJsonParserV5 issueLinkJsonParserV5 = new IssueLinkJsonParserV5();
98 private final BasicVotesJsonParser votesJsonParser = new BasicVotesJsonParser();
99 private final BasicStatusJsonParser statusJsonParser = new BasicStatusJsonParser();
100 private final WorklogJsonParser worklogJsonParser = new WorklogJsonParser();
101 private final JsonParser<BasicWatchers> watchersJsonParser
102 = WatchersJsonParserBuilder.createBasicWatchersParser();
103 private final VersionJsonParser versionJsonParser = new VersionJsonParser();
104 private final BasicComponentJsonParser basicComponentJsonParser = new BasicComponentJsonParser();
105 private final AttachmentJsonParser attachmentJsonParser = new AttachmentJsonParser();
106 private final JsonFieldParser fieldParser = new JsonFieldParser();
107 private final CommentJsonParser commentJsonParser = new CommentJsonParser();
108 private final BasicIssueTypeJsonParser issueTypeJsonParser = new BasicIssueTypeJsonParser();
109 private final BasicProjectJsonParser projectJsonParser = new BasicProjectJsonParser();
110 private final BasicPriorityJsonParser priorityJsonParser = new BasicPriorityJsonParser();
111 private final BasicResolutionJsonParser resolutionJsonParser = new BasicResolutionJsonParser();
112 private final BasicUserJsonParser userJsonParser = new BasicUserJsonParser();
113 private final TransitionJsonParser transitionJsonParser = new TransitionJsonParser();
114 private final SubtaskJsonParser subtaskJsonParser = new SubtaskJsonParser();
115 private final ChangelogJsonParser changelogJsonParser = new ChangelogJsonParser();
116 private final JsonWeakParserForString jsonWeakParserForString = new JsonWeakParserForString();
117
118 private static final String FIELDS = "fields";
119 private static final String VALUE_ATTR = "value";
120
121 static Iterable<String> parseExpandos(JSONObject json) throws JSONException {
122 final String expando = json.getString("expand");
123 return Splitter.on(',').split(expando);
124 }
125
126
127 private <T> Collection<T> parseArray(JSONObject jsonObject, JsonWeakParser<T> jsonParser, String arrayAttribute) throws JSONException {
128
129
130 final JSONArray valueObject = jsonObject.optJSONArray(arrayAttribute);
131 if (valueObject == null) {
132 return new ArrayList<T>();
133 }
134 Collection<T> res = new ArrayList<T>(valueObject.length());
135 for (int i = 0; i < valueObject.length(); i++) {
136 res.add(jsonParser.parse(valueObject.get(i)));
137 }
138 return res;
139 }
140
141 private <T> Collection<T> parseOptionalArrayNotNullable(boolean shouldUseNestedValueJson, JSONObject json, JsonWeakParser<T> jsonParser, String... path) throws JSONException {
142 Collection<T> res = parseOptionalArray(shouldUseNestedValueJson, json, jsonParser, path);
143 return res == null ? Collections.<T>emptyList() : res;
144 }
145
146 @Nullable
147 private <T> Collection<T> parseOptionalArray(boolean shouldUseNestedValueJson, JSONObject json, JsonWeakParser<T> jsonParser, String... path) throws JSONException {
148 if (shouldUseNestedValueJson) {
149 final JSONObject js = JsonParseUtil.getNestedOptionalObject(json, path);
150 if (js == null) {
151 return null;
152 }
153 return parseArray(js, jsonParser, VALUE_ATTR);
154 } else {
155 final JSONArray jsonArray = JsonParseUtil.getNestedOptionalArray(json, path);
156 if (jsonArray == null) {
157 return null;
158 }
159 final Collection<T> res = new ArrayList<T>(jsonArray.length());
160 for (int i = 0; i < jsonArray.length(); i++) {
161 res.add(jsonParser.parse(jsonArray.get(i)));
162 }
163 return res;
164 }
165 }
166
167 private String getFieldStringValue(JSONObject json, String attributeName) throws JSONException {
168 final JSONObject fieldsJson = json.getJSONObject(FIELDS);
169
170 final Object summaryObject = fieldsJson.get(attributeName);
171 if (summaryObject instanceof JSONObject) {
172 return ((JSONObject) summaryObject).getString(VALUE_ATTR);
173 }
174 if (summaryObject instanceof String) {
175 return (String) summaryObject;
176 }
177 throw new JSONException("Cannot parse [" + attributeName + "] from available fields");
178 }
179
180 private JSONObject getFieldUnisex(JSONObject json, String attributeName) throws JSONException {
181 final JSONObject fieldsJson = json.getJSONObject(FIELDS);
182 final JSONObject fieldJson = fieldsJson.getJSONObject(attributeName);
183 if (fieldJson.has(VALUE_ATTR)) {
184 return fieldJson.getJSONObject(VALUE_ATTR);
185 } else {
186 return fieldJson;
187 }
188 }
189
190 @Nullable
191 private String getOptionalFieldStringUnisex(boolean shouldUseNestedValueJson, JSONObject json, String attributeName) throws JSONException {
192 final JSONObject fieldsJson = json.getJSONObject(FIELDS);
193 if (shouldUseNestedValueJson) {
194 final JSONObject fieldJson = fieldsJson.optJSONObject(attributeName);
195 if (fieldJson != null) {
196 return JsonParseUtil.getOptionalString(((JSONObject) fieldJson), VALUE_ATTR);
197 } else {
198 return null;
199 }
200 }
201 return JsonParseUtil.getOptionalString(fieldsJson, attributeName);
202 }
203
204 private String getFieldStringUnisex(JSONObject json, String attributeName) throws JSONException {
205 final JSONObject fieldsJson = json.getJSONObject(FIELDS);
206 final Object fieldJson = fieldsJson.get(attributeName);
207 if (fieldJson instanceof JSONObject) {
208 return ((JSONObject)fieldJson).getString(VALUE_ATTR);
209 }
210 return fieldJson.toString();
211 }
212
213 @Override
214 public Issue parse(JSONObject s) throws JSONException {
215 final Iterable<String> expandos = parseExpandos(s);
216 final boolean isJira5x0OrNewer = Iterables.contains(expandos, SCHEMA_SECTION);
217 final boolean shouldUseNestedValueAttribute = !isJira5x0OrNewer;
218 final Collection<Comment> comments;
219 if (isJira5x0OrNewer) {
220 final JSONObject commentsJson = s.getJSONObject(FIELDS).getJSONObject(COMMENT_ATTR);
221 comments = parseArray(commentsJson, new JsonWeakParserForJsonObject<Comment>(commentJsonParser), "comments");
222
223 } else {
224 final Collection<Comment> commentsTmp = parseOptionalArray(
225 shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Comment>(commentJsonParser), FIELDS, COMMENT_ATTR);
226 comments = commentsTmp != null ? commentsTmp : Lists.<Comment>newArrayList();
227 }
228
229 final String summary = getFieldStringValue(s, SUMMARY_ATTR);
230 final String description = getOptionalFieldStringUnisex(shouldUseNestedValueAttribute, s, DESCRIPTION_ATTR);
231
232 final Collection<Attachment> attachments = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Attachment>(attachmentJsonParser), FIELDS, ATTACHMENT_ATTR);
233 final Collection<Field> fields = isJira5x0OrNewer ? parseFieldsJira5x0(s) : parseFields(s.getJSONObject(FIELDS));
234
235 final BasicIssueType issueType = issueTypeJsonParser.parse(getFieldUnisex(s, ISSUE_TYPE_ATTR));
236 final DateTime creationDate = JsonParseUtil.parseDateTime(getFieldStringUnisex(s, CREATED_ATTR));
237 final DateTime updateDate = JsonParseUtil.parseDateTime(getFieldStringUnisex(s, UPDATED_ATTR));
238
239 final BasicPriority priority = getOptionalField(shouldUseNestedValueAttribute, s, PRIORITY_ATTR, priorityJsonParser);
240 final BasicResolution resolution = getOptionalField(shouldUseNestedValueAttribute, s, RESOLUTION_ATTR, resolutionJsonParser);
241 final BasicUser assignee = getOptionalField(shouldUseNestedValueAttribute, s, ASSIGNEE_ATTR, userJsonParser);
242 final BasicUser reporter = getOptionalField(shouldUseNestedValueAttribute, s, REPORTER_ATTR, userJsonParser);
243
244 final String transitionsUri = getOptionalFieldStringUnisex(shouldUseNestedValueAttribute, s, TRANSITIONS_ATTR);
245 final BasicProject project = projectJsonParser.parse(getFieldUnisex(s, PROJECT_ATTR));
246 final Collection<IssueLink> issueLinks;
247 if (isJira5x0OrNewer) {
248 issueLinks = parseOptionalArray(shouldUseNestedValueAttribute, s,
249 new JsonWeakParserForJsonObject<IssueLink>(issueLinkJsonParserV5), FIELDS, LINKS_ATTR_5_0);
250 } else {
251 issueLinks = parseOptionalArray(shouldUseNestedValueAttribute, s,
252 new JsonWeakParserForJsonObject<IssueLink>(issueLinkJsonParser), FIELDS, LINKS_ATTR);
253 }
254
255 Collection<Subtask> subtasks = null;
256 if (isJira5x0OrNewer) {
257 subtasks = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Subtask>(subtaskJsonParser), FIELDS, SUBTASKS_ATTR);
258 }
259
260 final BasicVotes votes = getOptionalField(shouldUseNestedValueAttribute, s, VOTES_ATTR, votesJsonParser);
261 final BasicStatus status = statusJsonParser.parse(getFieldUnisex(s, STATUS_ATTR));
262
263 final Collection<Version> fixVersions = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Version>(versionJsonParser), FIELDS, FIX_VERSIONS_ATTR);
264 final Collection<Version> affectedVersions = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Version>(versionJsonParser), FIELDS, AFFECTS_VERSIONS_ATTR);
265 final Collection<BasicComponent> components = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<BasicComponent>(basicComponentJsonParser), FIELDS, COMPONENTS_ATTR);
266
267 final Collection<Worklog> worklogs;
268 if (isJira5x0OrNewer) {
269 if (JsonParseUtil.getNestedOptionalObject(s, FIELDS, WORKLOG_ATTR) != null) {
270 worklogs = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Worklog>(new WorklogJsonParserV5(JsonParseUtil.getSelfUri(s))), FIELDS, WORKLOG_ATTR, WORKLOGS_ATTR);
271 } else {
272 worklogs = Collections.emptyList();
273 }
274 } else {
275 worklogs = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Worklog>(worklogJsonParser), FIELDS, WORKLOG_ATTR);
276 }
277
278 final BasicWatchers watchers = getOptionalField(shouldUseNestedValueAttribute, s, isJira5x0OrNewer ? WATCHER_ATTR_5_0 : WATCHER_ATTR, watchersJsonParser);
279 final TimeTracking timeTracking = getOptionalField(shouldUseNestedValueAttribute, s, TIMETRACKING_ATTR,
280 isJira5x0OrNewer ? new TimeTrackingJsonParserV5() : new TimeTrackingJsonParser());
281
282 final Set<String> labels = Sets.newHashSet(parseOptionalArrayNotNullable(shouldUseNestedValueAttribute, s, jsonWeakParserForString, FIELDS, LABELS_ATTR));
283
284 final Collection<ChangelogGroup> changelog = parseOptionalArray(false, s, new JsonWeakParserForJsonObject<ChangelogGroup>(changelogJsonParser), "changelog", "histories");
285 return new Issue(summary, JsonParseUtil.getSelfUri(s), s.getString("key"), project, issueType, status, description, priority,
286 resolution, attachments, reporter, assignee, creationDate, updateDate, affectedVersions, fixVersions,
287 components, timeTracking, fields, comments, transitionsUri != null ? JsonParseUtil.parseURI(transitionsUri) : null, issueLinks,
288 votes, worklogs, watchers, expandos, subtasks, changelog, labels);
289 }
290
291 @Nullable
292 private <T> T getOptionalField(boolean shouldUseNestedValue, JSONObject s, final String fieldId, JsonParser<T> jsonParser) throws JSONException {
293 final JSONObject fieldJson = JsonParseUtil.getNestedOptionalObject(s, FIELDS, fieldId);
294
295 if (fieldJson != null) {
296 if (shouldUseNestedValue) {
297 final JSONObject valueJsonObject = fieldJson.optJSONObject(VALUE_ATTR);
298 if (valueJsonObject != null) {
299 return jsonParser.parse(valueJsonObject);
300 }
301
302 } else {
303 return jsonParser.parse(fieldJson);
304 }
305
306 }
307 return null;
308 }
309
310 private Collection<Field> parseFieldsJira5x0(JSONObject issueJson) throws JSONException {
311 final JSONObject names = issueJson.optJSONObject(NAMES_SECTION);
312 final Map<String, String> namesMap = parseNames(names);
313 final JSONObject types = issueJson.optJSONObject(SCHEMA_SECTION);
314 final Map<String, String> typesMap = parseSchema(types);
315
316 final JSONObject json = issueJson.getJSONObject(FIELDS);
317 final ArrayList<Field> res = new ArrayList<Field>(json.length());
318 @SuppressWarnings("unchecked")
319 final Iterator<String> iterator = json.keys();
320 while (iterator.hasNext()) {
321 final String key = iterator.next();
322 try {
323 if (SPECIAL_FIELDS.contains(key)) {
324 continue;
325 }
326 final Object value = json.opt(key);
327 res.add(new Field(key, namesMap.get(key), typesMap.get("key"), value != JSONObject.NULL ? value : null));
328 } catch (final Exception e) {
329 throw new JSONException("Error while parsing [" + key + "] field: " + e.getMessage()) {
330 @Override
331 public Throwable getCause() {
332 return e;
333 }
334 };
335 }
336 }
337 return res;
338 }
339
340 private Map<String, String> parseSchema(JSONObject json) throws JSONException {
341 final HashMap<String, String> res = Maps.newHashMap();
342
343 @SuppressWarnings("unchecked")
344 final Iterator<String> it = json.keys();
345 while (it.hasNext()) {
346 final String fieldId = it.next();
347 JSONObject fieldDefinition = json.getJSONObject(fieldId);
348 res.put(fieldId, fieldDefinition.getString("type"));
349
350 }
351 return res;
352 }
353
354 private Map<String, String> parseNames(JSONObject json) throws JSONException {
355 final HashMap<String, String> res = Maps.newHashMap();
356 for (Iterator<String> it = json.keys(); it.hasNext(); ) {
357 final String key = it.next();
358 res.put(key, json.getString(key));
359 }
360 return res;
361 }
362
363
364 private Collection<Field> parseFields(JSONObject json) throws JSONException {
365 ArrayList<Field> res = new ArrayList<Field>(json.length());
366 @SuppressWarnings("unchecked")
367 final Iterator<String> iterator = json.keys();
368 while (iterator.hasNext()) {
369 final String key = iterator.next();
370 if (SPECIAL_FIELDS.contains(key)) {
371 continue;
372 }
373 res.add(fieldParser.parse(json.getJSONObject(key), key));
374 }
375 return res;
376 }
377
378 }