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