View Javadoc

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