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