4
\$\begingroup\$

This is a follow-up to: Serializing JSON data coming from two URLs in the same object

I have two URLs (urlA and urlB) and they both give me a JSON response back in the same JSON format. Below is my JSON string which I am getting back by calling from urlA. I have shortened it down by having only three reportRecords for understanding purposes. In general, it might have more than ~500 reportRecords.

{
   "aggRecords": {
      "reportRecords": [
         {
            "min": 0,
            "max": 12,
            "avg": 0.3699187,
            "count": 246,
            "sumSq": 571,
            "stddev": 1.4779372,
            "median": 0,
            "percentileMap": {
               "95": 4
            },
            "metricName": "TransactionDuration",
            "dimensions": {
               "env": "dev",
               "pool": "titan",
               "Name": "PostProcessing",
               "Type": "PostProcessing"
            },
            "value": 91
         },
         {
            "min": 0,
            "max": 23,
            "avg": 2.3991289E-4,
            "count": 1463031,
            "sumSq": 3071,
            "stddev": 0.045814946,
            "median": 0,
            "percentileMap": {
               "95": 0
            },
            "metricName": "TransactionDuration",
            "dimensions": {
               "env": "dev",
               "pool": "titan",
               "Name": "ResourceContext",
               "Type": "ResourceContext"
            },
            "value": 351
         },
         {
            "min": 0,
            "max": 1209,
            "avg": 1.9203402,
            "count": 7344636,
            "sumSq": 71832774,
            "stddev": 2.4683187,
            "median": 2,
            "percentileMap": {
               "95": 4
            },
            "metricName": "TransactionDuration",
            "dimensions": {
               "env": "dev",
               "pool": "titan",
               "Name": "DataSync",
               "Type": "DataSync"
            },
            "value": 14104200
         }
      ]
   }
}

Similarly I am also getting another JSON response back by calling urlB and it is in the exact same JSON format as shown above, but the values for all the keys are different.

Now what I need to do is, I need to serialize JSON response coming from both the URLs (urlA and urlB) in DataTransactionMetrics object only and then iterate DataTransactionMetrics object and extract relevant information from it in which I am interested in it. Meaning, I will be extracting few Name values from the DataTransactionMetrics object in which I am interested in it and populate it into hostMetricsList object and at the end print out the full list.

Below is my full code which does the JSON serialization of both the URL's -

private static final List<String> hostNames = Arrays.asList("hostA", "hostB", "hostC"); 

public static void main(String[] args) throws IOException {
    List<HostMetrics> hostMetricsList = new ArrayList<HostMetrics>();
    for (String hostName : hostNames) {
        Calendar startDate = new GregorianCalendar();
        startDate.set(Calendar.MINUTE, 0);
        Calendar endDate = (Calendar) startDate.clone();
        startDate.set(Calendar.HOUR_OF_DAY, 0);

        HostMetrics hostMetrics = new HostMetrics(hostName, startDate, endDate);
        hostMetricsList.add(hostMetrics);
    }

    // and then print out the list
    System.out.println(hostMetricsList);
}

And below is my DataTransactionMetrics class -

public class DataTransactionMetrics {

    private String metricName;
    private Map<String, Integer> percentileMap;
    private String median;
    private String stddev;
    private String sumSq;
    private String count;
    private String avg;
    private String max;
    private String min;

    // getters here

    public Dimensions dimensions;

    class Dimensions {
        private String env;
        private String pool;
        @SerializedName("Name")
        private String name;

        // getters here
    }
}

And below is my HostMetrics class which has all the logic. Can we do some improvements here?

public class HostMetrics {

    private String hostName;
    private String totalNumberOfSyncCall;
    private String syncNinetyFivePercentileCall;
    private String syncAvgCall;
    private String syncMaxCall;
    private String totalNumberOfAsyncCall;
    private String asyncNinetyFivePercentileCall;
    private String asyncAvgCall;

    private static final DateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm");
    private static RestTemplate restTemplate = new RestTemplate();
    private static final Gson gson = new Gson();
    private static final Type listOfMetricsType = new TypeToken<List<DataTransactionMetrics>>() {}.getType();
    private static final String BASE_URL = "http://data.host.com/reports/";
    private static final String TRANSACTION_METRIC = "/Transaction?defaultDim=Name&defaultDim=Type&metric=TransactionDuration&percentile=95";
    private static final String EVENT_METRIC = "/Event?defaultDim=Name&defaultDim=Type&metric=TotalCount&percentile=95";

    public HostMetrics(String hostname, Calendar startDate, Calendar endDate) {
        constructURL(hostname, startDate, endDate);
    }

    private void constructURL(String hostname, Calendar startDate, Calendar endDate) {
        String startTime = df.format(startDate.getTime());
        String endTime = df.format(endDate.getTime());

        // construct urlA
        String urlA = BASE_URL + hostName + TRANSACTION_METRIC + "&startTime=" + startTime
                + "&endTime=" + endTime;

        // construct urlB
        String urlB = BASE_URL + hostName + EVENT_METRIC + "&startTime=" + startTime
                + "&endTime=" + endTime;

        // serialize everything in DataTransactionMetrics object, both the URL's
        List<DataTransactionMetrics> dataMetrics = loadMetrics(urlA);
        dataMetrics.addAll(loadMetrics(urlB));

        hostName = hostname;

        for (DataTransactionMetrics metrics : clientMetrics) {
            if (metrics.getDimensions().getName().equalsIgnoreCase("DataSync")) {
                totalNumberOfSyncCall = metrics.getCount();
                syncNinetyFivePercentileCall = String.valueOf(metrics.getPercentileMap().get("95"));
                syncAvgCall = metrics.getAvg();
                syncMaxCall = metrics.getMax();
            } else if (metrics.getDimensions().getName().equalsIgnoreCase("DataASync")) {
                totalNumberOfAsyncCall = metrics.getCount();
                asyncNinetyFivePercentileCall = String.valueOf(metrics.getPercentileMap().get("95"));
                asyncAvgCall = metrics.getAvg();
            }
        }
    }

