class CrudDispatcher extends Dispatcher {

  private static final String POST = "POST";
  private static final String PUT = "PUT";
  private static final String PATCH = "PATCH";
  private static final String GET = "GET";
  private static final String DELETE = "DELETE";

  protected final Map<AttributeSet, String> map = Collections.synchronizedMap(new LinkedHashMap<>());

  protected final Context context;
  protected final AttributeExtractor attributeExtractor;
  protected final ResponseComposer responseComposer;

  public CrudDispatcher(Context context, AttributeExtractor attributeExtractor, ResponseComposer responseComposer) {
    this.context = context;
    this.attributeExtractor = attributeExtractor;
    this.responseComposer = responseComposer;
  }

  @Override
  public MockResponse dispatch(RecordedRequest request) {
    String path = request.getPath();
    switch (request.getMethod().toUpperCase()) {
      case POST:
        return handleCreate(request);
      case PUT:
        return handleUpdate(request);
      case PATCH:
        return handlePatch(request);
      case GET:
        return handleGet(path);
      case DELETE:
        return handleDelete(path);
      default:
        return null;
    }
  }

  public MockResponse handleCreate(RecordedRequest request) {
    return handleCreate(request.getPath(), request.getBody().readUtf8());
  }

  /**
   * Adds the specified object to the in-memory db.
   *
   * @param path for the request.
   * @param body Request body as String (UTF-8).
   * @return a MockResponse to be dispatched.
   */
  public MockResponse handleCreate(String path, String body) {
    MockResponse response = new MockResponse();
    AttributeSet features = AttributeSet.merge(attributeExtractor.fromPath(path), attributeExtractor.fromResource(body));
    synchronized (map) {
      map.put(features, body);
    }
    response.setBody(body);
    response.setResponseCode(202);
    return response;
  }

  public MockResponse handlePatch(RecordedRequest request) {
    return handlePatch(request.getPath(), request.getBody().readUtf8());
  }

  /**
   * Patches the specified object to the in-memory db.
   *
   * @param path for the request.
   * @param body Request body as String (UTF-8).
   * @return a MockResponse to be dispatched.
   */
  public MockResponse handlePatch(String path, String body) {<FILL_FUNCTION_BODY>}

  public MockResponse handleUpdate(RecordedRequest request) {
    return handleUpdate(request.getPath(), request.getBody().readUtf8());
  }

  /**
   * Updates the specified object to the in-memory db.
   *
   * @param path for the request.
   * @param body Request body as String (UTF-8).
   * @return a MockResponse to be dispatched.
   */
  public MockResponse handleUpdate(String path, String body) {
    final String currentItem = doGet(path);
    final MockResponse response = handleCreate(path, body);
    if (currentItem == null) {
      response.setResponseCode(HttpURLConnection.HTTP_CREATED);
    }
    return response;
  }

  /**
   * Performs a get for the corresponding object from the in-memory db.
   *
   * @param path for the request.
   * @return a MockResponse to be dispatched.
   */
  public MockResponse handleGet(String path) {
    MockResponse response = new MockResponse();

    String body = doGet(path);
    if (body == null) {
      response.setResponseCode(404);
    } else {
      response.setResponseCode(200);
      response.setBody(body);
    }
    return response;
  }

  /**
   * Performs a delete for the corresponding object from the in-memory db.
   *
   * @param path for the request.
   * @return a MockResponse to be dispatched.
   */
  public MockResponse handleDelete(String path) {
    MockResponse response = new MockResponse();
    List<AttributeSet> items = new ArrayList<>();
    AttributeSet query = attributeExtractor.fromPath(path);

    synchronized (map) {
      for (Map.Entry<AttributeSet, String> entry : map.entrySet()) {
        if (entry.getKey().matches(query)) {
          items.add(entry.getKey());
        }
      }
      if (!items.isEmpty()) {
        for (AttributeSet item : items) {
          map.remove(item);
        }
        response.setResponseCode(200);
      } else {
        response.setResponseCode(404);
      }
    }
    return response;
  }

  public Map<AttributeSet, String> getMap() {
    return map;
  }

  public AttributeExtractor getAttributeExtractor() {
    return attributeExtractor;
  }

  public ResponseComposer getResponseComposer() {
    return responseComposer;
  }

  private String doGet(String path) {
    List<String> items = new ArrayList<>();
    AttributeSet query = attributeExtractor.fromPath(path);
    synchronized (map) {
      for (Map.Entry<AttributeSet, String> entry : map.entrySet()) {
        if (entry.getKey().matches(query)) {
          items.add(entry.getValue());
        }
      }
    }

    if (items.isEmpty()) {
      return null;
    } else if (items.size() == 1) {
      return items.get(0);
    } else {
      return responseComposer.compose(items);
    }
  }
}

    MockResponse response = new MockResponse();
    String existingObjectBody = doGet(path);
    if (existingObjectBody == null) {
      response.setResponseCode(404);
    } else {
      try {
        JsonNode patch = context.getMapper().readTree(body);
        JsonNode source = context.getMapper().readTree(existingObjectBody);
        JsonNode updated = JsonPatch.apply(patch, source);
        String updatedAsString = context.getMapper().writeValueAsString(updated);
        AttributeSet features = AttributeSet.merge(attributeExtractor.fromPath(path),
            attributeExtractor.fromResource(updatedAsString));
        synchronized (map) {
          map.put(features, updatedAsString);
        }
        response.setResponseCode(202);
        response.setBody(updatedAsString);
      } catch (Exception e) {
        throw new MockServerException("Exception when handling CRUD patch", e);
      }

    }
    return response;
  
<no_super_class>