0

As mentioned in this answer I've written a DTO like below

class Car { 
    ... 
    private TransmissionType transmissionType;
    
    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "transmissionType")
    private Transmission transmission; 
}


@JsonSubTypes({
        @JsonSubTypes.Type(value = AutomaticTransmission.class, name = "AUTOMATIC"),
        @JsonSubTypes.Type(value = ManualTransmission.class, name = "MANUAL")
})
public abstract class Transmission {
}

public class AutomaticTransmission {
     public Technology technology; // DCT/CVT/AMT
}

public class ManualTransmission {
     public int numGears; 
}

Now, while doing POST /api/v1/cars user can send the transmissionType but while editing the car i.e. PATCH /api/v1/cars/{id}, it's bit weird to ask users to send type of car as they've already created the car and service should know it. I was thinking how can I deduce type of car without asking the user.

One solution, I was thinking of writing some interceptor which will get the path parameter id and from database I'll figure out the type of the car and insert as transmissionType this way, without user passing transmissionType controller will get it.

Second solution it to get json body with car but with this approach user won't see strong types and validation needs to be manually done.

What approach should I choose? Is there any other better approach to tackle this problem?

Ganesh Satpute
  • 3,664
  • 6
  • 41
  • 78

2 Answers2

0

Well, it depends on how you understand the PATCH request. If you always expect the request to contain a whole entity (which would look to me more like a PUT, not a PATCH) than yes, the user needs to include the transmissionType even if it's not changed. I think it's pretty natural. Otherwise, how can you know if the type has not been changed?

On the other hand, with a "real" patch, the user can just sent changed fields and you need to patch an entity retrieved from the DB. In that case, the transmissionType is not needed if it's not altered.

I think it would be more natural to include transmissionType in the Transmission object instead of the Car (changing include to As.PROPERTY). Then, during PATCH, user either does not send transmission at all (no changes) or sends it with the transmissionType.

Mafor
  • 9,668
  • 2
  • 21
  • 36
0

I ended up solving the problem as below

public class TypeInferenceRequestWrapper extends HttpServletRequestWrapper {
    // https://stackoverflow.com/a/63073783/2870572
    private final String body;

    public TypeInferenceRequestWrapper(HttpServletRequest request) throws IOException {
        // So that other request method behave just like before
        super(request);

        String requestBody = readBody(request);

        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = objectMapper.readTree(requestBody);

        String requestURI = request.getRequestURI();
        String requestMethod = request.getMethod();

        if (jsonNode.getNodeType() == JsonNodeType.OBJECT) {
            Map.Entry<String, String> entry = TypeInference.inferType(requestURI, requestMethod);
            ((ObjectNode) jsonNode).put(entry.getKey(), entry.getValue());
        }

        body = jsonNode.toString();
    }

    private String readBody(HttpServletRequest request) {
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        try {
            InputStream inputStream = request.getInputStream();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                char[] charBuffer = new char[128];
                int bytesRead = -1;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }

            }
        } catch (IOException ex) {
            throw new ApiServiceException("Error while reading payload body", ex);
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException ex) {
                    throw new ApiServiceException("Error while closing the buffered reader", ex);
                }
            }
        }
        return stringBuilder.toString();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        return new ServletInputStream() {
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}
 

Then adding a new filter

@Component
public class TypeInferenceRequestFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        TypeInferenceRequestWrapper wrappedRequest = new TypeInferenceRequestWrapper((HttpServletRequest) request);
        chain.doFilter(wrappedRequest, response);
    }

    @Override
    public void destroy() {

    }
}

And the class where it'll be actual logic which will change with requirements

public class TypeInference {
    public static Map.Entry<String, String> inferType(String requestURI, String requestMethod) {
        if (requestURI.matches("\\/api\\/v1\\/cars\\/[0-9]+")
                && requestMethod.equals(HttpMethod.PATCH.toString())) {
    
            // Get transmission type from database as this is already saved in db
            long carId = getCarId(requestURI);  
            TransmissionType type = getTransmissionTypeForCar(carId)
            return new AbstractMap.SimpleEntry<>("transmissionType", type.toString());
        }

        return new AbstractMap.SimpleEntry<>("", "");
    }
}
Ganesh Satpute
  • 3,664
  • 6
  • 41
  • 78