    private static List<DataTransactionMetrics> loadMetrics(String url) {
        String jsonString = restTemplate.getForObject(url, String.class);

        JsonObject json = new JsonParser().parse(jsonString).getAsJsonObject();
        JsonArray jarr = json.getAsJsonObject("aggRecords").getAsJsonArray("reportRecords");

        return gson.fromJson(jarr, listOfMetricsType);
    }

    // toString method here
}   

Now I have got everything working in the above code. I am opting for code review to see whether we can improve anything here in the code.

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

The main method is doing too much. It would be better to extract its content to a method, with a meaningful name.


I don't really see the point of this:

List<HostMetrics> hostMetricsList = new ArrayList<HostMetrics>();
for (String hostName : hostNames) {
    // ...
    HostMetrics hostMetrics = new HostMetrics(hostName, startDate, endDate);
    hostMetricsList.add(hostMetrics);
}
System.out.println(hostMetricsList);

Do you really want to print out the HostMetrics as a list? Printing them one by one is not good enough?

for (String hostName : hostNames) {
    // ...
    HostMetrics hostMetrics = new HostMetrics(hostName, startDate, endDate);
    System.out.println(hostMetrics);
}

I don't know of a rule against cloning and passing around Calendar objects, but it just doesn't seem natural. Instead of this:

Calendar startDate = new GregorianCalendar();
startDate.set(Calendar.MINUTE, 0);
Calendar endDate = (Calendar) startDate.clone();
startDate.set(Calendar.HOUR_OF_DAY, 0);
HostMetrics hostMetrics = new HostMetrics(hostName, startDate, endDate);

I would do like this:

Calendar cal = new GregorianCalendar();
cal.set(Calendar.MINUTE, 0);
Date startDate = cal.getTime();
cal.set(Calendar.HOUR_OF_DAY, 0);
Date endDate = cal.getTime();

HostMetrics hostMetrics = new HostMetrics(hostName, startDate, endDate);

This means of course that you'll have to change the HostMetrics constructor to take Date params instead of Calendar, but I think that's a good thing.


Here, appending the startTime and endTime parameters is duplicated:

String urlA = BASE_URL + hostName + TRANSACTION_METRIC + "&startTime=" + startTime
        + "&endTime=" + endTime;

String urlB = BASE_URL + hostName + EVENT_METRIC + "&startTime=" + startTime
        + "&endTime=" + endTime;

I would extract the duplicated parts to a temp variable:

String extraParams = "&startTime=" + startTime + "&endTime=" + endTime;
String urlA = BASE_URL + hostName + TRANSACTION_METRIC + extraParams;
String urlB = BASE_URL + hostName + EVENT_METRIC + extraParams;

Sometimes you call a hostname variable as hostname, sometimes hostName. One day you might mix them up. I recommend to use hostname everywhere consistently.

\$\endgroup\$
1
\$\begingroup\$

java.text.SimpleDateFormat is not thread-safe:

Synchronization

Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

Consider using ThreadLocal<SimpleDateFormat> instead. Or, as you are working with Calendar instances, you may find JodaTime useful in your efforts.


The name of constructURL is misleading; while it does construct URLs, among other things, it also loads network resources. Consider splitting the responsibilities:

class HostMetricLoader {
  public HostMetrics loadMetrics(String hostname, Calendar startDate, Calendar endDate) {
    final List<DataTransactionMetrics> metrics = new ArrayList<>();
    metrics.addAll(loadMetrics(getUrl(EVENT, hostname, startDate, endDate)));
    metrics.addAll(loadMetrics(getUrl(TRANSACTION, hostname, startDate, endDate)));

    final HostMetrics hostMetrics = new HostMetrics(hostname, startDate, endDate);
    for ( final DataTransactionMetrics data : metrics ) {
      hostMetrics.addData(data);
    }

    return metrics;
  }

  protected String getURL(MetricType type, String hostname, Calendar startDate, Calendar endDate) {
    return String.format(/*...*/);
  }

  protected List<DataTransactionMetrics> loadMetrics(String url) {
    // ...
  }
}

enum MetricType {
  EVENT,
  TRANSACTION;
}

class HostMetrics {
  // fields
  // ...

  public void addData(DataTransactionMetrics data) {
    // if this gets larger, split into two functions
    if (metrics.getDimensions().getName().equalsIgnoreCase("DataSync")) {
      totalNumberOfSyncCall = metrics.getCount();
      syncNinetyFivePercentileCall = String.valueOf(metrics.getPercentileMap().get("95"));
      syncAvgCall = metrics.getAvg();
      syncMaxCall = metrics.getMax();
    } else if (metrics.getDimensions().getName().equalsIgnoreCase("DataASync")) {
      totalNumberOfAsyncCall = metrics.getCount();
      asyncNinetyFivePercentileCall = String.valueOf(metrics.getPercentileMap().get("95"));
      asyncAvgCall = metrics.getAvg();
    }
  }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.