Commit 105fd9a3 authored by Komal Makkar's avatar Komal Makkar
Browse files

Resolving comments

parent b135a400
package org.opengroup.osdu.notification.pubsub;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
public interface IPubsubHandshakeHandler {
String getHandshakeResponse();
}
package org.opengroup.osdu.notification.pubsub;
import org.springframework.stereotype.Component;
import org.springframework.http.ResponseEntity;
import java.util.Map;
public interface IPubsubRequestBodyExtractor {
Map<String, String> extractAttributesFromRequestBody() ;
String extractDataFromRequestBody();
String extractNotificationIdFromRequestBody() ;
boolean isHandshakeRequest();
}
......@@ -16,19 +16,16 @@ package org.opengroup.osdu.notification.provider.azure.pubsub;
import com.google.gson.JsonObject;
import org.opengroup.osdu.core.common.model.http.AppException;
import org.opengroup.osdu.notification.provider.azure.util.RequestAttributesDispatcher;
import org.opengroup.osdu.notification.pubsub.IPubsubHandshakeHandler;
import org.opengroup.osdu.notification.provider.interfaces.IPubsubHandshakeHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
@Component
@Lazy
public class EventGridHandshakeHandler implements IPubsubHandshakeHandler {
@Autowired
private RequestAttributesDispatcher requestDispatcher;
private EventGridRequestBodyExtractor eventGridRequestBodyExtractor;
/**
* Extract Handshake response string form Handshake request.
......@@ -38,17 +35,16 @@ public class EventGridHandshakeHandler implements IPubsubHandshakeHandler {
*/
@Override
public String getHandshakeResponse() {
String response = null;
String response;
try {
JsonObject requestRoot = requestDispatcher.getRequestRoot();
JsonObject data = (JsonObject) requestRoot.get("data");
String validationCode = data.get("validationCode").getAsString();
String validationCode = this.eventGridRequestBodyExtractor.getValidationCodeForHandshake();
JsonObject jsonResponse = new JsonObject();
jsonResponse.addProperty("ValidationResponse", validationCode);
response = jsonResponse.toString();
} catch (Exception exception) {
throw new AppException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Request payload parsing error",
throw new AppException(HttpStatus.BAD_REQUEST.value(), "Request payload parsing error",
"Unable to parse request payload.", exception);
}
......
......@@ -14,23 +14,29 @@
package org.opengroup.osdu.notification.provider.azure.pubsub;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.opengroup.osdu.core.common.logging.JaxRsDpsLog;
import org.opengroup.osdu.core.common.model.http.AppException;
import org.opengroup.osdu.notification.models.MessageContent;
import org.opengroup.osdu.notification.provider.azure.util.RequestAttributesDispatcher;
import org.opengroup.osdu.notification.pubsub.IPubsubRequestBodyExtractor;
import org.opengroup.osdu.notification.provider.azure.models.HandshakeRequestData;
import org.opengroup.osdu.notification.provider.azure.models.NotificationData;
import org.opengroup.osdu.notification.provider.azure.models.NotificationRequest;
import org.opengroup.osdu.notification.provider.interfaces.IPubsubRequestBodyExtractor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
// TODO : Make error messages as constants.
// TODO : Add TracePoints to exception logs so that monitoring is done well.
......@@ -39,16 +45,25 @@ import java.util.Map;
@RequestScope
public class EventGridRequestBodyExtractor implements IPubsubRequestBodyExtractor {
private static final String INVALID_EVENTGRID_MESSAGE = "Invalid Event Grid Message";
private static final String SUBSCRIPTION_ID = "Aeg-Subscription-Name";
private static final String EVENTGRID_VALIDATION_EVENT = "Microsoft.EventGrid.SubscriptionValidationEvent";
private static final Gson GSON = new Gson();
private MessageContent messageContent;
private static final ObjectMapper objectMapper = new ObjectMapper();
private JsonObject root = null;
private HttpServletRequest httpServletRequest;
private JaxRsDpsLog log;
@Autowired
private RequestAttributesDispatcher requestDispatcher;
private NotificationRequest notificationRequest;
private NotificationData notificationData;
private HandshakeRequestData handshakeRequestData;
private boolean isHandshakeRequest;
@Autowired
private JaxRsDpsLog log;
public EventGridRequestBodyExtractor(@Autowired HttpServletRequest httpServletRequest, @Autowired JaxRsDpsLog log) {
this.httpServletRequest = httpServletRequest;
this.log = log;
this.notificationRequest = extractNotificationRequestFromHttpRequest();
}
/**
* Extracts the attributes from the request that are filled in by publisher of the message.
......@@ -57,10 +72,10 @@ public class EventGridRequestBodyExtractor implements IPubsubRequestBodyExtracto
* @return Request Attributes Map
*/
public Map<String, String> extractAttributesFromRequestBody() {
if (this.messageContent == null) {
this.messageContent = this.extractPubsubMessageFromRequestBody();
if(isHandshakeRequest) {
return null;
}
return this.messageContent.getAttributes();
return this.notificationData.getAttributes();
}
/**
......@@ -70,10 +85,10 @@ public class EventGridRequestBodyExtractor implements IPubsubRequestBodyExtracto
* @return Request Data String
*/
public String extractDataFromRequestBody() {
if (this.messageContent == null) {
this.messageContent = this.extractPubsubMessageFromRequestBody();
if(isHandshakeRequest) {
return null;
}
return this.messageContent.getData();
return new String(Base64.getDecoder().decode(notificationData.getData()));
}
/**
......@@ -83,7 +98,11 @@ public class EventGridRequestBodyExtractor implements IPubsubRequestBodyExtracto
* @return Request NotificationId String.
*/
public String extractNotificationIdFromRequestBody() {
return this.requestDispatcher.getRequestSubscriptionId();
String subscriptionId = httpServletRequest.getHeader(SUBSCRIPTION_ID);
if (Strings.isNullOrEmpty(subscriptionId)) {
throw new AppException(HttpStatus.BAD_REQUEST.value(), "Invalid Event Grid Message", "Subscription ID not found");
}
return subscriptionId;
}
/**
......@@ -93,62 +112,77 @@ public class EventGridRequestBodyExtractor implements IPubsubRequestBodyExtracto
* @return Request Type Boolean
*/
public boolean isHandshakeRequest() {
if (this.root == null) {
this.root = this.requestDispatcher.getRequestRoot();
}
JsonElement data = this.root.get("eventType");
if (EVENTGRID_VALIDATION_EVENT.equals(data.getAsString())) {
return true;
}
return false;
return this.isHandshakeRequest ;
}
/**
* Utility method that wxtracts the core content in the request.
* This is what the publisher will publish.
* Return ValidationCode
*
* @throws AppException
* @return Message Content Object
* @return Request Type Boolean
*/
private MessageContent extractPubsubMessageFromRequestBody() {
if (this.root == null) {
this.root = this.requestDispatcher.getRequestRoot();
}
JsonElement message = this.root.get("data");
if (message == null) {
throw new AppException(HttpStatus.BAD_REQUEST.value(), INVALID_EVENTGRID_MESSAGE, "message object not found");
}
MessageContent content = GSON.fromJson(message.toString(), MessageContent.class);
String eventTime = this.root.getAsJsonObject().get("eventTime").getAsString();
if (Strings.isNullOrEmpty(eventTime)) {
throw new AppException(HttpStatus.BAD_REQUEST.value(), INVALID_EVENTGRID_MESSAGE, "Event time not found");
public String getValidationCodeForHandshake() {
if(!isHandshakeRequest) {
return null;
}
content.setPublishTime(eventTime);
return this.handshakeRequestData.getValidationCode();
}
Map<String, String> attributes = content.getAttributes();
if (attributes == null || attributes.isEmpty()) {
log.error("Incorrect Message: " + message.toString() );
throw new AppException(HttpStatus.BAD_REQUEST.value(), INVALID_EVENTGRID_MESSAGE, "attribute map not found");
}
String data = content.getData();
if (Strings.isNullOrEmpty(data)) {
throw new AppException(HttpStatus.BAD_REQUEST.value(), INVALID_EVENTGRID_MESSAGE, "data field not found");
}
Map<String, String> lowerCase = new HashMap<>();
attributes.forEach((key, value) -> lowerCase.put(key.toLowerCase(), value));
if (Strings.isNullOrEmpty(attributes.get("data-partition-id"))) {
throw new AppException(HttpStatus.BAD_REQUEST.value(), INVALID_EVENTGRID_MESSAGE,
"No tenant information from pubsub message.");
/**
* Utility method that extracts the core content in the request.
* This is what the publisher will publish.
*
* The attributes are validated.
*
* @throws AppException
* @return NotificationRequest Object
*/
private NotificationRequest extractNotificationRequestFromHttpRequest() {
NotificationRequest notificationRequest = null;
if (this.notificationRequest == null) {
try {
BufferedReader reader = httpServletRequest.getReader();
Stream<String> lines = reader.lines();
String requestBody = lines.collect(Collectors.joining("\n"));
NotificationRequest[] notificationRequestArray = GSON.fromJson(requestBody, NotificationRequest[].class);
notificationRequest = notificationRequestArray[0];
this.isHandshakeRequest = notificationRequest.getEventType().equals(EVENTGRID_VALIDATION_EVENT);
if (isHandshakeRequest) {
extractHandshakeData(notificationRequest);
} else {
extractNotificationData(notificationRequest);
}
} catch (Exception e) {
throw new AppException(HttpStatus.BAD_REQUEST.value(), "Request payload parsing error",
"Unable to parse request payload.", "Request contents are null or empty");
}
}
content.setAttributes(lowerCase);
String decoded = new String(Base64.getDecoder().decode(data));
content.setData(decoded);
return notificationRequest;
}
return content;
private void extractHandshakeData(NotificationRequest notificationRequest) {
this.handshakeRequestData = GSON.fromJson(notificationRequest.getData(), HandshakeRequestData.class);
Preconditions.checkNotNull(this.handshakeRequestData.getValidationCode(), "Request payload parsing error" );
}
private void extractNotificationData(NotificationRequest notificationRequest) {
NotificationData notificationData = GSON.fromJson(notificationRequest.getData(), NotificationData.class);
verifyNotificationData(notificationData);
this.notificationData = notificationData;
}
}
// TODO: Clean up for using @NonNull Lombok.
// The Gson deserialisation happens through constructor made out of reflection,
// The unsafe initialisation inspires us to use Jackson
//
// Making Gson use a custom constructor, involves boilerplating, hence adding precondition checks. https://github.com/google/gson/blob/master/UserGuide.md#TOC-Writing-a-Deserializer
// The required condition is not operation as of now.https://github.com/google/gson/issues/61
private void verifyNotificationData(NotificationData notificationData) {
Preconditions.checkNotNull(notificationData, "Request payload parsing error" );
Preconditions.checkNotNull(notificationData.getData(), "Request payload parsing error" );
Preconditions.checkNotNull(notificationData.getMessageId(), "Request payload parsing error" );
Preconditions.checkNotNull(notificationData.getAttributes().get("correlation-id") , "Request payload parsing error" );
Preconditions.checkNotNull(notificationData.getAttributes().get("data-partition-id") , "Request payload parsing error" );
}
}
\ No newline at end of file
// Copyright © Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.opengroup.osdu.notification.provider.azure.util;
import com.google.common.base.Strings;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.opengroup.osdu.core.common.model.http.AppException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Component
@RequestScope
public class RequestAttributesDispatcher {
private JsonObject requestRoot;
@Autowired
private HttpServletRequest request;
/**
* Extracts Request's root.
*
* @throws AppException
* @throws IOException
* @return Request Root JsonObject
*/
public JsonObject getRequestRoot(){
if(requestRoot == null) {
try {
JsonArray requestArray = getRequest();
JsonElement rootElement = requestArray.get(0);
if (!(rootElement instanceof JsonObject)) {
throw new AppException(HttpStatus.BAD_REQUEST.value(), "RequestBody is not JsonObject.",
"Request Body should be JsonObject to be processed.");
}
this.requestRoot = rootElement.getAsJsonObject();
} catch (Exception e) {
throw new AppException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Request payload parsing error",
"Unable to parse request payload.", e);
}
}
return requestRoot;
}
/**
* Extracts Subscription Name from the header.
*
* @throws AppException
* @throws IOException
* @return Sunscription Name String
*/
public String getRequestSubscriptionId(){
String subscriptionId = request.getHeader("Aeg-Subscription-Name");
if (Strings.isNullOrEmpty(subscriptionId)) {
throw new AppException(HttpStatus.BAD_REQUEST.value(), "Invalid Event Grid Message", "Subscription ID not found");
}
return subscriptionId;
}
/**
* Utility method to read the request's body as stream and
* convert it into JsonArray
* @throws AppException
* @throws IOException
* @return Request JsonArray
*/
private JsonArray getRequest() throws IOException {
JsonParser jsonParser = new JsonParser();
JsonArray requestArray;
try {
BufferedReader reader = request.getReader();
Stream<String> lines = reader.lines();
String requestBody = lines.collect(Collectors.joining("\n"));
requestArray = (JsonArray) jsonParser.parse(requestBody);
} catch (Exception e){
throw new AppException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Request payload parsing error",
"Unable to read from the request.", e);
}
return requestArray;
}
}
// Copyright © Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
......@@ -14,8 +15,6 @@
package org.opengroup.osdu.notification.pubsub;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
......@@ -25,41 +24,27 @@ import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.opengroup.osdu.core.common.model.http.AppException;
import org.opengroup.osdu.notification.provider.azure.pubsub.EventGridHandshakeHandler;
import org.opengroup.osdu.notification.provider.azure.util.RequestAttributesDispatcher;
import org.opengroup.osdu.notification.provider.azure.pubsub.EventGridRequestBodyExtractor;
import org.springframework.http.HttpStatus;
import java.io.IOException;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class EventGridHandshakeHandlerTest {
@Mock
RequestAttributesDispatcher requestAttributesDispatcherMock;
EventGridRequestBodyExtractor eventGridRequestBodyExtractor;
@InjectMocks
@Spy
private EventGridHandshakeHandler sut;
@Test
public void should_returnValidResponse_getHandshakeResponse() {
public void should_returnValidResponse_getHandshakeResponse() throws IOException {
// Set up
String validHandshakeRequestRoot =
" {\n" +
" \"id\": \"testId\",\n" +
" \"topic\": \"testTopic\",\n" +
" \"subject\": \"\",\n" +
" \"data\": {\n" +
" \"validationCode\": \"testValidationCode\",\n" +
" \"validationUrl\": \"testURL\"\n" +
" },\n" +
" \"eventType\": \"Microsoft.EventGrid.SubscriptionValidationEvent\",\n" +
" \"eventTime\": \"2020-08-14T11:18:55.9278057Z\",\n" +
" \"metadataVersion\": \"1\",\n" +
" \"dataVersion\": \"2\"\n" +
" }";
JsonObject requestRoot = new JsonParser().parse(validHandshakeRequestRoot).getAsJsonObject();
when(this.requestAttributesDispatcherMock.getRequestRoot()).thenReturn(requestRoot);
when(sut.getHandshakeResponse()).thenReturn("testValidationCode");
String expectedResponse = "{\"ValidationResponse\":\"testValidationCode\"}";
// Act
......@@ -70,33 +55,22 @@ public class EventGridHandshakeHandlerTest {
}
@Test
public void should_throwWhenDataIsMissing_getHandshakeResponse() {
public void should_throw_getHandshakeResponse() throws IOException {
// Set up
String validHandshakeRequestRoot =
" {\n" +
" \"id\": \"testId\",\n" +
" \"topic\": \"testTopic\",\n" +
" \"subject\": \"\",\n" +
" \"eventType\": \"Microsoft.EventGrid.SubscriptionValidationEvent\",\n" +
" \"eventTime\": \"2020-08-14T11:18:55.9278057Z\",\n" +
" \"metadataVersion\": \"1\",\n" +
" \"dataVersion\": \"2\"\n" +
" }";
JsonObject requestRoot = new JsonParser().parse(validHandshakeRequestRoot).getAsJsonObject();
when(this.requestAttributesDispatcherMock.getRequestRoot()).thenReturn(requestRoot);
try {
when(sut.getHandshakeResponse())
.thenThrow(new AppException(HttpStatus.BAD_REQUEST.value(), "Request payload parsing error", "" ));
try{
// Act
this.sut.getHandshakeResponse();
String observedResponse = this.sut.getHandshakeResponse();
// Asset
// Assert
fail("Should Throw Exception");
} catch (
AppException appException){
Assert.assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), appException.getError().getCode());
Assert.assertEquals("Unable to parse request payload." , appException.getError().getMessage());
} catch (AppException appException){
Assert.assertEquals(HttpStatus.BAD_REQUEST.value(), appException.getError().getCode());
Assert.assertEquals("Unable to parse request payload.", appException.getError().getMessage());
} catch (Exception exception) {
fail("Should Throw AppException");
}
}
}
......@@ -14,20 +14,20 @@
package org.opengroup.osdu.notification.pubsub;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.opengroup.osdu.core.common.logging.JaxRsDpsLog;
import org.opengroup.osdu.core.common.model.http.AppException;
import org.opengroup.osdu.notification.provider.azure.pubsub.EventGridRequestBodyExtractor;
import org.opengroup.osdu.notification.provider.azure.util.RequestAttributesDispatcher;
import org.springframework.http.HttpStatus;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.Map;
import static org.junit.Assert.fail;
......@@ -35,20 +35,20 @@ import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class EventGridRequestBodyExtractorTest {
private static final String INVALID_EVENTGRID_MESSAGE = "Invalid Event Grid Message";