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.IssueFieldId;
34 import com.atlassian.jira.rest.client.domain.IssueLink;
35 import com.atlassian.jira.rest.client.domain.Subtask;
36 import com.atlassian.jira.rest.client.domain.TimeTracking;
37 import com.atlassian.jira.rest.client.domain.Version;
38 import com.atlassian.jira.rest.client.domain.Worklog;
39 import com.google.common.base.Splitter;
40 import com.google.common.collect.Iterables;
41 import com.google.common.collect.Lists;
42 import com.google.common.collect.Maps;
43 import com.google.common.collect.Sets;
44 import org.codehaus.jettison.json.JSONArray;
45 import org.codehaus.jettison.json.JSONException;
46 import org.codehaus.jettison.json.JSONObject;
47 import org.joda.time.DateTime;
48
49 import javax.annotation.Nullable;
50 import javax.ws.rs.core.UriBuilder;
51 import java.net.URI;
52 import java.util.ArrayList;
53 import java.util.Collection;
54 import java.util.Collections;
55 import java.util.HashMap;
56 import java.util.Iterator;
57 import java.util.Map;
58 import java.util.Set;
59
60 import static com.atlassian.jira.rest.client.domain.IssueFieldId.AFFECTS_VERSIONS_FIELD;
61 import static com.atlassian.jira.rest.client.domain.IssueFieldId.ASSIGNEE_FIELD;
62 import static com.atlassian.jira.rest.client.domain.IssueFieldId.ATTACHMENT_FIELD;
63 import static com.atlassian.jira.rest.client.domain.IssueFieldId.COMMENT_FIELD;
64 import static com.atlassian.jira.rest.client.domain.IssueFieldId.COMPONENTS_FIELD;
65 import static com.atlassian.jira.rest.client.domain.IssueFieldId.CREATED_FIELD;
66 import static com.atlassian.jira.rest.client.domain.IssueFieldId.DESCRIPTION_FIELD;
67 import static com.atlassian.jira.rest.client.domain.IssueFieldId.DUE_DATE_FIELD;
68 import static com.atlassian.jira.rest.client.domain.IssueFieldId.FIX_VERSIONS_FIELD;
69 import static com.atlassian.jira.rest.client.domain.IssueFieldId.ISSUE_TYPE_FIELD;
70 import static com.atlassian.jira.rest.client.domain.IssueFieldId.LABELS_FIELD;
71 import static com.atlassian.jira.rest.client.domain.IssueFieldId.LINKS_FIELD;
72 import static com.atlassian.jira.rest.client.domain.IssueFieldId.LINKS_PRE_5_0_FIELD;
73 import static com.atlassian.jira.rest.client.domain.IssueFieldId.PRIORITY_FIELD;
74 import static com.atlassian.jira.rest.client.domain.IssueFieldId.PROJECT_FIELD;
75 import static com.atlassian.jira.rest.client.domain.IssueFieldId.REPORTER_FIELD;
76 import static com.atlassian.jira.rest.client.domain.IssueFieldId.RESOLUTION_FIELD;
77 import static com.atlassian.jira.rest.client.domain.IssueFieldId.STATUS_FIELD;
78 import static com.atlassian.jira.rest.client.domain.IssueFieldId.SUBTASKS_FIELD;
79 import static com.atlassian.jira.rest.client.domain.IssueFieldId.SUMMARY_FIELD;
80 import static com.atlassian.jira.rest.client.domain.IssueFieldId.TIMETRACKING_FIELD;
81 import static com.atlassian.jira.rest.client.domain.IssueFieldId.TRANSITIONS_FIELD;
82 import static com.atlassian.jira.rest.client.domain.IssueFieldId.UPDATED_FIELD;
83 import static com.atlassian.jira.rest.client.domain.IssueFieldId.VOTES_FIELD;
84 import static com.atlassian.jira.rest.client.domain.IssueFieldId.WATCHER_FIELD;
85 import static com.atlassian.jira.rest.client.domain.IssueFieldId.WATCHER_PRE_5_0_FIELD;
86 import static com.atlassian.jira.rest.client.domain.IssueFieldId.WORKLOGS_FIELD;
87 import static com.atlassian.jira.rest.client.domain.IssueFieldId.WORKLOG_FIELD;
88 import static com.atlassian.jira.rest.client.internal.json.JsonParseUtil.getStringKeys;
89
90 public class IssueJsonParser implements JsonObjectParser<Issue> {
91
92 private static Set<String> SPECIAL_FIELDS = Sets.newHashSet(IssueFieldId.ids());
93
94 public static final String SCHEMA_SECTION = "schema";
95 public static final String NAMES_SECTION = "names";
96
97 private final IssueLinkJsonParser issueLinkJsonParser = new IssueLinkJsonParser();
98 private final IssueLinkJsonParserV5 issueLinkJsonParserV5 = new IssueLinkJsonParserV5();
99 private final BasicVotesJsonParser votesJsonParser = new BasicVotesJsonParser();
100 private final BasicStatusJsonParser statusJsonParser = new BasicStatusJsonParser();
101 private final WorklogJsonParser worklogJsonParser = new WorklogJsonParser();
102 private final JsonObjectParser<BasicWatchers> watchersJsonParser
103 = WatchersJsonParserBuilder.createBasicWatchersParser();
104 private final VersionJsonParser versionJsonParser = new VersionJsonParser();
105 private final BasicComponentJsonParser basicComponentJsonParser = new BasicComponentJsonParser();
106 private final AttachmentJsonParser attachmentJsonParser = new AttachmentJsonParser();
107 private final JsonFieldParser fieldParser = new JsonFieldParser();
108 private final CommentJsonParser commentJsonParser = new CommentJsonParser();
109 private final BasicIssueTypeJsonParser issueTypeJsonParser = new BasicIssueTypeJsonParser();
110 private final BasicProjectJsonParser projectJsonParser = new BasicProjectJsonParser();
111 private final BasicPriorityJsonParser priorityJsonParser = new BasicPriorityJsonParser();
112 private final BasicResolutionJsonParser resolutionJsonParser = new BasicResolutionJsonParser();
113 private final BasicUserJsonParser userJsonParser = new BasicUserJsonParser();
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)
128 throws JSONException {
129
130
131 final JSONArray valueObject = jsonObject.optJSONArray(arrayAttribute);
132 if (valueObject == null) {
133 return new ArrayList<T>();
134 }
135 Collection<T> res = new ArrayList<T>(valueObject.length());
136 for (int i = 0; i < valueObject.length(); i++) {
137 res.add(jsonParser.parse(valueObject.get(i)));
138 }
139 return res;
140 }
141
142 private <T> Collection<T> parseOptionalArrayNotNullable(boolean shouldUseNestedValueJson, JSONObject json, JsonWeakParser<T> jsonParser, String... path)
143 throws JSONException {
144 Collection<T> res = parseOptionalArray(shouldUseNestedValueJson, json, jsonParser, path);
145 return res == null ? Collections.<T>emptyList() : res;
146 }
147
148 @Nullable
149 private <T> Collection<T> parseOptionalArray(boolean shouldUseNestedValueJson, JSONObject json, JsonWeakParser<T> jsonParser, String... path)
150 throws JSONException {
151 if (shouldUseNestedValueJson) {
152 final JSONObject js = JsonParseUtil.getNestedOptionalObject(json, path);
153 if (js == null) {
154 return null;
155 }
156 return parseArray(js, jsonParser, VALUE_ATTR);
157 } else {
158 final JSONArray jsonArray = JsonParseUtil.getNestedOptionalArray(json, path);
159 if (jsonArray == null) {
160 return null;
161 }
162 final Collection<T> res = new ArrayList<T>(jsonArray.length());
163 for (int i = 0; i < jsonArray.length(); i++) {
164 res.add(jsonParser.parse(jsonArray.get(i)));
165 }
166 return res;
167 }
168 }
169
170 private String getFieldStringValue(JSONObject json, String attributeName) throws JSONException {
171 final JSONObject fieldsJson = json.getJSONObject(FIELDS);
172
173 final Object summaryObject = fieldsJson.get(attributeName);
174 if (summaryObject instanceof JSONObject) {
175 return ((JSONObject) summaryObject).getString(VALUE_ATTR);
176 }
177 if (summaryObject instanceof String) {
178 return (String) summaryObject;
179 }
180 throw new JSONException("Cannot parse [" + attributeName + "] from available fields");
181 }
182
183 private JSONObject getFieldUnisex(JSONObject json, String attributeName) throws JSONException {
184 final JSONObject fieldsJson = json.getJSONObject(FIELDS);
185 final JSONObject fieldJson = fieldsJson.getJSONObject(attributeName);
186 if (fieldJson.has(VALUE_ATTR)) {
187 return fieldJson.getJSONObject(VALUE_ATTR);
188 } else {
189 return fieldJson;
190 }
191 }
192
193 @Nullable
194 private String getOptionalFieldStringUnisex(boolean shouldUseNestedValueJson, JSONObject json, String attributeName)
195 throws JSONException {
196 final JSONObject fieldsJson = json.getJSONObject(FIELDS);
197 if (shouldUseNestedValueJson) {
198 final JSONObject fieldJson = fieldsJson.optJSONObject(attributeName);
199 if (fieldJson != null) {
200 return JsonParseUtil.getOptionalString(fieldJson, VALUE_ATTR);
201 } else {
202 return null;
203 }
204 }
205 return JsonParseUtil.getOptionalString(fieldsJson, attributeName);
206 }
207
208 private String getFieldStringUnisex(JSONObject json, String attributeName) throws JSONException {
209 final JSONObject fieldsJson = json.getJSONObject(FIELDS);
210 final Object fieldJson = fieldsJson.get(attributeName);
211 if (fieldJson instanceof JSONObject) {
212 return ((JSONObject) fieldJson).getString(VALUE_ATTR);
213 }
214 return fieldJson.toString();
215 }
216
217 @Override
218 public Issue parse(JSONObject s) throws JSONException {
219 final Iterable<String> expandos = parseExpandos(s);
220 final boolean isJira5x0OrNewer = Iterables.contains(expandos, SCHEMA_SECTION);
221 final boolean shouldUseNestedValueAttribute = !isJira5x0OrNewer;
222 final Collection<Comment> comments;
223 if (isJira5x0OrNewer) {
224 final JSONObject commentsJson = s.getJSONObject(FIELDS).getJSONObject(COMMENT_FIELD.id);
225 comments = parseArray(commentsJson, new JsonWeakParserForJsonObject<Comment>(commentJsonParser), "comments");
226
227 } else {
228 final Collection<Comment> commentsTmp = parseOptionalArray(
229 shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Comment>(commentJsonParser), FIELDS, COMMENT_FIELD.id);
230 comments = commentsTmp != null ? commentsTmp : Lists.<Comment>newArrayList();
231 }
232
233 final String summary = getFieldStringValue(s, SUMMARY_FIELD.id);
234 final String description = getOptionalFieldStringUnisex(shouldUseNestedValueAttribute, s, DESCRIPTION_FIELD.id);
235
236 final Collection<Attachment> attachments = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Attachment>(attachmentJsonParser), FIELDS, ATTACHMENT_FIELD.id);
237 final Collection<Field> fields = isJira5x0OrNewer ? parseFieldsJira5x0(s) : parseFields(s.getJSONObject(FIELDS));
238
239 final BasicIssueType issueType = issueTypeJsonParser.parse(getFieldUnisex(s, ISSUE_TYPE_FIELD.id));
240 final DateTime creationDate = JsonParseUtil.parseDateTime(getFieldStringUnisex(s, CREATED_FIELD.id));
241 final DateTime updateDate = JsonParseUtil.parseDateTime(getFieldStringUnisex(s, UPDATED_FIELD.id));
242
243 final String dueDateString = getOptionalFieldStringUnisex(shouldUseNestedValueAttribute, s, DUE_DATE_FIELD.id);
244 final DateTime dueDate = dueDateString == null ? null : JsonParseUtil.parseDateTimeOrDate(dueDateString);
245
246 final BasicPriority priority = getOptionalField(shouldUseNestedValueAttribute, s, PRIORITY_FIELD.id, priorityJsonParser);
247 final BasicResolution resolution = getOptionalField(shouldUseNestedValueAttribute, s, RESOLUTION_FIELD.id, resolutionJsonParser);
248 final BasicUser assignee = getOptionalField(shouldUseNestedValueAttribute, s, ASSIGNEE_FIELD.id, userJsonParser);
249 final BasicUser reporter = getOptionalField(shouldUseNestedValueAttribute, s, REPORTER_FIELD.id, userJsonParser);
250
251 final BasicProject project = projectJsonParser.parse(getFieldUnisex(s, PROJECT_FIELD.id));
252 final Collection<IssueLink> issueLinks;
253 if (isJira5x0OrNewer) {
254 issueLinks = parseOptionalArray(shouldUseNestedValueAttribute, s,
255 new JsonWeakParserForJsonObject<IssueLink>(issueLinkJsonParserV5), FIELDS, LINKS_FIELD.id);
256 } else {
257 issueLinks = parseOptionalArray(shouldUseNestedValueAttribute, s,
258 new JsonWeakParserForJsonObject<IssueLink>(issueLinkJsonParser), FIELDS, LINKS_PRE_5_0_FIELD.id);
259 }
260
261 Collection<Subtask> subtasks = null;
262 if (isJira5x0OrNewer) {
263 subtasks = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Subtask>(subtaskJsonParser), FIELDS, SUBTASKS_FIELD.id);
264 }
265
266 final BasicVotes votes = getOptionalField(shouldUseNestedValueAttribute, s, VOTES_FIELD.id, votesJsonParser);
267 final BasicStatus status = statusJsonParser.parse(getFieldUnisex(s, STATUS_FIELD.id));
268
269 final Collection<Version> fixVersions = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Version>(versionJsonParser), FIELDS, FIX_VERSIONS_FIELD.id);
270 final Collection<Version> affectedVersions = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Version>(versionJsonParser), FIELDS, AFFECTS_VERSIONS_FIELD.id);
271 final Collection<BasicComponent> components = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<BasicComponent>(basicComponentJsonParser), FIELDS, COMPONENTS_FIELD.id);
272
273 final Collection<Worklog> worklogs;
274 final URI selfUri = JsonParseUtil.getSelfUri(s);
275
276 final String transitionsUriString;
277 if (s.has(TRANSITIONS_FIELD.id)) {
278 Object transitionsObj = s.get(TRANSITIONS_FIELD.id);
279 transitionsUriString = (transitionsObj instanceof String) ? (String) transitionsObj : null;
280 }
281 else {
282 transitionsUriString = getOptionalFieldStringUnisex(shouldUseNestedValueAttribute, s, TRANSITIONS_FIELD.id);
283 }
284 final URI transitionsUri = parseTransisionsUri(transitionsUriString, selfUri);
285
286 if (isJira5x0OrNewer) {
287 if (JsonParseUtil.getNestedOptionalObject(s, FIELDS, WORKLOG_FIELD.id) != null) {
288 worklogs = parseOptionalArray(shouldUseNestedValueAttribute, s,
289 new JsonWeakParserForJsonObject<Worklog>(new WorklogJsonParserV5(selfUri)),
290 FIELDS, WORKLOG_FIELD.id, WORKLOGS_FIELD.id);
291 } else {
292 worklogs = Collections.emptyList();
293 }
294 } else {
295 worklogs = parseOptionalArray(shouldUseNestedValueAttribute, s, new JsonWeakParserForJsonObject<Worklog>(worklogJsonParser), FIELDS, WORKLOG_FIELD.id);
296 }
297
298 final BasicWatchers watchers = getOptionalField(shouldUseNestedValueAttribute, s,
299 isJira5x0OrNewer ? WATCHER_FIELD.id : WATCHER_PRE_5_0_FIELD.id, watchersJsonParser);
300 final TimeTracking timeTracking = getOptionalField(shouldUseNestedValueAttribute, s, TIMETRACKING_FIELD.id,
301 isJira5x0OrNewer ? new TimeTrackingJsonParserV5() : new TimeTrackingJsonParser());
302
303 final Set<String> labels = Sets.newHashSet(parseOptionalArrayNotNullable(shouldUseNestedValueAttribute, s,
304 jsonWeakParserForString, FIELDS, LABELS_FIELD.id));
305
306 final Collection<ChangelogGroup> changelog = parseOptionalArray(false, s, new JsonWeakParserForJsonObject<ChangelogGroup>(changelogJsonParser), "changelog", "histories");
307 return new Issue(summary, selfUri, s.getString("key"), project, issueType, status,
308 description, priority, resolution, attachments, reporter, assignee, creationDate, updateDate,
309 dueDate, affectedVersions, fixVersions, components, timeTracking, fields, comments,
310 transitionsUri, issueLinks,
311 votes, worklogs, watchers, expandos, subtasks, changelog, labels);
312 }
313
314 private URI parseTransisionsUri(String transitionsUriString, URI selfUri) {
315 return transitionsUriString != null
316 ? JsonParseUtil.parseURI(transitionsUriString)
317 : UriBuilder.fromUri(selfUri).path("transitions").queryParam("expand", "transitions.fields").build();
318 }
319
320 @Nullable
321 private <T> T getOptionalField(boolean shouldUseNestedValue, JSONObject s, final String fieldId, JsonObjectParser<T> jsonParser)
322 throws JSONException {
323 final JSONObject fieldJson = JsonParseUtil.getNestedOptionalObject(s, FIELDS, fieldId);
324
325 if (fieldJson != null) {
326 if (shouldUseNestedValue) {
327 final JSONObject valueJsonObject = fieldJson.optJSONObject(VALUE_ATTR);
328 if (valueJsonObject != null) {
329 return jsonParser.parse(valueJsonObject);
330 }
331
332 } else {
333 return jsonParser.parse(fieldJson);
334 }
335
336 }
337 return null;
338 }
339
340 private Collection<Field> parseFieldsJira5x0(JSONObject issueJson) throws JSONException {
341 final JSONObject names = issueJson.optJSONObject(NAMES_SECTION);
342 final Map<String, String> namesMap = parseNames(names);
343 final JSONObject types = issueJson.optJSONObject(SCHEMA_SECTION);
344 final Map<String, String> typesMap = parseSchema(types);
345
346 final JSONObject json = issueJson.getJSONObject(FIELDS);
347 final ArrayList<Field> res = new ArrayList<Field>(json.length());
348 @SuppressWarnings("unchecked")
349 final Iterator<String> iterator = json.keys();
350 while (iterator.hasNext()) {
351 final String key = iterator.next();
352 try {
353 if (SPECIAL_FIELDS.contains(key)) {
354 continue;
355 }
356 final Object value = json.opt(key);
357 res.add(new Field(key, namesMap.get(key), typesMap.get("key"), value != JSONObject.NULL ? value : null));
358 } catch (final Exception e) {
359 throw new JSONException("Error while parsing [" + key + "] field: " + e.getMessage()) {
360 @Override
361 public Throwable getCause() {
362 return e;
363 }
364 };
365 }
366 }
367 return res;
368 }
369
370 private Map<String, String> parseSchema(JSONObject json) throws JSONException {
371 final HashMap<String, String> res = Maps.newHashMap();
372 final Iterator<String> it = getStringKeys(json);
373 while (it.hasNext()) {
374 final String fieldId = it.next();
375 JSONObject fieldDefinition = json.getJSONObject(fieldId);
376 res.put(fieldId, fieldDefinition.getString("type"));
377
378 }
379 return res;
380 }
381
382 private Map<String, String> parseNames(JSONObject json) throws JSONException {
383 final HashMap<String, String> res = Maps.newHashMap();
384 final Iterator<String> iterator = getStringKeys(json);
385 while (iterator.hasNext()) {
386 final String key = iterator.next();
387 res.put(key, json.getString(key));
388 }
389 return res;
390 }
391
392
393 private Collection<Field> parseFields(JSONObject json) throws JSONException {
394 ArrayList<Field> res = new ArrayList<Field>(json.length());
395 final Iterator<String> iterator = getStringKeys(json);
396 while (iterator.hasNext()) {
397 final String key = iterator.next();
398 if (SPECIAL_FIELDS.contains(key)) {
399 continue;
400 }
401 res.add(fieldParser.parse(json.getJSONObject(key), key));
402 }
403 return res;
404 }
405
406 }