View Javadoc

1   /*
2    * Copyright (C) 2010 Atlassian
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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 //        String type = jsonObject.getString("type");
130 //        final String name = jsonObject.getString("name");
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) { // pre JIRA 5.0 way
175 			return ((JSONObject) summaryObject).getString(VALUE_ATTR);
176 		}
177 		if (summaryObject instanceof String) { // JIRA 5.0 way
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); // pre 5.0 way
188 		} else {
189 			return fieldJson; // JIRA 5.0 way
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); // pre 5.0 way
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); // pre 5.0 way
213 		}
214 		return fieldJson.toString(); // JIRA 5.0 way
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 		// for fields like assignee (when unassigned) value attribute may be missing completely
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 }