-
Notifications
You must be signed in to change notification settings - Fork 139
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
HTTP interface for tenant API. #481
Conversation
response | ||
.putHeader(HttpHeaders.CONTENT_TYPE, HttpUtils.CONTENT_TYPE_JSON_UFT8) | ||
.putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(msg.length())) | ||
.write(msg); |
There was a problem hiding this comment.
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"); | ||
} | ||
} |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatting
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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"). |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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"). |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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"). |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
update the JavaDoc accordingly
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatting
@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). |
5b471ad
to
bb3eefa
Compare
There was a problem hiding this 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 |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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}. |
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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);
33c8b9d
to
e8cf7e0
Compare
@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). 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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final?
document NullPointerException
?
There was a problem hiding this comment.
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. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
document NullPointerException
?
There was a problem hiding this comment.
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>
030b9b9
to
35abb9f
Compare
@sophokles73 : thanks for the last comments, I (forced) pushed 2 new commits. 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). |
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
@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; |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
@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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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>
@sophokles73 : comments adressed, thx. |
Commits combined and provided in PR [#501] for merge. This PR is closed instead. |
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.