public virtual void Consumer_ReceiveMessage(IBasicGetResult message) { ContinueProcessing = false; if (message == null) { return; // queue is empty } Message = CustomJsonSerializer.BytesToMessage <T>(message.Body); CorrelationId = message.BasicProperties.CorrelationId; RoutingKey = message.RoutingKey; if (message.Body.Count() == 0) { Log.Error( "SubscriberBase: Consumer_ReceiveMessage(message) - message.Body contains no data for message id {0}", message.BasicProperties.MessageId); } if (Message != null) { ContinueProcessing = true; return; } if (message.Body.Count() > 0) { Log.Error("SubscriberBase: Consumer_ReceiveMessage(message) - message.Body contains data which is not compatible with {0} for message id {1}", typeof(T).ToString(), message.BasicProperties.MessageId); } InvalidExchange.SendMessage(message.Body, InvalidRoutingKey, ""); }
protected void OnReceiveMessage(IBasicGetResult message) { var receiveMessage = ReceiveMessage; if (receiveMessage != null) { receiveMessage(message); } }
public static void Consumer_ReceiveMessage(IBasicGetResult message) { // Process Message from queue if (message == null) return;// queue is empty var batch = CustomJsonSerializer.BytesToMessage<RecogniseBatchCourtesyAmountRequest>(message.Body); //var batch = message; if (message.Body.Count() == 0) Log.Error( "ProcessingService: Queue_MessageRecieved(message) - message.Body contains no data for message id {0}", message.BasicProperties.MessageId); if (batch == null) { if (message.Body.Count() > 0) Log.Error( "ProcessingService: Queue_MessageRecieved(message) - message.Body contains data which is not compatible with RecogniseBatchCourtesyAmountRequest for message id {0}", message.BasicProperties.MessageId); // need to re-route message to CAR.Invalid queue if (!string.IsNullOrEmpty(InvalidQueueName)) InvalidExchange.SendMessage(message.Body, InvalidRoutingKey, ""); return; // acknowledge message to remove from queue; } RoutingKey = message.RoutingKey; if (batch.voucher == null || batch.voucher.Length == 0) { Log.Error( "ProcessingService: Queue_MessageRecieved(message) - there are no vouchers present for message id {0}", message.BasicProperties.MessageId); // need to re-route message to CAR.Invalid queue if (!string.IsNullOrEmpty(InvalidQueueName)) InvalidExchange.SendMessage(message.Body, InvalidRoutingKey, ""); return; // acknowledge message to remove from queue; } var ocrBatch = new OcrBatch { JobIdentifier = batch.jobIdentifier, Vouchers = batch.voucher.Select(v => new OcrVoucher { Id = v.documentReferenceNumber, ImagePath = Path.Combine(ImageFilePath, batch.jobIdentifier, string.Format(ImageFileNameTemplate, v.processingDate, v.documentReferenceNumber)), VoucherType = ParseTransactionCode(v.transactionCode) }).ToList() }; // Validate the file path if (!ValidateImageFiles(ocrBatch)) return;// probably should send to an error queue Log.Information("Batch {0} received from message queue containing {1} vouchers", ocrBatch.JobIdentifier, ocrBatch.Vouchers.Count()); OcrService.ProcessBatch(ocrBatch); }
public async Task Publisher_Publish_MessageMustBeSerializedAndCorrectlyRouted() { //prepare IConfiguration configuration = ConfigurationHelper.ProvideConfiguration(); var exchangeName = "Publisher_Publish_MessageMustBeSerializedAndCorrectlyRouted.exchangename"; configuration["rabbitmq:exchangename"] = exchangeName; IContainer container = ConfigurationHelper.ConfigureContainer(configuration); var publisher = container.Resolve <IPublisher>(); string routingKey = "changetracker.entity.create.booking"; var bus = container.Resolve <IAdvancedBus>(); IExchange exchange = bus.ExchangeDeclare(exchangeName, ExchangeType.Topic); var queue = bus.QueueDeclare("Publisher_Publish_MessageMustBeSentAndCorrectlyRouted.subscriberqueue"); var binding = bus.Bind(exchange, queue, "changetracker.entity.create.booking"); //act BookingCreated bookingCreated = new BookingCreated() { BookingName = string.Concat("Microsoft Sale", Guid.NewGuid().ToString()) }; var bookingCreatedIntegrationEvent = new IntegrationEvent <BookingCreated>() { Content = bookingCreated, EventType = "bookingcreated" }; await publisher.PublishEventAsync(bookingCreatedIntegrationEvent, routingKey); await Task.Delay(300); //check IBasicGetResult result = bus.Get(queue); var message = Encoding.UTF8.GetString(result.Body); IntegrationEvent <BookingCreated> eventToCheck = JsonConvert.DeserializeObject <IntegrationEvent <BookingCreated> >(message); eventToCheck.ShouldBeEquivalentTo(bookingCreatedIntegrationEvent); container.Dispose(); }
protected void OnReceiveMessage(IBasicGetResult message) { var receiveMessage = ReceiveMessage; if (receiveMessage != null) receiveMessage(message); }
public override void Consumer_ReceiveMessage(IBasicGetResult message) { base.Consumer_ReceiveMessage(message); if (!ContinueProcessing) { return; } var request = Message; Log.Information("Processing GenerateCorrespondingVoucherRequest '{@request}', '{@correlationId}'", request, CorrelationId); try { //Mapping queue table var queue = QueueMapper.Map(request); queue.CorrelationId = CorrelationId; queue.RoutingKey = RoutingKey; //Mapping voucher fields var vouchers = VoucherMapper.Map(request).ToList(); var jobIdentifier = CorrelationId; var batchNumber = string.Empty; //Mapping index fields var dbIndexes = DbIndexMapper.Map(request); using (var dbConnection = new SqlConnection(Configuration.SqlConnectionString)) using (var dBContext = new DipsDbContext(dbConnection)) { using (var tx = dBContext.BeginTransaction()) { try { //Adding to queue table dBContext.Queues.Add(queue); Log.Verbose("Adding new queue {@batchNumber} to the database", queue.S_BATCH); dBContext.SaveChanges(); //Adding to voucher table foreach (var voucher in vouchers) { dBContext.NabChqPods.Add(voucher); Log.Verbose( "Adding new voucher {@batchNumber} - {@sequenceNumber} to the database", voucher.S_BATCH, voucher.S_SEQUENCE); } dBContext.SaveChanges(); //Adding to index table foreach (var dbIndex in dbIndexes) { dBContext.DbIndexes.Add(dbIndex); Log.Verbose("Adding new db index {@batchNumber} - {@sequenceNumber} to the database", dbIndex.BATCH, dbIndex.SEQUENCE); } dBContext.SaveChanges(); tx.Commit(); Log.Information("Successfully processed CorrectCodelineRequest '{@batchNumber}', '{@jobIdentifier}'", batchNumber, jobIdentifier); } catch (OptimisticConcurrencyException) { //this is to handle the race condition where more than instance of this service is running at the same time and tries to update the row. //basically ignore the message by loggin a warning and rolling back. //if this row was not included by mistake (e.g. it should be included), it will just come in in the next batch run. Log.Warning( "Could not create a CorrectCodelineRequest '{@GenerateCorrespondingVoucherRequest}', '{@jobIdentifier}' because the DIPS database row was updated by another connection", request, jobIdentifier); tx.Rollback(); InvalidExchange.SendMessage(message.Body, RecoverableRoutingKey, CorrelationId); } catch (Exception ex) { Log.Error( ex, "Could not complete and create a CorrectCodelineRequest '{@GenerateCorrespondingVoucherRequest}', '{@jobIdentifier}'", request, jobIdentifier); tx.Rollback(); InvalidExchange.SendMessage(message.Body, RecoverableRoutingKey, CorrelationId); } } } Log.Information("Successfully processed GenerateCorrespondingVoucherRequest {@CorrelationId}", CorrelationId); } catch (Exception ex) { Log.Error(ex, "Error processing GenerateCorrespondingVoucherRequest {@GenerateCorrespondingVoucherRequest}", request); InvalidExchange.SendMessage(message.Body, InvalidRoutingKey, CorrelationId); } }
public async Task Subscriber_MalformedMessage_MessageMustBeMovedToGlobalDeadLetterQueue(string caseName, string messageBody) { var testName = $"{nameof(Subscriber_MalformedMessage_MessageMustBeMovedToGlobalDeadLetterQueue)}_{caseName}"; //prepare IConfiguration configuration = ConfigurationHelper.ProvideConfiguration(); var exchangeName = $"{testName}.exchangename"; configuration["rabbitmq:exchangename"] = exchangeName; configuration["rabbitmq:waitexchangename"] = exchangeName.Replace("exchangename", "waitexchangename"); var maxretrycount = 2; IContainer container = ConfigurationHelper.ConfigureContainer(configuration); //create Subscriber var bookingCreatedSubscriberFactory = container.Resolve <ISubscriberFactory>(); ISubscriber typedSubscriber = await bookingCreatedSubscriberFactory .CreateSubscriberAsync(testName + ".stubconsumer", new List <string> { "*.entity.create.booking" }, maxretrycount); var consumer = new StubConsumer(); typedSubscriber.Subscribe(SubscriptionBuilder.Create().AddDefaultSubscription(() => consumer).Build()); //create Bus var bus = container.Resolve <IAdvancedBus>(); string routingKey = "changetracker.entity.create.booking"; //act var body = Encoding.UTF8.GetBytes(messageBody); var properties = new MessageProperties { DeliveryMode = 2//persistent }; await bus.PublishAsync(await bus.ExchangeDeclareAsync(exchangeName, ExchangeType.Topic), routingKey, false, properties, body); await Task.Delay(3000); //check var deadleterqueue = bus.QueueDeclare($"{exchangeName}.defaultTombQueue"); IBasicGetResult result = bus.Get(deadleterqueue); var message = Encoding.UTF8.GetString(result.Body); JObject errorEvent = JObject.Parse(message); errorEvent["RoutingKey"].Value <string>().Should().Be("changetracker.entity.create.booking"); errorEvent["Exchange"] .Value <string>().Should().Be(exchangeName); errorEvent["Queue"] .Value <string>().Should().Be($"{exchangeName}.{testName}.stubconsumer"); //check bindings var managementClient = new ManagementClient(configuration["rabbitmqmanagement:hostUrl"], configuration["rabbitmqmanagement:username"], configuration["rabbitmqmanagement:password"], configuration.GetValue <int>("rabbitmqmanagement:portNumber")); var virtualHostName = new ConnectionStringParser().Parse(configuration["rabbitmq:connectionstring"]).VirtualHost; var virtualhost = await managementClient.GetVhostAsync(virtualHostName); var deadleterq = await managementClient.GetQueueAsync(deadleterqueue.Name, virtualhost); var deadleterqbindings = (await managementClient.GetBindingsForQueueAsync(deadleterq)).ToList(); deadleterqbindings.Should().HaveCount(2);//one is default deadleterqbindings.Where(x => x.Source == $"{exchangeName}_error" && x.RoutingKey == routingKey).Should().HaveCount(1); container.Dispose(); }
public async Task Subscriber_DiscardException_MessageMustBeMovedToGlobalDeadLetterQueue() { //prepare IConfiguration configuration = ConfigurationHelper.ProvideConfiguration(); var exchangeName = "Subscriber_DiscardException_MessageMustBeMovedToGlobalDeadLetterQueue.exchangename"; configuration["rabbitmq:exchangename"] = exchangeName; configuration["rabbitmq:waitexchangename"] = exchangeName.Replace("exchangename", "waitexchangename"); var maxretrycount = 2; IContainer container = ConfigurationHelper.ConfigureContainer(configuration); //create Subscriber var bookingCreatedSubscriberFactory = container.Resolve <ISubscriberFactory>(); ISubscriber typedSubscriber = await bookingCreatedSubscriberFactory .CreateSubscriberAsync("Subscriber_DiscardException_MessageMustBeMovedToGlobalDeadLetterQueue" + ".typedbookingcreatedconsumer", new List <string> { "*.entity.create.booking" }, maxretrycount); DiscardConsumer discardConsumer = new DiscardConsumer(); typedSubscriber.Subscribe(SubscriptionBuilder.Create().AddDefaultSubscription(() => discardConsumer).Build()); //create Publisher var publisher = container.Resolve <IPublisher>(); string routingKey = "changetracker.entity.create.booking"; //act BookingCreated bookingCreated = new BookingCreated() { BookingName = string.Concat("Microsoft Sale", Guid.NewGuid().ToString()) }; var bookingCreatedIntegrationEvent = new IntegrationEvent <BookingCreated>() { Content = bookingCreated, EventType = "bookingcreated", CorrelationId = Guid.NewGuid() }; await publisher.PublishEventAsync(bookingCreatedIntegrationEvent, routingKey); await Task.Delay(3000); //check var bus = container.Resolve <IAdvancedBus>(); var deadleterqueue = bus.QueueDeclare($"{exchangeName}.defaultTombQueue"); IBasicGetResult result = bus.Get(deadleterqueue); var message = Encoding.UTF8.GetString(result.Body); JObject errorEvent = JObject.Parse(message); errorEvent["RoutingKey"].Value <string>().Should().Be("changetracker.entity.create.booking"); errorEvent["Exchange"] .Value <string>().Should().Be("Subscriber_DiscardException_MessageMustBeMovedToGlobalDeadLetterQueue.exchangename"); errorEvent["Queue"] .Value <string>().Should().Be($"{exchangeName}.Subscriber_DiscardException_MessageMustBeMovedToGlobalDeadLetterQueue.typedbookingcreatedconsumer"); //check bindings var managementClient = new ManagementClient(configuration["rabbitmqmanagement:hostUrl"], configuration["rabbitmqmanagement:username"], configuration["rabbitmqmanagement:password"], configuration.GetValue <int>("rabbitmqmanagement:portNumber")); var virtualHostName = new ConnectionStringParser().Parse(configuration["rabbitmq:connectionstring"]).VirtualHost; var virtualhost = await managementClient.GetVhostAsync(virtualHostName); var deadleterq = await managementClient.GetQueueAsync(deadleterqueue.Name, virtualhost); var deadleterqbindings = (await managementClient.GetBindingsForQueueAsync(deadleterq)).ToList(); deadleterqbindings.Should().HaveCount(2);//one is default deadleterqbindings.Where(x => x.Source == $"{exchangeName}_error" && x.RoutingKey == routingKey).Should().HaveCount(1); container.Dispose(); }
public async Task Subscriber_ExceededCountOfAttempts_MessageMustBeMovedToSpecificDeadLetterQueue() { //prepare IConfiguration configuration = ConfigurationHelper.ProvideConfiguration(); var exchangeName = "Subscriber_ExceededCountOfAttempts_MessageMustBeMovedToDeadLetterQueue.exchangename"; configuration["rabbitmq:exchangename"] = exchangeName; configuration["rabbitmq:waitexchangename"] = exchangeName.Replace("exchangename", "waitexchangename"); configuration["rabbitmq:retryfactor"] = 100.ToString(); var maxretrycount = 2; IContainer container = ConfigurationHelper.ConfigureContainer(configuration); //create Subscriber var bookingCreatedSubscriberFactory = container.Resolve <ISubscriberFactory>(); ISubscriber typedSubscriber = await bookingCreatedSubscriberFactory .CreateSubscriberAsync("Subscriber_ExceededCountOfAttempts_MessageMustBeMovedToDeadLetterQueue" + ".typedbookingcreatedconsumer", new List <string> { "*.entity.create.booking" }, maxretrycount); BookingTypedConsumer typedConsumer = new BookingTypedConsumer(3); IntegrationEvent <BookingCreated> actualDeadLetterCallbackIntegrationEvent = null; Exception actualDeadLetterCallbackException = null; typedSubscriber.Subscribe(SubscriptionBuilder.Create().AddDefaultSubscription(() => typedConsumer, (integrationEvent, exception) => { actualDeadLetterCallbackIntegrationEvent = integrationEvent; actualDeadLetterCallbackException = exception; return(Task.CompletedTask); }).Build()); //create Publisher var publisher = container.Resolve <IPublisher>(); string routingKey = "changetracker.entity.create.booking"; //act BookingCreated bookingCreated = new BookingCreated() { BookingName = string.Concat("Microsoft Sale", Guid.NewGuid().ToString()) }; var bookingCreatedIntegrationEvent = new IntegrationEvent <BookingCreated>() { Content = bookingCreated, EventType = "bookingcreated", CorrelationId = Guid.NewGuid() }; await publisher.PublishEventAsync(bookingCreatedIntegrationEvent, routingKey); //wait 1 second await Task.Delay(1000); //check var bus = container.Resolve <IAdvancedBus>(); var deadleterqueue = bus.QueueDeclare($"{exchangeName}.deadletter" + ".Subscriber_ExceededCountOfAttempts_MessageMustBeMovedToDeadLetterQueue." + "typedbookingcreatedconsumer"); IBasicGetResult result = bus.Get(deadleterqueue); var message = Encoding.UTF8.GetString(result.Body); var actualDeadLetterQueueIntergrationEvent = JsonConvert. DeserializeObject <IntegrationEvent <DeadLetterEventDescriptor <BookingCreated> > >(message); result.Info.RoutingKey.Should().Be("deadletter" + ".Subscriber_ExceededCountOfAttempts_MessageMustBeMovedToDeadLetterQueue" + ".typedbookingcreatedconsumer"); actualDeadLetterQueueIntergrationEvent.Content.Original .ShouldBeEquivalentTo(bookingCreatedIntegrationEvent); actualDeadLetterQueueIntergrationEvent.CorrelationId .ShouldBeEquivalentTo(bookingCreatedIntegrationEvent.CorrelationId); actualDeadLetterQueueIntergrationEvent.EventType.Should() .BeEquivalentTo(string.Concat("deadletter.", bookingCreatedIntegrationEvent.EventType)); actualDeadLetterCallbackException.Message.Should().Be("ExceptionHasBeenThrown"); actualDeadLetterCallbackException.Should().BeOfType <Exception>(); actualDeadLetterCallbackIntegrationEvent.ShouldBeEquivalentTo(bookingCreatedIntegrationEvent); typedConsumer.ProcessedIntegrationEvent.Should().BeNull(); typedConsumer.CountOfAttempts.Should().Be(3); container.Dispose(); }
public static void Consumer_ReceiveMessage(IBasicGetResult message) { // Process Message from queue if (message == null) { return; // queue is empty } var batch = CustomJsonSerializer.BytesToMessage <RecogniseBatchCourtesyAmountRequest>(message.Body); //var batch = message; if (message.Body.Count() == 0) { Log.Error( "ProcessingService: Queue_MessageRecieved(message) - message.Body contains no data for message id {0}", message.BasicProperties.MessageId); } if (batch == null) { if (message.Body.Count() > 0) { Log.Error( "ProcessingService: Queue_MessageRecieved(message) - message.Body contains data which is not compatible with RecogniseBatchCourtesyAmountRequest for message id {0}", message.BasicProperties.MessageId); } // need to re-route message to CAR.Invalid queue if (!string.IsNullOrEmpty(InvalidQueueName)) { InvalidExchange.SendMessage(message.Body, InvalidRoutingKey, ""); } return; // acknowledge message to remove from queue; } RoutingKey = message.RoutingKey; if (batch.voucher == null || batch.voucher.Length == 0) { Log.Error( "ProcessingService: Queue_MessageRecieved(message) - there are no vouchers present for message id {0}", message.BasicProperties.MessageId); // need to re-route message to CAR.Invalid queue if (!string.IsNullOrEmpty(InvalidQueueName)) { InvalidExchange.SendMessage(message.Body, InvalidRoutingKey, ""); } return; // acknowledge message to remove from queue; } var ocrBatch = new OcrBatch { JobIdentifier = batch.jobIdentifier, Vouchers = batch.voucher.Select(v => new OcrVoucher { Id = v.documentReferenceNumber, ImagePath = Path.Combine(ImageFilePath, batch.jobIdentifier, string.Format(ImageFileNameTemplate, v.processingDate, v.documentReferenceNumber)), VoucherType = ParseTransactionCode(v.transactionCode) }).ToList() }; // Validate the file path if (!ValidateImageFiles(ocrBatch)) { return; // probably should send to an error queue } Log.Information("Batch {0} received from message queue containing {1} vouchers", ocrBatch.JobIdentifier, ocrBatch.Vouchers.Count()); OcrService.ProcessBatch(ocrBatch); }
public override void Consumer_ReceiveMessage(IBasicGetResult message) { base.Consumer_ReceiveMessage(message); if (!ContinueProcessing) { return; } var request = Message; Log.Information("Processing GetVouchersInformationResponse '{@request}', '{@correlationId}'", request, CorrelationId); try { var jobIdentifier = CorrelationId; var batchNumber = string.Empty; if (request.voucherInformation.Length == 0) { Log.Information("No matching vouchers found GetVouchersInformationResponse '{@batchNumber}', '{@jobIdentifier}'", batchNumber, jobIdentifier); //Mapping responseDone fields var responseDoneOutput = new DipsResponseDone { guid_name = CorrelationId, response_time = DateTime.Now, number_of_results = 0 }; using (var dbConnection = new SqlConnection(Configuration.SqlConnectionString)) using (var dipsDbContext = new DipsDbContext(dbConnection)) { using (var tx = dipsDbContext.BeginTransaction()) { try { //Adding to DipsResponseDone table dipsDbContext.DipsResponseDone.Add(responseDoneOutput); dipsDbContext.SaveChanges(); tx.Commit(); Log.Information("Successfully processed GetVouchersInformationResponse '{@batchNumber}', '{@jobIdentifier}'", batchNumber, jobIdentifier); } catch (OptimisticConcurrencyException) { //this is to handle the race condition where more than instance of this service is running at the same time and tries to update the row. //basically ignore the message by loggin a warning and rolling back. //if this row was not included by mistake (e.g. it should be included), it will just come in in the next batch run. Log.Warning( "Could not create a GetVouchersInformationResponse '{@GetVouchersInformationResponse}', '{@jobIdentifier}' because the DIPS database row was updated by another connection", request, jobIdentifier); tx.Rollback(); } catch (Exception ex) { Log.Error( ex, "Could not complete and create a GetVouchersInformationResponse '{@GetVouchersInformationResponse}', '{@jobIdentifier}'", request, jobIdentifier); tx.Rollback(); } } } } else { //Mapping criteria fields var firstVoucher = request.voucherInformation.First(); var criterias = MapToCriterias(firstVoucher); var jsonPayload = JsonConvert.SerializeObject(criterias); //Mapping responseDone fields var responseDoneOutput = new DipsResponseDone { guid_name = CorrelationId, response_time = DateTime.Now, number_of_results = 1 }; //Mapping responsData fields var responseDataOutput = new DipsResponseData { doc_ref_number = firstVoucher.voucher.documentReferenceNumber, guid_name = CorrelationId, payload = jsonPayload, front_image = System.Text.Encoding.Default.GetString(firstVoucher.voucherImage[0].content), rear_image = System.Text.Encoding.Default.GetString(firstVoucher.voucherImage[1].content) }; using (var dbConnection = new SqlConnection(Configuration.SqlConnectionString)) using (var dipsDbContext = new DipsDbContext(dbConnection)) { using (var tx = dipsDbContext.BeginTransaction()) { try { //Adding to DipsResponseData table dipsDbContext.DipsResponseData.Add(responseDataOutput); Log.Verbose("Adding new response data {@drn} to the database", request.voucherInformation.First().voucher.documentReferenceNumber); dipsDbContext.SaveChanges(); //Adding to DipsResponseDone table dipsDbContext.DipsResponseDone.Add(responseDoneOutput); Log.Verbose("Adding new response data done row to the database"); dipsDbContext.SaveChanges(); tx.Commit(); Log.Information("Successfully processed GetVouchersInformationResponse '{@batchNumber}', '{@jobIdentifier}'", batchNumber, jobIdentifier); } catch (OptimisticConcurrencyException) { //this is to handle the race condition where more than instance of this service is running at the same time and tries to update the row. //basically ignore the message by loggin a warning and rolling back. //if this row was not included by mistake (e.g. it should be included), it will just come in in the next batch run. Log.Warning( "Could not create a GetVouchersInformationResponse '{@GetVouchersInformationResponse}', '{@jobIdentifier}' because the DIPS database row was updated by another connection", request, jobIdentifier); tx.Rollback(); InvalidExchange.SendMessage(message.Body, RecoverableRoutingKey, CorrelationId); } catch (Exception ex) { Log.Error( ex, "Could not complete and create a GetVouchersInformationResponse '{@GetVouchersInformationResponse}', '{@jobIdentifier}'", request, jobIdentifier); tx.Rollback(); InvalidExchange.SendMessage(message.Body, RecoverableRoutingKey, CorrelationId); } } } } } catch (Exception ex) { Log.Error(ex, "Error processing GetVouchersInformationResponse {@GetVouchersInformationResponse}", request); InvalidExchange.SendMessage(message.Body, InvalidRoutingKey, CorrelationId); } }