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