Swap Mongo's Document for Vert.x's JsonObject

This commit is contained in:
Colin McDonald 2016-06-22 19:44:39 -04:00
parent 2bdd54836d
commit 5c3274d483
19 changed files with 94 additions and 107 deletions

View File

@ -1,8 +1,8 @@
package net.frozenorb.apiv3.maxmind;
import io.vertx.core.json.JsonObject;
import lombok.Getter;
import net.frozenorb.apiv3.util.MaxMindUtils;
import org.bson.Document;
public final class MaxMindCity {
@ -12,7 +12,7 @@ public final class MaxMindCity {
public MaxMindCity() {} // For Jackson
public MaxMindCity(Document legacy) {
public MaxMindCity(JsonObject legacy) {
this.confidence = legacy.getInteger("confidence");
this.geonameId = legacy.getInteger("geoname_id");
this.name = MaxMindUtils.getEnglishName(legacy);

View File

@ -1,8 +1,8 @@
package net.frozenorb.apiv3.maxmind;
import io.vertx.core.json.JsonObject;
import lombok.Getter;
import net.frozenorb.apiv3.util.MaxMindUtils;
import org.bson.Document;
public final class MaxMindContinent {
@ -12,7 +12,7 @@ public final class MaxMindContinent {
public MaxMindContinent() {} // For Jackson
public MaxMindContinent(Document legacy) {
public MaxMindContinent(JsonObject legacy) {
this.code = legacy.getString("code");
this.geonameId = legacy.getInteger("geoname_id");
this.name = MaxMindUtils.getEnglishName(legacy);

View File

@ -1,8 +1,8 @@
package net.frozenorb.apiv3.maxmind;
import io.vertx.core.json.JsonObject;
import lombok.Getter;
import net.frozenorb.apiv3.util.MaxMindUtils;
import org.bson.Document;
public final class MaxMindCountry {
@ -13,7 +13,7 @@ public final class MaxMindCountry {
public MaxMindCountry() {} // For Jackson
public MaxMindCountry(Document legacy) {
public MaxMindCountry(JsonObject legacy) {
this.isoCode = legacy.getString("iso_code");
this.confidence = legacy.getInteger("confidence");
this.geonameId = legacy.getInteger("geoname_id");

View File

@ -1,7 +1,7 @@
package net.frozenorb.apiv3.maxmind;
import io.vertx.core.json.JsonObject;
import lombok.Getter;
import org.bson.Document;
public final class MaxMindLocation {
@ -15,7 +15,7 @@ public final class MaxMindLocation {
public MaxMindLocation() {} // For Jackson
public MaxMindLocation(Document legacy) {
public MaxMindLocation(JsonObject legacy) {
this.latitude = legacy.getDouble("latitude");
this.longitude = legacy.getDouble("longitude");
this.accuracyRadius = legacy.getInteger("accuracy_radius");

View File

@ -1,7 +1,7 @@
package net.frozenorb.apiv3.maxmind;
import io.vertx.core.json.JsonObject;
import lombok.Getter;
import org.bson.Document;
public final class MaxMindPostal {
@ -10,14 +10,8 @@ public final class MaxMindPostal {
public MaxMindPostal() {} // For Jackson
public MaxMindPostal(Document legacy) {
this.code = legacy.getString("code");
// Postal codes aren't guaranteed to exist for all areas
if (code == null) {
code = "";
}
public MaxMindPostal(JsonObject legacy) {
this.code = legacy.getString("code", "");
this.confidence = legacy.getInteger("confidence");
}

View File

@ -1,8 +1,8 @@
package net.frozenorb.apiv3.maxmind;
import io.vertx.core.json.JsonObject;
import lombok.Getter;
import net.frozenorb.apiv3.util.MaxMindUtils;
import org.bson.Document;
public final class MaxMindRegisteredCountry {
@ -12,7 +12,7 @@ public final class MaxMindRegisteredCountry {
public MaxMindRegisteredCountry() {} // For Jackson
public MaxMindRegisteredCountry(Document legacy) {
public MaxMindRegisteredCountry(JsonObject legacy) {
this.isoCode = legacy.getString("iso_code");
this.geonameId = legacy.getInteger("geoname_id");
this.name = MaxMindUtils.getEnglishName(legacy);

View File

@ -1,7 +1,7 @@
package net.frozenorb.apiv3.maxmind;
import io.vertx.core.json.JsonObject;
import lombok.Getter;
import org.bson.Document;
import java.util.ArrayList;
import java.util.List;
@ -19,19 +19,19 @@ public final class MaxMindResult {
public MaxMindResult() {} // For Jackson
public MaxMindResult(Document legacy) {
this.continent = new MaxMindContinent((Document) legacy.get("continent"));
this.city = new MaxMindCity((Document) legacy.get("city"));
this.postal = new MaxMindPostal((Document) legacy.get("postal"));
this.traits = new MaxMindTraits((Document) legacy.get("traits"));
this.location = new MaxMindLocation((Document) legacy.get("location"));
this.country = new MaxMindCountry((Document) legacy.get("country"));
this.registeredCountry = new MaxMindRegisteredCountry((Document) legacy.get("registered_country"));
public MaxMindResult(JsonObject legacy) {
this.continent = new MaxMindContinent(legacy.getJsonObject("continent"));
this.city = new MaxMindCity(legacy.getJsonObject("city"));
this.postal = new MaxMindPostal(legacy.getJsonObject("postal"));
this.traits = new MaxMindTraits(legacy.getJsonObject("traits"));
this.location = new MaxMindLocation(legacy.getJsonObject("location"));
this.country = new MaxMindCountry(legacy.getJsonObject("country"));
this.registeredCountry = new MaxMindRegisteredCountry(legacy.getJsonObject("registered_country"));
List<MaxMindSubdivision> subdivisions = new ArrayList<>();
for (Object subdivision : (List<Object>) legacy.get("subdivisions")) {
subdivisions.add(new MaxMindSubdivision((Document) subdivision));
for (Object subdivision : legacy.getJsonArray("subdivisions")) {
subdivisions.add(new MaxMindSubdivision((JsonObject) subdivision));
}
this.subdivisions = subdivisions.toArray(new MaxMindSubdivision[subdivisions.size()]);

View File

@ -1,8 +1,8 @@
package net.frozenorb.apiv3.maxmind;
import io.vertx.core.json.JsonObject;
import lombok.Getter;
import net.frozenorb.apiv3.util.MaxMindUtils;
import org.bson.Document;
public final class MaxMindSubdivision {
@ -13,7 +13,7 @@ public final class MaxMindSubdivision {
public MaxMindSubdivision() {} // For Jackson
public MaxMindSubdivision(Document legacy) {
public MaxMindSubdivision(JsonObject legacy) {
this.isoCode = legacy.getString("iso_code");
this.confidence = legacy.getInteger("confidence", -1);
this.geonameId = legacy.getInteger("geoname_id");

View File

@ -1,7 +1,7 @@
package net.frozenorb.apiv3.maxmind;
import io.vertx.core.json.JsonObject;
import lombok.Getter;
import org.bson.Document;
public final class MaxMindTraits {
@ -14,7 +14,7 @@ public final class MaxMindTraits {
public MaxMindTraits() {} // For Jackson
public MaxMindTraits(Document legacy) {
public MaxMindTraits(JsonObject legacy) {
this.isp = legacy.getString("isp");
this.domain = legacy.getString("domain");
this.asn = legacy.getInteger("autonomous_system_number");

View File

@ -24,7 +24,6 @@ import net.frozenorb.apiv3.unsorted.BlockingCallback;
import net.frozenorb.apiv3.util.*;
import org.bson.Document;
import java.math.BigInteger;
import java.time.Instant;
import java.util.*;
@ -258,7 +257,7 @@ public final class User {
public void startRegistration(String pendingEmail) {
this.pendingEmail = pendingEmail;
this.pendingEmailToken = new BigInteger(130, new Random()).toString(32);
this.pendingEmailToken = UUID.randomUUID().toString().replace("-", "");
this.pendingEmailTokenSetAt = Instant.now();
}

View File

@ -8,7 +8,6 @@ import net.frozenorb.apiv3.auditLog.AuditLogActionType;
import net.frozenorb.apiv3.model.User;
import net.frozenorb.apiv3.util.ErrorUtils;
import net.frozenorb.apiv3.util.IpUtils;
import org.bson.Document;
public final class POSTAuditLog implements Handler<RoutingContext> {
@ -35,7 +34,7 @@ public final class POSTAuditLog implements Handler<RoutingContext> {
return;
}
AuditLog.log(user, userIp, ctx.get("actor"), type, Document.parse(ctx.getBodyAsString()), (auditLogEntry, error2) -> {
AuditLog.log(user, userIp, ctx.get("actor"), type, ctx.getBodyAsJson().getMap(), (auditLogEntry, error2) -> {
if (error2 != null) {
ErrorUtils.respondInternalError(ctx, error2);
} else {

View File

@ -1,6 +1,7 @@
package net.frozenorb.apiv3.route.grants;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.model.Grant;
@ -8,7 +9,6 @@ import net.frozenorb.apiv3.model.Rank;
import net.frozenorb.apiv3.model.ServerGroup;
import net.frozenorb.apiv3.model.User;
import net.frozenorb.apiv3.util.ErrorUtils;
import org.bson.Document;
import java.time.Instant;
import java.util.HashSet;
@ -18,15 +18,15 @@ import java.util.Set;
public final class POSTGrants implements Handler<RoutingContext> {
public void handle(RoutingContext ctx) {
Document body = Document.parse(ctx.getBodyAsString());
User target = User.findByIdSync(body.getString("user"));
JsonObject requestBody = ctx.getBodyAsJson();
User target = User.findByIdSync(requestBody.getString("user"));
if (target == null) {
ErrorUtils.respondNotFound(ctx, "User", body.getString("user"));
ErrorUtils.respondNotFound(ctx, "User", requestBody.getString("user"));
return;
}
String reason = body.getString("reason");
String reason = requestBody.getString("reason");
if (reason == null || reason.trim().isEmpty()) {
ErrorUtils.respondRequiredInput(ctx, "reason");
@ -34,7 +34,7 @@ public final class POSTGrants implements Handler<RoutingContext> {
}
Set<ServerGroup> scopes = new HashSet<>();
List<String> scopeIds = (List<String>) body.get("scopes"); // TODO: SHOULD BE ARRAY
List<String> scopeIds = (List<String>) requestBody.getJsonArray("scopes").getList(); // TODO: SHOULD BE ARRAY
if (!scopeIds.isEmpty()) {
for (String serverGroupId : scopeIds) {
@ -49,17 +49,17 @@ public final class POSTGrants implements Handler<RoutingContext> {
}
}
Rank rank = Rank.findById(body.getString("rank"));
Rank rank = Rank.findById(requestBody.getString("rank"));
if (rank == null) {
ErrorUtils.respondNotFound(ctx, "Rank", body.getString("rank"));
ErrorUtils.respondNotFound(ctx, "Rank", requestBody.getString("rank"));
return;
}
Instant expiresAt = null;
if (body.containsKey("expiresAt") && body.get("expiresAt", Number.class).longValue() != -1) {
expiresAt = Instant.ofEpochMilli(body.get("expiresAt", Number.class).longValue());
if (requestBody.containsKey("expiresAt") && requestBody.getLong("expiresAt") != -1) {
expiresAt = Instant.ofEpochMilli(requestBody.getLong("expiresAt"));
}
if (expiresAt != null && expiresAt.isBefore(Instant.now())) {
@ -68,7 +68,7 @@ public final class POSTGrants implements Handler<RoutingContext> {
}
// We purposely don't do a null check, grants don't have to have a source.
User addedBy = User.findByIdSync(body.getString("addedBy"));
User addedBy = User.findByIdSync(requestBody.getString("addedBy"));
Grant grant = new Grant(target, reason, scopes, rank, expiresAt, addedBy);
grant.insert();

View File

@ -3,6 +3,7 @@ package net.frozenorb.apiv3.route.punishments;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.model.IpBan;
@ -10,7 +11,6 @@ import net.frozenorb.apiv3.model.Punishment;
import net.frozenorb.apiv3.model.User;
import net.frozenorb.apiv3.unsorted.Permissions;
import net.frozenorb.apiv3.util.ErrorUtils;
import org.bson.Document;
import java.time.Instant;
import java.util.Map;
@ -18,22 +18,22 @@ import java.util.Map;
public final class POSTPunishments implements Handler<RoutingContext> {
public void handle(RoutingContext ctx) {
Document body = Document.parse(ctx.getBodyAsString());
User target = User.findByIdSync(body.getString("user"));
JsonObject requestBody = ctx.getBodyAsJson();
User target = User.findByIdSync(requestBody.getString("user"));
if (target == null) {
ErrorUtils.respondNotFound(ctx, "User", body.getString("user"));
ErrorUtils.respondNotFound(ctx, "User", requestBody.getString("user"));
return;
}
String reason = body.getString("reason");
String reason = requestBody.getString("reason");
if (reason == null || reason.trim().isEmpty()) {
ErrorUtils.respondRequiredInput(ctx, "reason");
return;
}
Punishment.PunishmentType type = Punishment.PunishmentType.valueOf(body.getString("type"));
Punishment.PunishmentType type = Punishment.PunishmentType.valueOf(requestBody.getString("type"));
if (type != Punishment.PunishmentType.WARN) {
for (Punishment punishment : Punishment.findByUserAndTypeSync(target, ImmutableSet.of(type))) {
@ -46,8 +46,8 @@ public final class POSTPunishments implements Handler<RoutingContext> {
Instant expiresAt = null;
if (body.containsKey("expiresAt") && body.get("expiresAt", Number.class).longValue() != -1) {
expiresAt = Instant.ofEpochMilli(body.get("expiresAt", Number.class).longValue());
if (requestBody.containsKey("expiresAt") && requestBody.getLong("expiresAt") != -1) {
expiresAt = Instant.ofEpochMilli(requestBody.getLong("expiresAt"));
}
if (expiresAt != null && expiresAt.isBefore(Instant.now())) {
@ -55,7 +55,7 @@ public final class POSTPunishments implements Handler<RoutingContext> {
return;
}
Map<String, Object> meta = (Map<String, Object>) body.get("metadata");
Map<String, Object> meta = requestBody.getJsonObject("metadata").getMap();
if (meta == null) {
ErrorUtils.respondRequiredInput(ctx, "request body meta");
@ -63,7 +63,7 @@ public final class POSTPunishments implements Handler<RoutingContext> {
}
// We purposely don't do a null check, punishments don't have to have a source.
User addedBy = User.findByIdSync(body.getString("addedBy"));
User addedBy = User.findByIdSync(requestBody.getString("addedBy"));
if (target.hasPermissionAnywhere(Permissions.PROTECTED_PUNISHMENT)) {
ErrorUtils.respondGeneric(ctx, 200, target.getLastSeenOn() + " is protected from punishments.");
@ -72,7 +72,7 @@ public final class POSTPunishments implements Handler<RoutingContext> {
Punishment punishment = new Punishment(target, reason, type, expiresAt, addedBy, ctx.get("actor"), meta);
String accessDenialReason = punishment.getAccessDenialReason();
String userIp = body.getString("userIp");
String userIp = requestBody.getString("userIp");
if ((type == Punishment.PunishmentType.BAN || type == Punishment.PunishmentType.BLACKLIST) && userIp != null) {
IpBan ipBan = new IpBan(userIp, punishment);

View File

@ -8,8 +8,7 @@ import net.frozenorb.apiv3.model.ServerGroup;
import net.frozenorb.apiv3.util.ErrorUtils;
import net.frozenorb.apiv3.util.IpUtils;
import java.math.BigInteger;
import java.util.Random;
import java.util.UUID;
public final class POSTServers implements Handler<RoutingContext> {
@ -29,7 +28,7 @@ public final class POSTServers implements Handler<RoutingContext> {
return;
}
String generatedApiKey = new BigInteger(130, new Random()).toString(32);
String generatedApiKey = UUID.randomUUID().toString();
Server server = new Server(id, displayName, generatedApiKey, group, ip);
server.insert();
APIv3.respondJson(ctx, server);

View File

@ -5,6 +5,8 @@ import com.google.common.collect.ImmutableMap;
import io.vertx.core.CompositeFuture;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import lombok.extern.slf4j.Slf4j;
import net.frozenorb.apiv3.APIv3;
@ -14,7 +16,6 @@ import net.frozenorb.apiv3.model.*;
import net.frozenorb.apiv3.util.ErrorUtils;
import net.frozenorb.apiv3.util.PermissionUtils;
import net.frozenorb.apiv3.util.UuidUtils;
import org.bson.Document;
import java.util.HashMap;
import java.util.List;
@ -34,14 +35,14 @@ public final class POSTServersHeartbeat implements Handler<RoutingContext> {
Server actorServer = Server.findById(actor.getName());
ServerGroup actorServerGroup = ServerGroup.findById(actorServer.getServerGroup());
Document reqJson = Document.parse(ctx.getBodyAsString());
Map<UUID, String> playerNames = extractPlayerNames(reqJson);
JsonObject requestBody = ctx.getBodyAsJson();
Map<UUID, String> playerNames = extractPlayerNames(requestBody.getJsonArray("players"));
CompositeFuture.all(
createInfoResponse(actorServer, reqJson.getDouble("lastTps"), playerNames),
createInfoResponse(actorServer, requestBody.getDouble("lastTps"), playerNames),
createPlayerResponse(actorServer, playerNames),
createPermissionsResponse(actorServerGroup),
createEventsResponse((List<Object>) reqJson.get("events"))
createEventsResponse(requestBody.getJsonArray("events"))
).setHandler((result) -> {
if (result.succeeded()) {
// We don't do anything with the info callback, as
@ -160,11 +161,11 @@ public final class POSTServersHeartbeat implements Handler<RoutingContext> {
return callback;
}
private Future<Map<String, Object>> createEventsResponse(List<Object> eventsData) {
private Future<Map<String, Object>> createEventsResponse(JsonArray events) {
Future<Map<String, Object>> callback = Future.future();
for (Object event : eventsData) {
Document eventJson = (Document) event;
for (Object event : events) {
JsonObject eventJson = (JsonObject) event;
String type = eventJson.getString("type");
switch (type) {
@ -183,11 +184,11 @@ public final class POSTServersHeartbeat implements Handler<RoutingContext> {
return callback;
}
private Map<UUID, String> extractPlayerNames(Document reqJson) {
private Map<UUID, String> extractPlayerNames(JsonArray players) {
Map<UUID, String> result = new HashMap<>();
for (Object player : (List<Object>) reqJson.get("players")) {
Document playerJson = (Document) player;
for (Object player : players) {
JsonObject playerJson = (JsonObject) player;
UUID uuid = UUID.fromString(playerJson.getString("uuid"));
String username = playerJson.getString("username");

View File

@ -10,10 +10,7 @@ import net.frozenorb.apiv3.model.User;
import net.frozenorb.apiv3.unsorted.Notification;
import net.frozenorb.apiv3.util.ErrorUtils;
import java.math.BigInteger;
import java.time.Instant;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

View File

@ -1,12 +1,12 @@
package net.frozenorb.apiv3.unsorted;
import com.google.common.collect.ImmutableList;
import com.google.common.net.MediaType;
import com.mongodb.async.SingleResultCallback;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.model.NotificationTemplate;
import org.bson.Document;
import java.util.Map;
@ -21,31 +21,32 @@ public final class Notification {
}
public void sendAsEmail(String email, SingleResultCallback<Void> callback) {
Document messageJson = new Document();
JsonObject message = new JsonObject();
messageJson.put("html", body);
messageJson.put("subject", subject);
messageJson.put("from_email", "no-reply@minehq.com");
messageJson.put("from_name", "MineHQ Network");
messageJson.put("to", ImmutableList.of(
new Document("email", email)
.append("name", null)
.append("type", "to")
message.put("html", body);
message.put("subject", subject);
message.put("from_email", "no-reply@minehq.com");
message.put("from_name", "MineHQ Network");
message.put("to", new JsonArray().add(
new JsonObject()
.put("email", email)
.putNull("name")
.put("type", "to")
));
Document bodyJson = new Document();
bodyJson.put("key", APIv3.getConfig().getProperty("mandrill.apiKey"));
bodyJson.put("message", messageJson);
JsonObject body = new JsonObject()
.put("key", APIv3.getConfig().getProperty("mandrill.apiKey"))
.put("message", message);
APIv3.getHttpClient().post("mandrillapp.com", "/api/1.0/messages/send.json", (response) -> {
response.bodyHandler((body) -> {
response.bodyHandler((resultBody) -> {
callback.onResult(null, null);
});
response.exceptionHandler((error) -> {
callback.onResult(null, error);
});
}).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()).end(bodyJson.toJson());
}).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()).end(body.encode());
}
public void sendAsText(String phoneNumber, SingleResultCallback<Void> callback) {

View File

@ -2,13 +2,12 @@ package net.frozenorb.apiv3.util;
import com.google.common.base.Charsets;
import com.mongodb.async.SingleResultCallback;
import io.vertx.core.json.JsonObject;
import lombok.experimental.UtilityClass;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.maxmind.MaxMindResult;
import org.bson.Document;
import java.util.Base64;
import java.util.Map;
@UtilityClass
public class MaxMindUtils {
@ -20,8 +19,8 @@ public class MaxMindUtils {
// We have to specifically use getHttpSClient(), vertx's http client is dumb.
APIv3.getHttpsClient().get(443, "geoip.maxmind.com", "/geoip/v2.1/insights/" + ip, (response) -> {
response.bodyHandler((body) -> {
Document resJson = Document.parse(body.toString());
callback.onResult(new MaxMindResult(resJson), null);
JsonObject bodyJson = new JsonObject(body.toString());
callback.onResult(new MaxMindResult(bodyJson), null);
});
response.exceptionHandler((error) -> {
@ -30,8 +29,8 @@ public class MaxMindUtils {
}).putHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString((maxMindUserId + ":" + maxMindLicenseKey).getBytes(Charsets.UTF_8))).end();
}
public static String getEnglishName(Document source) {
return ((Map<String, String>) source.get("names")).get("en");
public static String getEnglishName(JsonObject source) {
return source.getJsonObject("names").getString("en", "INVALID");
}
}

View File

@ -1,10 +1,10 @@
package net.frozenorb.apiv3.util;
import com.mongodb.async.SingleResultCallback;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonObject;
import lombok.experimental.UtilityClass;
import net.frozenorb.apiv3.APIv3;
import org.bson.Document;
import org.bson.json.JsonParseException;
import java.io.IOException;
import java.util.UUID;
@ -13,20 +13,18 @@ import java.util.UUID;
public class MojangUtils {
public static void getName(UUID id, SingleResultCallback<String> callback) {
System.out.println("GET " + id.toString().replace("-", ""));
APIv3.getHttpClient().get("sessionserver.mojang.com", "/session/minecraft/profile/" + id.toString().replace("-", ""), (response) -> {
response.bodyHandler((body) -> {
try {
Document resJson = Document.parse(body.toString());
String name = resJson.getString("name");
JsonObject bodyJson = new JsonObject(body.toString());
String name = bodyJson.getString("name");
if (name == null) {
callback.onResult(null, new IOException("Hit Mojang API rate limit: " + resJson.toJson()));
callback.onResult(null, new IOException("Hit Mojang API rate limit: " + bodyJson.encode()));
} else {
callback.onResult(name, null);
}
} catch (JsonParseException ex) {
} catch (DecodeException ex) {
callback.onResult(null, new RuntimeException(body.toString(), ex));
}
});