public void serveDescription(RestServiceRequest rsr) {
    rsr.startDoc();

    if (rsr.getResponseContentType() == ResponseType.HTML)
      rsr.write("<h1>Operation: ").write(getRelativeUrl()).write("</h1>");

    rsr.datawriter
        .object()
        .key("name")
        .value(getRelativeUrl())
        .key("description")
        .value(description)
        .key("url")
        .value(RestServices.getAbsoluteUrl(getRelativeUrl()))
        .key("arguments")
        .value(
            hasArgument ? JSONSchemaBuilder.build(Utils.getFirstArgumentType(microflowname)) : null)
        .key("accepts_binary_data")
        .value(isFileSource)
        .key("result")
        .value(
            isFileTarget
                ? RestServices.CONTENTTYPE_OCTET + " stream"
                : JSONSchemaBuilder.build(Core.getReturnType(microflowname)))
        .endObject();

    rsr.endDoc();
  }
  public void serveServiceDescription() {
    rsr.startDoc();
    if (isHTML) {
      rsr.write(
          "<h1>Service: " + def.getName() + "</h1><a href='/" + RestServices.PATH_REST + "'>");
    }

    rsr.datawriter
        .object()
        .key("name")
        .value(def.getName())
        .key("description")
        .value(def.getDescription())
        .key("baseurl")
        .value(RestServices.getAbsoluteUrl(def.getName()))
        .key("worldreadable")
        .value("*".equals(def.getAccessRole()))
        .key("requiresETags")
        .value(def.getUseStrictVersioning());

    if (isHTML) rsr.datawriter.endObject();
    else rsr.datawriter.key("endpoints").array();

    startEndpoint("GET", "?" + RestServices.PARAM_ABOUT, "This page");
    addContentType();
    endEndpoint();

    if (def.getEnableListing()) {
      startEndpoint(
          "GET",
          "?" + RestServices.PARAM_COUNT,
          "Returns the amount of objects available in this service");
      addContentType();
      endEndpoint();

      startEndpoint("GET", "", "List the URL of all objects published by this service");
      addEndpointParam(
          RestServices.PARAM_DATA,
          "'true' or 'false'. Whether to list the URLs (false) of each of the objects, or output the objects themselves (true). Defaults to 'false'");
      addEndpointParam(RestServices.PARAM_OFFSET, "positive number, optional argument");
      addEndpointParam(RestServices.PARAM_LIMIT, "positive number, optional argument");
      addContentType();
      endEndpoint();
    }
    if (def.getEnableGet()) {
      startEndpoint(
          "GET",
          "<" + def.getSourceKeyAttribute() + ">",
          "Returns the object specified by the URL, which is retrieved from the database by using the given key.");
      addEndpointParam(
          RestServices.HEADER_IFNONEMATCH + " (header)",
          "If the current version of the object matches the ETag provided by this optional header, status 304 NOT MODIFIED will be returned instead of returning the whole objects. This header can be used for caching / performance optimization");
      addContentType();

      JSONObject schema = JSONSchemaBuilder.build(Core.getReturnType(def.getOnPublishMicroflow()));
      addEndpointParam("(request body)", schema);

      endEndpoint();
    }
    if (def.getEnableChangeLog()) {
      startEndpoint(
          "GET",
          "changes/list",
          "Returns a list of incremental changes that allows the client to synchronize with recent changes on the server");
      addEndpointParam(RestServices.PARAM_SINCE, SINCEPARAM_HELPTEXT);
      addContentType();
      endEndpoint();

      startEndpoint(
          "GET",
          "changes/feed",
          "Returns a list of incremental changes that allows the client to synchronize with recent changes on the server. The feed, in contrast to list, keeps the connection open to be able to push any new change directly to the client, without the client needing to actively request for new changes. (a.k.a. push over longpolling HTTP)");
      addEndpointParam(RestServices.PARAM_SINCE, SINCEPARAM_HELPTEXT);
      addEndpointParam(
          RestServices.PARAM_TIMEOUT,
          "Maximum time the current feed connecion is kept open. Defaults to 50 seconds to avoid firewall issues. Once this timeout exceeds, the connection is closed and the client should automatically reconnect. Use zero to never expire. Use a negative number to indicate that the connection should expire whenever the timeout is exceed, *or* when a new change arrives. This is useful for clients that cannot read partial responses");
      addContentType();
      endEndpoint();
    }
    if (def.getEnableCreate()) {
      startEndpoint(
          "POST",
          "",
          "Stores an object as new entry in the collection served by this service. Returns the (generated) key of the new object");
      addBodyParam();
      addEtagParam();
      endEndpoint();

      startEndpoint(
          "PUT",
          "<" + def.getSourceKeyAttribute() + ">",
          "Stores an object as new entry in the collection served by this service, under te given key. This key shouldn't exist yet.");
      addBodyParam();
      addEtagParam();
      endEndpoint();
    }
    if (def.getEnableUpdate()) {
      startEndpoint(
          "PUT",
          "<" + def.getSourceKeyAttribute() + ">",
          "Updates the object with the given key. If the key does not exist yet, "
              + (def.getEnableCreate() ? "the object will be created" : " the request will fail"));
      addBodyParam();
      addEtagParam();
      endEndpoint();
    }
    if (def.getEnableDelete()) {
      startEndpoint(
          "DELETE",
          "<" + def.getSourceKeyAttribute() + ">",
          "Deletes the object identified by the key");
      addBodyParam();
      addEtagParam();
      endEndpoint();
    }

    if (!isHTML) rsr.datawriter.endArray().endObject();

    rsr.endDoc();
  }