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.api.domain.Attachment;
20 import com.atlassian.jira.rest.client.api.domain.BasicComponent;
21 import com.atlassian.jira.rest.client.api.domain.BasicIssue;
22 import com.atlassian.jira.rest.client.api.domain.BasicIssueType;
23 import com.atlassian.jira.rest.client.api.domain.BasicPriority;
24 import com.atlassian.jira.rest.client.api.domain.BasicProject;
25 import com.atlassian.jira.rest.client.api.domain.BasicResolution;
26 import com.atlassian.jira.rest.client.api.domain.BasicStatus;
27 import com.atlassian.jira.rest.client.api.domain.BasicVotes;
28 import com.atlassian.jira.rest.client.api.domain.BasicWatchers;
29 import com.atlassian.jira.rest.client.api.domain.ChangelogGroup;
30 import com.atlassian.jira.rest.client.api.domain.Comment;
31 import com.atlassian.jira.rest.client.api.domain.Issue;
32 import com.atlassian.jira.rest.client.api.domain.IssueField;
33 import com.atlassian.jira.rest.client.api.domain.IssueFieldId;
34 import com.atlassian.jira.rest.client.api.domain.IssueLink;
35 import com.atlassian.jira.rest.client.api.domain.Subtask;
36 import com.atlassian.jira.rest.client.api.domain.TimeTracking;
37 import com.atlassian.jira.rest.client.api.domain.User;
38 import com.atlassian.jira.rest.client.api.domain.Version;
39 import com.atlassian.jira.rest.client.api.domain.Worklog;
40 import com.google.common.base.Splitter;
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 javax.ws.rs.core.UriBuilder;
50 import java.net.URI;
51 import java.util.ArrayList;
52 import java.util.Collection;
53 import java.util.Collections;
54 import java.util.HashMap;
55 import java.util.Iterator;
56 import java.util.Map;
57 import java.util.Set;
58
59 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.AFFECTS_VERSIONS_FIELD;
60 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.ASSIGNEE_FIELD;
61 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.ATTACHMENT_FIELD;
62 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.COMMENT_FIELD;
63 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.COMPONENTS_FIELD;
64 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.CREATED_FIELD;
65 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.DESCRIPTION_FIELD;
66 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.DUE_DATE_FIELD;
67 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.FIX_VERSIONS_FIELD;
68 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.ISSUE_TYPE_FIELD;
69 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.LABELS_FIELD;
70 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.LINKS_FIELD;
71 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.PRIORITY_FIELD;
72 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.PROJECT_FIELD;
73 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.REPORTER_FIELD;
74 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.RESOLUTION_FIELD;
75 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.STATUS_FIELD;
76 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.SUBTASKS_FIELD;
77 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.SUMMARY_FIELD;
78 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.TIMETRACKING_FIELD;
79 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.UPDATED_FIELD;
80 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.VOTES_FIELD;
81 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.WATCHER_FIELD;
82 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.WORKLOGS_FIELD;
83 import static com.atlassian.jira.rest.client.api.domain.IssueFieldId.WORKLOG_FIELD;
84 import static com.atlassian.jira.rest.client.internal.json.JsonParseUtil.getStringKeys;
85
86 public class IssueJsonParser implements JsonObjectParser<Issue> {
87
88 private static Set<String> SPECIAL_FIELDS = Sets.newHashSet(IssueFieldId.ids());
89
90 public static final String SCHEMA_SECTION = "schema";
91 public static final String NAMES_SECTION = "names";
92
93 private final BasicIssueJsonParser basicIssueJsonParser = new BasicIssueJsonParser();
94 private final IssueLinkJsonParserV5 issueLinkJsonParserV5 = new IssueLinkJsonParserV5();
95 private final BasicVotesJsonParser votesJsonParser = new BasicVotesJsonParser();
96 private final BasicStatusJsonParser statusJsonParser = new BasicStatusJsonParser();
97 private final JsonObjectParser<BasicWatchers> watchersJsonParser = 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 CommentJsonParser commentJsonParser = new CommentJsonParser();
102 private final BasicIssueTypeJsonParser issueTypeJsonParser = new BasicIssueTypeJsonParser();
103 private final BasicProjectJsonParser projectJsonParser = new BasicProjectJsonParser();
104 private final BasicPriorityJsonParser priorityJsonParser = new BasicPriorityJsonParser();
105 private final BasicResolutionJsonParser resolutionJsonParser = new BasicResolutionJsonParser();
106 private final UserJsonParser userJsonParser = new UserJsonParser();
107 private final SubtaskJsonParser subtaskJsonParser = new SubtaskJsonParser();
108 private final ChangelogJsonParser changelogJsonParser = new ChangelogJsonParser();
109 private final JsonWeakParserForString jsonWeakParserForString = new JsonWeakParserForString();
110
111 private static final String FIELDS = "fields";
112 private static final String VALUE_ATTR = "value";
113
114 private final JSONObject providedNames;
115 private final JSONObject providedSchema;
116
117 public IssueJsonParser() {
118 providedNames = null;
119 providedSchema = null;
120 }
121
122 public IssueJsonParser(final JSONObject providedNames, final JSONObject providedSchema) {
123 this.providedNames = providedNames;
124 this.providedSchema = providedSchema;
125 }
126
127 static Iterable<String> parseExpandos(final JSONObject json) throws JSONException {
128 final String expando = json.getString("expand");
129 return Splitter.on(',').split(expando);
130 }
131
132
133 private <T> Collection<T> parseArray(final JSONObject jsonObject, final JsonWeakParser<T> jsonParser, final String arrayAttribute)
134 throws JSONException {
135
136
137 final JSONArray valueObject = jsonObject.optJSONArray(arrayAttribute);
138 if (valueObject == null) {
139 return new ArrayList<T>();
140 }
141 Collection<T> res = new ArrayList<T>(valueObject.length());
142 for (int i = 0; i < valueObject.length(); i++) {
143 res.add(jsonParser.parse(valueObject.get(i)));
144 }
145 return res;
146 }
147
148 private <T> Collection<T> parseOptionalArrayNotNullable(final JSONObject json, final JsonWeakParser<T> jsonParser, final String... path)
149 throws JSONException {
150 Collection<T> res = parseOptionalArray(json, jsonParser, path);
151 return res == null ? Collections.<T>emptyList() : res;
152 }
153
154 @Nullable
155 private <T> Collection<T> parseOptionalArray(final JSONObject json, final JsonWeakParser<T> jsonParser, final String... path)
156 throws JSONException {
157 final JSONArray jsonArray = JsonParseUtil.getNestedOptionalArray(json, path);
158 if (jsonArray == null) {
159 return null;
160 }
161 final Collection<T> res = new ArrayList<T>(jsonArray.length());
162 for (int i = 0; i < jsonArray.length(); i++) {
163 res.add(jsonParser.parse(jsonArray.get(i)));
164 }
165 return res;
166 }
167
168 private String getFieldStringValue(final JSONObject json, final String attributeName) throws JSONException {
169 final JSONObject fieldsJson = json.getJSONObject(FIELDS);
170
171 final Object summaryObject = fieldsJson.get(attributeName);
172 if (summaryObject instanceof JSONObject) {
173 return ((JSONObject) summaryObject).getString(VALUE_ATTR);
174 }
175 if (summaryObject instanceof String) {
176 return (String) summaryObject;
177 }
178 throw new JSONException("Cannot parse [" + attributeName + "] from available fields");
179 }
180
181 private JSONObject getFieldUnisex(final JSONObject json, final String attributeName) throws JSONException {
182 final JSONObject fieldsJson = json.getJSONObject(FIELDS);
183 final JSONObject fieldJson = fieldsJson.getJSONObject(attributeName);
184 if (fieldJson.has(VALUE_ATTR)) {
185 return fieldJson.getJSONObject(VALUE_ATTR);
186 } else {
187 return fieldJson;
188 }
189 }
190
191 @Nullable
192 private String getOptionalFieldStringUnisex(final JSONObject json, final String attributeName)
193 throws JSONException {
194 final JSONObject fieldsJson = json.getJSONObject(FIELDS);
195 return JsonParseUtil.getOptionalString(fieldsJson, attributeName);
196 }
197
198 private String getFieldStringUnisex(final JSONObject json, final String attributeName) throws JSONException {
199 final JSONObject fieldsJson = json.getJSONObject(FIELDS);
200 final Object fieldJson = fieldsJson.get(attributeName);
201 if (fieldJson instanceof JSONObject) {
202 return ((JSONObject) fieldJson).getString(VALUE_ATTR);
203 }
204 return fieldJson.toString();
205 }
206
207 @Override
208 public Issue parse(final JSONObject s) throws JSONException {
209 final BasicIssue basicIssue = basicIssueJsonParser.parse(s);
210 final Iterable<String> expandos = parseExpandos(s);
211 final JSONObject jsonFields = s.getJSONObject(FIELDS);
212 final JSONObject commentsJson = jsonFields.optJSONObject(COMMENT_FIELD.id);
213 final Collection<Comment> comments = (commentsJson == null) ? Collections.<Comment>emptyList()
214 : parseArray(commentsJson, new JsonWeakParserForJsonObject<Comment>(commentJsonParser), "comments");
215
216 final String summary = getFieldStringValue(s, SUMMARY_FIELD.id);
217 final String description = getOptionalFieldStringUnisex(s, DESCRIPTION_FIELD.id);
218
219 final Collection<Attachment> attachments = parseOptionalArray(s, new JsonWeakParserForJsonObject<Attachment>(attachmentJsonParser), FIELDS, ATTACHMENT_FIELD.id);
220 final Collection<IssueField> fields = parseFields(s);
221
222 final BasicIssueType issueType = issueTypeJsonParser.parse(getFieldUnisex(s, ISSUE_TYPE_FIELD.id));
223 final DateTime creationDate = JsonParseUtil.parseDateTime(getFieldStringUnisex(s, CREATED_FIELD.id));
224 final DateTime updateDate = JsonParseUtil.parseDateTime(getFieldStringUnisex(s, UPDATED_FIELD.id));
225
226 final String dueDateString = getOptionalFieldStringUnisex(s, DUE_DATE_FIELD.id);
227 final DateTime dueDate = dueDateString == null ? null : JsonParseUtil.parseDateTimeOrDate(dueDateString);
228
229 final BasicPriority priority = getOptionalNestedField(s, PRIORITY_FIELD.id, priorityJsonParser);
230 final BasicResolution resolution = getOptionalNestedField(s, RESOLUTION_FIELD.id, resolutionJsonParser);
231 final User assignee = getOptionalNestedField(s, ASSIGNEE_FIELD.id, userJsonParser);
232 final User reporter = getOptionalNestedField(s, REPORTER_FIELD.id, userJsonParser);
233
234 final BasicProject project = projectJsonParser.parse(getFieldUnisex(s, PROJECT_FIELD.id));
235 final Collection<IssueLink> issueLinks;
236 issueLinks = parseOptionalArray(s, new JsonWeakParserForJsonObject<IssueLink>(issueLinkJsonParserV5), FIELDS, LINKS_FIELD.id);
237
238 Collection<Subtask> subtasks = parseOptionalArray(s, new JsonWeakParserForJsonObject<Subtask>(subtaskJsonParser), FIELDS, SUBTASKS_FIELD.id);
239
240 final BasicVotes votes = getOptionalNestedField(s, VOTES_FIELD.id, votesJsonParser);
241 final BasicStatus status = statusJsonParser.parse(getFieldUnisex(s, STATUS_FIELD.id));
242
243 final Collection<Version> fixVersions = parseOptionalArray(s, new JsonWeakParserForJsonObject<Version>(versionJsonParser), FIELDS, FIX_VERSIONS_FIELD.id);
244 final Collection<Version> affectedVersions = parseOptionalArray(s, new JsonWeakParserForJsonObject<Version>(versionJsonParser), FIELDS, AFFECTS_VERSIONS_FIELD.id);
245 final Collection<BasicComponent> components = parseOptionalArray(s, new JsonWeakParserForJsonObject<BasicComponent>(basicComponentJsonParser), FIELDS, COMPONENTS_FIELD.id);
246
247 final Collection<Worklog> worklogs;
248 final URI selfUri = basicIssue.getSelf();
249
250 final String transitionsUriString;
251 if (s.has(IssueFieldId.TRANSITIONS_FIELD.id)) {
252 Object transitionsObj = s.get(IssueFieldId.TRANSITIONS_FIELD.id);
253 transitionsUriString = (transitionsObj instanceof String) ? (String) transitionsObj : null;
254 } else {
255 transitionsUriString = getOptionalFieldStringUnisex(s, IssueFieldId.TRANSITIONS_FIELD.id);
256 }
257 final URI transitionsUri = parseTransisionsUri(transitionsUriString, selfUri);
258
259 if (JsonParseUtil.getNestedOptionalObject(s, FIELDS, WORKLOG_FIELD.id) != null) {
260 worklogs = parseOptionalArray(s,
261 new JsonWeakParserForJsonObject<Worklog>(new WorklogJsonParserV5(selfUri)),
262 FIELDS, WORKLOG_FIELD.id, WORKLOGS_FIELD.id);
263 } else {
264 worklogs = Collections.emptyList();
265 }
266
267
268 final BasicWatchers watchers = getOptionalNestedField(s, WATCHER_FIELD.id, watchersJsonParser);
269 final TimeTracking timeTracking = getOptionalNestedField(s, TIMETRACKING_FIELD.id, new TimeTrackingJsonParserV5());
270
271 final Set<String> labels = Sets
272 .newHashSet(parseOptionalArrayNotNullable(s, jsonWeakParserForString, FIELDS, LABELS_FIELD.id));
273
274 final Collection<ChangelogGroup> changelog = parseOptionalArray(
275 s, new JsonWeakParserForJsonObject<ChangelogGroup>(changelogJsonParser), "changelog", "histories");
276 return new Issue(summary, selfUri, basicIssue.getKey(), basicIssue.getId(), project, issueType, status,
277 description, priority, resolution, attachments, reporter, assignee, creationDate, updateDate,
278 dueDate, affectedVersions, fixVersions, components, timeTracking, fields, comments,
279 transitionsUri, issueLinks,
280 votes, worklogs, watchers, expandos, subtasks, changelog, labels);
281 }
282
283 private URI parseTransisionsUri(final String transitionsUriString, final URI selfUri) {
284 return transitionsUriString != null
285 ? JsonParseUtil.parseURI(transitionsUriString)
286 : UriBuilder.fromUri(selfUri).path("transitions").queryParam("expand", "transitions.fields").build();
287 }
288
289 @Nullable
290 private <T> T getOptionalNestedField(final JSONObject s, final String fieldId, final JsonObjectParser<T> jsonParser)
291 throws JSONException {
292 final JSONObject fieldJson = JsonParseUtil.getNestedOptionalObject(s, FIELDS, fieldId);
293
294 if (fieldJson != null) {
295 return jsonParser.parse(fieldJson);
296 }
297 return null;
298 }
299
300 private Collection<IssueField> parseFields(final JSONObject issueJson) throws JSONException {
301 final JSONObject names = (providedNames != null) ? providedNames : issueJson.optJSONObject(NAMES_SECTION);
302 final Map<String, String> namesMap = parseNames(names);
303 final JSONObject schema = (providedSchema != null) ? providedSchema : issueJson.optJSONObject(SCHEMA_SECTION);
304 final Map<String, String> typesMap = parseSchema(schema);
305
306 final JSONObject json = issueJson.getJSONObject(FIELDS);
307 final ArrayList<IssueField> res = new ArrayList<IssueField>(json.length());
308 @SuppressWarnings("unchecked")
309 final Iterator<String> iterator = json.keys();
310 while (iterator.hasNext()) {
311 final String key = iterator.next();
312 try {
313 if (SPECIAL_FIELDS.contains(key)) {
314 continue;
315 }
316
317
318
319 final Object value = json.opt(key);
320 res.add(new IssueField(key, namesMap.get(key), typesMap.get("key"), value != JSONObject.NULL ? value : null));
321 } catch (final Exception e) {
322 throw new JSONException("Error while parsing [" + key + "] field: " + e.getMessage()) {
323 @Override
324 public Throwable getCause() {
325 return e;
326 }
327 };
328 }
329 }
330 return res;
331 }
332
333 private Map<String, String> parseSchema(final JSONObject json) throws JSONException {
334 final HashMap<String, String> res = Maps.newHashMap();
335 final Iterator<String> it = JsonParseUtil.getStringKeys(json);
336 while (it.hasNext()) {
337 final String fieldId = it.next();
338 JSONObject fieldDefinition = json.getJSONObject(fieldId);
339 res.put(fieldId, fieldDefinition.getString("type"));
340
341 }
342 return res;
343 }
344
345 private Map<String, String> parseNames(final JSONObject json) throws JSONException {
346 final HashMap<String, String> res = Maps.newHashMap();
347 final Iterator<String> iterator = getStringKeys(json);
348 while (iterator.hasNext()) {
349 final String key = iterator.next();
350 res.put(key, json.getString(key));
351 }
352 return res;
353 }
354
355 }