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