public PagedResult<ReviewReply> readReviewReplies(String source) throws ReviewboardException {

    try {
      JSONObject rootObject = checkedGetJSonRootObject(source);

      int totalResults = rootObject.getInt("total_results");

      JSONArray jsonReplies = rootObject.getJSONArray("replies");
      List<ReviewReply> replies = new ArrayList<ReviewReply>();

      for (int i = 0; i < jsonReplies.length(); i++) {

        JSONObject jsonReply = jsonReplies.getJSONObject(i);

        ReviewReply reply = new ReviewReply();
        reply.setId(jsonReply.getInt("id"));
        reply.setBodyTop(jsonReply.getString("body_top"));
        reply.setBodyBottom(jsonReply.getString("body_bottom"));
        reply.setPublicReply(jsonReply.getBoolean("public"));
        reply.setTimestamp(ReviewboardUtil.marshallDate(jsonReply.getString("timestamp")));
        reply.setUser(jsonReply.getJSONObject("links").getJSONObject("user").getString("title"));

        replies.add(reply);
      }

      return PagedResult.create(replies, totalResults);
    } catch (JSONException e) {
      throw new ReviewboardException(e.getMessage(), e);
    }
  }
  private void mapComment(JSONObject jsonComment, Comment comment) throws JSONException {

    comment.setId(jsonComment.getInt("id"));
    comment.setUsername(
        jsonComment.getJSONObject("links").getJSONObject("user").getString("title"));
    comment.setText(jsonComment.getString("text"));
    comment.setTimestamp(ReviewboardUtil.marshallDate(jsonComment.getString("timestamp")));
  }
  private Diff parseDiff(JSONObject jsonDiff) throws JSONException {

    int revision = jsonDiff.getInt("revision");
    String name = jsonDiff.getString("name");
    int id = jsonDiff.getInt("id");
    Date timestamp = ReviewboardUtil.marshallDate(jsonDiff.getString("timestamp"));

    return new Diff(id, name, timestamp, revision);
  }
  private Review getReview(JSONObject jsonReview) throws JSONException {

    Review review = new Review();
    review.setId(jsonReview.getInt("id"));
    review.setBodyTop(jsonReview.getString("body_top"));
    review.setBodyBottom(jsonReview.getString("body_bottom"));
    review.setUser(jsonReview.getJSONObject("links").getJSONObject("user").getString("title"));
    review.setPublicReview(jsonReview.getBoolean("public"));
    review.setShipIt(jsonReview.getBoolean("ship_it"));
    review.setTimestamp(ReviewboardUtil.marshallDate(jsonReview.getString("timestamp")));
    return review;
  }
  private ReviewRequest readReviewRequest(JSONObject jsonReviewRequest) throws JSONException {

    ReviewRequest reviewRequest = fillReviewRequestBase(new ReviewRequest(), jsonReviewRequest);

    JSONObject links = jsonReviewRequest.getJSONObject("links");

    reviewRequest.setSubmitter(links.getJSONObject("submitter").getString("title"));
    reviewRequest.setStatus(ReviewRequestStatus.parseStatus(jsonReviewRequest.getString("status")));
    reviewRequest.setLastUpdated(
        ReviewboardUtil.marshallDate(jsonReviewRequest.getString("last_updated")));
    reviewRequest.setTimeAdded(
        ReviewboardUtil.marshallDate(jsonReviewRequest.getString("time_added")));
    if (links.has("repository"))
      reviewRequest.setRepository(links.getJSONObject("repository").getString("title"));

    // change number
    String changeNumString = jsonReviewRequest.getString("changenum");
    Integer changeNum = changeNumString.equals("null") ? null : Integer.valueOf(changeNumString);
    reviewRequest.setChangeNumber(changeNum);

    return reviewRequest;
  }
  private Change readChangeObject(JSONObject jsonChange) throws JSONException {

    JSONObject jsonFieldsChanged = jsonChange.getJSONObject("fields_changed");
    List<FieldChange> fieldChanges = Lists.<Change.FieldChange>newArrayList();

    for (Field field : Field.values()) {

      if (!jsonFieldsChanged.has(field.toString())) continue;

      JSONObject jsonFieldChange = jsonFieldsChanged.getJSONObject(field.toString());

      switch (field) {
        case bugs_closed:
          String newBugs = CSV_JOINER.join(readStringArray(jsonFieldChange, "added"));
          String oldBugs = CSV_JOINER.join(readStringArray(jsonFieldChange, "removed"));

          fieldChanges.add(new FieldChange(field, null, newBugs, oldBugs, null, null));
          break;

        case diff:
          JSONObject addedDiff = jsonFieldChange.getJSONObject("added");

          fieldChanges.add(
              new FieldChange(
                  field,
                  new ObjectLink(addedDiff.getInt("id"), Diff.class),
                  null,
                  null,
                  null,
                  null));
          break;

          // TODO unhandled
        case screenshots:
        case file_attachments:
        case target_groups:
        case target_people:
          continue;

        case status:
          fieldChanges.add(
              new FieldChange(
                  field,
                  null,
                  ReviewRequestStatus.parseStatus(jsonFieldChange.getString("new")).toString(),
                  ReviewRequestStatus.parseStatus(jsonFieldChange.getString("old")).toString(),
                  null,
                  null));
          break;

        case summary:
        case description:
        case testing_done:
        case branch:
          fieldChanges.add(
              new FieldChange(
                  field,
                  null,
                  jsonFieldChange.getString("new"),
                  jsonFieldChange.getString("old"),
                  null,
                  null));
          break;

        default:
          ReviewboardCorePlugin.getDefault()
              .log(
                  IStatus.WARNING,
                  "Could not parse change due to unhandled field with name " + field.toString());
          continue;
      }
    }

    return new Change(
        jsonChange.getInt("id"),
        jsonChange.getString("text"),
        ReviewboardUtil.marshallDate(jsonChange.getString("timestamp")),
        fieldChanges);
  }