diff --git a/mocks/README.md b/mocks/README.md new file mode 100644 index 0000000..7135682 --- /dev/null +++ b/mocks/README.md @@ -0,0 +1,29 @@ +In this directory are some files to mock an auction house to test WebSub local. + +To run a local WebSubHub instance + +1. Start a mongodb in docker: + +- docker run -d -p 27017:27017 -p 28017:28017 -e AUTH=no tutum/mongodb + +2. Install a local hub + +- yarn global add websub-hub + +3. Run the hub localy + +- websub-hub -l info -m mongodb://localhost:27017/hub + +Create an example subscription + +- node auction-house/subscriber.js + +Create an example auctionhouse + +- node auction-house/auctions.js + +Publish to the hub + +- node auction-house/publisher.js + +Mostly inspired by: https://github.com/hemerajs/websub-hub diff --git a/mocks/auction-house/auctions.js b/mocks/auction-house/auctions.js new file mode 100644 index 0000000..68a6aa5 --- /dev/null +++ b/mocks/auction-house/auctions.js @@ -0,0 +1,39 @@ +// Require the framework and instantiate it +const fastify = require('fastify')({ logger: true }) + +// Declare a route +fastify.get('/auctions', async (request, reply) => { + console.log('content provided') + + return [ + { + id: '2', + content_text: 'This is a second item.', + url: 'https://example.org/second-item' + }, + { + id: '1', + content_html: '

Hello, world!

