├── common ├── build │ ├── tmp │ │ └── jar │ │ │ └── MANIFEST.MF │ └── resources │ │ └── main │ │ └── log4j2.xml ├── src │ └── main │ │ ├── java │ │ ├── routerutils │ │ │ ├── Middleware.java │ │ │ ├── RouteConfig.java │ │ │ ├── BaseHandler.java │ │ │ ├── RouteBuilder.java │ │ │ └── HandlerProcessor.java │ │ └── logging │ │ │ └── ContextLogger.java │ │ └── resources │ │ └── log4j2.xml └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── conduit ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── src │ ├── main │ │ ├── java │ │ │ └── io │ │ │ │ └── vertx │ │ │ │ └── conduit │ │ │ │ ├── entities │ │ │ │ ├── package-info.java │ │ │ │ ├── Comment.java │ │ │ │ ├── Base.java │ │ │ │ ├── Article.java │ │ │ │ └── User.java │ │ │ │ ├── services │ │ │ │ ├── package-info.java │ │ │ │ ├── MorphiaServiceOperator.java │ │ │ │ ├── ArticleService.java │ │ │ │ ├── CommentService.java │ │ │ │ ├── UserService.java │ │ │ │ ├── MongoDbService.java │ │ │ │ ├── MorphiaService.java │ │ │ │ ├── CommentServiceImpl.java │ │ │ │ ├── ArticleServiceImpl.java │ │ │ │ ├── UserServiceImpl.java │ │ │ │ ├── MongoDbServiceImpl.java │ │ │ │ └── MorphiaServiceImpl.java │ │ │ │ ├── handlers │ │ │ │ ├── Constants.java │ │ │ │ ├── QueryHandler.java │ │ │ │ ├── JwtOptionalHandler.java │ │ │ │ ├── ConduitJwtAuthHandlerImpl.java │ │ │ │ ├── ConduitHandler.java │ │ │ │ ├── UserHandler.java │ │ │ │ └── ArticleHandler.java │ │ │ │ ├── verticles │ │ │ │ ├── UserServiceVerticle.java │ │ │ │ ├── ArticleServiceVerticle.java │ │ │ │ ├── CommentServiceVerticle.java │ │ │ │ ├── HttpVerticle.java │ │ │ │ ├── MorphiaServiceVerticle.java │ │ │ │ └── MongoDbServiceVerticle.java │ │ │ │ └── App.java │ │ └── resources │ │ │ └── app.json │ └── test │ │ └── java │ │ ├── services │ │ ├── MongoServiceTest.java │ │ └── MorphiaServiceTest.java │ │ └── routes │ │ ├── QueryTest.java │ │ ├── UserTest.java │ │ ├── ArticleTest.java │ │ └── TestBase.java ├── gradlew.bat ├── build.gradle └── gradlew ├── .idea ├── vcs.xml ├── misc.xml ├── gradle.xml └── uiDesigner.xml ├── .gitignore ├── LICENSE ├── gradlew.bat ├── README.md └── gradlew /common/build/tmp/jar/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'vertx-examples' 2 | 3 | include ":common", ":conduit" -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndierZ/conduit/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /conduit/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndierZ/conduit/HEAD/conduit/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/entities/package-info.java: -------------------------------------------------------------------------------- 1 | @ModuleGen(name = "realworld-vertx-entities", groupPackage = "io.vertx.conduit.entities") 2 | package io.vertx.conduit.entities; 3 | 4 | import io.vertx.codegen.annotations.ModuleGen; -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/services/package-info.java: -------------------------------------------------------------------------------- 1 | @ModuleGen(name = "realworld-vertx-entities", groupPackage = "io.vertx.conduit.services") 2 | package io.vertx.conduit.services; 3 | 4 | import io.vertx.codegen.annotations.ModuleGen; -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /conduit/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/services/MorphiaServiceOperator.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.services; 2 | 3 | public class MorphiaServiceOperator { 4 | 5 | public final static String PUSH = "$push"; 6 | public final static String POP = "$pop"; 7 | } 8 | -------------------------------------------------------------------------------- /conduit/src/main/resources/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "test", 3 | "secret" : "1summerday20!9", 4 | 5 | "mongodb": { 6 | "db_name": "conduit", 7 | "useObjectId": true, 8 | "connection_string": "mongodb://localhost:27017", 9 | "host": "localhost", 10 | "port": 27017 11 | } 12 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /common/src/main/java/routerutils/Middleware.java: -------------------------------------------------------------------------------- 1 | package routerutils; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @SuppressWarnings("unused") 9 | @Target({ ElementType.METHOD }) 10 | @Retention(RetentionPolicy.SOURCE) 11 | public @interface Middleware { 12 | } 13 | -------------------------------------------------------------------------------- /common/src/main/java/logging/ContextLogger.java: -------------------------------------------------------------------------------- 1 | package logging; 2 | 3 | import io.vertx.core.logging.Logger; 4 | import io.vertx.core.logging.LoggerFactory; 5 | 6 | import java.lang.management.ManagementFactory; 7 | 8 | public class ContextLogger { 9 | 10 | public static Logger create() { 11 | StackTraceElement[] stackTrade = ManagementFactory.getThreadMXBean().getThreadInfo(Thread.currentThread().getId(), 5).getStackTrace(); 12 | String clz = stackTrade[4].getClassName(); 13 | return LoggerFactory.getLogger(clz); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/handlers/Constants.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.handlers; 2 | 3 | public class Constants { 4 | public static final String ARTICLE = "article"; 5 | public static final String COMMENT = "comment"; 6 | public static final String AUTH_KEY = "token"; // send in the response 7 | public static final String AUTH_HEADER = "Token"; // received in the authorization header 8 | public static final String USER = "user"; 9 | public static final String USER_ID = "userId"; 10 | public static final String QUERY = "query"; 11 | } 12 | -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | version '1.0-SNAPSHOT' 6 | 7 | sourceCompatibility = 1.8 8 | 9 | repositories { 10 | jcenter() 11 | } 12 | 13 | dependencies { 14 | compile group: 'io.vertx', name:'vertx-core', version: '3.8.4' 15 | compile group: 'io.vertx', name: 'vertx-web', version: '3.8.4' 16 | compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.2' 17 | compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.2' 18 | testCompile group: 'junit', name: 'junit', version: '4.12' 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # .gradle files 26 | .gradle/ 27 | !gradle-wrapper.jar 28 | 29 | # build files 30 | build/ 31 | common/buid/ 32 | conduit/build/ 33 | 34 | # intellij project file 35 | .idea/workspace.xml 36 | 37 | # generated code 38 | conduit/src/main/generated -------------------------------------------------------------------------------- /common/src/main/java/routerutils/RouteConfig.java: -------------------------------------------------------------------------------- 1 | package routerutils; 2 | 3 | import io.vertx.core.http.HttpMethod; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target({ ElementType.METHOD, ElementType.TYPE }) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface RouteConfig { 13 | String path(); 14 | 15 | HttpMethod method() default HttpMethod.GET; 16 | 17 | String[] consumes() default {}; 18 | 19 | String[] produces() default {}; 20 | 21 | boolean authRequired() default true; 22 | 23 | String[] middlewares() default {}; 24 | } -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/services/ArticleService.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.services; 2 | 3 | import io.vertx.codegen.annotations.ProxyGen; 4 | import io.vertx.codegen.annotations.VertxGen; 5 | import io.vertx.conduit.entities.Article; 6 | import io.vertx.core.AsyncResult; 7 | import io.vertx.core.Handler; 8 | import io.vertx.core.json.JsonObject; 9 | 10 | import java.util.List; 11 | 12 | @VertxGen 13 | @ProxyGen 14 | public interface ArticleService { 15 | 16 | String ADDRESS = ArticleService.class.getName(); 17 | 18 | void create(JsonObject article, Handler> resultHandler); 19 | 20 | void update(String slug, JsonObject update, Handler> resultHandler); 21 | 22 | void get(String slug, Handler> resultHandler); 23 | 24 | void delete(String slug, Handler> resultHandler); 25 | } 26 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/services/CommentService.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.services; 2 | 3 | import io.vertx.codegen.annotations.ProxyGen; 4 | import io.vertx.codegen.annotations.VertxGen; 5 | import io.vertx.conduit.entities.Article; 6 | import io.vertx.conduit.entities.Comment; 7 | import io.vertx.core.AsyncResult; 8 | import io.vertx.core.Handler; 9 | import io.vertx.core.json.JsonObject; 10 | 11 | @VertxGen 12 | @ProxyGen 13 | public interface CommentService { 14 | 15 | String ADDRESS = CommentService.class.getName(); 16 | 17 | void create(JsonObject comment, Handler> resultHandler); 18 | 19 | void update(String id, JsonObject update, Handler> resultHandler); 20 | 21 | void get(String id, Handler> resultHandler); 22 | 23 | void delete(String id, Handler> resultHandler); 24 | } 25 | -------------------------------------------------------------------------------- /common/src/main/java/routerutils/BaseHandler.java: -------------------------------------------------------------------------------- 1 | package routerutils; 2 | 3 | import io.netty.handler.codec.http.HttpResponseStatus; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.core.json.Json; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.ext.web.RoutingContext; 8 | 9 | import javax.xml.ws.http.HTTPBinding; 10 | 11 | public abstract class BaseHandler { 12 | 13 | protected final Vertx vertx; 14 | 15 | protected BaseHandler(Vertx vertx) { 16 | this.vertx = vertx; 17 | } 18 | 19 | protected static void handleError(RoutingContext event, Throwable ex) { 20 | if (ex != null) { 21 | event.fail(HttpResponseStatus.INTERNAL_SERVER_ERROR.code(), ex); 22 | } 23 | } 24 | 25 | protected static void handleResponse(RoutingContext event, JsonObject res, HttpResponseStatus successStatus) { 26 | event.response() 27 | .setStatusCode(successStatus.code()) 28 | .end(Json.encodePrettily(res)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /common/build/resources/main/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /common/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/services/UserService.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.services; 2 | 3 | import io.vertx.codegen.annotations.ProxyGen; 4 | import io.vertx.codegen.annotations.VertxGen; 5 | import io.vertx.conduit.entities.User; 6 | import io.vertx.core.AsyncResult; 7 | import io.vertx.core.Handler; 8 | import io.vertx.core.json.JsonObject; 9 | 10 | @VertxGen 11 | @ProxyGen 12 | public interface UserService { 13 | String ADDRESS = UserService.class.getName(); 14 | 15 | void create(JsonObject create, Handler> resultHandler); 16 | 17 | void get(JsonObject query, Handler> resultHandler); 18 | 19 | void getById(String id, Handler> resultHandler); 20 | 21 | void getByEmail(String email, Handler> resultHandler); 22 | 23 | void update(String id, JsonObject update, Handler> resultHandler); 24 | 25 | void getFavoriteCount(String slug, Handler> resultHandler); 26 | 27 | void deleteByUsername(String username, Handler> resultHandler); 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 AndierZ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/handlers/QueryHandler.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.handlers; 2 | 3 | import io.netty.handler.codec.http.HttpResponseStatus; 4 | import io.vertx.codegen.annotations.Nullable; 5 | import io.vertx.conduit.entities.User; 6 | import io.vertx.core.Vertx; 7 | import io.vertx.core.http.HttpMethod; 8 | import io.vertx.core.json.Json; 9 | import io.vertx.core.json.JsonArray; 10 | import io.vertx.core.json.JsonObject; 11 | import io.vertx.ext.web.RoutingContext; 12 | import routerutils.RouteConfig; 13 | 14 | @RouteConfig(path="/api", produces = "application/json") 15 | public class QueryHandler extends ConduitHandler { 16 | 17 | private static final String QUERY = "query"; 18 | 19 | public QueryHandler(Vertx vertx) { 20 | super(vertx); 21 | } 22 | 23 | @RouteConfig(path="/tags", method= HttpMethod.GET, authRequired = false) 24 | public void getTags(RoutingContext event){ 25 | morphiaService.rxQueryTags() 26 | .subscribe((tags, ex) -> { 27 | if (ex == null) { 28 | JsonArray array = new JsonArray(); 29 | tags.forEach(array::add); 30 | event.response() 31 | .setStatusCode(HttpResponseStatus.OK.code()) 32 | .end(Json.encodePrettily(new JsonObject().put("tags", array))); 33 | } else { 34 | event.fail(ex); 35 | } 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /common/src/main/java/routerutils/RouteBuilder.java: -------------------------------------------------------------------------------- 1 | package routerutils; 2 | 3 | import io.vertx.core.Handler; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.ext.web.Router; 6 | import io.vertx.ext.web.RoutingContext; 7 | import io.vertx.ext.web.handler.AuthHandler; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class RouteBuilder { 13 | 14 | private boolean finalized; 15 | private final List handlers; 16 | private final Router baseRouter; 17 | private final List> preHandlers; 18 | 19 | private AuthHandler authHandler; 20 | private Handler optionalAuthHandler; 21 | 22 | public RouteBuilder(Router baseRouter) { 23 | this.handlers = new ArrayList<>(); 24 | this.preHandlers = new ArrayList<>(); 25 | this.baseRouter = baseRouter; 26 | } 27 | 28 | public RouteBuilder addAuthHandler(AuthHandler authHandler) { 29 | if (finalized) { 30 | throw new RuntimeException("Routes already built."); 31 | } 32 | this.authHandler = authHandler; 33 | return this; 34 | } 35 | 36 | public RouteBuilder addPreHandler(Handler preHandler) { 37 | if (finalized) { 38 | throw new RuntimeException("Routes already built."); 39 | } 40 | this.preHandlers.add(preHandler); 41 | return this; 42 | } 43 | 44 | public RouteBuilder add(BaseHandler handler){ 45 | if (finalized) { 46 | throw new RuntimeException("Routes already built."); 47 | } 48 | 49 | handlers.add(handler); 50 | 51 | return this; 52 | } 53 | 54 | public void build() { 55 | if (finalized) { 56 | throw new RuntimeException("Routes already built."); 57 | } 58 | 59 | for(BaseHandler handler : handlers) { 60 | HandlerProcessor.buildHandler(baseRouter, preHandlers, handler, authHandler); 61 | } 62 | 63 | finalized = true; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/services/MongoDbService.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.services; 2 | 3 | import io.vertx.codegen.annotations.Fluent; 4 | import io.vertx.codegen.annotations.ProxyClose; 5 | import io.vertx.codegen.annotations.ProxyGen; 6 | import io.vertx.codegen.annotations.VertxGen; 7 | import io.vertx.core.AsyncResult; 8 | import io.vertx.core.Handler; 9 | import io.vertx.core.json.JsonObject; 10 | import io.vertx.ext.mongo.FindOptions; 11 | import io.vertx.ext.mongo.MongoClientDeleteResult; 12 | import io.vertx.ext.mongo.MongoClientUpdateResult; 13 | import io.vertx.ext.mongo.UpdateOptions; 14 | 15 | import java.util.List; 16 | 17 | @VertxGen 18 | @ProxyGen 19 | public interface MongoDbService { 20 | 21 | String ADDRESS = MongoDbService.class.getName(); 22 | 23 | void findOne(final String collection, final JsonObject query, final JsonObject fields, final Handler> resultHandler); 24 | 25 | void findById(final String collection, String id, final JsonObject fields, final Handler> resultHandler); 26 | 27 | void find(final String collection, final JsonObject query, final FindOptions options, Handler>> resultHandler); 28 | 29 | void insertOne(final String collection, final JsonObject document, final Handler> resultHandler); 30 | 31 | void upsert(final String collection, final JsonObject query, final JsonObject update, final UpdateOptions options, Handler> resultHandler); 32 | 33 | void findOneAndUpdate(final String collection, final JsonObject query, final JsonObject update, final FindOptions findOptions, final UpdateOptions updateOptions, final Handler> resultHandler); 34 | 35 | void findOneAndReplace(String collection, JsonObject query, JsonObject update, FindOptions findOptions, UpdateOptions updateOptions, Handler> resultHandler); 36 | 37 | void delete(final String collection, final JsonObject query, Handler> resultHandler); 38 | 39 | @ProxyClose 40 | void close(); 41 | } 42 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/verticles/UserServiceVerticle.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.verticles; 2 | 3 | import io.vertx.conduit.handlers.Constants; 4 | import io.vertx.core.Promise; 5 | import logging.ContextLogger; 6 | import io.vertx.conduit.services.UserService; 7 | import io.vertx.conduit.services.UserServiceImpl; 8 | import io.vertx.core.AbstractVerticle; 9 | import io.vertx.core.eventbus.MessageConsumer; 10 | import io.vertx.core.json.JsonObject; 11 | import io.vertx.core.logging.Logger; 12 | import io.vertx.servicediscovery.Record; 13 | import io.vertx.servicediscovery.ServiceDiscovery; 14 | import io.vertx.servicediscovery.types.EventBusService; 15 | import io.vertx.serviceproxy.ServiceBinder; 16 | 17 | public class UserServiceVerticle extends AbstractVerticle { 18 | 19 | private static Logger LOGGER = ContextLogger.create(); 20 | 21 | private Record record; 22 | private ServiceBinder binder; 23 | private MessageConsumer consumer; 24 | 25 | @Override 26 | public void start(Promise startPromise) { 27 | 28 | ServiceDiscovery.create(vertx, discovery -> { 29 | binder = new ServiceBinder(vertx); 30 | // Create the services object 31 | UserServiceImpl service = new UserServiceImpl(vertx); 32 | 33 | // Register the services proxy on the event bus 34 | this.consumer = binder 35 | .setAddress(UserService.ADDRESS) 36 | .register(UserService.class, service); 37 | 38 | Record record = EventBusService.createRecord(Constants.USER, UserService.ADDRESS, UserService.class.getName()); 39 | discovery.publish(record, ar -> { 40 | if (ar.succeeded()) { 41 | this.record = record; 42 | LOGGER.info("User service published"); 43 | startPromise.complete(); 44 | } else { 45 | LOGGER.error("Error publishing user service", ar.cause()); 46 | startPromise.fail(ar.cause()); 47 | } 48 | }); 49 | 50 | }); 51 | } 52 | 53 | @Override 54 | public void stop() { 55 | if (record != null) { 56 | binder.unregister(consumer); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/entities/Comment.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.entities; 2 | 3 | import dev.morphia.annotations.Entity; 4 | import dev.morphia.annotations.Reference; 5 | import io.vertx.codegen.annotations.DataObject; 6 | import io.vertx.core.json.JsonObject; 7 | import org.bson.types.ObjectId; 8 | 9 | import javax.validation.constraints.NotNull; 10 | import java.util.Date; 11 | 12 | @DataObject(generateConverter = true) 13 | @Entity("comments") 14 | public class Comment extends Base { 15 | 16 | public Comment() {} 17 | 18 | public Comment(JsonObject jsonObject) { 19 | fromJson(jsonObject); 20 | } 21 | 22 | private String body; 23 | 24 | @NotNull 25 | @Reference(idOnly = true, lazy = true) 26 | private User author; 27 | 28 | // Not using reference to avoid circular dependency; also not necessary. 29 | @NotNull 30 | private ObjectId article; 31 | 32 | public String getBody() { 33 | return body; 34 | } 35 | 36 | public void setBody(String body) { 37 | this.body = body; 38 | } 39 | 40 | public User getAuthor() { 41 | return author; 42 | } 43 | 44 | public void setAuthor(User author) { 45 | this.author = author; 46 | } 47 | 48 | public ObjectId getArticle() { 49 | return article; 50 | } 51 | 52 | public void setArticle(ObjectId article) { 53 | this.article = article; 54 | } 55 | 56 | public JsonObject toJson() { 57 | JsonObject json = new JsonObject(); 58 | CommentConverter.toJson(this, json); 59 | super.toJson(json); 60 | json.put("article", article != null ? article.toHexString() : null); 61 | return json; 62 | } 63 | 64 | protected void fromJson(JsonObject jsonObject) { 65 | super.fromJson(jsonObject); 66 | CommentConverter.fromJson(jsonObject, this); 67 | this.article = new ObjectId(jsonObject.getString("article")); 68 | } 69 | 70 | public JsonObject toJsonFor(User user) { 71 | JsonObject json = toJson(); 72 | json.put("author", author.toProfileJsonFor(user)); 73 | json.put("createdAt", DF.format(new Date(getCreatedAt()))); 74 | json.put("updatedAt", DF.format(new Date(getCreatedAt()))); 75 | return json; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/verticles/ArticleServiceVerticle.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.verticles; 2 | 3 | import io.vertx.conduit.handlers.Constants; 4 | import io.vertx.conduit.services.ArticleService; 5 | import io.vertx.conduit.services.ArticleServiceImpl; 6 | import io.vertx.core.AbstractVerticle; 7 | import io.vertx.core.Promise; 8 | import io.vertx.core.eventbus.MessageConsumer; 9 | import io.vertx.core.json.JsonObject; 10 | import io.vertx.core.logging.Logger; 11 | import io.vertx.servicediscovery.Record; 12 | import io.vertx.servicediscovery.ServiceDiscovery; 13 | import io.vertx.servicediscovery.types.EventBusService; 14 | import io.vertx.serviceproxy.ServiceBinder; 15 | import logging.ContextLogger; 16 | 17 | public class ArticleServiceVerticle extends AbstractVerticle { 18 | 19 | private static Logger LOGGER = ContextLogger.create(); 20 | 21 | private Record record; 22 | private ServiceBinder binder; 23 | private MessageConsumer consumer; 24 | 25 | @Override 26 | public void start(Promise startPromise) { 27 | 28 | ServiceDiscovery.create(vertx, discovery -> { 29 | binder = new ServiceBinder(vertx); 30 | // Create the services object 31 | ArticleServiceImpl service = new ArticleServiceImpl(vertx); 32 | 33 | // Register the services proxy on the event bus 34 | this.consumer = binder 35 | .setAddress(ArticleService.ADDRESS) 36 | .register(ArticleService.class, service); 37 | 38 | Record record = EventBusService.createRecord(Constants.ARTICLE, ArticleService.ADDRESS, ArticleService.class.getName()); 39 | discovery.publish(record, ar -> { 40 | if (ar.succeeded()) { 41 | this.record = record; 42 | LOGGER.info("Article service published"); 43 | startPromise.complete(); 44 | } else { 45 | LOGGER.error("Error publishing article service", ar.cause()); 46 | startPromise.fail(ar.cause()); 47 | } 48 | }); 49 | 50 | }); 51 | } 52 | 53 | @Override 54 | public void stop() { 55 | if (record != null) { 56 | binder.unregister(consumer); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/verticles/CommentServiceVerticle.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.verticles; 2 | 3 | import io.vertx.conduit.handlers.Constants; 4 | import io.vertx.conduit.services.CommentService; 5 | import io.vertx.conduit.services.CommentServiceImpl; 6 | import io.vertx.core.AbstractVerticle; 7 | import io.vertx.core.Promise; 8 | import io.vertx.core.eventbus.MessageConsumer; 9 | import io.vertx.core.json.JsonObject; 10 | import io.vertx.core.logging.Logger; 11 | import io.vertx.servicediscovery.Record; 12 | import io.vertx.servicediscovery.ServiceDiscovery; 13 | import io.vertx.servicediscovery.types.EventBusService; 14 | import io.vertx.serviceproxy.ServiceBinder; 15 | import logging.ContextLogger; 16 | 17 | public class CommentServiceVerticle extends AbstractVerticle { 18 | 19 | private static Logger LOGGER = ContextLogger.create(); 20 | 21 | private Record record; 22 | private ServiceBinder binder; 23 | private MessageConsumer consumer; 24 | 25 | @Override 26 | public void start(Promise startPromise) { 27 | 28 | ServiceDiscovery.create(vertx, discovery -> { 29 | binder = new ServiceBinder(vertx); 30 | // Create the services object 31 | CommentServiceImpl service = new CommentServiceImpl(vertx); 32 | 33 | // Register the services proxy on the event bus 34 | this.consumer = binder 35 | .setAddress(CommentService.ADDRESS) 36 | .register(CommentService.class, service); 37 | 38 | Record record = EventBusService.createRecord(Constants.COMMENT, CommentService.ADDRESS, CommentService.class.getName()); 39 | discovery.publish(record, ar -> { 40 | if (ar.succeeded()) { 41 | this.record = record; 42 | LOGGER.info("Comment service published"); 43 | startPromise.complete(); 44 | } else { 45 | LOGGER.error("Error publishing comment service", ar.cause()); 46 | startPromise.fail(ar.cause()); 47 | } 48 | }); 49 | 50 | }); 51 | } 52 | 53 | @Override 54 | public void stop() { 55 | if (record != null) { 56 | binder.unregister(consumer); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/verticles/HttpVerticle.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.verticles; 2 | 3 | import io.vertx.conduit.handlers.*; 4 | import io.vertx.core.Promise; 5 | import logging.ContextLogger; 6 | import io.vertx.core.AbstractVerticle; 7 | import io.vertx.core.json.JsonObject; 8 | import io.vertx.core.logging.Logger; 9 | import io.vertx.ext.auth.PubSecKeyOptions; 10 | import io.vertx.ext.auth.jwt.JWTAuth; 11 | import io.vertx.ext.auth.jwt.JWTAuthOptions; 12 | import io.vertx.ext.web.Router; 13 | import io.vertx.ext.web.handler.BodyHandler; 14 | import routerutils.RouteBuilder; 15 | 16 | public class HttpVerticle extends AbstractVerticle { 17 | 18 | private static Logger LOGGER = ContextLogger.create(); 19 | private JWTAuth jwtAuth; 20 | 21 | @Override 22 | public void start(Promise startPromise) { 23 | LOGGER.info("Starting Http Verticle"); 24 | 25 | JsonObject config = config(); 26 | 27 | JWTAuthOptions jwtAuthOptions = new JWTAuthOptions().addPubSecKey( 28 | new PubSecKeyOptions() 29 | .setAlgorithm("HS256") 30 | .setPublicKey(config.getString("secret")) 31 | .setSymmetric(true)); 32 | 33 | this.jwtAuth = JWTAuth.create(vertx, jwtAuthOptions); 34 | 35 | Router baseRouter = Router.router(vertx); 36 | 37 | new RouteBuilder(baseRouter) 38 | .addAuthHandler(new ConduitJwtAuthHandlerImpl(jwtAuth, Constants.AUTH_HEADER)) // To expect the header value defined by the Conduit api "token". Otherwise could just use JWTAuthHandler.create(jwtAuth) 39 | .addPreHandler(BodyHandler.create()) 40 | .addPreHandler(new JwtOptionalHandler(jwtAuthOptions)) 41 | .add(new UserHandler(vertx, jwtAuth)) 42 | .add(new ArticleHandler(vertx)) 43 | .add(new QueryHandler(vertx)) 44 | .build(); 45 | 46 | vertx.createHttpServer() 47 | .requestHandler(baseRouter) 48 | .listen(3000, ar -> { 49 | if (ar.succeeded()) { 50 | startPromise.complete(); 51 | } else { 52 | startPromise.fail(ar.cause()); 53 | } 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/handlers/JwtOptionalHandler.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.handlers; 2 | 3 | import io.vertx.core.Handler; 4 | import io.vertx.core.http.HttpHeaders; 5 | import io.vertx.core.json.JsonObject; 6 | import io.vertx.ext.auth.PubSecKeyOptions; 7 | import io.vertx.ext.auth.jwt.JWTAuthOptions; 8 | import io.vertx.ext.auth.jwt.impl.JWTUser; 9 | import io.vertx.ext.jwt.JWK; 10 | import io.vertx.ext.jwt.JWT; 11 | import io.vertx.ext.web.RoutingContext; 12 | 13 | import java.util.List; 14 | 15 | import static io.vertx.conduit.handlers.Constants.USER_ID; 16 | 17 | public class JwtOptionalHandler implements Handler { 18 | 19 | private final JWT jwt; 20 | private final String permissionsClaimKey; 21 | 22 | public JwtOptionalHandler(JWTAuthOptions config) { 23 | this.jwt = new JWT(); 24 | this.permissionsClaimKey = config.getPermissionsClaimKey(); 25 | 26 | final List keys = config.getPubSecKeys(); 27 | 28 | if (keys != null) { 29 | for (PubSecKeyOptions pubSecKey : config.getPubSecKeys()) { 30 | if (pubSecKey.isSymmetric()) { 31 | jwt.addJWK(new JWK(pubSecKey.getAlgorithm(), pubSecKey.getPublicKey())); 32 | } else { 33 | jwt.addJWK(new JWK(pubSecKey.getAlgorithm(), pubSecKey.isCertificate(), pubSecKey.getPublicKey(), pubSecKey.getSecretKey())); 34 | } 35 | } 36 | } 37 | } 38 | 39 | @Override 40 | public void handle(RoutingContext event) { 41 | if (event.user() == null) { 42 | final String authorization = event.request().headers().get(HttpHeaders.AUTHORIZATION); 43 | if (authorization != null) { 44 | String[] tokens = authorization.split(" "); 45 | if (tokens.length == 2) { 46 | JsonObject payload = jwt.decode(tokens[1]); 47 | event.setUser(new JWTUser(payload, permissionsClaimKey)); 48 | } 49 | } 50 | } 51 | 52 | if (event.user() != null && event.user().principal() != null) { 53 | event.put(USER_ID, event.user().principal().getString("id")); 54 | } else { 55 | event.put(USER_ID, null); 56 | } 57 | 58 | event.next(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/services/MorphiaService.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.services; 2 | 3 | import io.vertx.codegen.annotations.ProxyClose; 4 | import io.vertx.codegen.annotations.ProxyGen; 5 | import io.vertx.codegen.annotations.VertxGen; 6 | import io.vertx.conduit.entities.Article; 7 | import io.vertx.conduit.entities.Comment; 8 | import io.vertx.conduit.entities.User; 9 | import io.vertx.core.AsyncResult; 10 | import io.vertx.core.Handler; 11 | import io.vertx.core.json.JsonObject; 12 | import io.vertx.ext.mongo.FindOptions; 13 | import io.vertx.ext.mongo.MongoClientDeleteResult; 14 | import io.vertx.ext.mongo.MongoClientUpdateResult; 15 | import io.vertx.ext.mongo.UpdateOptions; 16 | 17 | import java.util.List; 18 | 19 | @VertxGen 20 | @ProxyGen 21 | public interface MorphiaService { 22 | 23 | String ADDRESS = MorphiaService.class.getName(); 24 | 25 | void getUser(final JsonObject query, Handler>> resultHandler); 26 | 27 | void getArticle(final JsonObject query, Handler>> resultHandler); 28 | 29 | void getComment(final JsonObject query, Handler>> resultHandler); 30 | 31 | void createUser(User entity, final Handler> resultHandler); 32 | 33 | void createArticle(Article entity, final Handler> resultHandler); 34 | 35 | void createComment(Comment entity, final Handler> resultHandler); 36 | 37 | void updateUser(final JsonObject query, final JsonObject update, final Handler>> resultHandler); 38 | 39 | void updateArticle(final JsonObject query, final JsonObject update, final Handler>> resultHandler); 40 | 41 | void updateComment(final JsonObject query, final JsonObject update, final Handler>> resultHandler); 42 | 43 | void deleteUser(final JsonObject query, Handler> resultHandler); 44 | 45 | void deleteArticle(final JsonObject query, Handler> resultHandler); 46 | 47 | void deleteComment(final JsonObject query, Handler> resultHandler); 48 | 49 | void queryTags(Handler>> resultHandler); 50 | 51 | void queryArticles(User user, JsonObject query, Handler> resultHandler); 52 | 53 | void queryArticlesFeed(User user, JsonObject json, Handler> resultHandler); 54 | 55 | @ProxyClose 56 | void close(); 57 | } 58 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/verticles/MorphiaServiceVerticle.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.verticles; 2 | 3 | import io.vertx.conduit.services.MorphiaService; 4 | import io.vertx.conduit.services.MorphiaServiceImpl; 5 | import io.vertx.core.AbstractVerticle; 6 | import io.vertx.core.Future; 7 | import io.vertx.core.Promise; 8 | import io.vertx.core.eventbus.MessageConsumer; 9 | import io.vertx.core.json.JsonObject; 10 | import io.vertx.core.logging.Logger; 11 | import io.vertx.servicediscovery.Record; 12 | import io.vertx.servicediscovery.ServiceDiscovery; 13 | import io.vertx.servicediscovery.types.EventBusService; 14 | import io.vertx.serviceproxy.ServiceBinder; 15 | import logging.ContextLogger; 16 | 17 | public class MorphiaServiceVerticle extends AbstractVerticle { 18 | 19 | private static Logger LOGGER = ContextLogger.create(); 20 | 21 | private Record record; 22 | private ServiceBinder binder; 23 | private MessageConsumer consumer; 24 | 25 | @Override 26 | public void start(Promise startPromise) { 27 | 28 | ServiceDiscovery.create(vertx, discovery -> { 29 | binder = new ServiceBinder(vertx); 30 | // Create the services object 31 | new MorphiaServiceImpl(vertx, config().getJsonObject("mongodb"), ready -> { 32 | if (ready.succeeded()) { 33 | this.consumer = binder 34 | .setAddress(MorphiaService.ADDRESS) 35 | .register(MorphiaService.class, ready.result()); 36 | Record record = EventBusService.createRecord("morphia", MorphiaService.ADDRESS, MorphiaService.class.getName()); 37 | discovery.publish(record, ar -> { 38 | if (ar.succeeded()) { 39 | this.record = record; 40 | LOGGER.info("Morphia service published"); 41 | startPromise.complete(); 42 | } else { 43 | LOGGER.error("Error publishing Morphia service", ar.cause()); 44 | startPromise.fail(ar.cause()); 45 | } 46 | }); 47 | } else { 48 | startPromise.fail(ready.cause()); 49 | } 50 | }); 51 | }); 52 | 53 | 54 | } 55 | 56 | @Override 57 | public void stop() { 58 | if (record != null) { 59 | binder.unregister(consumer); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /conduit/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/verticles/MongoDbServiceVerticle.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.verticles; 2 | 3 | import io.vertx.conduit.services.MongoDbService; 4 | import io.vertx.conduit.services.MongoDbServiceImpl; 5 | import io.vertx.core.Future; 6 | import io.vertx.core.Promise; 7 | import io.vertx.core.eventbus.MessageConsumer; 8 | import io.vertx.core.json.JsonObject; 9 | import io.vertx.core.logging.Logger; 10 | import io.vertx.reactivex.core.AbstractVerticle; 11 | import io.vertx.reactivex.ext.mongo.MongoClient; 12 | import io.vertx.servicediscovery.Record; 13 | import io.vertx.servicediscovery.ServiceDiscovery; 14 | import io.vertx.servicediscovery.types.EventBusService; 15 | import io.vertx.serviceproxy.ServiceBinder; 16 | import logging.ContextLogger; 17 | 18 | public class MongoDbServiceVerticle extends AbstractVerticle { 19 | 20 | private static Logger LOGGER = ContextLogger.create(); 21 | 22 | private Record record; 23 | private ServiceBinder binder; 24 | private MessageConsumer consumer; 25 | 26 | @Override 27 | public void start(Promise startPromise) { 28 | 29 | final MongoClient mongoClient = MongoClient.createShared(vertx, config().getJsonObject("mongodb")); 30 | ServiceDiscovery.create(vertx.getDelegate(), discovery -> { 31 | binder = new ServiceBinder(vertx.getDelegate()); 32 | // Create the services object 33 | new MongoDbServiceImpl(mongoClient, ready -> { 34 | if (ready.succeeded()) { 35 | this.consumer = binder 36 | .setAddress(MongoDbService.ADDRESS) 37 | .register(MongoDbService.class, ready.result()); 38 | Record record = EventBusService.createRecord("mongodb", MongoDbService.ADDRESS, MongoDbService.class.getName()); 39 | discovery.publish(record, ar -> { 40 | if (ar.succeeded()) { 41 | this.record = record; 42 | LOGGER.info("MongoDb service published"); 43 | startPromise.complete(); 44 | } else { 45 | LOGGER.error("Error publishing MongoDb service", ar.cause()); 46 | startPromise.fail(ready.cause()); 47 | } 48 | }); 49 | } else { 50 | startPromise.fail(ready.cause()); 51 | } 52 | }); 53 | }); 54 | 55 | 56 | } 57 | 58 | @Override 59 | public void stop() { 60 | if (record != null) { 61 | binder.unregister(consumer); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /conduit/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'io.vertx.vertx-plugin' version '1.0.1' 3 | 4 | } 5 | 6 | repositories { 7 | jcenter() 8 | mavenCentral() 9 | } 10 | 11 | vertx { 12 | mainVerticle = 'io.vertx.conduit.App' 13 | vertxVersion = "3.8.4" 14 | jvmArgs = ["-Dvertx.logger-delegate-factory-class-name=io.vertx.core.logging.Log4j2LogDelegateFactory"] 15 | debugSuspend = true 16 | debugPort = 5005 17 | } 18 | 19 | dependencies{ 20 | compile group: 'io.vertx', name: 'vertx-config' 21 | compile group: 'io.vertx', name: 'vertx-web' 22 | compile group: 'io.vertx', name: 'vertx-mongo-client' 23 | compile group: 'io.vertx', name: 'vertx-auth-jwt' 24 | compile group: 'io.vertx', name: 'vertx-service-proxy' 25 | compile group: 'io.vertx', name: 'vertx-service-discovery' 26 | compile group: 'io.vertx', name: 'vertx-rx-java2' 27 | compile group: 'io.vertx', name: 'vertx-rx-java2-gen', version: '3.8.4' 28 | compile group: 'io.vertx', name: 'vertx-web-client' 29 | compile group: 'com.github.slugify', name: 'slugify', version: '2.2' 30 | compile group: 'org.springframework.security', name: 'spring-security-crypto', version: '3.1.0.RELEASE' 31 | compile group: 'dev.morphia.morphia', name: 'core', version: '1.5.8' 32 | compile group: 'dev.morphia.morphia', name: 'validation', version: '1.5.8' 33 | compile group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final' 34 | compile group: 'org.hibernate', name: 'hibernate-validator', version: '6.1.0.Final' 35 | compile group: 'javax.el', name: 'javax.el-api', version: '3.0.0' 36 | compile group: 'org.glassfish', name: 'javax.el', version: '3.0.0' 37 | compileOnly group: 'io.vertx', name: 'vertx-codegen' 38 | compile project (':common') 39 | 40 | testCompile group: 'io.vertx', name:'vertx-unit' 41 | testCompile group: 'junit', name:'junit', version:'4.12' 42 | } 43 | 44 | def generated_dir = 'src/main/generated' 45 | 46 | task cleanGenerate(type: Delete, group: 'build') { 47 | delete project.file(generated_dir) 48 | } 49 | 50 | task generate(type: JavaCompile, group: 'build') { 51 | source = sourceSets.main.java 52 | classpath = configurations.compile + configurations.compileOnly 53 | destinationDir = project.file(generated_dir) 54 | options.annotationProcessorPath = configurations.compile + configurations.compileOnly 55 | options.compilerArgs = [ 56 | "-proc:only", 57 | "-processor", "io.vertx.codegen.CodeGenProcessor" 58 | ] 59 | } 60 | 61 | sourceSets { 62 | main { 63 | java { 64 | srcDirs += generated_dir 65 | } 66 | } 67 | test { 68 | java { 69 | srcDirs 'test/java' 70 | } 71 | } 72 | } 73 | 74 | compileJava { 75 | targetCompatibility = 1.8 76 | sourceCompatibility = 1.8 77 | 78 | dependsOn generate 79 | } -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/App.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit; 2 | 3 | import io.vertx.conduit.verticles.*; 4 | import io.vertx.config.ConfigRetriever; 5 | import io.vertx.config.ConfigRetrieverOptions; 6 | import io.vertx.config.ConfigStoreOptions; 7 | import io.vertx.core.*; 8 | import io.vertx.core.json.JsonObject; 9 | import io.vertx.core.logging.Logger; 10 | import logging.ContextLogger; 11 | 12 | public class App extends AbstractVerticle { 13 | private static final String CONFIG_PATH = "app.json"; 14 | private static Logger LOGGER = ContextLogger.create(); 15 | 16 | @Override 17 | public void start(Promise startPromise) { 18 | 19 | getConfig().setHandler(ar1 -> { 20 | if (ar1.succeeded()) { 21 | LOGGER.info("Config successfully retrived: " + ar1.result().toString()); 22 | DeploymentOptions deploymentOptions = new DeploymentOptions().setConfig(ar1.result()); 23 | CompositeFuture.all( 24 | deployVerticle(HttpVerticle.class, deploymentOptions), 25 | deployVerticle(UserServiceVerticle.class, deploymentOptions), 26 | deployVerticle(ArticleServiceVerticle.class, deploymentOptions), 27 | deployVerticle(CommentServiceVerticle.class, deploymentOptions), 28 | deployVerticle(MorphiaServiceVerticle.class, deploymentOptions)) 29 | .setHandler(ar2 -> { 30 | if (ar2.succeeded()) { 31 | LOGGER.info("Successfully deployed verticals."); 32 | startPromise.complete(); 33 | } else { 34 | LOGGER.error("Failed to deploy verticles: " + ar2.cause().getMessage()); 35 | startPromise.fail(ar2.cause()); 36 | } 37 | }); 38 | } else { 39 | startPromise.fail(ar1.cause()); 40 | } 41 | }); 42 | } 43 | 44 | private Future deployVerticle(Class clz, DeploymentOptions deploymentOptions){ 45 | Promise deploymentFuture = Promise.promise(); 46 | vertx.deployVerticle(clz, deploymentOptions, ar ->{ 47 | if (ar.succeeded()) { 48 | deploymentFuture.complete(); 49 | } else { 50 | deploymentFuture.fail(ar.cause()); 51 | } 52 | }); 53 | return deploymentFuture.future(); 54 | } 55 | 56 | private Future getConfig() { 57 | ConfigStoreOptions co = new ConfigStoreOptions(). 58 | setType("file"). 59 | setFormat("json"). 60 | setConfig(new JsonObject().put("path", CONFIG_PATH)); 61 | 62 | ConfigRetrieverOptions cro = new ConfigRetrieverOptions(). 63 | addStore(co); 64 | 65 | ConfigRetriever cr = ConfigRetriever.create(vertx, cro); 66 | 67 | return ConfigRetriever.getConfigAsFuture(cr); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/services/CommentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.services; 2 | 3 | import io.vertx.conduit.entities.Comment; 4 | import io.vertx.core.AsyncResult; 5 | import io.vertx.core.Future; 6 | import io.vertx.core.Handler; 7 | import io.vertx.core.Vertx; 8 | import io.vertx.core.json.JsonObject; 9 | import io.vertx.serviceproxy.ServiceProxyBuilder; 10 | import org.bson.types.ObjectId; 11 | 12 | import java.util.List; 13 | 14 | public class CommentServiceImpl implements CommentService { 15 | 16 | private final io.vertx.conduit.services.reactivex.MorphiaService morphiaService; 17 | 18 | public CommentServiceImpl(Vertx vertx){ 19 | ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx).setAddress(MorphiaService.ADDRESS); 20 | MorphiaService delegate = builder.build(MorphiaService.class); 21 | morphiaService = new io.vertx.conduit.services.reactivex.MorphiaService(delegate); 22 | } 23 | 24 | @Override 25 | public void create(JsonObject comment, Handler> resultHandler) { 26 | Comment commentEntity = new Comment(comment); 27 | morphiaService.rxCreateComment(commentEntity) 28 | .subscribe((id, ex) -> { 29 | if (ex == null) { 30 | commentEntity.setId(new ObjectId(id)); 31 | resultHandler.handle(Future.succeededFuture(commentEntity)); 32 | } else { 33 | resultHandler.handle(Future.failedFuture(ex)); 34 | } 35 | }); 36 | } 37 | 38 | @Override 39 | public void update(String id, JsonObject article, Handler> resultHandler) { 40 | morphiaService.rxUpdateComment(new JsonObject().put("_id", id), article) 41 | .subscribe((comments, ex) -> handleComment(resultHandler, comments, ex)); 42 | } 43 | 44 | @Override 45 | public void get(String id, Handler> resultHandler) { 46 | morphiaService.rxGetComment(new JsonObject().put("_id", id)) 47 | .subscribe((comments, ex) -> handleComment(resultHandler, comments, ex)); 48 | } 49 | 50 | @Override 51 | public void delete(String id, Handler> resultHandler) { 52 | morphiaService.rxDeleteComment(new JsonObject().put("_id", id)) 53 | .subscribe((res, ex) -> { 54 | if (ex == null) { 55 | resultHandler.handle(Future.succeededFuture(res)); 56 | } else { 57 | resultHandler.handle(Future.failedFuture(ex)); 58 | } 59 | }); 60 | } 61 | 62 | private static void handleComment(Handler> resultHandler, List comments, Throwable ex) { 63 | if (comments == null || comments.size() != 1) { 64 | resultHandler.handle(Future.failedFuture(new RuntimeException("Couldn't find unique comment"))); 65 | } else { 66 | if (ex == null) { 67 | resultHandler.handle(Future.succeededFuture(comments.get(0))); 68 | } else { 69 | resultHandler.handle(Future.failedFuture(ex)); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/services/ArticleServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.services; 2 | 3 | import io.vertx.conduit.entities.Article; 4 | import io.vertx.core.AsyncResult; 5 | import io.vertx.core.Future; 6 | import io.vertx.core.Handler; 7 | import io.vertx.core.Vertx; 8 | import io.vertx.core.json.JsonObject; 9 | import io.vertx.ext.mongo.FindOptions; 10 | import io.vertx.ext.mongo.UpdateOptions; 11 | import io.vertx.serviceproxy.ServiceProxyBuilder; 12 | import org.bson.types.ObjectId; 13 | 14 | import java.util.List; 15 | 16 | public class ArticleServiceImpl implements ArticleService { 17 | 18 | private final io.vertx.conduit.services.reactivex.MorphiaService morphiaService; 19 | 20 | public ArticleServiceImpl(Vertx vertx){ 21 | ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx).setAddress(MorphiaService.ADDRESS); 22 | MorphiaService delegate = builder.build(MorphiaService.class); 23 | morphiaService = new io.vertx.conduit.services.reactivex.MorphiaService(delegate); 24 | } 25 | 26 | @Override 27 | public void create(JsonObject article, Handler> resultHandler) { 28 | Article articleEntity = new Article(article); 29 | morphiaService.rxCreateArticle(articleEntity) 30 | .subscribe((id, ex) -> { 31 | if (ex == null) { 32 | articleEntity.setId(new ObjectId(id)); 33 | resultHandler.handle(Future.succeededFuture(articleEntity)); 34 | } else { 35 | resultHandler.handle(Future.failedFuture(ex)); 36 | } 37 | }); 38 | } 39 | 40 | @Override 41 | public void update(String slug, JsonObject article, Handler> resultHandler) { 42 | morphiaService.rxUpdateArticle(new JsonObject().put("slug", slug), article) 43 | .subscribe((articles, ex) -> handleArticle(resultHandler, articles, ex)); 44 | } 45 | 46 | @Override 47 | public void get(String slug, Handler> resultHandler) { 48 | morphiaService.rxGetArticle(new JsonObject().put("slug", slug)) 49 | .subscribe((articles, ex) -> handleArticle(resultHandler, articles, ex)); 50 | } 51 | 52 | @Override 53 | public void delete(String slug, Handler> resultHandler) { 54 | morphiaService.rxDeleteArticle(new JsonObject().put("slug", slug)) 55 | .subscribe((res, ex) -> { 56 | if (ex == null) { 57 | resultHandler.handle(Future.succeededFuture()); 58 | } else { 59 | resultHandler.handle(Future.failedFuture(ex)); 60 | } 61 | }); 62 | } 63 | 64 | private static void handleArticle(Handler> resultHandler, List
articles, Throwable ex) { 65 | if (articles == null || articles.size() != 1) { 66 | resultHandler.handle(Future.failedFuture(new RuntimeException("Couldn't find unique article"))); 67 | } else { 68 | if (ex == null) { 69 | resultHandler.handle(Future.succeededFuture(articles.get(0))); 70 | } else { 71 | resultHandler.handle(Future.failedFuture(ex)); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/entities/Base.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.entities; 2 | 3 | import dev.morphia.annotations.Entity; 4 | import dev.morphia.annotations.Id; 5 | import dev.morphia.annotations.Version; 6 | import io.vertx.core.json.JsonObject; 7 | import org.bson.types.ObjectId; 8 | 9 | import java.io.Serializable; 10 | import java.text.DateFormat; 11 | import java.text.ParseException; 12 | import java.text.SimpleDateFormat; 13 | import java.util.Date; 14 | import java.util.Objects; 15 | import java.util.TimeZone; 16 | 17 | @Entity 18 | public abstract class Base implements Serializable { 19 | 20 | public static DateFormat DF; 21 | static { 22 | TimeZone tz = TimeZone.getTimeZone("UTC"); 23 | DF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); 24 | DF.setTimeZone(tz); 25 | } 26 | 27 | @Id 28 | private ObjectId id; 29 | 30 | @Version 31 | private long version; 32 | private String createUser; 33 | private long createdAt; 34 | private long updatedAt; 35 | private String updateUser; 36 | private boolean isRetired; 37 | 38 | public ObjectId getId() { 39 | return id; 40 | } 41 | 42 | public void setId(ObjectId id) { 43 | this.id = id; 44 | } 45 | 46 | public String getCreateUser() { 47 | return createUser; 48 | } 49 | 50 | public void setCreateUser(String createUser) { 51 | this.createUser = createUser; 52 | } 53 | 54 | public long getCreatedAt() { 55 | return createdAt; 56 | } 57 | 58 | public void setCreatedAt(long createdAt) { 59 | this.createdAt = createdAt; 60 | } 61 | 62 | public long getUpdatedAt() { 63 | return updatedAt; 64 | } 65 | 66 | public void setUpdatedAt(long updatedAt) { 67 | this.updatedAt = updatedAt; 68 | } 69 | 70 | public String getUpdateUser() { 71 | return updateUser; 72 | } 73 | 74 | public void setUpdateUser(String updateUser) { 75 | this.updateUser = updateUser; 76 | } 77 | 78 | public boolean getIsActive() { 79 | return isRetired; 80 | } 81 | 82 | public void setIsActive(boolean isActive) { 83 | this.isRetired = isActive; 84 | } 85 | 86 | protected void toJson(JsonObject json) { 87 | json.put("id", id == null ? null : id.toHexString()); 88 | json.put("createdAt", this.createdAt); 89 | json.put("updatedAt", this.updatedAt); 90 | } 91 | 92 | protected void fromJson(JsonObject json) { 93 | String id = json.getString("id"); 94 | if (id != null) { 95 | this.id = new ObjectId(id); 96 | } 97 | if (json.getLong("createdAt") != null) { 98 | this.createdAt = json.getLong("createdAt"); 99 | } 100 | if (json.getLong("updatedAt") != null) { 101 | this.updatedAt = json.getLong("updatedAt"); 102 | } 103 | } 104 | 105 | public long getVersion() { 106 | return version; 107 | } 108 | 109 | public void setVersion(long version) { 110 | this.version = version; 111 | } 112 | 113 | @Override 114 | public boolean equals(Object o) { 115 | if (this == o) return true; 116 | if (!(o instanceof Base)) return false; 117 | Base base = (Base) o; 118 | return Objects.equals(id, base.id); 119 | } 120 | 121 | @Override 122 | public int hashCode() { 123 | return Objects.hash(id); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/handlers/ConduitJwtAuthHandlerImpl.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.handlers; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Future; 5 | import io.vertx.core.Handler; 6 | import io.vertx.core.http.HttpHeaders; 7 | import io.vertx.core.http.HttpServerRequest; 8 | import io.vertx.core.json.JsonArray; 9 | import io.vertx.core.json.JsonObject; 10 | import io.vertx.ext.auth.jwt.JWTAuth; 11 | import io.vertx.ext.web.RoutingContext; 12 | import io.vertx.ext.web.handler.impl.AuthHandlerImpl; 13 | import io.vertx.ext.web.handler.impl.HttpStatusException; 14 | 15 | import java.util.List; 16 | import java.util.Objects; 17 | 18 | public class ConduitJwtAuthHandlerImpl extends AuthHandlerImpl { 19 | 20 | private static final HttpStatusException UNAUTHORIZED = new HttpStatusException(401); 21 | private static final HttpStatusException BAD_REQUEST = new HttpStatusException(400); 22 | 23 | private final JsonObject options = new JsonObject(); 24 | private final String type; 25 | 26 | public ConduitJwtAuthHandlerImpl(JWTAuth authProvider, String type) { 27 | super(authProvider, null); 28 | this.type = type; 29 | } 30 | 31 | public ConduitJwtAuthHandlerImpl setAudience(List audience) { 32 | options.put("audience", new JsonArray(audience)); 33 | return this; 34 | } 35 | 36 | public ConduitJwtAuthHandlerImpl setIssuer(String issuer) { 37 | options.put("issuer", issuer); 38 | return this; 39 | } 40 | 41 | public ConduitJwtAuthHandlerImpl setIgnoreExpiration(boolean ignoreExpiration) { 42 | options.put("ignoreExpiration", ignoreExpiration); 43 | return this; 44 | } 45 | 46 | @Override 47 | protected String authenticateHeader(RoutingContext context) { 48 | return Constants.AUTH_KEY; 49 | } 50 | 51 | @Override 52 | public void parseCredentials(RoutingContext context, Handler> handler) { 53 | 54 | parseAuthorizationToken(context, false, parseAuthorization -> { 55 | if (parseAuthorization.failed()) { 56 | handler.handle(Future.failedFuture(parseAuthorization.cause())); 57 | return; 58 | } 59 | 60 | handler.handle(Future.succeededFuture(new JsonObject().put("jwt", parseAuthorization.result()).put("options", options))); 61 | }); 62 | } 63 | 64 | private void parseAuthorizationToken(RoutingContext ctx, boolean optional, Handler> handler) { 65 | 66 | final HttpServerRequest request = ctx.request(); 67 | final String authorization = request.headers().get(HttpHeaders.AUTHORIZATION); 68 | 69 | if (authorization == null) { 70 | if (optional) { 71 | // this is allowed 72 | handler.handle(Future.succeededFuture()); 73 | } else { 74 | handler.handle(Future.failedFuture(UNAUTHORIZED)); 75 | } 76 | return; 77 | } 78 | 79 | try { 80 | int idx = authorization.indexOf(' '); 81 | 82 | if (idx <= 0) { 83 | handler.handle(Future.failedFuture(BAD_REQUEST)); 84 | return; 85 | } 86 | 87 | if (!Objects.equals(type, (authorization.substring(0, idx)))) { 88 | handler.handle(Future.failedFuture(UNAUTHORIZED)); 89 | return; 90 | } 91 | 92 | handler.handle(Future.succeededFuture(authorization.substring(idx + 1))); 93 | } catch (RuntimeException e) { 94 | handler.handle(Future.failedFuture(e)); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/entities/Article.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.entities; 2 | 3 | import dev.morphia.annotations.*; 4 | import io.vertx.codegen.annotations.DataObject; 5 | import io.vertx.core.json.JsonArray; 6 | import io.vertx.core.json.JsonObject; 7 | 8 | import javax.validation.Valid; 9 | import javax.validation.constraints.NotEmpty; 10 | import java.util.Date; 11 | import java.util.List; 12 | 13 | @Entity("articles") 14 | @Indexes(@Index(fields = { @Field("slug") }, options = @IndexOptions(unique = true))) 15 | @DataObject(generateConverter = true) 16 | public class Article extends Base { 17 | 18 | public Article(){} 19 | 20 | public Article(JsonObject jsonObject) { 21 | fromJson(jsonObject); 22 | } 23 | 24 | @NotEmpty 25 | private String slug; 26 | 27 | @NotEmpty 28 | private String title; 29 | 30 | private String description; 31 | 32 | private String body; 33 | 34 | private List tagList; 35 | 36 | private boolean favorited; 37 | 38 | private int favoritesCount; 39 | 40 | @Reference (idOnly = true, lazy = true) 41 | @Valid 42 | private User author; 43 | 44 | public String getSlug() { 45 | return slug; 46 | } 47 | 48 | @Reference (idOnly = true, lazy = true) 49 | @Valid 50 | private List comments; 51 | 52 | public void setSlug(String slug) { 53 | this.slug = slug; 54 | } 55 | 56 | public String getTitle() { 57 | return title; 58 | } 59 | 60 | public void setTitle(String title) { 61 | this.title = title; 62 | } 63 | 64 | public String getDescription() { 65 | return description; 66 | } 67 | 68 | public void setDescription(String description) { 69 | this.description = description; 70 | } 71 | 72 | public String getBody() { 73 | return body; 74 | } 75 | 76 | public void setBody(String body) { 77 | this.body = body; 78 | } 79 | 80 | public List getTagList() { 81 | return tagList; 82 | } 83 | 84 | public void setTagList(List tagList) { 85 | this.tagList = tagList; 86 | } 87 | 88 | public boolean isFavorited() { 89 | return favorited; 90 | } 91 | 92 | public void setFavorited(boolean favorited) { 93 | this.favorited = favorited; 94 | } 95 | 96 | public int getFavoritesCount() { 97 | return favoritesCount; 98 | } 99 | 100 | public void setFavoritesCount(int favoritesCount) { 101 | this.favoritesCount = favoritesCount; 102 | } 103 | 104 | public User getAuthor() { 105 | return author; 106 | } 107 | 108 | public void setAuthor(User author) { 109 | this.author = author; 110 | } 111 | 112 | public JsonObject toJson() { 113 | JsonObject json = new JsonObject(); 114 | ArticleConverter.toJson(this, json); 115 | super.toJson(json); 116 | // To make postman test script happy 117 | if (json.getJsonArray("tagList") == null) { 118 | json.put("tagList", new JsonArray()); 119 | } 120 | return json; 121 | } 122 | 123 | protected void fromJson(JsonObject jsonObject) { 124 | super.fromJson(jsonObject); 125 | ArticleConverter.fromJson(jsonObject, this); 126 | } 127 | 128 | public JsonObject toJsonFor(User user) { 129 | JsonObject json = toJson(); 130 | json.put("createdAt", DF.format(new Date(getCreatedAt()))); 131 | json.put("updatedAt", DF.format(new Date(getCreatedAt()))); 132 | json.put("author", author.toProfileJsonFor(user)); 133 | json.put("favorited", user != null && user.isFavorite(getSlug())); 134 | return json; 135 | } 136 | 137 | public List getComments() { 138 | return comments; 139 | } 140 | 141 | public void setComments(List comments) { 142 | this.comments = comments; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/entities/User.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.entities; 2 | 3 | import dev.morphia.annotations.*; 4 | import io.vertx.codegen.annotations.DataObject; 5 | import io.vertx.core.json.JsonObject; 6 | 7 | import javax.validation.constraints.Email; 8 | import javax.validation.constraints.NotEmpty; 9 | import javax.validation.constraints.Pattern; 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | import static io.vertx.conduit.handlers.Constants.USER; 16 | 17 | 18 | @Entity("users") 19 | @Indexes({@Index(fields = { @Field("username") }, options = @IndexOptions(unique = true)), 20 | @Index(fields = { @Field("email") }, options = @IndexOptions(unique = true))}) 21 | @DataObject(generateConverter = true) 22 | public class User extends Base { 23 | 24 | public User() {} 25 | 26 | public User(JsonObject json) { 27 | fromJson(json); 28 | } 29 | 30 | @NotEmpty 31 | @Pattern(regexp="[a-zA-Z0-9_]+") 32 | private String username; 33 | 34 | @NotEmpty 35 | @Email 36 | private String email; 37 | 38 | @NotEmpty 39 | private String password; 40 | 41 | private String bio; 42 | 43 | private String image; 44 | 45 | private final List favorites = new ArrayList<>(); 46 | 47 | @Reference (idOnly = true, lazy = true) 48 | private final List following = new ArrayList<>(); 49 | 50 | public void setUsername(String username) { 51 | this.username = username; 52 | } 53 | 54 | public String getEmail() { 55 | return email; 56 | } 57 | 58 | public void setEmail(String email) { 59 | this.email = email; 60 | } 61 | 62 | public String getPassword() { 63 | return password; 64 | } 65 | 66 | public void setPassword(String password) { 67 | this.password = password; 68 | } 69 | 70 | public String getBio() { 71 | return bio; 72 | } 73 | 74 | public void setBio(String bio) { 75 | this.bio = bio; 76 | } 77 | 78 | public String getImage() { 79 | return image; 80 | } 81 | 82 | public void setImage(String image) { 83 | this.image = image; 84 | } 85 | 86 | public JsonObject toJson() { 87 | JsonObject json = new JsonObject(); 88 | UserConverter.toJson(this, json); 89 | super.toJson(json); 90 | return json; 91 | } 92 | 93 | protected void fromJson(JsonObject jsonObject) { 94 | super.fromJson(jsonObject); 95 | UserConverter.fromJson(jsonObject, this); 96 | } 97 | 98 | public String getUsername() { 99 | return username; 100 | } 101 | 102 | public JsonObject toAuthJson() { 103 | JsonObject retJson = new JsonObject(); 104 | retJson.put("bio", bio); 105 | retJson.put("email", email); 106 | retJson.put("image", image); 107 | retJson.put("username", username); 108 | 109 | return retJson; 110 | } 111 | 112 | public JsonObject toProfileJsonFor(User user) { 113 | JsonObject retJson = new JsonObject(); 114 | retJson.put("bio", bio); 115 | retJson.put("image", image); 116 | retJson.put("username", username); 117 | retJson.put("following", user != null && user.isFollowing(this)); 118 | 119 | return retJson; 120 | } 121 | 122 | public void addFavorite(String slug) { 123 | favorites.add(slug); 124 | } 125 | 126 | public void removeFavorite(String slug) { 127 | favorites.remove(slug); 128 | } 129 | 130 | public boolean isFavorite(String id) { 131 | return this.favorites.contains(id); 132 | } 133 | 134 | public void follow(User user) { 135 | if (!this.following.contains(user)) { 136 | this.following.add(user); 137 | } 138 | } 139 | 140 | public void unfollow(User user) { 141 | this.following.remove(user); 142 | } 143 | 144 | public boolean isFollowing(User user) { 145 | return this.following.contains(user); 146 | } 147 | 148 | public List getFollowingUsers() { 149 | return Collections.unmodifiableList(this.following); 150 | } 151 | 152 | public List getFavorites() { 153 | return Collections.unmodifiableList(this.favorites); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | - Clone the repo 4 | - Make sure you have Java1.8+ JDK installed 5 | - Open the project in your IDE and allow gradle to finish downloading all dependencies 6 | - A trick for Intellij, If you don't see your gradle menu refresh with new tasks and dependencies after it's done processing, try restarting your IDE 7 | - Install Mongodb community version and launch it in the background 8 | - Install Robo 3T if you want to manually examine the values in the database 9 | - Compile and try running all the unit tests under `/vertx-examples/conduit/src/test/java/` 10 | - Run mongodb in terminal `mongod --config /usr/local/etc/mongod.conf --fork` 11 | - `Gradle tasks -> application -> vertxRun` to run the application 12 | - Run the test script from here `https://github.com/gothinkster/realworld/tree/master/api` 13 | 14 | # Application structure 15 | 16 | - Entity class: defines the data object and the translation from and to json 17 | - Handler class: defines routes and invoke the appropriate service(s) for processing 18 | - Service class: performs entity specific CRUD operations by invoking the database service 19 | - Database Service class: performs generic database operations using Json as the media 20 | - Service Verticles: instantiates a service 21 | - Http Verticle: sets up all the routes 22 | - App Verticle: main verticle that creates all the other verticles and entry point of the application 23 | 24 | # Gradle tasks 25 | - `application >- vertxRun` runs the application. It supports a feature similar to nodmon in Node.js, meaning you can make code changes and it'll recompile and relaunch the app on the fly 26 | - `application -> vertxDebug` launches the application in remote debuging mode. You can then start up a remote debugging session on port 5005 to tap into the application 27 | - `build -> generate` creates all the auto-generated classes. This is run as part of the build step and you shoulnd't have to run it manually 28 | - `shadow -> shadowJar` creates a fat jar file 29 | - more info can be found here https://github.com/jponge/vertx-gradle-plugin 30 | 31 | # Development steps 32 | 33 | - Setup workspace using vertx gradle plugin 34 | - Setup first http verticle with hello world 35 | - Setup config file in resources 36 | - Setup first route with a simple handler for post request - register new user 37 | - Setup second route with a simple handler for get request - get user 38 | - Setup jwt auth handler for protected routes 39 | - Setup a user service to process the requests 40 | - Use bcrypt for password hash 41 | - Connect the handler and the service using event bus 42 | - Use service proxy instead of working with event bus directly 43 | - Create a UserServiceVerticle to publish the service 44 | - Move all user handlers into its own class 45 | - Create annotation for configuring routes 46 | - CRUD for user with authentication 47 | - Rxify the MongoDb service and User service. 48 | - Add unit test for mongodb service 49 | - Create Morphia service to replace MorgonDb service 50 | - Make sure Morphia annotation works regarding index and validation 51 | - Use Morphia service in handlers 52 | - CRUD articles 53 | - add favorite/unfavorite 54 | - CRUD comments 55 | - add follow/unfollow 56 | - add tags 57 | - add query routes 58 | - add annotation for middleware methods for the sake of clarity 59 | - add user unit tests 60 | - add article unit tests 61 | - add comment unit tests 62 | - add query unit tests 63 | - Put all middleware methods in a shared base class 64 | - populate base fields 65 | - cleanup string literals 66 | - add custom jwt handler to expect "Token" instead of "Bearer" 67 | - Plow through the Postman test script from the realworld project page and make everything pass. 68 | 69 | # After thoughts 70 | 71 | - Wasn't a fan of how ObjectId works and the fact that it can be null 72 | - Certain join-like queries using morphia and mongodb is quite painful 73 | - Still can't avoid relational data model 74 | - rxJava2 is a little rough to debug, and is probably an overkill. 75 | - CompletableFuture should suffice since we are only dealing with rx Singles, not streams. And we don't need to deal with threading because of Vert.x, throwing away a major benefit of rxJava2 76 | - Limitations of Vert.x 77 | - lack of flexibility regarding message codec for event bus messages (and hence service proxy) 78 | - lack of ability to customize transport layer (multicast, retry, back pressure and etc) 79 | - lack of support of CompletableFuture within the service proxy framework (Can't auto generate service proxy that uses CompletableFuture) 80 | p.s. Any insight/comment/correction is welcome 81 | 82 | # Credits (Inspired by) 83 | - greyseal's work at https://github.com/greyseal/vertx-realworld-example-app 84 | - skanjo's work at https://github.com/skanjo/realworld-vertx 85 | -------------------------------------------------------------------------------- /conduit/src/test/java/services/MongoServiceTest.java: -------------------------------------------------------------------------------- 1 | package services; 2 | 3 | import io.vertx.conduit.entities.User; 4 | import io.vertx.conduit.services.MongoDbServiceImpl; 5 | import io.vertx.conduit.services.reactivex.MongoDbService; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.core.logging.Logger; 8 | import io.vertx.ext.mongo.FindOptions; 9 | import io.vertx.ext.mongo.UpdateOptions; 10 | import io.vertx.ext.unit.Async; 11 | import io.vertx.ext.unit.TestContext; 12 | import io.vertx.ext.unit.junit.VertxUnitRunner; 13 | import io.vertx.reactivex.core.Vertx; 14 | import io.vertx.reactivex.ext.mongo.MongoClient; 15 | import logging.ContextLogger; 16 | import org.junit.After; 17 | import org.junit.Before; 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | 21 | import static org.junit.Assert.assertTrue; 22 | 23 | @RunWith(VertxUnitRunner.class) 24 | public class MongoServiceTest { 25 | 26 | private static final String USER_COLLETION = "users"; 27 | private static Logger LOGGER = ContextLogger.create(); 28 | 29 | private Vertx vertx; 30 | private MongoDbService mongoDbService; 31 | 32 | @Before 33 | public void setup(TestContext tc) { 34 | this.vertx = Vertx.vertx(); 35 | JsonObject config = new JsonObject().put("db_name", "conduit_test") 36 | .put("connection_string", "mongodb://localhost:27017").put("useObjectId", true); 37 | MongoClient mongoClient = MongoClient.createShared(vertx, config); 38 | MongoDbServiceImpl delegate = new MongoDbServiceImpl(mongoClient, tc.asyncAssertSuccess()); 39 | this.mongoDbService = new io.vertx.conduit.services.reactivex.MongoDbService(delegate); 40 | } 41 | 42 | @Test 43 | public void TestWriteUser(TestContext tc) { 44 | Async async = tc.async(); 45 | 46 | System.out.println("Test writing user"); 47 | 48 | JsonObject user = new JsonObject(); 49 | user.put("password", "123"); 50 | user.put("bio", "abc"); 51 | user.put("email", "1@2.com"); 52 | user.put("username", "xyz"); 53 | 54 | this.mongoDbService.rxInsertOne(USER_COLLETION, user) 55 | .map(id -> { 56 | LOGGER.info("Creating document"); 57 | user.put("id", user.getString("_id")); 58 | user.remove("_id"); 59 | User userEntity = new User(user); 60 | return userEntity; 61 | }).flatMap(userEntity -> { 62 | LOGGER.info("Reading document"); 63 | return this.mongoDbService.rxFindById(USER_COLLETION, userEntity.getId().toHexString(), null); 64 | }).flatMap(json -> { 65 | LOGGER.info("Updating document"); 66 | json.put("id", json.getString("_id")); 67 | json.remove("_id"); 68 | assertTrue(user.equals(json)); 69 | JsonObject updateJson = new JsonObject(); 70 | updateJson.put("email", "3@4.com"); 71 | user.put("email", "3@4.com"); 72 | return this.mongoDbService.rxFindOneAndUpdate(USER_COLLETION, new JsonObject().put("_id", user.getString("id")), updateJson, new FindOptions(), new UpdateOptions()); 73 | }).flatMap(json -> { 74 | LOGGER.info("Reading updated document"); 75 | // FIXME why does this return a nested object id? 76 | return this.mongoDbService.rxFindById(USER_COLLETION, json.getJsonObject("_id").getString("$oid"), null); 77 | }).flatMap(json -> { 78 | json.put("id", json.getString("_id")); 79 | json.remove("_id"); 80 | assertTrue(user.equals(json)); 81 | LOGGER.info("Deleting document: " + json); 82 | return this.mongoDbService.rxDelete(USER_COLLETION, new JsonObject().put("_id", user.getString("id"))); 83 | }).subscribe((res, e) -> { 84 | if (e == null) { 85 | async.complete(); 86 | } else { 87 | tc.fail(e); 88 | } 89 | }); 90 | } 91 | 92 | @After 93 | public void tearDown(TestContext tc) { 94 | vertx.setTimer(1000, t -> { System.out.println("timer complete"); }); 95 | vertx.close(tc.asyncAssertSuccess()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/services/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.services; 2 | 3 | import io.vertx.conduit.entities.User; 4 | import io.vertx.core.AsyncResult; 5 | import io.vertx.core.Future; 6 | import io.vertx.core.Handler; 7 | import io.vertx.core.Vertx; 8 | import io.vertx.core.json.JsonObject; 9 | import io.vertx.core.logging.Logger; 10 | import io.vertx.serviceproxy.ServiceProxyBuilder; 11 | import logging.ContextLogger; 12 | import org.bson.types.ObjectId; 13 | 14 | import java.util.List; 15 | 16 | 17 | public class UserServiceImpl implements UserService { 18 | 19 | private static final Logger LOGGER = ContextLogger.create(); 20 | 21 | // active user being processed 22 | private final io.vertx.conduit.services.reactivex.MorphiaService morphiaService; 23 | 24 | public UserServiceImpl(Vertx vertx) { 25 | ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx).setAddress(MorphiaService.ADDRESS); 26 | MorphiaService delegate = builder.build(MorphiaService.class); 27 | morphiaService = new io.vertx.conduit.services.reactivex.MorphiaService(delegate); 28 | } 29 | 30 | @Override 31 | public void create(JsonObject user, Handler> resultHandler) { 32 | User userEntity = new User(user); 33 | morphiaService.rxCreateUser(userEntity) 34 | .subscribe((id, ex) -> { 35 | if (ex == null) { 36 | userEntity.setId(new ObjectId(id)); 37 | resultHandler.handle(Future.succeededFuture(userEntity)); 38 | } else { 39 | resultHandler.handle(Future.failedFuture(ex)); 40 | } 41 | }); 42 | } 43 | 44 | @Override 45 | public void getByEmail(String email, Handler> resultHandler) { 46 | morphiaService.rxGetUser(new JsonObject().put("email", email)) 47 | .subscribe((users, ex) -> handleUser(resultHandler, users, ex)); 48 | 49 | } 50 | 51 | @Override 52 | public void get(JsonObject query, Handler> resultHandler) { 53 | 54 | morphiaService.rxGetUser(query) 55 | .subscribe((users, ex) -> handleUser(resultHandler, users, ex)); 56 | } 57 | 58 | @Override 59 | public void getById(String id, Handler> resultHandler) { 60 | 61 | morphiaService.rxGetUser(new JsonObject().put("_id", id)) 62 | .subscribe((users, ex) -> handleUser(resultHandler, users, ex)); 63 | } 64 | 65 | @Override 66 | public void update(String id, JsonObject update, Handler> resultHandler) { 67 | // Have to filter on all unique fields for mongodb to replace the document instead of inserting a new one 68 | morphiaService.rxUpdateUser(new JsonObject().put("_id", id), update) 69 | .subscribe((users, ex) -> handleUser(resultHandler, users, ex)); 70 | } 71 | 72 | private static void handleUser(Handler> resultHandler, List users, Throwable ex) { 73 | 74 | if (users == null || users.size() != 1) { 75 | resultHandler.handle(Future.failedFuture(new RuntimeException("Couldn't find unique user"))); 76 | } 77 | else { 78 | if (ex == null) { 79 | resultHandler.handle(Future.succeededFuture(users.get(0))); 80 | } else { 81 | resultHandler.handle(Future.failedFuture(ex)); 82 | } 83 | } 84 | } 85 | 86 | public void getFavoriteCount(String slug, Handler> resultHandler){ 87 | morphiaService.rxGetUser(new JsonObject().put("favorites", slug)) 88 | .subscribe((users, ex) -> { 89 | if (ex == null) { 90 | resultHandler.handle(Future.succeededFuture(users.size())); 91 | } else { 92 | resultHandler.handle(Future.failedFuture(ex)); 93 | } 94 | }); 95 | } 96 | 97 | @Override 98 | public void deleteByUsername(String username, Handler> resultHandler) { 99 | morphiaService.rxDeleteUser(new JsonObject().put("username", username)) 100 | .subscribe((deleteCount, ex) -> { 101 | if (ex == null) { 102 | resultHandler.handle(Future.succeededFuture(deleteCount)); 103 | } else { 104 | resultHandler.handle(Future.failedFuture(ex)); 105 | } 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/handlers/ConduitHandler.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.handlers; 2 | 3 | import io.vertx.conduit.entities.User; 4 | import io.vertx.conduit.services.ArticleService; 5 | import io.vertx.conduit.services.CommentService; 6 | import io.vertx.conduit.services.MorphiaService; 7 | import io.vertx.conduit.services.UserService; 8 | import io.vertx.core.Vertx; 9 | import io.vertx.core.json.JsonObject; 10 | import io.vertx.ext.web.RoutingContext; 11 | import io.vertx.serviceproxy.ServiceProxyBuilder; 12 | import routerutils.BaseHandler; 13 | import routerutils.Middleware; 14 | 15 | public class ConduitHandler extends BaseHandler { 16 | 17 | protected final io.vertx.conduit.services.reactivex.ArticleService articleService; 18 | protected final io.vertx.conduit.services.reactivex.UserService userService; 19 | protected final io.vertx.conduit.services.reactivex.CommentService commentService; 20 | protected final io.vertx.conduit.services.reactivex.MorphiaService morphiaService; 21 | 22 | protected ConduitHandler(Vertx vertx) { 23 | super(vertx); 24 | 25 | { 26 | ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx).setAddress(ArticleService.ADDRESS); 27 | ArticleService delegate = builder.build(ArticleService.class); 28 | this.articleService = new io.vertx.conduit.services.reactivex.ArticleService(delegate); 29 | } 30 | 31 | { 32 | ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx).setAddress(UserService.ADDRESS); 33 | UserService delegate = builder.build(UserService.class); 34 | this.userService = new io.vertx.conduit.services.reactivex.UserService(delegate); 35 | } 36 | 37 | { 38 | ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx).setAddress(CommentService.ADDRESS); 39 | CommentService delegate = builder.build(CommentService.class); 40 | this.commentService = new io.vertx.conduit.services.reactivex.CommentService(delegate); 41 | } 42 | 43 | { 44 | ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx).setAddress(MorphiaService.ADDRESS); 45 | MorphiaService delegate = builder.build(MorphiaService.class); 46 | this.morphiaService = new io.vertx.conduit.services.reactivex.MorphiaService(delegate); 47 | } 48 | } 49 | 50 | 51 | @Middleware 52 | public void extractUser(RoutingContext event) { 53 | if (event.get(Constants.USER_ID) == null) { 54 | event.next(); 55 | return; 56 | } 57 | userService.rxGetById(event.get(Constants.USER_ID)) 58 | .subscribe((user, ex) -> { 59 | if (ex == null) { 60 | event.put(Constants.USER, user); 61 | event.next(); 62 | } else { 63 | event.fail(ex); 64 | } 65 | }); 66 | } 67 | 68 | @Middleware 69 | public void extractProfile(RoutingContext event) { 70 | String username = event.request().getParam("username"); 71 | userService.rxGet(new JsonObject().put("username", username)) 72 | .subscribe((user, ex) -> { 73 | if (ex == null) { 74 | event.put("profile", user); 75 | event.next(); 76 | } else { 77 | event.fail(ex); 78 | } 79 | }); 80 | } 81 | 82 | @Middleware 83 | public void extractArticle(RoutingContext event) { 84 | String slug = event.request().getParam(Constants.ARTICLE); 85 | if (slug != null) { 86 | articleService.rxGet(slug) 87 | .subscribe((article, ex) -> { 88 | if (ex == null) { 89 | event.put(Constants.ARTICLE, article); 90 | event.next(); 91 | } else { 92 | event.fail(ex); 93 | } 94 | }); 95 | } else { 96 | event.next(); 97 | } 98 | } 99 | 100 | @Middleware 101 | public void extractComment(RoutingContext event) { 102 | String commentId = event.request().getParam(Constants.COMMENT); 103 | commentService.rxGet(commentId) 104 | .subscribe((comment, ex) -> { 105 | if (ex == null) { 106 | event.put(Constants.COMMENT, comment); 107 | event.next(); 108 | } else { 109 | event.fail(ex); 110 | } 111 | }); 112 | } 113 | 114 | protected void setCreateFields(RoutingContext event, JsonObject jsonObject) { 115 | jsonObject.put("createdAt", System.currentTimeMillis()); 116 | User user = event.get(Constants.USER); 117 | if (user != null) { 118 | jsonObject.put("createUser", user.getUsername()); 119 | } 120 | } 121 | 122 | protected void setUpdateFields(RoutingContext event, JsonObject jsonObject) { 123 | jsonObject.put("updatedAt", System.currentTimeMillis()); 124 | User user = event.get(Constants.USER); 125 | if (user != null) { 126 | jsonObject.put("updateUser", user.getUsername()); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /conduit/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /common/src/main/java/routerutils/HandlerProcessor.java: -------------------------------------------------------------------------------- 1 | package routerutils; 2 | 3 | import logging.ContextLogger; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.http.HttpMethod; 6 | import io.vertx.core.logging.Logger; 7 | import io.vertx.ext.web.Route; 8 | import io.vertx.ext.web.Router; 9 | import io.vertx.ext.web.RoutingContext; 10 | import io.vertx.ext.web.handler.AuthHandler; 11 | 12 | import java.lang.annotation.Annotation; 13 | import java.lang.reflect.Method; 14 | import java.lang.reflect.Modifier; 15 | import java.util.*; 16 | import java.util.stream.Collectors; 17 | 18 | /* 19 | Inspired by greyseal's work 20 | https://github.com/greyseal/vertx-realworld-example-app/blob/master/vertx-boot/src/main/java/com/greyseal/vertx/boot/annotation/AnnotationProcessor.java 21 | */ 22 | 23 | public final class HandlerProcessor { 24 | 25 | private static Logger LOGGER = ContextLogger.create(); 26 | 27 | public static void buildHandler(final Router router, final List> preHandlers, final H handler, final AuthHandler authHandler) { 28 | Class clazz = handler.getClass(); 29 | RouteConfig baseRouteConfig = clazz.getAnnotation(RouteConfig.class); 30 | if (baseRouteConfig != null) { 31 | String[] baseConsumes = baseRouteConfig.consumes(); 32 | String[] baseProduces = baseRouteConfig.produces(); 33 | 34 | String basePath = baseRouteConfig.path().length() != 0 ? baseRouteConfig.path() : ""; 35 | Set methods = getMethodsAnnotatedWith(clazz, RouteConfig.class); 36 | String[] baseMiddlewares = baseRouteConfig.middlewares(); 37 | List baseMiddlewareMethods = getMiddlewareMethods(clazz, baseMiddlewares); 38 | 39 | methods.forEach(method -> { 40 | RouteConfig annotation = method.getAnnotation(RouteConfig.class); 41 | if (!Modifier.isPublic(method.getModifiers())) { 42 | throw new RuntimeException("Method must be public to work with RouteConfig " + clazz.getName() + "#" + method.getName()); 43 | } 44 | String[] methodConsumes = annotation.consumes(); 45 | String[] methodProduces = annotation.produces(); 46 | String methodPath = annotation.path(); 47 | String[] middlewares = annotation.middlewares(); 48 | HttpMethod httpMethod = annotation.method(); 49 | boolean authRequired = annotation.authRequired(); 50 | List middlewareMethods = getMiddlewareMethods(clazz, middlewares); 51 | 52 | String path = basePath + methodPath; 53 | Route route = router.route(httpMethod, path); 54 | setMediaType(route, methodConsumes.length != 0 ? methodConsumes : baseConsumes , false); 55 | setMediaType(route, methodProduces.length != 0 ? methodProduces : baseProduces , true); 56 | 57 | if (authRequired) { 58 | if (authHandler == null) { 59 | throw new IllegalArgumentException("authHandler must be specified when authRequired = true"); 60 | } 61 | route.handler(authHandler); 62 | } 63 | 64 | if (preHandlers != null) { 65 | preHandlers.forEach(h -> route.handler(h)); 66 | } 67 | 68 | baseMiddlewareMethods.forEach(m -> createHandler(handler, m, route)); 69 | middlewareMethods.forEach(m -> createHandler(handler, m, route)); 70 | 71 | createHandler(handler, method, route); 72 | }); 73 | } 74 | } 75 | 76 | private static List getMiddlewareMethods(Class clazz, String[] baseMiddlewares) { 77 | return Arrays.asList(baseMiddlewares) 78 | .stream() 79 | .map(s -> { 80 | try { 81 | return clazz.getMethod(s, RoutingContext.class); 82 | } catch (NoSuchMethodException e) { 83 | throw new IllegalArgumentException("Middleware method not properly defined. Must take RoutingContext as argument " + clazz.getName() + "#" + s); 84 | } 85 | }) 86 | .collect(Collectors.toList()); 87 | } 88 | 89 | private static void createHandler(final H handler, final Method method, final Route route) { 90 | route.handler(event -> { 91 | try { 92 | method.invoke(handler, event); 93 | } catch (Exception e) { 94 | LOGGER.error("Error calling handler {}#{} {}", handler.getClass(), method.getName(), e); 95 | } 96 | }); 97 | } 98 | 99 | private static void setMediaType(final Route route, final String[] mediaTypes, final boolean isProduced) { 100 | if (null != mediaTypes && mediaTypes.length > 0) { 101 | Arrays.asList(mediaTypes).forEach(contentType -> { 102 | if (!isProduced) { 103 | route.consumes(contentType); 104 | } else { 105 | route.produces(contentType); 106 | } 107 | }); 108 | } 109 | } 110 | 111 | private static Set getMethodsAnnotatedWith(final Class type, 112 | final Class annotation) { 113 | final List methodNames = new ArrayList<>(); 114 | final Set methods = new HashSet<>(); 115 | Class clazz = type; 116 | while (clazz != Object.class) { 117 | final List allMethods = new ArrayList(Arrays.asList(clazz.getDeclaredMethods())); 118 | for (final Method method : allMethods) { 119 | if (method.isAnnotationPresent(annotation)) { 120 | if (!methodNames.contains(method.getName())) { 121 | methods.add(method); 122 | methodNames.add(method.getName()); 123 | } 124 | } 125 | } 126 | clazz = clazz.getSuperclass(); 127 | } 128 | return methods; 129 | } 130 | } -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/services/MongoDbServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.services; 2 | 3 | import com.mongodb.MongoWriteException; 4 | import io.vertx.core.AsyncResult; 5 | import io.vertx.core.Future; 6 | import io.vertx.core.Handler; 7 | import io.vertx.core.json.JsonObject; 8 | import io.vertx.ext.mongo.FindOptions; 9 | import io.vertx.ext.mongo.MongoClientDeleteResult; 10 | import io.vertx.ext.mongo.MongoClientUpdateResult; 11 | import io.vertx.ext.mongo.UpdateOptions; 12 | import io.vertx.reactivex.ext.mongo.MongoClient; 13 | 14 | import java.util.List; 15 | 16 | public class MongoDbServiceImpl implements MongoDbService { 17 | 18 | private final MongoClient client; 19 | 20 | public MongoDbServiceImpl(final MongoClient mongoClient, final Handler> readyHandler) { 21 | this.client = mongoClient; 22 | this.client.rxGetCollections().subscribe(resp -> { 23 | readyHandler.handle(Future.succeededFuture(this)); 24 | }, cause -> { 25 | readyHandler.handle(Future.failedFuture(cause)); 26 | }); 27 | } 28 | 29 | @Override 30 | public void findOne(final String collection, final JsonObject query, final JsonObject fields, final Handler> resultHandler) { 31 | try { 32 | client.rxFindOne(collection, query, fields).subscribe(resp -> { 33 | resultHandler.handle(Future.succeededFuture(resp)); 34 | }, cause -> { 35 | resultHandler.handle(Future.failedFuture(cause)); 36 | }); 37 | } catch (Exception ex) { 38 | resultHandler.handle(Future.failedFuture(ex)); 39 | } 40 | } 41 | 42 | @Override 43 | public void findById(final String collection, final String id, final JsonObject fields, final Handler> resultHandler) { 44 | findOne(collection, new JsonObject().put("_id", id), fields, resultHandler); 45 | } 46 | 47 | @Override 48 | public void find(final String collection, final JsonObject query, final FindOptions options, Handler>> resultHandler) { 49 | try { 50 | client.rxFindWithOptions(collection, query, options).subscribe(resp -> { 51 | resultHandler.handle(Future.succeededFuture(resp)); 52 | }, cause -> { 53 | resultHandler.handle(Future.failedFuture(cause)); 54 | }); 55 | } catch (Exception ex) { 56 | resultHandler.handle(Future.failedFuture(ex)); 57 | } 58 | } 59 | 60 | @Override 61 | public void insertOne(final String collection, final JsonObject document, final Handler> resultHandler) { 62 | try { 63 | // make sure _id field doesn't exist to force database create one 64 | document.remove("id"); 65 | client.rxInsert(collection, document).subscribe(resp -> { 66 | resultHandler.handle(Future.succeededFuture(resp)); 67 | }, cause -> { 68 | final MongoWriteException mwx = (MongoWriteException) cause; 69 | if (mwx.getCode() == 11000) { 70 | resultHandler.handle(Future.failedFuture(new RuntimeException("DuplicateEntity"))); 71 | } else { 72 | resultHandler.handle(Future.failedFuture(cause)); 73 | } 74 | }); 75 | } catch (Exception ex) { 76 | resultHandler.handle(Future.failedFuture(ex)); 77 | } 78 | } 79 | 80 | @Override 81 | 82 | public void upsert(final String collection, final JsonObject query, final JsonObject toUpdate, final UpdateOptions options, Handler> resultHandler) { 83 | try { 84 | client.rxUpdateCollectionWithOptions(collection, query, toUpdate, options).subscribe(resp -> { 85 | resultHandler.handle(Future.succeededFuture(resp)); 86 | }, cause -> { 87 | resultHandler.handle(Future.failedFuture(cause)); 88 | }); 89 | } catch (Exception ex) { 90 | resultHandler.handle(Future.failedFuture(ex)); 91 | } 92 | } 93 | 94 | @Override 95 | public void findOneAndUpdate(final String collection, final JsonObject query, final JsonObject toUpdate, final FindOptions findOptions, final UpdateOptions updateOptions, final Handler> resultHandler) { 96 | try { 97 | client.rxFindOneAndUpdateWithOptions(collection, query, new JsonObject().put("$set", toUpdate), findOptions, updateOptions).subscribe(resp -> { 98 | resultHandler.handle(Future.succeededFuture(resp)); 99 | }, cause -> { 100 | resultHandler.handle(Future.failedFuture(cause)); 101 | }); 102 | } catch (Exception ex) { 103 | resultHandler.handle(Future.failedFuture(ex)); 104 | } 105 | } 106 | 107 | @Override 108 | public void findOneAndReplace(final String collection, final JsonObject query, final JsonObject replacement, final FindOptions findOptions, final UpdateOptions updateOptions, final Handler> resultHandler) { 109 | try { 110 | client.rxFindOneAndReplaceWithOptions(collection, query, replacement, findOptions, updateOptions).subscribe(resp -> { 111 | resultHandler.handle(Future.succeededFuture(resp)); 112 | }, cause -> { 113 | resultHandler.handle(Future.failedFuture(cause)); 114 | }); 115 | } catch (Exception ex) { 116 | resultHandler.handle(Future.failedFuture(ex)); 117 | } 118 | } 119 | 120 | @Override 121 | public void delete(final String collection, final JsonObject query, Handler> resultHandler){ 122 | try { 123 | client.rxRemoveDocuments(collection, query).subscribe(resp -> { 124 | resultHandler.handle(Future.succeededFuture(resp)); 125 | }, cause -> { 126 | resultHandler.handle(Future.failedFuture(cause)); 127 | }); 128 | } catch (Exception ex) { 129 | resultHandler.handle(Future.failedFuture(ex)); 130 | } 131 | } 132 | 133 | @Override 134 | public void close() { 135 | client.close(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/handlers/UserHandler.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.handlers; 2 | 3 | import io.netty.handler.codec.http.HttpResponseStatus; 4 | import io.vertx.conduit.entities.User; 5 | import io.vertx.conduit.services.MorphiaServiceOperator; 6 | import io.vertx.conduit.services.UserService; 7 | import io.vertx.core.Vertx; 8 | import io.vertx.core.http.HttpMethod; 9 | import io.vertx.core.json.Json; 10 | import io.vertx.core.json.JsonObject; 11 | import io.vertx.ext.auth.jwt.JWTAuth; 12 | import io.vertx.ext.auth.jwt.JWTOptions; 13 | import io.vertx.ext.web.RoutingContext; 14 | import io.vertx.serviceproxy.ServiceProxyBuilder; 15 | import org.springframework.security.crypto.bcrypt.BCrypt; 16 | import routerutils.RouteConfig; 17 | 18 | @RouteConfig(path="/api", produces = "application/json") 19 | public class UserHandler extends ConduitHandler { 20 | 21 | private final JWTAuth jwtAuth; 22 | 23 | public UserHandler(Vertx vertx, JWTAuth jwtAuth) { 24 | super(vertx); 25 | ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx).setAddress(UserService.ADDRESS); 26 | this.jwtAuth = jwtAuth; 27 | } 28 | 29 | private void appendJwt(JsonObject user, String id) { 30 | JsonObject principal = new JsonObject(); 31 | principal.put("id", id); 32 | principal.put("username", user.getString("username")); 33 | user.put(Constants.AUTH_KEY, jwtAuth.generateToken(principal, new JWTOptions().setExpiresInMinutes(120))); 34 | } 35 | 36 | @RouteConfig(path="/users/login", method=HttpMethod.POST, authRequired=false) 37 | public void login(RoutingContext event) { 38 | JsonObject message = event.getBodyAsJson().getJsonObject(Constants.USER); 39 | if (message != null) { 40 | userService.rxGetByEmail(message.getString("email")) 41 | .subscribe((res, ex) -> { 42 | if (ex != null) { 43 | event.fail(new RuntimeException("Invalid user credentials")); 44 | } else { 45 | String hashed = res.getPassword(); 46 | if (BCrypt.checkpw(message.getString("password"), hashed)) { 47 | JsonObject authJson = createAuthJson(res); 48 | event.response() 49 | .setStatusCode(HttpResponseStatus.CREATED.code()) 50 | .end(Json.encodePrettily(authJson)); 51 | } else { 52 | event.fail(new RuntimeException("Invalid user credentials")); 53 | } 54 | } 55 | }); 56 | } else { 57 | event.fail(new RuntimeException("Invalid user credentials")); 58 | } 59 | 60 | } 61 | 62 | private static String setPassword(String password) { 63 | String salt = BCrypt.gensalt(); 64 | return BCrypt.hashpw(password, salt); 65 | } 66 | 67 | private JsonObject createAuthJson(User user) { 68 | if (user.getId() != null) { 69 | JsonObject authJson = user.toAuthJson(); 70 | appendJwt(authJson, user.getId().toHexString()); 71 | return new JsonObject().put("user", authJson); 72 | } 73 | return new JsonObject(); 74 | } 75 | 76 | @RouteConfig(path="/users", method=HttpMethod.POST, authRequired=false) 77 | public void register(RoutingContext event) { 78 | JsonObject message = event.getBodyAsJson().getJsonObject(Constants.USER); 79 | setCreateFields(event, message); 80 | message.put("password", setPassword(message.getString("password"))); 81 | userService.rxCreate(message) 82 | .subscribe(res -> handleResponse(event, createAuthJson(res), HttpResponseStatus.CREATED), e -> handleError(event, e)); 83 | } 84 | 85 | 86 | 87 | @RouteConfig(path="/user", method = HttpMethod.PUT) 88 | public void put(RoutingContext event) { 89 | JsonObject message = event.getBodyAsJson().getJsonObject(Constants.USER); 90 | setUpdateFields(event, message); 91 | userService.rxUpdate(event.get(Constants.USER_ID), message) 92 | .subscribe(res -> handleResponse(event, createAuthJson(res), HttpResponseStatus.OK), e -> handleError(event, e)); 93 | } 94 | 95 | @RouteConfig(path="/user") 96 | public void get(RoutingContext event) { 97 | userService.rxGetById(event.get(Constants.USER_ID)) 98 | .subscribe(res -> handleResponse(event, createAuthJson(res), HttpResponseStatus.OK), e -> handleError(event, e)); 99 | } 100 | 101 | @RouteConfig(path="/profiles/:username", authRequired = false, middlewares = {"extractProfile", "extractUser"}) 102 | public void getProfile(RoutingContext event) { 103 | User profile = event.get("profile"); 104 | User queryingUser = event.get(Constants.USER); 105 | JsonObject json = new JsonObject().put("profile" ,profile.toProfileJsonFor(queryingUser)); 106 | handleResponse(event, json, HttpResponseStatus.OK); 107 | } 108 | 109 | @RouteConfig(path="/profiles/:username/follow", method = HttpMethod.POST, middlewares = {"extractProfile", "extractUser"}) 110 | public void follow(RoutingContext event) { 111 | 112 | User profileUser = event.get("profile"); 113 | User queryingUser = event.get(Constants.USER); 114 | 115 | queryingUser.follow(profileUser); 116 | 117 | // following is a list of dbrefs. probably this is not the right way to append to it 118 | JsonObject update = new JsonObject().put("following", new JsonObject().put(MorphiaServiceOperator.PUSH, new JsonObject().put("_id", profileUser.getId().toHexString()))); 119 | userService.rxUpdate(queryingUser.getId().toHexString(), update) 120 | .map(user -> new JsonObject().put("profile", profileUser.toProfileJsonFor(queryingUser))) 121 | .subscribe(res -> handleResponse(event, res, HttpResponseStatus.OK), e -> handleError(event, e)); 122 | } 123 | 124 | @RouteConfig(path="/profiles/:username/follow", method = HttpMethod.DELETE, middlewares = {"extractProfile", "extractUser"}) 125 | public void unfollow(RoutingContext event) { 126 | 127 | User profileUser = event.get("profile"); 128 | User queryingUser = event.get(Constants.USER); 129 | 130 | queryingUser.unfollow(profileUser); 131 | JsonObject update = new JsonObject().put("following", new JsonObject().put(MorphiaServiceOperator.POP, new JsonObject().put("_id", profileUser.getId().toHexString()))); 132 | userService.rxUpdate(queryingUser.getId().toHexString(), update) 133 | .map(user -> new JsonObject().put("profile", profileUser.toProfileJsonFor(queryingUser))) 134 | .subscribe(res -> handleResponse(event, res, HttpResponseStatus.OK), e -> handleError(event, e)); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /conduit/src/test/java/routes/QueryTest.java: -------------------------------------------------------------------------------- 1 | package routes; 2 | 3 | import io.netty.handler.codec.http.HttpResponseStatus; 4 | import io.vertx.codegen.annotations.Nullable; 5 | import io.vertx.conduit.handlers.Constants; 6 | import io.vertx.core.json.JsonArray; 7 | import io.vertx.core.json.JsonObject; 8 | import io.vertx.ext.unit.Async; 9 | import io.vertx.ext.unit.TestContext; 10 | import io.vertx.ext.unit.junit.VertxUnitRunner; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | 14 | @RunWith(VertxUnitRunner.class) 15 | public class QueryTest extends TestBase { 16 | 17 | @Test(timeout = 120000) 18 | public void testQuery(TestContext tc) { 19 | cleanupUser(tc); 20 | cleanupArticles(tc); 21 | registerUser(tc, user1); 22 | registerUser(tc, user2); 23 | loginUser(tc, user1); 24 | 25 | createArticle(tc, user1, testArticle1); 26 | createArticle(tc, user1, testArticle2); 27 | 28 | loginUser(tc, user2); 29 | 30 | Async favoriteArticle = tc.async(); 31 | 32 | webClient.post(PORT, "localhost", "/api/articles/first-article/favorite") 33 | .putHeader(CONTENT_TYPE, JSON) 34 | .putHeader(AUTHORIZATION, getJwt(tc)) 35 | .send(ar -> { 36 | if (ar.succeeded()) { 37 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 38 | JsonObject json = ar.result().bodyAsJsonObject().getJsonObject(Constants.ARTICLE); 39 | tc.assertNotNull(json); 40 | JsonObject expected = testArticle1.toJsonFor(user2); 41 | expected.put("id", json.getString("id")); 42 | expected.put("version", json.getLong("version")); 43 | expected.put("favoritesCount", 1); 44 | expected.put("favorited", true); 45 | expected.put("createdAt", json.getString("createdAt")); 46 | expected.put("createUser", json.getString("createUser")); 47 | expected.put("updatedAt", json.getString("updatedAt")); 48 | tc.assertEquals(expected, json); 49 | favoriteArticle.complete(); 50 | } else { 51 | tc.fail(); 52 | } 53 | }); 54 | 55 | favoriteArticle.awaitSuccess(); 56 | 57 | Async tagsQuery = tc.async(); 58 | 59 | webClient.get(PORT, "localhost", "/api/tags/") 60 | .putHeader(CONTENT_TYPE, JSON) 61 | .putHeader(AUTHORIZATION, getJwt(tc)) 62 | .send( 63 | ar -> { 64 | if (ar.succeeded()) { 65 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 66 | JsonArray json = ar.result().bodyAsJsonObject().getJsonArray("tags"); 67 | JsonArray expected = new JsonArray().add("food").add("travel").add("dance").add("music"); 68 | tc.assertNotNull(json); 69 | tc.assertEquals(expected, json); 70 | tagsQuery.complete(); 71 | } else { 72 | tc.fail(); 73 | } 74 | }); 75 | 76 | tagsQuery.awaitSuccess(); 77 | 78 | Async articleQuery = tc.async(); 79 | 80 | webClient.get(PORT, "localhost", "/api/articles") 81 | .putHeader(CONTENT_TYPE, JSON) 82 | .putHeader(AUTHORIZATION, getJwt(tc)) 83 | .sendJsonObject(new JsonObject().put("query", new JsonObject() 84 | .put("limit", 10) 85 | .put("offset", 0) 86 | .put("author", "user1") 87 | .put("favoriter", "user2") 88 | .put("tags", new JsonArray().add("food").add("music")) 89 | ), 90 | ar -> { 91 | if (ar.succeeded()) { 92 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 93 | @Nullable JsonObject res = ar.result().bodyAsJsonObject(); 94 | int count = res.getInteger("articlesCount"); 95 | tc.assertEquals(1, count); 96 | 97 | JsonArray articles = res.getJsonArray("articles"); 98 | tc.assertEquals(1, articles.size()); 99 | 100 | tc.assertEquals(testArticle1.getId().toHexString(), articles.getJsonObject(0).getString("id")); 101 | articleQuery.complete(); 102 | } else { 103 | tc.fail(); 104 | } 105 | }); 106 | 107 | articleQuery.awaitSuccess(); 108 | 109 | Async follow = tc.async(); 110 | 111 | webClient.post(PORT, "localhost", "/api/profiles/user1/follow") 112 | .putHeader(CONTENT_TYPE, JSON) 113 | .putHeader(AUTHORIZATION, getJwt(tc)) 114 | .send( 115 | ar -> { 116 | if (ar.succeeded()) { 117 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 118 | JsonObject json = ar.result().bodyAsJsonObject().getJsonObject("profile"); 119 | JsonObject jsonExpected = user1.toProfileJsonFor(user2); 120 | jsonExpected.put("following", true); 121 | 122 | tc.assertEquals(jsonExpected, json); 123 | follow.complete(); 124 | } else { 125 | tc.fail(ar.cause()); 126 | } 127 | }); 128 | 129 | follow.awaitSuccess(); 130 | 131 | Async feedQuery = tc.async(); 132 | 133 | webClient.get(PORT, "localhost", "/api/articles/feed") 134 | .putHeader(CONTENT_TYPE, JSON) 135 | .putHeader(AUTHORIZATION, getJwt(tc)) 136 | .sendJsonObject(new JsonObject().put("query", new JsonObject() 137 | .put("limit", 10) 138 | .put("offset", 0) 139 | .put("author", "user1") 140 | .put("favoriter", "user2") 141 | .put("tags", new JsonArray().add("food").add("music")) 142 | ), 143 | ar -> { 144 | if (ar.succeeded()) { 145 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 146 | @Nullable JsonObject res = ar.result().bodyAsJsonObject(); 147 | int count = res.getInteger("articlesCount"); 148 | tc.assertEquals(2, count); 149 | 150 | JsonArray articles = res.getJsonArray("articles"); 151 | tc.assertEquals(2, articles.size()); 152 | 153 | // ordered by create time in descending order 154 | tc.assertEquals(testArticle2.getId().toHexString(), articles.getJsonObject(0).getString("id")); 155 | tc.assertEquals(testArticle1.getId().toHexString(), articles.getJsonObject(1).getString("id")); 156 | feedQuery.complete(); 157 | } else { 158 | tc.fail(); 159 | } 160 | }); 161 | 162 | feedQuery.awaitSuccess(); 163 | 164 | cleanupArticles(tc); 165 | cleanupUser(tc); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /conduit/src/test/java/routes/UserTest.java: -------------------------------------------------------------------------------- 1 | package routes; 2 | 3 | import io.netty.handler.codec.http.HttpResponseStatus; 4 | import io.vertx.conduit.handlers.Constants; 5 | import io.vertx.core.json.JsonObject; 6 | import io.vertx.ext.unit.Async; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import org.junit.After; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | 13 | @RunWith(VertxUnitRunner.class) 14 | public class UserTest extends TestBase { 15 | 16 | @Test(timeout = TIMEOUT) 17 | public void testRegisterUser(TestContext tc) { 18 | cleanupUser(tc); 19 | 20 | registerUser(tc, user1); 21 | 22 | Async createDupe = tc.async(); 23 | 24 | webClient.post(PORT, "localhost", "/api/users") 25 | .putHeader(CONTENT_TYPE, JSON) 26 | .sendJsonObject(new JsonObject() 27 | .put(Constants.USER, user1.toJson() 28 | ), ar -> { 29 | if (ar.succeeded()) { 30 | String body = ar.result().bodyAsString(); 31 | tc.assertEquals("Internal Server Error", body); 32 | createDupe.complete(); 33 | } else { 34 | tc.fail(); 35 | } 36 | }); 37 | 38 | createDupe.awaitSuccess(); 39 | } 40 | 41 | @Test(timeout = 120000) 42 | public void testUpdateUser(TestContext tc) { 43 | cleanupUser(tc); 44 | registerUser(tc, user1); 45 | loginUser(tc, user1); 46 | 47 | Async update = tc.async(); 48 | 49 | webClient.put(PORT, "localhost", "/api/user") 50 | .putHeader(CONTENT_TYPE, JSON) 51 | .putHeader(AUTHORIZATION, getJwt(tc)) 52 | .sendJsonObject(new JsonObject().put(Constants.USER, new JsonObject() 53 | .put("bio", "updatedBio")), 54 | ar -> { 55 | if (ar.succeeded()) { 56 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 57 | JsonObject returnedUser = ar.result().bodyAsJsonObject().getJsonObject("user"); 58 | tc.assertEquals(user1.getUsername(), returnedUser.getString("username")); 59 | tc.assertEquals(user1.getEmail(), returnedUser.getString("email")); 60 | tc.assertEquals("updatedBio", returnedUser.getString("bio")); 61 | tc.assertNull(returnedUser.getString("image")); 62 | update.complete(); 63 | } else { 64 | tc.fail(ar.cause()); 65 | } 66 | }); 67 | } 68 | 69 | @Test(timeout = TIMEOUT) 70 | public void testGetUser(TestContext tc) { 71 | cleanupUser(tc); 72 | registerUser(tc, user1); 73 | loginUser(tc, user1); 74 | 75 | Async get = tc.async(); 76 | 77 | webClient.get(PORT, "localhost", "/api/user") 78 | .putHeader(CONTENT_TYPE, JSON) 79 | .putHeader(AUTHORIZATION, getJwt(tc)) 80 | .send( 81 | ar -> { 82 | if (ar.succeeded()) { 83 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 84 | JsonObject returnedUser = ar.result().bodyAsJsonObject().getJsonObject("user"); 85 | JsonObject authJson = user1.toAuthJson(); 86 | authJson.put("token", tc.get("jwt").toString()); 87 | tc.assertEquals(authJson, returnedUser); 88 | get.complete(); 89 | } else { 90 | tc.fail(ar.cause()); 91 | } 92 | }); 93 | } 94 | 95 | @Test(timeout = TIMEOUT) 96 | public void testGetProfile(TestContext tc) { 97 | cleanupUser(tc); 98 | registerUser(tc, user1); 99 | 100 | Async getProfileNoLogin = tc.async(); 101 | 102 | webClient.get(PORT, "localhost", "/api/profiles/user1") 103 | .putHeader(CONTENT_TYPE, JSON) 104 | .send( 105 | ar -> { 106 | if (ar.succeeded()) { 107 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 108 | JsonObject json = ar.result().bodyAsJsonObject().getJsonObject("profile"); 109 | tc.assertEquals(user1.toProfileJsonFor(user1), json); 110 | getProfileNoLogin.complete(); 111 | } else { 112 | tc.fail(ar.cause()); 113 | } 114 | }); 115 | 116 | getProfileNoLogin.awaitSuccess(); 117 | 118 | loginUser(tc, user1); 119 | 120 | Async getProfileWithLogin = tc.async(); 121 | 122 | webClient.get(PORT, "localhost", "/api/profiles/user1") 123 | .putHeader(CONTENT_TYPE, JSON) 124 | .putHeader(AUTHORIZATION, getJwt(tc)) 125 | .send( 126 | ar -> { 127 | if (ar.succeeded()) { 128 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 129 | JsonObject json = ar.result().bodyAsJsonObject().getJsonObject("profile"); 130 | tc.assertEquals(user1.toProfileJsonFor(user1), json); 131 | getProfileWithLogin.complete(); 132 | } else { 133 | tc.fail(ar.cause()); 134 | } 135 | }); 136 | } 137 | 138 | @Test(timeout = TIMEOUT) 139 | public void testFollow(TestContext tc) { 140 | cleanupUser(tc); 141 | registerUser(tc, user1); 142 | registerUser(tc, user2); 143 | loginUser(tc, user1); 144 | 145 | Async follow = tc.async(); 146 | 147 | webClient.post(PORT, "localhost", "/api/profiles/user2/follow") 148 | .putHeader(CONTENT_TYPE, JSON) 149 | .putHeader(AUTHORIZATION, getJwt(tc)) 150 | .send( 151 | ar -> { 152 | if (ar.succeeded()) { 153 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 154 | JsonObject json = ar.result().bodyAsJsonObject().getJsonObject("profile"); 155 | JsonObject jsonExpected = user2.toProfileJsonFor(user1); 156 | jsonExpected.put("following", true); 157 | 158 | tc.assertEquals(jsonExpected, json); 159 | follow.complete(); 160 | } else { 161 | tc.fail(ar.cause()); 162 | } 163 | }); 164 | 165 | follow.awaitSuccess(); 166 | 167 | Async unfollow = tc.async(); 168 | 169 | webClient.delete(PORT, "localhost", "/api/profiles/user2/follow") 170 | .putHeader(CONTENT_TYPE, JSON) 171 | .putHeader(AUTHORIZATION, getJwt(tc)) 172 | .send( 173 | ar -> { 174 | if (ar.succeeded()) { 175 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 176 | JsonObject json = ar.result().bodyAsJsonObject().getJsonObject("profile"); 177 | JsonObject jsonExpected = user2.toProfileJsonFor(user1); 178 | jsonExpected.put("following", false); 179 | 180 | tc.assertEquals(jsonExpected, json); 181 | unfollow.complete(); 182 | } else { 183 | tc.fail(ar.cause()); 184 | } 185 | }); 186 | 187 | unfollow.awaitSuccess(); 188 | } 189 | 190 | @After 191 | public void tearDown(TestContext tc) { 192 | cleanupUser(tc); 193 | super.tearDown(tc); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /conduit/src/test/java/routes/ArticleTest.java: -------------------------------------------------------------------------------- 1 | package routes; 2 | 3 | import io.netty.handler.codec.http.HttpResponseStatus; 4 | import io.vertx.conduit.handlers.Constants; 5 | import io.vertx.core.json.JsonArray; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.ext.unit.Async; 8 | import io.vertx.ext.unit.TestContext; 9 | import io.vertx.ext.unit.junit.VertxUnitRunner; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | 13 | @RunWith(VertxUnitRunner.class) 14 | public class ArticleTest extends TestBase { 15 | 16 | @Test(timeout = TIMEOUT) 17 | public void testCreateArticle(TestContext tc) { 18 | cleanupUser(tc); 19 | cleanupArticles(tc); 20 | registerUser(tc, user1); 21 | loginUser(tc, user1); 22 | 23 | createArticle(tc, user1, testArticle1); 24 | 25 | Async updateArticle = tc.async(); 26 | 27 | // Must include the forward slash at the end 28 | 29 | webClient.put(PORT, "localhost", "/api/articles/first-article") 30 | .putHeader(CONTENT_TYPE, JSON) 31 | .putHeader(AUTHORIZATION, getJwt(tc)) 32 | .sendJsonObject(new JsonObject() 33 | .put(Constants.ARTICLE, new JsonObject().put("body", "updatedBody") 34 | ), ar -> { 35 | if (ar.succeeded()) { 36 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 37 | JsonObject json = ar.result().bodyAsJsonObject().getJsonObject(Constants.ARTICLE); 38 | tc.assertNotNull(json); 39 | JsonObject expected = testArticle1.toJsonFor(user1); 40 | expected.put("id", json.getString("id")); 41 | expected.put("version", json.getLong("version")); 42 | expected.put("body", "updatedBody"); 43 | expected.put("createdAt", json.getString("createdAt")); 44 | expected.put("createUser", json.getString("createUser")); 45 | expected.put("updatedAt", json.getString("updatedAt")); 46 | expected.put("updateUser", json.getString("updateUser")); 47 | tc.assertEquals(expected, json); 48 | updateArticle.complete(); 49 | } else { 50 | tc.fail(); 51 | } 52 | }); 53 | 54 | updateArticle.awaitSuccess(); 55 | 56 | Async favoriteArticle = tc.async(); 57 | 58 | webClient.post(PORT, "localhost", "/api/articles/first-article/favorite") 59 | .putHeader(CONTENT_TYPE, JSON) 60 | .putHeader(AUTHORIZATION, getJwt(tc)) 61 | .send(ar -> { 62 | if (ar.succeeded()) { 63 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 64 | JsonObject json = ar.result().bodyAsJsonObject().getJsonObject(Constants.ARTICLE); 65 | tc.assertNotNull(json); 66 | JsonObject expected = testArticle1.toJsonFor(user1); 67 | expected.put("id", json.getString("id")); 68 | expected.put("version", json.getLong("version")); 69 | expected.put("body", "updatedBody"); 70 | expected.put("favoritesCount", 1); 71 | expected.put("favorited", true); 72 | expected.put("createdAt", json.getString("createdAt")); 73 | expected.put("createUser", json.getString("createUser")); 74 | expected.put("updatedAt", json.getString("updatedAt")); 75 | expected.put("updateUser", json.getString("updateUser")); 76 | tc.assertEquals(expected, json); 77 | favoriteArticle.complete(); 78 | } else { 79 | tc.fail(); 80 | } 81 | }); 82 | 83 | favoriteArticle.awaitSuccess(); 84 | 85 | Async unfavoriteArticle = tc.async(); 86 | 87 | webClient.delete(PORT, "localhost", "/api/articles/first-article/favorite") 88 | .putHeader(CONTENT_TYPE, JSON) 89 | .putHeader(AUTHORIZATION, getJwt(tc)) 90 | .send(ar -> { 91 | if (ar.succeeded()) { 92 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 93 | JsonObject json = ar.result().bodyAsJsonObject().getJsonObject(Constants.ARTICLE); 94 | tc.assertNotNull(json); 95 | JsonObject expected = testArticle1.toJsonFor(user1); 96 | expected.put("id", json.getString("id")); 97 | expected.put("version", json.getLong("version")); 98 | expected.put("body", "updatedBody"); 99 | expected.put("favoritesCount", 0); 100 | expected.put("favorited", false); 101 | expected.put("createdAt", json.getString("createdAt")); 102 | expected.put("createUser", json.getString("createUser")); 103 | expected.put("updatedAt", json.getString("updatedAt")); 104 | expected.put("updateUser", json.getString("updateUser")); 105 | tc.assertEquals(expected, json); 106 | unfavoriteArticle.complete(); 107 | } else { 108 | tc.fail(); 109 | } 110 | }); 111 | 112 | unfavoriteArticle.awaitSuccess(); 113 | 114 | Async createComment = tc.async(); 115 | 116 | webClient.post(PORT, "localhost", "/api/articles/first-article/comments") 117 | .putHeader(CONTENT_TYPE, JSON) 118 | .putHeader(AUTHORIZATION, getJwt(tc)) 119 | .sendJsonObject(new JsonObject().put("comment", new JsonObject().put("body", "a comment")), 120 | ar -> { 121 | if (ar.succeeded()) { 122 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 123 | JsonObject json = ar.result().bodyAsJsonObject().getJsonObject(Constants.COMMENT); 124 | tc.assertNotNull(json); 125 | tc.put("commentId", json.getString("id")); 126 | tc.assertEquals(testArticle1.getId().toHexString(), json.getString("article")); 127 | tc.assertEquals(testArticle1.getAuthor().getUsername(), json.getJsonObject("author").getString("username")); 128 | tc.assertEquals("a comment", json.getString("body")); 129 | createComment.complete(); 130 | } else { 131 | tc.fail(); 132 | } 133 | }); 134 | 135 | createComment.awaitSuccess(); 136 | 137 | Async getComment = tc.async(); 138 | 139 | webClient.get(PORT, "localhost", "/api/articles/first-article/comments") 140 | .putHeader(CONTENT_TYPE, JSON) 141 | .putHeader(AUTHORIZATION, getJwt(tc)) 142 | .sendJsonObject(new JsonObject().put("comment", new JsonObject().put("body", "a comment")), 143 | ar -> { 144 | if (ar.succeeded()) { 145 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 146 | JsonArray json = ar.result().bodyAsJsonObject().getJsonArray("comments"); 147 | tc.assertNotNull(json); 148 | tc.assertEquals(1, json.size()); 149 | tc.assertEquals("a comment", json.getJsonObject(0).getString("body")); 150 | getComment.complete(); 151 | } else { 152 | tc.fail(); 153 | } 154 | }); 155 | 156 | getComment.awaitSuccess(); 157 | 158 | Async deleteComment = tc.async(); 159 | 160 | webClient.delete(PORT, "localhost", "/api/articles/first-article/comments/" + tc.get("commentId")) 161 | .putHeader(CONTENT_TYPE, JSON) 162 | .putHeader(AUTHORIZATION, getJwt(tc)) 163 | .send( 164 | ar -> { 165 | if (ar.succeeded()) { 166 | tc.assertEquals(HttpResponseStatus.OK.code(), ar.result().statusCode()); 167 | deleteComment.complete(); 168 | } else { 169 | tc.fail(); 170 | } 171 | }); 172 | 173 | deleteComment.awaitSuccess(); 174 | 175 | cleanupArticles(tc); 176 | cleanupUser(tc); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /conduit/src/test/java/services/MorphiaServiceTest.java: -------------------------------------------------------------------------------- 1 | package services; 2 | 3 | import io.vertx.conduit.entities.Article; 4 | import io.vertx.conduit.entities.User; 5 | import io.vertx.conduit.services.MorphiaServiceImpl; 6 | import io.vertx.conduit.services.reactivex.MorphiaService; 7 | import io.vertx.core.Vertx; 8 | import io.vertx.core.json.JsonObject; 9 | import io.vertx.core.logging.Logger; 10 | import io.vertx.ext.unit.Async; 11 | import io.vertx.ext.unit.TestContext; 12 | import io.vertx.ext.unit.junit.VertxUnitRunner; 13 | import logging.ContextLogger; 14 | import org.bson.types.ObjectId; 15 | import org.junit.After; 16 | import org.junit.Before; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | 20 | import static org.junit.Assert.*; 21 | 22 | @RunWith(VertxUnitRunner.class) 23 | public class MorphiaServiceTest { 24 | 25 | private Vertx vertx; 26 | private MorphiaService morphiaService; 27 | 28 | @Before 29 | public void setup(TestContext tc) { 30 | this.vertx = Vertx.vertx(); 31 | JsonObject config = new JsonObject().put("db_name", "conduit_test") 32 | .put("host", "localhost").put("port", 27017); 33 | MorphiaServiceImpl delegate = new MorphiaServiceImpl(vertx, config, tc.asyncAssertSuccess()); 34 | this.morphiaService = new io.vertx.conduit.services.reactivex.MorphiaService(delegate); 35 | } 36 | 37 | @Test 38 | public void UserCRUD(TestContext tc) throws InterruptedException { 39 | Async async = tc.async(); 40 | 41 | User user = new User(); 42 | user.setPassword("123"); 43 | user.setBio("abc"); 44 | user.setEmail("1@2.com"); 45 | user.setUsername("xyz"); 46 | 47 | this.morphiaService.rxCreateUser(user) 48 | .flatMap(id -> { 49 | user.setId(new ObjectId(id)); 50 | return this.morphiaService.rxGetUser(new JsonObject().put("_id", user.getId().toHexString())); 51 | }).flatMap(users -> { 52 | assertEquals(1, users.size()); 53 | assertEquals(user.getUsername(), users.get(0).getUsername()); 54 | user.setBio("bio_updated"); 55 | return this.morphiaService.rxUpdateUser(new JsonObject().put("_id", user.getId().toHexString()), new JsonObject().put("bio", user.getBio())); 56 | }).flatMap(users -> { 57 | assertEquals(1, users.size()); 58 | assertEquals(user.getBio(), users.get(0).getBio()); 59 | return this.morphiaService.rxDeleteUser(new JsonObject().put("_id", user.getId().toHexString())); 60 | }).subscribe((numDeleted, e) -> { 61 | if (e == null) { 62 | assertEquals(1, (long) numDeleted); 63 | async.complete(); 64 | } else { 65 | tc.fail(e); 66 | } 67 | }); 68 | } 69 | 70 | @Test 71 | public void userInvalid(TestContext tc) { 72 | Async async1 = tc.async(); 73 | Async async2 = tc.async(); 74 | Async async3 = tc.async(); 75 | Async async4 = tc.async(); 76 | 77 | User user = new User(); 78 | user.setPassword("123"); 79 | user.setBio("abc"); 80 | 81 | this.morphiaService.rxCreateUser(user) 82 | .subscribe((id, e) -> { 83 | if (e == null) { 84 | tc.fail("User should not be created"); 85 | } else { 86 | async1.complete(); 87 | } 88 | }); 89 | 90 | user.setUsername("xyz"); 91 | this.morphiaService.rxCreateUser(user) 92 | .subscribe((id, e) -> { 93 | if (e == null) { 94 | tc.fail("User should not be created"); 95 | } else { 96 | async2.complete(); 97 | } 98 | }); 99 | 100 | user.setEmail("123"); 101 | this.morphiaService.rxCreateUser(user) 102 | .subscribe((id, e) -> { 103 | if (e == null) { 104 | tc.fail("User should not be created"); 105 | } else { 106 | async3.complete(); 107 | } 108 | }); 109 | 110 | user.setEmail("123@xyz.com"); 111 | user.setUsername("#*$"); 112 | this.morphiaService.rxCreateUser(user) 113 | .subscribe((id, e) -> { 114 | if (e == null) { 115 | tc.fail("User should not be created"); 116 | } else { 117 | async4.complete(); 118 | } 119 | }); 120 | } 121 | 122 | @Test 123 | public void userDupe(TestContext tc) throws InterruptedException { 124 | Async async1 = tc.async(); 125 | Async async2 = tc.async(); 126 | Async async3 = tc.async(); 127 | Async async4 = tc.async(); 128 | 129 | User user = new User(); 130 | user.setPassword("123"); 131 | user.setBio("abc"); 132 | user.setEmail("1@2.com"); 133 | user.setUsername("xyz"); 134 | 135 | User user2 = new User(); 136 | user2.setPassword("123"); 137 | user2.setBio("abc"); 138 | user2.setEmail("3@4.com"); 139 | user2.setUsername("xyz"); 140 | 141 | User user3 = new User(); 142 | user3.setPassword("123"); 143 | user3.setBio("abc"); 144 | user3.setEmail("1@2.com"); 145 | user3.setUsername("xyz2"); 146 | 147 | this.morphiaService.rxCreateUser(user) 148 | .subscribe((id, e) -> { 149 | if (e == null) { 150 | user.setId(new ObjectId(id)); 151 | async1.complete(); 152 | } else { 153 | tc.fail(e); 154 | } 155 | }); 156 | 157 | async1.await(); 158 | this.morphiaService.rxCreateUser(user2) 159 | .subscribe((id, e) -> { 160 | if (e == null) { 161 | tc.fail("User2 shouldn't be written"); 162 | } 163 | async2.complete(); 164 | }); 165 | 166 | async2.await(); 167 | 168 | this.morphiaService.rxCreateUser(user3) 169 | .subscribe((id, e) -> { 170 | if (e == null) { 171 | tc.fail("User3 shouldn't be written"); 172 | } 173 | async3.complete(); 174 | }); 175 | 176 | async3.await(); 177 | 178 | this.morphiaService.rxDeleteUser(new JsonObject().put("_id", user.getId().toHexString())) 179 | .subscribe((numDeleted, e) -> { 180 | if (e == null) { 181 | assertEquals(1, (long) numDeleted); 182 | async4.complete(); 183 | } else { 184 | tc.fail(e); 185 | } 186 | }); 187 | } 188 | 189 | @Test 190 | public void ArticleCRUD(TestContext tc) { 191 | Async async = tc.async(); 192 | 193 | User user = new User(); 194 | user.setPassword("123"); 195 | user.setBio("abc"); 196 | user.setEmail("1@2.com"); 197 | user.setUsername("xyz"); 198 | 199 | Article article = new Article(); 200 | article.setAuthor(user); 201 | article.setTitle("Test article"); 202 | article.setSlug("Test-article"); 203 | article.setBody("No body no body"); 204 | 205 | // have to store the user first and assign it an id for the @Reference annotation to work 206 | 207 | this.morphiaService.rxCreateUser(user) 208 | .flatMap(id -> { 209 | user.setId(new ObjectId(id)); 210 | return this.morphiaService.rxCreateArticle(article); 211 | }).flatMap(id -> { 212 | article.setId(new ObjectId(id)); 213 | return this.morphiaService.rxGetArticle(new JsonObject().put("_id", article.getId().toHexString())); 214 | }).flatMap(articles -> { 215 | assertEquals(1, articles.size()); 216 | assertEquals(article.getSlug(), articles.get(0).getSlug()); 217 | assertNotNull(article.getAuthor()); 218 | assertEquals(user.getUsername(), article.getAuthor().getUsername()); 219 | article.setBody("but you~"); 220 | return this.morphiaService.rxUpdateArticle(new JsonObject().put("_id", article.getId().toHexString()), new JsonObject().put("body", article.getBody())); 221 | }).flatMap(articles -> { 222 | assertEquals(1, articles.size()); 223 | assertEquals(article.getBody(), articles.get(0).getBody()); 224 | return this.morphiaService.rxDeleteArticle(new JsonObject().put("_id", article.getId().toHexString())); 225 | }).flatMap(numDeleted -> { 226 | assertEquals(1, (long) numDeleted); 227 | return this.morphiaService.rxDeleteUser(new JsonObject().put("_id", user.getId().toHexString())); 228 | }).subscribe((numDeleted, e) -> { 229 | if (e == null) { 230 | assertEquals(1, (long) numDeleted); 231 | async.complete(); 232 | } else { 233 | tc.fail(e); 234 | } 235 | }); 236 | } 237 | 238 | 239 | @After 240 | public void tearDown(TestContext tc) { 241 | vertx.setTimer(1000, t -> { System.out.println("timer complete"); }); 242 | vertx.close(tc.asyncAssertSuccess()); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /conduit/src/test/java/routes/TestBase.java: -------------------------------------------------------------------------------- 1 | package routes; 2 | 3 | 4 | import io.netty.handler.codec.http.HttpResponseStatus; 5 | import io.vertx.conduit.entities.Article; 6 | import io.vertx.conduit.entities.User; 7 | import io.vertx.conduit.handlers.Constants; 8 | import io.vertx.conduit.services.ArticleService; 9 | import io.vertx.conduit.services.CommentService; 10 | import io.vertx.conduit.services.UserService; 11 | import io.vertx.conduit.verticles.*; 12 | import io.vertx.core.DeploymentOptions; 13 | import io.vertx.core.Vertx; 14 | import io.vertx.core.json.JsonArray; 15 | import io.vertx.core.json.JsonObject; 16 | import io.vertx.ext.unit.Async; 17 | import io.vertx.ext.unit.TestContext; 18 | import io.vertx.ext.unit.junit.VertxUnitRunner; 19 | import io.vertx.ext.web.client.WebClient; 20 | import io.vertx.serviceproxy.ServiceProxyBuilder; 21 | import org.bson.types.ObjectId; 22 | import org.junit.After; 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | 27 | @RunWith(VertxUnitRunner.class) 28 | public class TestBase { 29 | 30 | static { 31 | System.getProperties().setProperty("vertx.logger-delegate-factory-class-name", "io.vertx.core.logging.Log4j2LogDelegateFactory"); 32 | } 33 | 34 | protected final static int TIMEOUT = 5000; 35 | protected static final String CONTENT_TYPE = "Content-Type"; 36 | protected static final String JSON = "application/json"; 37 | protected static final String AUTHORIZATION = "Authorization"; 38 | protected static int PORT = 3000; 39 | 40 | protected Vertx vertx; 41 | 42 | protected WebClient webClient; 43 | 44 | protected User user1; 45 | 46 | protected User user2; 47 | 48 | protected Article testArticle1; 49 | 50 | protected Article testArticle2; 51 | 52 | protected io.vertx.conduit.services.reactivex.UserService userService; 53 | 54 | protected io.vertx.conduit.services.reactivex.ArticleService articleService; 55 | 56 | protected io.vertx.conduit.services.reactivex.CommentService commentService; 57 | 58 | @Before 59 | public void setup(TestContext tc) { 60 | 61 | vertx = Vertx.vertx(); 62 | 63 | webClient = WebClient.create(vertx); 64 | 65 | DeploymentOptions options = new DeploymentOptions() 66 | .setConfig(new JsonObject() 67 | .put("secret", "testsecret") 68 | .put("mongodb", new JsonObject().put("db_name", "conduit_test") 69 | .put("host", "localhost") 70 | .put("port", 27017)) 71 | ); 72 | 73 | JsonObject user1Json = new JsonObject().put("username", "user1").put("password", "123").put("email", "user1@t.com").put("bio", "I am not a robot"); 74 | JsonObject user2Json = new JsonObject().put("username", "user2").put("password", "456").put("email", "user2@t.com").put("bio", "I work 24/7"); 75 | 76 | user1 = new User(user1Json); 77 | user2 = new User(user2Json); 78 | 79 | JsonObject article1Json = new JsonObject().put("title", "first article").put("slug", "first-article").put("description", "We have to test it out.").put("body", "Not much to say").put("comments", new JsonArray()).put("tagList", new JsonArray().add("food").add("travel")); 80 | JsonObject article2Json = new JsonObject().put("title", "second article").put("slug", "second-article").put("description", "Last article is really funny").put("body", "It's not over").put("comments", new JsonArray()).put("tagList", new JsonArray().add("music").add("dance")); 81 | 82 | testArticle1 = new Article(article1Json); 83 | testArticle2 = new Article(article2Json); 84 | 85 | testArticle1.setAuthor(user1); 86 | testArticle2.setAuthor(user1); 87 | 88 | vertx.deployVerticle(ArticleServiceVerticle.class.getName(), tc.asyncAssertSuccess()); 89 | vertx.deployVerticle(HttpVerticle.class.getName(), options, tc.asyncAssertSuccess()); 90 | vertx.deployVerticle(MorphiaServiceVerticle.class.getName(), options, tc.asyncAssertSuccess()); 91 | vertx.deployVerticle(UserServiceVerticle.class.getName(), options, tc.asyncAssertSuccess()); 92 | vertx.deployVerticle(CommentServiceVerticle.class.getName(), options, tc.asyncAssertSuccess()); 93 | 94 | { 95 | ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx).setAddress(UserService.ADDRESS); 96 | UserService delegate = builder.build(UserService.class); 97 | this.userService = new io.vertx.conduit.services.reactivex.UserService(delegate); 98 | } 99 | 100 | { 101 | ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx).setAddress(ArticleService.ADDRESS); 102 | ArticleService delegate = builder.build(ArticleService.class); 103 | this.articleService = new io.vertx.conduit.services.reactivex.ArticleService(delegate); 104 | } 105 | 106 | { 107 | ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx).setAddress(CommentService.ADDRESS); 108 | CommentService delegate = builder.build(CommentService.class); 109 | this.commentService = new io.vertx.conduit.services.reactivex.CommentService(delegate); 110 | } 111 | } 112 | 113 | protected void cleanupUser(TestContext tc) { 114 | Async cleanup = tc.async(); 115 | userService.rxDeleteByUsername(user1.getUsername()) 116 | .flatMap(ignored -> userService.rxDeleteByUsername(user2.getUsername())) 117 | .subscribe((deleteCount, ex) -> { 118 | cleanup.complete(); 119 | }); 120 | 121 | cleanup.awaitSuccess(); 122 | } 123 | 124 | protected void cleanupArticles(TestContext tc) { 125 | Async cleanup = tc.async(); 126 | articleService.rxDelete(testArticle1.getSlug()) 127 | .flatMap(ignored -> articleService.rxDelete(testArticle2.getSlug())) 128 | .subscribe((deleteCount, ex) -> { 129 | cleanup.complete(); 130 | }); 131 | 132 | cleanup.awaitSuccess(); 133 | } 134 | 135 | protected void registerUser(TestContext tc, User user) { 136 | Async create = tc.async(); 137 | 138 | webClient.post(PORT, "localhost", "/api/users") 139 | .putHeader(CONTENT_TYPE, JSON) 140 | .sendJsonObject(new JsonObject() 141 | .put(Constants.USER, user.toJson() 142 | ), ar -> { 143 | if (ar.succeeded()) { 144 | tc.assertEquals(HttpResponseStatus.CREATED.code(), ar.result().statusCode()); 145 | JsonObject returnedUser = ar.result().bodyAsJsonObject().getJsonObject("user"); 146 | tc.assertEquals(user.getUsername(), returnedUser.getString("username")); 147 | tc.assertEquals(user.getEmail(), returnedUser.getString("email")); 148 | tc.assertEquals(user.getBio(), returnedUser.getString("bio")); 149 | tc.assertNull(returnedUser.getString("image")); 150 | create.complete(); 151 | } else { 152 | tc.fail(ar.cause()); 153 | } 154 | }); 155 | 156 | create.awaitSuccess(); 157 | } 158 | 159 | protected void loginUser(TestContext tc, User user) { 160 | Async login = tc.async(); 161 | 162 | webClient.post(PORT, "localhost", "/api/users/login") 163 | .putHeader(CONTENT_TYPE, JSON) 164 | .sendJsonObject(new JsonObject().put(Constants.USER, new JsonObject() 165 | .put("email", user.getEmail()) 166 | .put("password", user.getPassword())), 167 | ar -> { 168 | if (ar.succeeded()) { 169 | tc.assertEquals(HttpResponseStatus.CREATED.code(), ar.result().statusCode()); 170 | JsonObject returnedUser = ar.result().bodyAsJsonObject().getJsonObject("user"); 171 | tc.assertNotNull(returnedUser.getString(Constants.AUTH_KEY)); 172 | tc.put("jwt", returnedUser.getString(Constants.AUTH_KEY)); 173 | login.complete(); 174 | } else { 175 | tc.fail(ar.cause()); 176 | } 177 | }); 178 | 179 | login.awaitSuccess(); 180 | } 181 | 182 | protected void createArticle(TestContext tc, User user, Article article) { 183 | Async createArticle = tc.async(); 184 | 185 | // Must include the forward slash at the end 186 | 187 | webClient.post(PORT, "localhost", "/api/articles/") 188 | .putHeader(CONTENT_TYPE, JSON) 189 | .putHeader(AUTHORIZATION, getJwt(tc)) 190 | .sendJsonObject(new JsonObject() 191 | .put(Constants.ARTICLE, article.toJson() 192 | ), ar -> { 193 | if (ar.succeeded()) { 194 | tc.assertEquals(HttpResponseStatus.CREATED.code(), ar.result().statusCode()); 195 | JsonObject json = ar.result().bodyAsJsonObject().getJsonObject(Constants.ARTICLE); 196 | tc.assertNotNull(json); 197 | JsonObject expected = article.toJsonFor(user); 198 | expected.put("id", json.getString("id")); 199 | expected.put("createdAt", json.getString("createdAt")); 200 | expected.put("createUser", json.getString("createUser")); 201 | expected.put("updatedAt", json.getString("updatedAt")); 202 | tc.assertEquals(expected, json); 203 | article.setId(new ObjectId(json.getString("id"))); 204 | createArticle.complete(); 205 | } else { 206 | tc.fail(); 207 | } 208 | }); 209 | 210 | createArticle.awaitSuccess(); 211 | } 212 | 213 | 214 | protected static String getJwt(TestContext tc) { 215 | return Constants.AUTH_HEADER + " " + tc.get("jwt").toString(); 216 | } 217 | 218 | @Test 219 | public void testSetup(TestContext testContext){ 220 | testContext.assertTrue(true); 221 | } 222 | 223 | @After 224 | public void tearDown(TestContext tc) { 225 | vertx.setTimer(1000, t -> { System.out.println("timer complete"); }); 226 | vertx.close(tc.asyncAssertSuccess()); 227 | } 228 | 229 | 230 | } 231 | 232 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/handlers/ArticleHandler.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.handlers; 2 | 3 | import com.github.slugify.Slugify; 4 | import io.netty.handler.codec.http.HttpResponseStatus; 5 | import io.vertx.conduit.entities.Article; 6 | import io.vertx.conduit.entities.Comment; 7 | import io.vertx.conduit.entities.User; 8 | import io.vertx.conduit.services.MorphiaServiceOperator; 9 | import io.vertx.core.Vertx; 10 | import io.vertx.core.http.HttpMethod; 11 | import io.vertx.core.json.Json; 12 | import io.vertx.core.json.JsonArray; 13 | import io.vertx.core.json.JsonObject; 14 | import io.vertx.ext.web.RoutingContext; 15 | import routerutils.RouteConfig; 16 | 17 | import java.util.Objects; 18 | 19 | import static io.vertx.conduit.handlers.Constants.QUERY; 20 | 21 | @RouteConfig(path="/api/articles", produces = "application/json") 22 | public class ArticleHandler extends ConduitHandler { 23 | 24 | private final Slugify slugify; 25 | 26 | public ArticleHandler(Vertx vertx) { 27 | super(vertx); 28 | this.slugify = new Slugify(); 29 | } 30 | 31 | @RouteConfig(path="", method= HttpMethod.POST, middlewares = "extractUser") 32 | public void create(RoutingContext event) { 33 | JsonObject message = event.getBodyAsJson().getJsonObject(Constants.ARTICLE); 34 | setCreateFields(event, message); 35 | User user = event.get(Constants.USER); 36 | message.put("slug", slugify.slugify(message.getString("title"))); 37 | message.put("author", user.toJson()); 38 | 39 | articleService.rxCreate(message) 40 | .map(article -> new JsonObject().put(Constants.ARTICLE, article.toJsonFor(user))) 41 | .subscribe(res -> handleResponse(event, res, HttpResponseStatus.CREATED), e -> handleError(event, e)); 42 | } 43 | 44 | @RouteConfig(path="", method = HttpMethod.GET, authRequired = false, middlewares = "extractUser") 45 | public void queryArticles(RoutingContext event) { 46 | JsonObject query; 47 | JsonObject bodyAsJson = event.getBodyAsJson(); 48 | if (bodyAsJson != null && bodyAsJson.getJsonObject(QUERY) != null) { 49 | query = bodyAsJson.getJsonObject(QUERY); 50 | } else { 51 | query = new JsonObject(); 52 | } 53 | User queryingUser = event.get(Constants.USER); 54 | 55 | morphiaService.rxQueryArticles(queryingUser, query) 56 | .doOnError(e -> event.fail(e)) 57 | .subscribe(json -> { 58 | event.response() 59 | .setStatusCode(HttpResponseStatus.OK.code()) 60 | .end(Json.encodePrettily(json)); 61 | }); 62 | } 63 | 64 | //this conflicts with /articles/:article 65 | //Put here before specifically to override so /articles/feed/ goes to this method instead of the get method 66 | @RouteConfig(path="/feed", method = HttpMethod.GET, middlewares = "extractUser") 67 | public void queryArticlesFeed(RoutingContext event) { 68 | JsonObject query; 69 | JsonObject bodyAsJson = event.getBodyAsJson(); 70 | if (bodyAsJson != null && bodyAsJson.getJsonObject(QUERY) != null) { 71 | query = bodyAsJson.getJsonObject(QUERY); 72 | } else { 73 | query = new JsonObject(); 74 | } 75 | User queryingUser = event.get(Constants.USER); 76 | 77 | morphiaService.rxQueryArticlesFeed(queryingUser, query) 78 | .doOnError(e -> event.fail(e)) 79 | .subscribe(json -> { 80 | event.response() 81 | .setStatusCode(HttpResponseStatus.OK.code()) 82 | .end(Json.encodePrettily(json)); 83 | }); 84 | } 85 | 86 | @RouteConfig(path="/:article", method=HttpMethod.GET, middlewares = {"extractArticle", "extractUser"}) 87 | public void get(RoutingContext event){ 88 | 89 | Article article = event.get(Constants.ARTICLE); 90 | event.response() 91 | .setStatusCode(HttpResponseStatus.OK.code()) 92 | .end(Json.encodePrettily(new JsonObject().put(Constants.ARTICLE, article.toJsonFor(event.get(Constants.USER))))); 93 | } 94 | 95 | @RouteConfig(path="/:article", method=HttpMethod.PUT, middlewares = {"extractArticle", "extractUser"}) 96 | public void update(RoutingContext event){ 97 | Article article = event.get(Constants.ARTICLE); 98 | User user = event.get(Constants.USER); 99 | 100 | if (!Objects.equals(user.getId(), article.getAuthor().getId())) { 101 | event.fail(new RuntimeException("Invalid User")); 102 | } 103 | 104 | JsonObject message = event.getBodyAsJson().getJsonObject(Constants.ARTICLE); 105 | setUpdateFields(event, message); 106 | 107 | articleService.rxUpdate(article.getSlug(), message) 108 | .map(updatedArticle -> new JsonObject().put(Constants.ARTICLE, updatedArticle.toJsonFor(user))) 109 | .subscribe(res -> handleResponse(event, res, HttpResponseStatus.OK), e -> handleError(event, e)); 110 | } 111 | 112 | @RouteConfig(path="/:article", method=HttpMethod.DELETE, middlewares = {"extractArticle", "extractUser"}) 113 | public void delete(RoutingContext event){ 114 | Article article = event.get(Constants.ARTICLE); 115 | if (article.getAuthor().getId() != null && Objects.equals(event.get(Constants.USER_ID), article.getAuthor().getId().toHexString())) { 116 | articleService.rxDelete(article.getSlug()).subscribe((ignored, ex) -> { 117 | if (ex == null) { 118 | event.response() 119 | .setStatusCode(HttpResponseStatus.OK.code()) 120 | .end(); 121 | } else { 122 | event.fail(ex); 123 | } 124 | }); 125 | } else { 126 | event.fail(new RuntimeException("Invalid user")); 127 | } 128 | } 129 | 130 | @RouteConfig(path="/:article/favorite", method=HttpMethod.POST, middlewares = {"extractArticle", "extractUser"}) 131 | public void favorite(RoutingContext event) { 132 | Article article = event.get(Constants.ARTICLE); 133 | User user = event.get(Constants.USER); 134 | if (!user.isFavorite(article.getSlug())) { 135 | user.addFavorite(article.getSlug()); 136 | } else { 137 | event.response() 138 | .setStatusCode(HttpResponseStatus.OK.code()) 139 | .end(Json.encodePrettily(new JsonObject().put(Constants.ARTICLE, article.toJsonFor(user)))); 140 | return; 141 | } 142 | JsonObject update = new JsonObject().put("favorites", new JsonObject().put(MorphiaServiceOperator.PUSH, article.getSlug())); 143 | 144 | userService.rxUpdate(user.getId().toHexString(), update) 145 | .flatMap(ignored -> userService.rxGetFavoriteCount(article.getSlug())) 146 | .flatMap(count -> { 147 | article.setFavoritesCount(count); 148 | return articleService.rxUpdate(article.getSlug(), new JsonObject().put("favoritesCount", count)); 149 | }) 150 | .map(updatedArticle -> new JsonObject().put(Constants.ARTICLE, updatedArticle.toJsonFor(user))) 151 | .subscribe(res -> handleResponse(event, res, HttpResponseStatus.OK), e -> handleError(event, e)); 152 | } 153 | 154 | @RouteConfig(path="/:article/favorite", method=HttpMethod.DELETE, middlewares = {"extractArticle", "extractUser"}) 155 | public void unfavorite(RoutingContext event) { 156 | Article article = event.get(Constants.ARTICLE); 157 | User user = event.get(Constants.USER); 158 | if (user.isFavorite(article.getSlug())) { 159 | user.removeFavorite(article.getSlug()); 160 | } else { 161 | event.response() 162 | .setStatusCode(HttpResponseStatus.OK.code()) 163 | .end(Json.encodePrettily(article.toJsonFor(user))); 164 | return; 165 | } 166 | JsonObject update = new JsonObject().put("favorites", new JsonObject().put(MorphiaServiceOperator.POP, article.getSlug())); 167 | 168 | userService.rxUpdate(user.getId().toHexString(), update) 169 | .flatMap(ignored -> { 170 | return userService.rxGetFavoriteCount(article.getSlug()); 171 | }) 172 | .flatMap(count -> { 173 | article.setFavoritesCount(count); 174 | return articleService.rxUpdate(article.getSlug(), new JsonObject().put("favoritesCount", count)); 175 | }) 176 | .map(updatedArticle -> new JsonObject().put(Constants.ARTICLE, updatedArticle.toJsonFor(user))) 177 | .subscribe(res -> handleResponse(event, res, HttpResponseStatus.OK), e -> handleError(event, e)); 178 | } 179 | 180 | @RouteConfig(path="/:article/comments", method= HttpMethod.POST, middlewares = {"extractUser", "extractArticle"}) 181 | public void createComment(RoutingContext event) { 182 | User user = event.get(Constants.USER); 183 | Article article = event.get(Constants.ARTICLE); 184 | 185 | JsonObject message = event.getBodyAsJson().getJsonObject(Constants.COMMENT); 186 | message.put("author", user.toJson()); 187 | message.put(Constants.ARTICLE, article.getId().toHexString()); 188 | setCreateFields(event, message); 189 | 190 | commentService.rxCreate(message) 191 | .map(comment -> { 192 | JsonObject update = new JsonObject().put("comments", new JsonObject().put(MorphiaServiceOperator.PUSH, new JsonObject().put("_id", comment.getId().toHexString()))); 193 | // FIXME how can we avoid this? 194 | articleService.rxUpdate(article.getSlug(), update).subscribe(); 195 | return comment; 196 | }) 197 | .map(comment -> new JsonObject().put(Constants.COMMENT, comment.toJsonFor(user))) 198 | .subscribe(res -> handleResponse(event, res, HttpResponseStatus.OK), e -> handleError(event, e)); 199 | } 200 | 201 | @RouteConfig(path="/:article/comments", method= HttpMethod.GET, middlewares = {"extractUser", "extractArticle"}) 202 | public void getComments(RoutingContext event) { 203 | User user = event.get(Constants.USER); 204 | Article article = event.get(Constants.ARTICLE); 205 | 206 | JsonObject comments = new JsonObject(); 207 | JsonArray array = new JsonArray(); 208 | if (article.getComments() != null) { 209 | article.getComments().forEach(x -> array.add(x.toJsonFor(user))); 210 | } 211 | comments.put("comments", array); 212 | 213 | event.response() 214 | .setStatusCode(HttpResponseStatus.OK.code()) 215 | .end(Json.encodePrettily(comments)); 216 | } 217 | 218 | @RouteConfig(path="/:article/comments/:comment", method= HttpMethod.DELETE, middlewares = {"extractUser", "extractComment", "extractArticle"}) 219 | public void deleteComment(RoutingContext event) { 220 | User user = event.get(Constants.USER); 221 | Article article = event.get(Constants.ARTICLE); 222 | Comment comment = event.get(Constants.COMMENT); 223 | 224 | if (comment.getAuthor().getId().equals(user.getId())) { 225 | JsonObject update = new JsonObject().put("comments", new JsonObject().put(MorphiaServiceOperator.POP, new JsonObject().put("_id", comment.getId().toHexString()))); 226 | articleService.rxUpdate(article.getSlug(), update) 227 | .flatMap(ignored -> commentService.rxDelete(comment.getId().toHexString())) 228 | .subscribe((ignored, ex) -> { 229 | if (ex == null) { 230 | event.response() 231 | .setStatusCode(HttpResponseStatus.OK.code()) 232 | .end(); 233 | } else { 234 | event.fail(ex); 235 | } 236 | }); 237 | 238 | } else { 239 | event.fail(new RuntimeException("Invalid user")); 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /conduit/src/main/java/io/vertx/conduit/services/MorphiaServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.vertx.conduit.services; 2 | 3 | import com.mongodb.MongoClient; 4 | import dev.morphia.Datastore; 5 | import dev.morphia.Key; 6 | import dev.morphia.Morphia; 7 | import dev.morphia.ValidationExtension; 8 | import dev.morphia.query.*; 9 | import io.vertx.conduit.entities.Article; 10 | import io.vertx.conduit.entities.Base; 11 | import io.vertx.conduit.entities.Comment; 12 | import io.vertx.conduit.entities.User; 13 | import io.vertx.core.AsyncResult; 14 | import io.vertx.core.Future; 15 | import io.vertx.core.Handler; 16 | import io.vertx.core.Vertx; 17 | import io.vertx.core.json.JsonArray; 18 | import io.vertx.core.json.JsonObject; 19 | import org.bson.Document; 20 | import org.bson.types.ObjectId; 21 | 22 | import java.util.Iterator; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.stream.Collectors; 26 | 27 | public class MorphiaServiceImpl implements MorphiaService { 28 | 29 | private final Morphia morphia; 30 | private final Vertx vertx; 31 | private final Datastore datastore; 32 | private final MongoClient mongoClient; 33 | 34 | public MorphiaServiceImpl(final Vertx vertx, final JsonObject dbConfig, final Handler> readyHandler) { 35 | this.vertx = vertx; 36 | this.mongoClient = new MongoClient(dbConfig.getString("host"), dbConfig.getInteger("port")); 37 | this.morphia = new Morphia(); 38 | this.morphia.map(User.class); 39 | this.morphia.map(Article.class); 40 | this.morphia.map(Comment.class); 41 | this.datastore = morphia.createDatastore(mongoClient, dbConfig.getString("db_name")); 42 | 43 | new ValidationExtension(morphia); 44 | 45 | vertx.executeBlocking(future -> { 46 | try { 47 | mongoClient.getDatabase(dbConfig.getString("db_name")).runCommand(new Document("ping", "1")); 48 | datastore.ensureIndexes(); 49 | future.complete(); 50 | } catch (Exception e) { 51 | future.fail(e); 52 | } 53 | }, res -> { 54 | if (res.succeeded()) { 55 | readyHandler.handle(Future.succeededFuture(this)); 56 | } else { 57 | readyHandler.handle(Future.failedFuture(res.cause())); 58 | } 59 | }); 60 | } 61 | 62 | @Override 63 | public void getUser(final JsonObject query, Handler>> resultHandler) { 64 | find(User.class, query, resultHandler); 65 | } 66 | 67 | @Override 68 | public void getArticle(final JsonObject query, Handler>> resultHandler) { 69 | find(Article.class, query, resultHandler); 70 | } 71 | 72 | @Override 73 | public void getComment(JsonObject query, Handler>> resultHandler) { 74 | find(Comment.class, query, resultHandler); 75 | } 76 | 77 | @Override 78 | public void createUser(User entity, final Handler> resultHandler) { 79 | save(entity, resultHandler); 80 | } 81 | 82 | @Override 83 | public void createArticle(Article entity, Handler> resultHandler) { 84 | save(entity, resultHandler); 85 | } 86 | 87 | @Override 88 | public void createComment(Comment entity, Handler> resultHandler) { 89 | save(entity, resultHandler); 90 | } 91 | 92 | @Override 93 | public void updateUser(JsonObject query, JsonObject update, Handler>> resultHandler) { 94 | update(User.class, query, update, resultHandler); 95 | } 96 | 97 | @Override 98 | public void updateArticle(JsonObject query, JsonObject update, Handler>> resultHandler) { 99 | update(Article.class, query, update, resultHandler); 100 | } 101 | 102 | @Override 103 | public void updateComment(JsonObject query, JsonObject update, Handler>> resultHandler) { 104 | update(Comment.class, query, update, resultHandler); 105 | } 106 | 107 | @Override 108 | public void deleteUser(JsonObject query, Handler> resultHandler) { 109 | delete(User.class, query, resultHandler); 110 | } 111 | 112 | @Override 113 | public void deleteArticle(JsonObject query, Handler> resultHandler) { 114 | delete(Article.class, query, resultHandler); 115 | } 116 | 117 | @Override 118 | public void deleteComment(JsonObject query, Handler> resultHandler) { 119 | delete(Comment.class, query, resultHandler); 120 | } 121 | 122 | @Override 123 | public void queryTags(Handler>> resultHandler) { 124 | vertx.executeBlocking(future -> { 125 | future.complete(datastore.getCollection(Article.class).distinct("tagList")); 126 | }, res -> { 127 | if (res.succeeded()) { 128 | resultHandler.handle(Future.succeededFuture((List)res.result())); 129 | } else { 130 | resultHandler.handle(Future.failedFuture(res.cause())); 131 | } 132 | }); 133 | } 134 | 135 | @Override 136 | public void queryArticles(User user, JsonObject json, Handler> resultHandler) { 137 | vertx.executeBlocking(future -> { 138 | FindOptions findOptions = new FindOptions() 139 | .limit(json.getInteger("limit", 20)) 140 | .skip(json.getInteger("offset", 0)); 141 | 142 | Query
query = datastore.createQuery(Article.class); 143 | query.order(Sort.descending("createdAt")); 144 | 145 | if (json.getString("author") != null) { 146 | Query userQuery = datastore.createQuery(User.class); 147 | userQuery.field("username").equal(json.getString("author")); 148 | User author = userQuery.first(); 149 | query.field("author").equal(author); 150 | } 151 | 152 | if (json.getString("favoriter") != null) { 153 | Query userQuery = datastore.createQuery(User.class); 154 | userQuery.field("username").equal(json.getString("favoriter")); 155 | User favoriter = userQuery.first(); 156 | query.field("slug").in(favoriter.getFavorites()); 157 | } 158 | 159 | if (json.getJsonArray("tags") != null) { 160 | List criteria = json.getJsonArray("tags").stream().map(tag -> query.criteria("tagList").in(json.getJsonArray("tags"))).collect(Collectors.toList()); 161 | query.or(criteria.toArray(new Criteria[0])); 162 | } 163 | 164 | long count = query.count(); 165 | List
articles = query.find(findOptions).toList(); 166 | JsonArray array = new JsonArray(); 167 | articles.forEach(article -> array.add(article.toJsonFor(user))); 168 | 169 | JsonObject res = new JsonObject(); 170 | 171 | res.put("articlesCount", count); 172 | res.put("articles", array); 173 | 174 | future.complete(res); 175 | }, res -> { 176 | if (res.succeeded()) { 177 | resultHandler.handle(Future.succeededFuture((JsonObject)res.result())); 178 | } else { 179 | resultHandler.handle(Future.failedFuture(res.cause())); 180 | } 181 | }); 182 | } 183 | 184 | @Override 185 | public void queryArticlesFeed(User user, JsonObject json, Handler> resultHandler) { 186 | vertx.executeBlocking(future -> { 187 | FindOptions findOptions = new FindOptions() 188 | .limit(json.getInteger("limit", 20)) 189 | .skip(json.getInteger("offset", 0)); 190 | 191 | 192 | Query
query = datastore.createQuery(Article.class); 193 | query.order(Sort.descending("createdAt")); 194 | 195 | if (json.getString("queryingUser") != null) { 196 | Query userQuery = datastore.createQuery(User.class); 197 | userQuery.field("username").equal(json.getString("queryingUser")); 198 | User queryingUser = userQuery.first(); 199 | query.field("author").in(queryingUser.getFollowingUsers()); 200 | } 201 | 202 | long count = query.count(); 203 | List
articles = query.find(findOptions).toList(); 204 | JsonArray array = new JsonArray(); 205 | articles.forEach(article -> array.add(article.toJsonFor(user))); 206 | 207 | JsonObject res = new JsonObject(); 208 | 209 | res.put("articlesCount", count); 210 | res.put("articles", array); 211 | 212 | future.complete(res); 213 | }, res -> { 214 | if (res.succeeded()) { 215 | resultHandler.handle(Future.succeededFuture((JsonObject) res.result())); 216 | } else { 217 | resultHandler.handle(Future.failedFuture(res.cause())); 218 | } 219 | }); 220 | } 221 | 222 | public void update(final Class clazz, final JsonObject query, final JsonObject update, final Handler>> resultHandler) { 223 | vertx.executeBlocking(future -> { 224 | Query updateQuery = datastore.createQuery(clazz); 225 | 226 | for(Iterator> it = query.iterator(); it.hasNext();) { 227 | Map.Entry pair = it.next(); 228 | 229 | if ("_id".equals(pair.getKey())) { 230 | updateQuery.filter("_id", new ObjectId(pair.getValue().toString())); 231 | } else { 232 | updateQuery.filter(pair.getKey(), pair.getValue()); 233 | } 234 | } 235 | 236 | final UpdateOperations updateOperations = datastore.createUpdateOperations(clazz); 237 | for(Iterator> it = update.iterator(); it.hasNext();) { 238 | Map.Entry pair = it.next(); 239 | if(pair.getValue() instanceof JsonObject) { 240 | String field = pair.getKey(); 241 | JsonObject operations = (JsonObject) pair.getValue(); 242 | for(Iterator> it2 = operations.iterator(); it2.hasNext(); ) { 243 | Map.Entry operation = it2.next(); 244 | String operator = operation.getKey(); 245 | Object value = operation.getValue(); 246 | if (value instanceof JsonObject) { 247 | JsonObject valueJson = (JsonObject) value; 248 | if (valueJson.size() == 1 && valueJson.getString("_id") != null) { 249 | value = new ObjectId(valueJson.getString("_id")); 250 | } 251 | } 252 | 253 | if (MorphiaServiceOperator.PUSH.equals(operator)) { 254 | updateOperations.addToSet(field, value); 255 | } else if (MorphiaServiceOperator.POP.equals(operator)) { 256 | updateOperations.removeAll(field, value); 257 | } 258 | } 259 | } else { 260 | updateOperations.set(pair.getKey(), pair.getValue()); 261 | } 262 | } 263 | 264 | datastore.update(updateQuery, updateOperations); 265 | 266 | future.complete(updateQuery.find().toList()); 267 | }, res -> { 268 | if (res.succeeded()) { 269 | resultHandler.handle(Future.succeededFuture((List) res.result())); 270 | } else { 271 | resultHandler.handle(Future.failedFuture(res.cause())); 272 | } 273 | }); 274 | } 275 | 276 | public void find(final Class clazz, final JsonObject query, final Handler>> resultHandler) { 277 | 278 | vertx.executeBlocking(future -> { 279 | Query findQuery = datastore.createQuery(clazz); 280 | 281 | for(Iterator> it = query.iterator(); it.hasNext();) { 282 | Map.Entry pair = it.next(); 283 | if ("_id".equals(pair.getKey())) { 284 | findQuery.filter("_id", new ObjectId(pair.getValue().toString())); 285 | } else { 286 | findQuery.filter(pair.getKey(), pair.getValue()); 287 | } 288 | } 289 | 290 | future.complete(findQuery.find().toList()); 291 | }, res -> { 292 | if (res.succeeded()) { 293 | resultHandler.handle(Future.succeededFuture((List) res.result())); 294 | } else { 295 | resultHandler.handle(Future.failedFuture(res.cause())); 296 | } 297 | }); 298 | } 299 | 300 | public void delete(final Class clazz, final JsonObject query, final Handler> resultHandler) { 301 | vertx.executeBlocking(future -> { 302 | final Query deleteQuery = datastore.createQuery(clazz); 303 | for(Iterator> it = query.iterator(); it.hasNext();) { 304 | Map.Entry pair = it.next(); 305 | if ("_id".equals(pair.getKey())) { 306 | deleteQuery.filter("_id", new ObjectId(pair.getValue().toString())); 307 | } else { 308 | deleteQuery.filter(pair.getKey(), pair.getValue()); 309 | } 310 | } 311 | long numDeleted = deleteQuery.count(); 312 | datastore.delete(deleteQuery); 313 | future.complete(numDeleted); 314 | }, res -> { 315 | if (res.succeeded()) { 316 | resultHandler.handle(Future.succeededFuture( (Long) res.result())); 317 | } else { 318 | resultHandler.handle(Future.failedFuture(res.cause())); 319 | } 320 | }); 321 | } 322 | 323 | private void save(final T entity, final Handler> resultHandler) { 324 | vertx.executeBlocking(future -> { 325 | Key key = this.datastore.save(entity); 326 | future.complete(key.getId().toString()); 327 | },res -> { 328 | if (res.succeeded()) { 329 | resultHandler.handle(Future.succeededFuture(res.result().toString())); 330 | } else { 331 | resultHandler.handle(Future.failedFuture(res.cause())); 332 | } 333 | }); 334 | } 335 | 336 | 337 | @Override 338 | public void close() { 339 | this.mongoClient.close(); 340 | } 341 | } 342 | --------------------------------------------------------------------------------