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