', + url: 'https://example.org/initial-post' + } + ] +}) + +fastify.get('/websub', async (request, reply) => { + console.log('content provided') + + return { + topic: 'http://localhost:3100/auctions' + } +}) + +// Run the server! +const start = async () => { + try { + await fastify.listen(3100) + } catch (err) { + fastify.log.error(err) + process.exit(1) + } +} +start() diff --git a/mocks/auction-house/publisher.js b/mocks/auction-house/publisher.js new file mode 100644 index 0000000..c760820 --- /dev/null +++ b/mocks/auction-house/publisher.js @@ -0,0 +1,17 @@ +const axios = require('axios').default + +// Run the server! +const start = async () => { + await axios + .post('http://localhost:3000/publish', { + 'hub.mode': 'publish', + 'hub.url': 'http://localhost:3100/auctions' + }) + .then(response => { + console.log(response.data) + }) + .catch(error => { + console.log(error) + }) +} +start() diff --git a/mocks/auction-house/subscriber.js b/mocks/auction-house/subscriber.js new file mode 100644 index 0000000..ce27197 --- /dev/null +++ b/mocks/auction-house/subscriber.js @@ -0,0 +1,42 @@ +// Require the framework and instantiate it +const fastify = require('fastify')({ logger: true }) +const axios = require('axios').default + +// Declare a route +fastify.get('/auction-created', async (request, reply) => { + console.log('subscription verified', request.query) + console.log(request.query) + return request.query +}) + +fastify.post('/auction-created', async (request, reply) => { + console.log('received blog content', request.body) + reply.send() +}) + +// Run the server! +const start = async () => { + // subscribe to the feed + + try { + await fastify.listen(3200) + } catch (err) { + fastify.log.error(err) + process.exit(1) + } + + await axios + .post('http://localhost:3000', { + 'hub.callback': 'http://localhost:3200/auction-created', + 'hub.mode': 'subscribe', + 'hub.topic': 'http://localhost:3100/auctions', + 'hub.ws': false + }) + .then(response => { + console.log(response.data) + }) + .catch(error => { + console.log(error) + }) +} +start() diff --git a/tapas-auction-house/pom.xml b/tapas-auction-house/pom.xml index 4b9cbb6..df44681 100644 --- a/tapas-auction-house/pom.xml +++ b/tapas-auction-house/pom.xml @@ -58,6 +58,17 @@ validation-api 1.1.0.Final + + org.json + json + 20210307 + + + org.springframework.boot + spring-boot-devtools + runtime + true + diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java index 46dafb7..7438032 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java @@ -8,6 +8,7 @@ import ch.unisg.tapas.common.ConfigProperties; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.paho.client.mqttv3.MqttException; +import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -32,18 +33,16 @@ public class TapasAuctionHouseApplication { SpringApplication tapasAuctioneerApp = new SpringApplication(TapasAuctionHouseApplication.class); // We will use these bootstrap methods in Week 6: - // bootstrapMarketplaceWithWebSub(); + bootstrapMarketplaceWithWebSub(); bootstrapMarketplaceWithMqtt(); tapasAuctioneerApp.run(args); } - /** * Discovers auction houses and subscribes to WebSub notifications */ private static void bootstrapMarketplaceWithWebSub() { List auctionHouseEndpoints = discoverAuctionHouseEndpoints(); - LOGGER.info("Found auction house endpoints: " + auctionHouseEndpoints); WebSubSubscriber subscriber = new WebSubSubscriber(); diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java index da2b096..5b3fc32 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java @@ -1,6 +1,15 @@ package ch.unisg.tapas.auctionhouse.adapter.common.clients; +import java.io.IOException; import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.json.JSONObject; +import org.springframework.http.HttpStatus; /** * Subscribes to the WebSub hubs of auction houses discovered at run time. This class is instantiated @@ -9,7 +18,24 @@ import java.net.URI; */ public class WebSubSubscriber { + // TODO get this somehow from properties file. But on clue how to do this with static variables + static String WEBSUB_HUB_ENDPOINT = "http://localhost:3000"; + static String AUCTION_HOUSE_ENDPOINT = "http://localhost:8086"; + + Logger logger = Logger.getLogger(WebSubSubscriber.class.getName()); + public void subscribeToAuctionHouseEndpoint(URI endpoint) { + // TODO decide with other groups about auction house endpoint uri to discover websub topics + // and replace the hardcoded one with it + String topic = discoverWebSubTopic("http://localhost:3100/websub"); + + if (topic == null) { + return; + } + + subscribeToWebSub(topic); + + // Shoudl be done :D // TODO Subscribe to the auction house endpoint via WebSub: // 1. Send a request to the auction house in order to discover the WebSub hub to subscribe to. // The request URI should depend on the design of the Auction House HTTP API. @@ -25,4 +51,61 @@ public class WebSubSubscriber { // - W3C WebSub Recommendation: https://www.w3.org/TR/websub/ // - the implementation notes of the WebSub hub you are using to distribute events } + + private String discoverWebSubTopic(String endpoint) { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(endpoint)) + .header("Content-Type", "application/json") + .GET() + .build(); + + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == HttpStatus.OK.value()) { + // TODO decide with other groups about response structure and replace the hardcoded + // uri with response uri + JSONObject jsonObject = new JSONObject(response.body()); + System.out.println(jsonObject); + return jsonObject.getString("topic"); + } else { + logger.log(Level.SEVERE, "Could not find a websub uri"); + } + } catch (InterruptedException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + } + return null; + } + + private void subscribeToWebSub(String topic) { + HttpClient client = HttpClient.newHttpClient(); + + String body = new JSONObject() + .put("hub.callback", AUCTION_HOUSE_ENDPOINT + "/auction-started") + .put("hub.mode", "subscribe") + .put("hub.topic", topic) + .put("hub.ws", false) + .toString(); + + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(WEBSUB_HUB_ENDPOINT)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + + try { + client.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (InterruptedException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + } + } } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java index d156452..4f67dad 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java @@ -1,6 +1,22 @@ package ch.unisg.tapas.auctionhouse.adapter.in.messaging.websub; +import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation; import ch.unisg.tapas.auctionhouse.application.handler.AuctionStartedHandler; +import ch.unisg.tapas.auctionhouse.application.port.in.AuctionStartedEvent; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionDeadline; +import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionHouseUri; +import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionId; +import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionedTaskType; +import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionedTaskUri; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; + +import org.json.JSONArray; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; /** @@ -13,6 +29,25 @@ public class AuctionStartedEventListenerWebSubAdapter { public AuctionStartedEventListenerWebSubAdapter(AuctionStartedHandler auctionStartedHandler) { this.auctionStartedHandler = auctionStartedHandler; } + /** + * Controller which listens to auction-started callbacks + * @return 200 OK + * @throws URISyntaxException + **/ + @PostMapping(path = "/auction-started") + public ResponseEntity handleExecutorAddedEvent(@RequestBody Collection payload) throws URISyntaxException { - //TODO + for (AuctionJsonRepresentation auction : payload) { + auctionStartedHandler.handleAuctionStartedEvent( + new AuctionStartedEvent( + new Auction(new AuctionId(auction.getAuctionId()), + new AuctionHouseUri(new URI(auction.getAuctionHouseUri())), + new AuctionedTaskUri(new URI(auction.getTaskUri())), + new AuctionedTaskType(auction.getTaskType()), + new AuctionDeadline(auction.getDeadline())) + )); + } + + return new ResponseEntity<>(HttpStatus.OK); + } } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java new file mode 100644 index 0000000..7bfb450 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java @@ -0,0 +1,33 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.websub; + +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * This class validates the subscription intent from the websub hub + */ +@RestController +public class ValidateIntentWebSubAdapter { + + @Value("${application.environment}") + private String environment; + + @GetMapping(path = "/auction-started") + public ResponseEntity validateIntent(@RequestParam("hub.challenge") String challenge) { + // Different implementation depending on local development or production + if (environment.equalsIgnoreCase("development")) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + String body = new JSONObject() + .put("hub.challenge", challenge) + .toString(); + return new ResponseEntity<>(body, headers, HttpStatus.OK); + } else { + return new ResponseEntity<>(challenge, HttpStatus.OK); + } + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java index 73451e4..228f43b 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java @@ -4,12 +4,16 @@ import ch.unisg.tapas.auctionhouse.application.port.out.AuctionStartedEventPort; import ch.unisg.tapas.auctionhouse.domain.Auction; import ch.unisg.tapas.auctionhouse.domain.AuctionStartedEvent; import ch.unisg.tapas.common.ConfigProperties; + +import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import java.io.IOException; +import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -17,6 +21,8 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; /** @@ -29,8 +35,38 @@ public class PublishAuctionStartedEventWebSubAdapter implements AuctionStartedEv @Autowired private ConfigProperties config; + @Value("${auctionhouse.uri}") + private String auctionHouseUri; + + @Value("${websub.hub.uri}") + private String webSubHubUri; + + Logger logger = Logger.getLogger(PublishAuctionStartedEventWebSubAdapter.class.getName()); + @Override public void publishAuctionStartedEvent(AuctionStartedEvent event) { - // TODO + HttpClient client = HttpClient.newHttpClient(); + + String body = new JSONObject() + .put("hub.url", auctionHouseUri + "/auctions") + .put("hub.mode", "publish") + .toString(); + + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(webSubHubUri)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + + try { + client.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (InterruptedException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + } } } diff --git a/tapas-auction-house/src/main/resources/application.properties b/tapas-auction-house/src/main/resources/application.properties index 1ededee..706362e 100644 --- a/tapas-auction-house/src/main/resources/application.properties +++ b/tapas-auction-house/src/main/resources/application.properties @@ -7,5 +7,7 @@ group=tapas-group-tutors auction.house.uri=https://tapas-auction-house.86-119-34-23.nip.io/ tasks.list.uri=https://tapas-tasks.86-119-34-23.nip.io/ - +application.environment=development +auctionhouse.uri=http://localhost:8086 +websub.hub.uri=http://localhost:3000 mqtt.broker.uri=tcp://localhost:1883