Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP interface for tenant API. #481

Closed

Conversation

sysexcontrol
Copy link
Contributor

Contained is the implementation of the http interface for the tenant API, including tests.

Not contained is the documentation - this should be the discussion base for the implementation.

Please do not review the first commit - it is just the squashed version of the AMQP implementation PR that was discussed already. The two commits on top are the ones for the HTTP and will be the only ones that will make it to master later.

response
.putHeader(HttpHeaders.CONTENT_TYPE, HttpUtils.CONTENT_TYPE_JSON_UFT8)
.putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(msg.length()))
.write(msg);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this what setResponseBody() does?

} catch (final DecodeException e) {
HttpUtils.badRequest(ctx, "body does not contain a valid JSON object");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the same code as doAddTenantJson. Maybe we can refactor the body extraction code into a separate method?


doTenantAction(ctx, requestMsg, (status, tenantResult) -> {
response.setStatusCode(status);
if (status >=400) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting

Copy link
Contributor

@sophokles73 sophokles73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we seem to be duplicating test code over and over again. I have to admit that I do not like that at all because it makes maintenance harder and harder. We should start factoring out the CRUD HTTP client code. I would also actually like to move these tests to the integration test suite and have real unit tests here (without the need to start and bind a HTTP server)

* Constants used by protocol adapters of Hono.
*
*/
public final class ProtocolAdapterConstants {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we simply add these to Constants?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right.Will move them.

public static JsonObject buildTenantPayload(final String tenantId) {
final JsonObject adapterDetailsHttp = new JsonObject().
put(TenantConstants.FIELD_ADAPTERS_TYPE, ProtocolAdapterConstants.TYPE_HTTP).
put(TenantConstants.FIELD_ADAPTERS_DEVICE_AUTHENTICATION_REQUIRED, "true").
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a boolean, not a string literal

final JsonObject adapterDetailsHttp = new JsonObject().
put(TenantConstants.FIELD_ADAPTERS_TYPE, ProtocolAdapterConstants.TYPE_HTTP).
put(TenantConstants.FIELD_ADAPTERS_DEVICE_AUTHENTICATION_REQUIRED, "true").
put(TenantConstants.FIELD_ENABLED, "true");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a boolean, not a string literal

put(TenantConstants.FIELD_ENABLED, "true");
final JsonObject adapterDetailsMqtt = new JsonObject().
put(TenantConstants.FIELD_ADAPTERS_TYPE, ProtocolAdapterConstants.TYPE_MQTT).
put(TenantConstants.FIELD_ADAPTERS_DEVICE_AUTHENTICATION_REQUIRED, "true").
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a boolean, not a string literal

final JsonObject adapterDetailsMqtt = new JsonObject().
put(TenantConstants.FIELD_ADAPTERS_TYPE, ProtocolAdapterConstants.TYPE_MQTT).
put(TenantConstants.FIELD_ADAPTERS_DEVICE_AUTHENTICATION_REQUIRED, "true").
put(TenantConstants.FIELD_ENABLED, "true");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a boolean, not a string literal

put(TenantConstants.FIELD_ENABLED, "true");
final JsonObject tenantPayload = new JsonObject().
put(TenantConstants.FIELD_TENANT_ID, tenantId).
put(TenantConstants.FIELD_ENABLED, "true").
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a boolean, not a string literal

@@ -139,6 +165,18 @@ public static boolean testJsonObjectToBeContained(final JsonObject jsonObject, f
containResult.set(false);
}
}
} else if (entry.getValue() instanceof JsonArray) {
if (!(jsonObject.getValue(entry.getKey()) instanceof JsonArray)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update the JavaDoc accordingly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 thanks for spotting.

return false;
}

if (testJsonObjectToBeContained((JsonObject)elemOfBiggerArray, (JsonObject)containedElem)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting

@sysexcontrol
Copy link
Contributor Author

@sophokles73 : I pushed two new commits. The first one is a half rewrite of the TenantHttpEndpoint, which eliminates most code duplications existing before. Can you please have a look and tell me what you think about this way of coding it? I like it much better (although it still can be improved) - WDYT?

Not addressed the Test code duplications so far, would prefer to do this in another PR (after the release and after the merge of the Tenant API implementation).

Copy link
Contributor

@sophokles73 sophokles73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do like this approach a lot 👍
I thought about applying this style to the HTTP adapter in the past as well and I still think it would be worth refactoring it in this way. But that is something for 0.6 or 0.7, I guess :-)

@@ -68,155 +69,104 @@ public String getName() {
public void addRoutes(final Router router) {

final String path = String.format("/%s", TenantConstants.TENANT_ENDPOINT);

final BodyHandler bodyHandler = BodyHandler.create();
bodyHandler.setBodyLimit(2048); // limit body size to 2kb
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably come from a config property

}

private void doTenantAction(final RoutingContext ctx, final JsonObject requestMsg, final BiConsumer<Integer, JsonObject> responseHandler) {
private void sendTenantAction(final RoutingContext ctx, final JsonObject requestMsg, final BiConsumer<Integer, JsonObject> responseHandler) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to pull this up into AbstractHttpEndpoint, don't you think? There doesn't seem to be anything left that is tenant specific, is there?

if (status >=400) {
setResponseBody(tenantResult, response);
} else if (sendResponseForStatus != null) {
if (sendResponseForStatus.test(status)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not let another handler process the response? we can provide (and register) default handlers for the CRUD responses which can be overridden by sub-classes if appropriate

* successful.
* <p>
* The payload is parsed to ensure it is valid JSON and is put to the RoutingContext ctx with the
* key {@link #KEY_REQUEST_BODY}.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't seem to be correct

put(TenantConstants.FIELD_ADAPTERS_TYPE, ProtocolAdapterConstants.TYPE_HTTP).
put(TenantConstants.FIELD_ADAPTERS_DEVICE_AUTHENTICATION_REQUIRED, "true").
put(TenantConstants.FIELD_ENABLED, "true");
put(TenantConstants.FIELD_ADAPTERS_TYPE, Constants.TYPE_HTTP).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we usually put the period on the new line, e.g.

new JsonObject()
  .put("key", value);

@sysexcontrol
Copy link
Contributor Author

@sophokles73 : I pushed 2 new commits, one is a larger effort to further reduce the repeating code of the http endpoints (as already started before).
The registration and credentials endpoint are intentionally not fully refactored in here, but the base to do that afterwards is contained (and fully used for the tenant http endpoint).

Due to a necessary rebase, I needed to make a forced push (the last 2 commits are the new ones).

@@ -43,6 +43,37 @@
public static final String FIELD_PAYLOAD = "payload";
public static final String FIELD_TENANT_ID = "tenant-id";

/* request actions */
public enum Action {
ACTION_GET, ACTION_ADD, ACTION_UPDATE, ACTION_REMOVE, ACTION_UNKNOWN;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not believe that these are the same for all request/response APIs, are they?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately: you are right. I haven't looked into the registration API, which defines more actions than this. With the Credentials API it would have been fitting (but only for now).

I will come up with a better solution, this has been too much generalized obviously...

final RoutingContext ctx,
final RequestResponseApiConstants.Action action,
final Predicate<Integer> setResponseBodyForStatus,
final Function<Void,String> locationFormatter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would instead use a Handler<HttpServerResponse> and let the handler modify the response in any way it sees fit, i.e. not limiting it to provide a location header value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 much better indeed.
And this is solving another issue as well: the annoying action parameter, which was rather artificial in this signature.

* @param requestMsg The JSON object to send via the event bus.
* @param responseHandler The handler to be invoked for the response received as answer from the event bus.
*/
protected void sendAction(final RoutingContext ctx, final JsonObject requestMsg, final BiConsumer<Integer, JsonObject> responseHandler) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this method supposed to be overridden by subclasses?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, will make it final.

* @param ctx The routing context of the request.
* @return The tenantId retrieved from the request.
*/
protected String getTenantParam(final RoutingContext ctx) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

* @param ctx The routing context of the request.
* @return The deviceId retrieved from the request.
*/
protected String getDeviceIdParam(final RoutingContext ctx) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final?
document NullPointerException?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

*
* @param ctx The routing context of the request.
* @return The tenantId retrieved from the request.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document NullPointerException?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

Please ignore in this PR, only consider commits on top of this.

Signed-off-by: Karsten Frank <Karsten.Frank@bosch-si.com>
Signed-off-by: Karsten Frank <Karsten.Frank@bosch-si.com>
Signed-off-by: Karsten Frank <Karsten.Frank@bosch-si.com>
The payload is now checked for correct JSON.

Signed-off-by: Karsten Frank <Karsten.Frank@bosch-si.com>
Signed-off-by: Karsten Frank <Karsten.Frank@bosch-si.com>
…class.

This pushes the reduction of code in the always repeating http endpoint
implementations a step further. Focussing on the tenant http endpoint,
it is the base for rewriting (and reducing) the other endpoints as well.

Signed-off-by: Karsten Frank <Karsten.Frank@bosch-si.com>
Signed-off-by: Karsten Frank <Karsten.Frank@bosch-si.com>
…action enum.

The enum introduced for the tenant API actions is used in the
credentials HTTP endpoint and service implementation as well. Any
standard HTTP endpoint that supports GET,ADD,PUT,REMOVE can be based
on this enum in the future.

Signed-off-by: Karsten Frank <Karsten.Frank@bosch-si.com>
This allows for further reduction of code duplications in the HTTP
endpoint implementations by introducing more and shorter lambdas
instead.

Signed-off-by: Karsten Frank <Karsten.Frank@bosch-si.com>
@sysexcontrol
Copy link
Contributor Author

@sophokles73 : thanks for the last comments, I (forced) pushed 2 new commits.
Since they are not in the right order in the github display, here are the relevant ones (all other are coming from the again necessary rebase and were not changed at all):
9cf619a
35abb9f

The enum action and the generic methods are applied to the credentials implementations as well, where it fits well. The Registration API is not touched (yet).
Can you have another look?

Signed-off-by: Karsten Frank <Karsten.Frank@bosch-si.com>
* Request standard actions to support the standard lifecycle.
* If more or other actions shall be used, an own enum type should be defined.
*/
public enum StandardAction implements RequestResponseAction {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is wrong with simply defining the actions that are defined for an API in the corresponding *Constants class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, not too much anymore, except for:
it would be completely identical for tenant and credentials API, and it would double about 30 code lines.
Since it often is probably the case that the HTTP APIs have get,add,update,delete, I have put it into the StandardAction.

Previously, there were generic methods operating on this type. Due to the introduced handlers this does not exist anymore, so the left reason to keep them here is purely the code duplication.

@@ -75,21 +79,26 @@ public void addRoutes(final Router router) {
router.post(path).handler(this::extractRequiredJsonPayload);
router.post(path).handler(this::checkPayloadForTenantId);
router.post(path).handler(ctx -> doTenantHttpRequest(ctx, StandardAction.ACTION_ADD,
status -> status == HttpURLConnection.HTTP_CREATED));
status -> status == HttpURLConnection.HTTP_CREATED,
tenantId -> response -> response.putHeader(HttpHeaders.LOCATION,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this does make the code very hard to read and, worse, very hard to adapt later. FMPOV it would be much cleaner to have one method per operation/action and then implement them accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand your point and will come up with a less condensed version of this code.

Previously, the direct use of lambdas of lambdas allowed for very short
code, but the readability suffered.
Now each HTTP method has it's own internal method again and the used
predicates and location handler are explictly offered by the base
class.

Signed-off-by: Karsten Frank <Karsten.Frank@bosch-si.com>
@sysexcontrol
Copy link
Contributor Author

@sophokles73 : I refactored the TenantHttpEndpoint to a less condensed version, which IMHO improves readability and changeability a lot.

@@ -13,17 +13,17 @@

package org.eclipse.hono.service.tenant;

import static java.net.HttpURLConnection.HTTP_CREATED;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please do not import the constants statically

* @param statusToCheckFor The HTTP status, e.g. {@link HttpURLConnection#HTTP_OK}.
* @return The predicate.
*/
protected final Predicate<Integer> getHttpStatusPredicate(final int statusToCheckFor) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this obviously works but FMPOV it would improve readability a lot if we simply would put the lamba in the place where it is used instead of encapsulating this within a method. In the end, that's one of the reasons why lambdas have been introduced.
Compare

doTenantHttpRequest(ctx, tenantId, StandardAction.ACTION_ADD, 
   getHttpStatusPredicate(HTTP_CREATED),
   getLocationHeaderHandler(location));

with

doTenantHttpRequest(ctx, tenantId, StandardAction.ACTION_ADD, 
   status -> status == HTTP_CREATED,
   getLocationHeaderHandler(location));

How many bytes have we saved? Which version is easier to understand?
The same holds true for the getLocationHeaderHandler() method FMPOV.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, we fully agree on that - this is just like I wrote it before. Sometimes PR comments are not so clear...

The only difference with explicit getters for lambdas (or Handlers) is that they get a Javadoc and a (hopefully) good name, which sometimes can be helpful IMHO. In this case it is simple enough that this is not necessary.

Anyway, I will introduce the lambdas again and hope we both are satisfied then...

Signed-off-by: Karsten Frank <Karsten.Frank@bosch-si.com>
@sysexcontrol
Copy link
Contributor Author

@sophokles73 : I reintroduced the lambdas, hope we agree on this version :-) Thanks!

}

private void updateTenant(final RoutingContext ctx) {

final String tenantId = getTenantIdFromContext(ctx);

doTenantHttpRequest(ctx, tenantId, StandardAction.ACTION_UPDATE,null, null);
doTenantHttpRequest(ctx, tenantId, RequestResponseApiConstants.StandardAction.ACTION_UPDATE,null, null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting

}

private void removeTenant(final RoutingContext ctx) {

final String tenantId = getTenantIdFromContext(ctx);

doTenantHttpRequest(ctx, tenantId, StandardAction.ACTION_REMOVE,null, null);
doTenantHttpRequest(ctx, tenantId, RequestResponseApiConstants.StandardAction.ACTION_REMOVE,null, null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting

}

private void updateTenant(final RoutingContext ctx) {

final String tenantId = getTenantIdFromContext(ctx);

doTenantHttpRequest(ctx, tenantId, StandardAction.ACTION_UPDATE,null, null);
doTenantHttpRequest(ctx, tenantId, RequestResponseApiConstants.StandardAction.ACTION_UPDATE,null, null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should refer to the actions via the TenantConstants class (this is where they will end up anyway FMPOV)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Signed-off-by: Karsten Frank <Karsten.Frank@bosch-si.com>
@sysexcontrol
Copy link
Contributor Author

@sophokles73 : comments adressed, thx.

@sysexcontrol
Copy link
Contributor Author

Commits combined and provided in PR [#501] for merge. This PR is closed instead.

@sysexcontrol sysexcontrol deleted the honoTenantApiImplHttpIfPr2 branch March 6, 2018 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants