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