Read JSON Requests with Stripes 1.5

Outdated Code

In Stripes 1.7 there is a much better solution to all of this using @RestActionBean. Here's the RESTful Calculator Example:

I used Stripes 1.5 to communicate with an angular 1.5 client which posts with a JSON body by wrapping the HTTPServletRequest.

It uses Jackson to read the request body and stores each JSON property as an HTTP request param, with the property's value as a JSON string.

This allowed us to again use Jackson in our ActionBeans to populate our model objects.

Angular post
var params = {
  startDate: asIso8601String(new Date(2017, 0, 1)),
  endDate: asIso8601String(new Date(2018, 0, 1))
}
$http.post(restAPI.stats, { "params": params });
JsonRequestInterceptor
package com.example.util;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;

import com.example.util.JsonRequest;

import net.sourceforge.stripes.action.ActionBeanContext;
import net.sourceforge.stripes.action.Resolution;
import net.sourceforge.stripes.controller.ExecutionContext;
import net.sourceforge.stripes.controller.Interceptor;
import net.sourceforge.stripes.controller.Intercepts;
import net.sourceforge.stripes.controller.LifecycleStage;

/**
 * Wrap JSON requests in this wrapper so that Strings can access the JSON contents via request parameters.
 */
@Intercepts(LifecycleStage.ActionBeanResolution)
public class JsonRequestInterceptor implements Interceptor {

  @Override
  public Resolution intercept(ExecutionContext executionCtx) throws Exception {
    ActionBeanContext ctx = executionCtx.getActionBeanContext();
    HttpServletRequest request = ctx.getRequest();
    String contentType = request.getHeader("content-type");

    if (StringUtils.contains(contentType, MediaType.APPLICATION_JSON.toString())) {
      // wrap the request
      ctx.setRequest(new JsonRequest(ctx.getRequest()));
    }

    return executionCtx.proceed();
  }
}
JsonRequest
package com.example.util;

