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