import java.io.BufferedReader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.apache.commons.lang3.ArrayUtils;
import org.springframework.http.MediaType;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Used to wrap an {@link HttpServletRequest} with a {@link MediaType#APPLICATION_JSON} header so that the body of the request can be read as a JSON object and so that Stripes can still have access to
 * request parameters.
 */
public class JsonRequest extends HttpServletRequestWrapper {
  private Map<String, String[]> parameters = new HashMap<String, String[]>();

  public JsonRequest(HttpServletRequest request) throws Exception {
    super(request);
    buildParameters(request);
  }

  /**
   * Read the body of the request as JSON then map the root field names as parameter keys.
   */
  private void buildParameters(HttpServletRequest request) throws Exception {
    StringBuilder builder = new StringBuilder();
    String line;
    BufferedReader reader = request.getReader();
    while ((line = reader.readLine()) != null) {
      builder.append(line);
    }

    String body = builder.toString();

    ObjectMapper mapper = new ObjectMapper();
    JsonNode root = mapper.readValue(body, JsonNode.class);
    Iterator<String> fieldNames = root.fieldNames();
    while (fieldNames.hasNext()) {
      String key = fieldNames.next();
      JsonNode value = root.get(key);
      parameters.put(key, new String[] { value.toString() });
    }
  }

  @Override
  public String getParameter(String name) {
    String value = null;

    Map<String, String[]> mergedParams = getParameterMap();
    String[] values = mergedParams.get(name);
    if (ArrayUtils.getLength(values) > 0) {
      value = values[0];
    }

    return value;
  }

  @Override
  public Map<String, String[]> getParameterMap() {
    Map<String, String[]> parentParameters = super.getParameterMap();

    Map<String, String[]> mergedParams = new HashMap<String, String[]>(parentParameters.size() + parameters.size());
    mergedParams.putAll(parentParameters);
    mergedParams.putAll(parameters);
    return mergedParams;
  }

  @Override
  public String[] getParameterValues(String name) {
    Map<String, String[]> mergedParams = getParameterMap();
    return mergedParams.get(name);
  }
}

Example ActionBean

StatsWS
package com.example.stats;

import java.util.Date;

import org.apache.commons.lang3.StringUtils;

import com.example.stats.StatsService;
import com.example.util.JsonTypeConverter;
import com.example.util.BaseWS;
import com.example.util.JsonResolution;

import net.sourceforge.stripes.action.DefaultHandler;
import net.sourceforge.stripes.action.Resolution;
import net.sourceforge.stripes.action.UrlBinding;
import net.sourceforge.stripes.integration.spring.SpringBean;
import net.sourceforge.stripes.validation.SimpleError;
import net.sourceforge.stripes.validation.Validate;
import net.sourceforge.stripes.validation.ValidationErrors;
import net.sourceforge.stripes.validation.ValidationMethod;

/**
 * Stats web service.
 * 
 * Loads global stats, and if a date range is provided loads the interval stats as well.
 */
@UrlBinding("/ws/stats")
public class StatsWS extends BaseWS {
  private String START_DATE_FIELD_NAME = "params.startDate";

  private String END_DATE_FIELD_NAME = "params.endDate";

  @SpringBean
  private StatsService statsService;

  private StatsParamsRequest params;

  private Date startDate; // populated during validation

  private Date endDate; // populated during validation

  @Validate(required = true, converter = JsonTypeConverter.class)
  public void setParams(StatsParamsRequest params) {
    this.params = params;
  }

  @DefaultHandler
  public Resolution reply() {
    Stats global = statsService.loadGlobalStats();

    Stats interval = null;
    if (startDate != null && endDate != null) {
      interval = statsService.loadIntervalStats(startDate, endDate);
    }

    StatsReply reply = new StatsReply();
    reply.setGlobal(global);
    reply.setInterval(interval);
    return new JsonResolution(reply);
  }

  @ValidationMethod
  public void validateDates(ValidationErrors errors) {
    if (params == null) {
      errors.add("params", new SimpleError("params is null"));
    }

    boolean hasStart = !StringUtils.isEmpty(params.getStartDate());
    boolean hasEnd = !StringUtils.isEmpty(params.getEndDate());
    if (hasStart && hasEnd) {
      startDate = parseIso8601Date(params.getStartDate(), START_DATE_FIELD_NAME, errors);
      endDate = parseIso8601Date(params.getEndDate(), END_DATE_FIELD_NAME, errors);
    }
    else if (hasStart ^ hasEnd) { // both dates can be null, but not just one of them
      errors.add(START_DATE_FIELD_NAME, new SimpleError(StatsService.MSG_START_END_DATES));
    }
  }
}
JsonTypeConverter
package com.example.util;

import java.io.IOException;
import java.util.Collection;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.fasterxml.jackson.databind.ObjectMapper;

import net.sourceforge.stripes.validation.TypeConverter;

@SuppressWarnings("rawtypes")
public class JsonTypeConverter implements TypeConverter {

  @Override
  public void setLocale(Locale locale) {
    // nothing to do
  }

  @SuppressWarnings("unchecked")
  @Override
  public Object convert(String string, Class type, Collection clctn) {
    ObjectMapper mapper = new ObjectMapper();
    try {
      return mapper.readValue(string, type);
    }
    catch (IOException ex) {
      Logger.getLogger(JsonTypeConverter.class.getName()).log(Level.SEVERE, null, ex);
    }
    return null;
  }
}

How to respond with JSON

JsonResolution
package com.example.util;

import java.io.IOException;

import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.Validate;
import org.apache.commons.logging.LogFactory;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.example.util.LogUtil;

import net.sourceforge.stripes.action.StreamingResolution;

/**
 * The default response to a web service which {@link JsonReply replys} with {@link StreamingResolution streaming} JSON using a Jackson {@link ObjectMapper}.
 */
public class JsonResolution extends AbstractJsonResolution {

  private static final LogUtil LOG = LogUtil.getInstance(LogFactory.getLog(JsonResolution.class));

  private final JsonReply reply;

  public JsonResolution(JsonReply reply) {
    super();
    Validate.notNull(reply);
    this.reply = reply;
  }

  @Override
  public void stream(HttpServletResponse response) {
    Validate.notNull(response);
    LOG.trace("Replying with: ", reply.getClass().getSimpleName(), " : ", reply);

    ObjectMapper mapper = new ObjectMapper();
    if (reply instanceof EmptyReply) {
      mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    }
    try {
      if (reply instanceof FilterableJsonReply) { // ignore specific fields
        String[] nonFilteredFields = ((FilterableJsonReply) reply).getNonFilteredFields();
        FilterProvider filters = new SimpleFilterProvider().addFilter("filter", SimpleBeanPropertyFilter.filterOutAllExcept(nonFilteredFields));
        mapper.writer(filters).writeValue(response.getOutputStream(), reply);
      }
      else { // write full content
        mapper.writeValue(response.getOutputStream(), reply);
      }
    }
    catch (IOException e) {
      LOG.error(e);
    }
  }
}
AbstractJsonResolution
package com.example.util;

import javax.servlet.http.HttpServletResponse;

import net.sourceforge.stripes.action.StreamingResolution;

/**
 * An abstract base {@link StreamingResolution streaming} JSON response to a web service.
 */
public abstract class AbstractJsonResolution extends StreamingResolution {
  protected static final String DEFAULT_CHARSET = "UTF-8";
  protected static final String JSON_MEDIA_TYPE = "application/json";

  protected AbstractJsonResolution() {
    super(JSON_MEDIA_TYPE);
    setCharacterEncoding(DEFAULT_CHARSET);
  }

  @Override
  protected abstract void stream(HttpServletResponse response);
}