From 3b5bb12c32befa62f258559d19a28ecac205a634 Mon Sep 17 00:00:00 2001 From: Ralph Soika Date: Sun, 1 Dec 2024 14:23:25 +0100 Subject: [PATCH] refactoring metric api --- .../com/alexanderlogistics/InvoiceUtil.java | 12 + .../alexanderlogistics/OPListController.java | 2 +- .../ZahlungseingangSaldoAdapter.java | 2 +- .../api/CargosoftMigrationRestService.java | 4 +- .../MetricDebitorRestService.java | 59 +- .../{ => metrics}/MetricDebitorService.java | 79 +- workflow/dwc/rechnungsausgang-dwc-1.0.1.bpmn | 1721 +++++++++++++++++ 7 files changed, 1811 insertions(+), 68 deletions(-) rename office-alexander-logistics-app/src/main/java/com/alexanderlogistics/{ => metrics}/MetricDebitorRestService.java (74%) rename office-alexander-logistics-app/src/main/java/com/alexanderlogistics/{ => metrics}/MetricDebitorService.java (80%) create mode 100644 workflow/dwc/rechnungsausgang-dwc-1.0.1.bpmn diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceUtil.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceUtil.java index bedde9b..ebdff55 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceUtil.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceUtil.java @@ -38,6 +38,18 @@ public class InvoiceUtil { return bd.doubleValue(); } + /** + * Rounds a Flat Value + * + * @param value + * @return + */ + public static float roundToFloat(double value) { + BigDecimal bd = BigDecimal.valueOf(value); + bd = bd.setScale(2, RoundingMode.HALF_UP); + return bd.floatValue(); + } + /** * Hilfsmethode zum validieren der Mailadresse * diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/OPListController.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/OPListController.java index 12e3fda..e71d395 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/OPListController.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/OPListController.java @@ -173,7 +173,7 @@ public class OPListController implements Serializable { float rate = invoice.getItemValueFloat("invoice.rate"); float maxSaldo = invoice.getItemValueFloat("invoice.saldo"); if (rate != 0) { - maxSaldo = maxSaldo / rate; + maxSaldo = InvoiceUtil.roundToFloat(maxSaldo / rate); invoice.setItemValue("invoice.saldo.byrate", maxSaldo); } diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ZahlungseingangSaldoAdapter.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ZahlungseingangSaldoAdapter.java index 7e60d1d..7ab9887 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ZahlungseingangSaldoAdapter.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ZahlungseingangSaldoAdapter.java @@ -160,7 +160,7 @@ public class ZahlungseingangSaldoAdapter implements SignalAdapter { invoiceSaldo = invoiceSaldo - invoicePaymentAmount; } invoiceSaldo = (float) InvoiceUtil.round(invoiceSaldo); - invoice.setItemValue("invoice.saldo", invoiceSaldo); + invoice.setItemValue("invoice.saldo", InvoiceUtil.roundToFloat(invoiceSaldo)); invoice.event(ZahlungseingangService.EVENT_UPDATE_INVOICE); zahlungseingangService.processInvoice(invoice); diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/api/CargosoftMigrationRestService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/api/CargosoftMigrationRestService.java index bdaf303..4a20ba9 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/api/CargosoftMigrationRestService.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/api/CargosoftMigrationRestService.java @@ -621,8 +621,8 @@ public class CargosoftMigrationRestService implements Serializable { total = total + tax; saldo = saldo + tax; } - workitem.setItemValue("invoice.total", total); - workitem.setItemValue("invoice.saldo", saldo); + workitem.setItemValue("invoice.total", InvoiceUtil.round(total)); + workitem.setItemValue("invoice.saldo", InvoiceUtil.round(saldo)); workitem.setItemValue("cargosoft.fix.xmlerror", tax); log = log + "....... total neu=" + total + " saldo neu=" + saldo + "\n"; diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorRestService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/metrics/MetricDebitorRestService.java similarity index 74% rename from office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorRestService.java rename to office-alexander-logistics-app/src/main/java/com/alexanderlogistics/metrics/MetricDebitorRestService.java index 55638c3..a7a8f3e 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorRestService.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/metrics/MetricDebitorRestService.java @@ -1,7 +1,5 @@ -package com.alexanderlogistics; +package com.alexanderlogistics.metrics; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -9,11 +7,12 @@ import java.util.logging.Logger; import org.imixs.marty.team.TeamService; import org.imixs.workflow.ItemCollection; -import org.imixs.workflow.ItemCollectionComparator; import org.imixs.workflow.engine.DocumentService; import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.QueryException; +import com.alexanderlogistics.InvoiceUtil; + import jakarta.ejb.Stateless; import jakarta.inject.Inject; import jakarta.ws.rs.GET; @@ -88,22 +87,15 @@ public class MetricDebitorRestService { long l = System.currentTimeMillis(); logger.info("├── init metrics..."); try { - // compute all departments - List spaceNames = new ArrayList(); - List spaces = teamService.getSpaces(); - // sort by space.name - Collections.sort(spaces, new ItemCollectionComparator("space.name", true)); - for (ItemCollection space : spaces) { - String spaceName = space.getItemValueString("space.name"); - spaceNames.add(spaceName); - logger.info("│   ├── grouping invoices by debitor..."); - groupInvoicesByWeek(space.getUniqueID(), spaceName, metricCache); - } + groupInvoicesByDebitor(metricCache); logger.info("│   ├── grouping invoices finished in " + (System.currentTimeMillis() - l) + "ms"); rebuildMetrics(metricCache); - logger.info("├── init metrics completed in " + (System.currentTimeMillis() - l) + "ms"); - return Response.ok().entity("init metrics completed in " + (System.currentTimeMillis() - l) + "ms").build(); + String message = "├── init " + metricCache.size() + " metrics completed in " + + (System.currentTimeMillis() - l) + + "ms"; + logger.info(message); + return Response.ok().entity(message).build(); } catch (Exception e) { e.printStackTrace(); @@ -118,13 +110,8 @@ public class MetricDebitorRestService { for (String dbtrNumber : metricCache.keySet()) { ItemCollection metricData = metricCache.get(dbtrNumber); - // String dbtrName = metricData.getItemValueString("dbtr.name"); - - // ItemCollection metric = metricService.loadMetric(dbtrNumber, dbtrName); - // metric.setItemValue(MetricDebitorService.ITEM_TOTALS, metricData.totals); documentService.save(metricData); metricService.initMetric(metricData); - } } @@ -138,18 +125,17 @@ public class MetricDebitorRestService { * * */ - private void groupInvoicesByWeek(String spaceID, String spaceName, + private void groupInvoicesByDebitor( Map metricCache) { - logger.info("│   │   ├── group invoices for " + spaceName + "/" + spaceID); + logger.info("│   │   ├── group invoices by debitor..."); try { List invoices = documentService.find( - "$modelversion:rechnungsausgang-* AND type:workitem AND $uniqueidref:" + - spaceID, + "$modelversion:rechnungsausgang-* AND type:workitem", 9999, 0, "invoice.number", false); - logger.fine(" found " + invoices.size() + " for space " + spaceName); + logger.fine(" found " + invoices.size() + " open invoices"); for (ItemCollection invoice : invoices) { String debitorName = invoice.getItemValueString("dbtr.name"); @@ -157,15 +143,26 @@ public class MetricDebitorRestService { ItemCollection metricData = metricCache.get(debitorNumber); if (metricData == null) { - metricData = metricService.createMetaData(debitorNumber, debitorName); + try { + metricData = metricService.loadMetric(debitorNumber, debitorName); + // reset old metrics now! + for (String itemName : metricData.getItemNames()) { + if (itemName.startsWith(MetricDebitorService.ITEM_SALDO + ".")) { + metricData.setItemValue(itemName, 0.0); + } + } + } catch (PluginException e) { + logger.severe( + "Unable to load metric for invoice " + invoice.getUniqueID() + " : " + e.getMessage()); + continue; + } } - // Jetzt Rechnung addieren addInvoice(metricData, invoice); // invoiceData.add(invoice); metricCache.put(debitorNumber, metricData); } - + logger.info("│   │   ├── grouped " + invoices.size() + " invoices."); } catch (QueryException e) { e.printStackTrace(); } @@ -180,7 +177,7 @@ public class MetricDebitorRestService { */ public void addInvoice(ItemCollection metricData, ItemCollection invoice) { String currency = invoice.getItemValueString("invoice.currency"); - double saldo = invoice.getItemValueDouble("invoice.saldo"); + double saldo = invoice.getItemValueDouble(MetricDebitorService.ITEM_SALDO); double saldoOld = metricData.getItemValueDouble(MetricDebitorService.ITEM_SALDO + "." + currency); double saldoNew = InvoiceUtil.round(saldoOld + saldo); diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/metrics/MetricDebitorService.java similarity index 80% rename from office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorService.java rename to office-alexander-logistics-app/src/main/java/com/alexanderlogistics/metrics/MetricDebitorService.java index 03e3b7e..009daa2 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorService.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/metrics/MetricDebitorService.java @@ -1,7 +1,8 @@ -package com.alexanderlogistics; +package com.alexanderlogistics.metrics; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; @@ -16,6 +17,8 @@ import org.imixs.workflow.engine.ProcessingEvent; import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.QueryException; +import com.alexanderlogistics.InvoiceUtil; + import jakarta.annotation.security.DeclareRoles; import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RunAs; @@ -49,6 +52,7 @@ public class MetricDebitorService { private static Logger logger = Logger.getLogger(MetricDebitorService.class.getName()); private final ConcurrentHashMap metricCache = new ConcurrentHashMap<>(); + private final Set registeredGauges = ConcurrentHashMap.newKeySet(); public static final String TYPE_METRIC_DEBITOR = "metric.debitor"; public static final String ITEM_SALDO = "invoice.saldo"; @@ -84,6 +88,10 @@ public class MetricDebitorService { // verify if saldo has changed..... double invoiceSaldo = invoice.getItemValueDouble(ITEM_SALDO); + if (!"workitem".equals(invoice.getType())) { + // vorgang ist archiviert oder gelöscht worden => saldo = 0! + invoiceSaldo = 0.0; + } String dbtrNumber = invoice.getItemValueString("dbtr.number"); String dbtrName = invoice.getItemValueString("dbtr.name"); String currency = invoice.getItemValueString("invoice.currency"); @@ -115,7 +123,7 @@ public class MetricDebitorService { documentService.save(metricData); // Gauge registrieren - registerGauge(metricData, currency); + updateGauge(metricData, currency); } } @@ -181,7 +189,7 @@ public class MetricDebitorService { * @param dbtrName * @return */ - public ItemCollection createMetaData(String dbtrNumber, String dbtrName) { + private ItemCollection createMetaData(String dbtrNumber, String dbtrName) { ItemCollection metricData = new ItemCollection(); metricData = new ItemCollection(); @@ -192,6 +200,38 @@ public class MetricDebitorService { return metricData; } + /** + * Helper method to register a gauge for a debitor + * + * @param dbtrNumber - the debitor number + * @param dbtrName - the debitor name + */ + private void updateGauge(ItemCollection metricData, String currency) { + String dbtrNumber = metricData.getItemValueString("dbtr.number"); + String dbtrName = metricData.getItemValueString("dbtr.name"); + String metricKey = "dbtr_" + dbtrNumber + "_" + dbtrName + "_" + currency; + // Prüfen ob Gauge bereits registriert ist + if (registeredGauges.add(metricKey)) { // returns true newly added + List tags = new ArrayList<>(); + tags.add(new Tag("type", "dbtr")); + tags.add(new Tag("number", dbtrNumber)); + tags.add(new Tag("name", dbtrName)); + tags.add(new Tag("currency", currency)); + logger.info("register new metric for debitor: " + dbtrNumber + " -> " + currency); + Metadata metadata = Metadata.builder() + .withName("dbtr.invoice.saldo") + .withDescription("Debitor Balance by Currency") + .build(); + metricRegistry.gauge(metadata, + () -> metricCache.getOrDefault(dbtrNumber, createMetaData(dbtrNumber, dbtrName)) + .getItemValueDouble(ITEM_SALDO + "." + currency), + tags.toArray(new Tag[0])); + } else { + logger.fine("Gauge already registered for debitor: " + dbtrNumber + " -> " + currency); + } + + } + /** * Hilfsmethode um Metriken initial aufzubauen. Wird vom Metric Rest Service * verwendet. @@ -206,38 +246,11 @@ public class MetricDebitorService { if (itemName.startsWith(ITEM_SALDO + ".")) { // currency value found int pos = (ITEM_SALDO + ".").length(); - String currency = itemName.substring(pos + 1); - logger.info("Init metric for debitor " + dbtrNumber + " -> " + currency); - registerGauge(metric, currency); + String currency = itemName.substring(pos).toUpperCase(); + logger.info("│   │   ├──init metric for debitor " + dbtrNumber + " -> " + currency); + updateGauge(metric, currency); } } } - /** - * Helper method to register a gauge for a debitor - * - * @param dbtrNumber - the debitor number - * @param dbtrName - the debitor name - */ - private void registerGauge(ItemCollection metricData, String currency) { - String dbtrNumber = metricData.getItemValueString("dbtr.number"); - String dbtrName = metricData.getItemValueString("dbtr.name"); - List tags = new ArrayList<>(); - tags.add(new Tag("type", "dbtr")); - tags.add(new Tag("number", dbtrNumber)); - tags.add(new Tag("name", dbtrName)); - tags.add(new Tag("currency", currency)); - - logger.info("Register metric for debitor: " + dbtrNumber + " -> " + currency); - Metadata metadata = Metadata.builder() - .withName("dbtr.invoice.saldo") - .withDescription("Debitor Balance by Currency") - .build(); - - metricRegistry.gauge(metadata, - () -> metricCache.getOrDefault(dbtrNumber, createMetaData(dbtrNumber, dbtrName)) - .getItemValueDouble(ITEM_SALDO + "." + currency), - tags.toArray(new Tag[0])); - } - } \ No newline at end of file diff --git a/workflow/dwc/rechnungsausgang-dwc-1.0.1.bpmn b/workflow/dwc/rechnungsausgang-dwc-1.0.1.bpmn new file mode 100644 index 0000000..7d6a20e --- /dev/null +++ b/workflow/dwc/rechnungsausgang-dwc-1.0.1.bpmn @@ -0,0 +1,1721 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + +<itemvalue>$workflowstatus</itemvalue> + +]]> + + + In case you have already made the payment, please disregard this notice. If you require any information regarding your account, please do not hesitate to contact us.

+

Thank you for your cooperation.

+

 

+

Kind regards
Alexander Global Logistics GmbH

+ +]]>
+
+ + + + + + + + + + + + + Task_2 + ExclusiveGateway_1 + StartEvent_1 + IntermediateCatchEvent_6 + EventBasedGateway_1 + IntermediateCatchEvent_13 + IntermediateThrowEvent_11 + IntermediateThrowEvent_8 + IntermediateCatchEvent_5000-20 + Task_8 + Task_6 + Task_5 + IntermediateCatchEvent_23 + IntermediateCatchEvent_27 + EndEvent_1 + EndEvent_3 + EndEvent_2 + IntermediateCatchEvent_3 + ExclusiveGateway_5 + IntermediateCatchEvent_18 + IntermediateCatchEvent_17 + IntermediateCatchEvent_2 + IntermediateCatchEvent_5 + + TextAnnotation_1 + + + Task_1 + IntermediateCatchEvent_9 + ExclusiveGateway_3 + IntermediateCatchEvent_8 + IntermediateThrowEvent_10 + IntermediateThrowEvent_9 + + DataObject_2 + TextAnnotation_2 + IntermediateCatchEvent_1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + home]]> + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + SequenceFlow_22 + SequenceFlow_27 + + + + SequenceFlow_0 + SequenceFlow_21 + SequenceFlow_20 + SequenceFlow_33 + + + + + + + SequenceFlow_0 + + + + + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + SequenceFlow_33 + SequenceFlow_14 + SequenceFlow_15 + SequenceFlow_39 + SequenceFlow_22 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + false + + + + + SequenceFlow_20 + + + + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + SequenceFlow_52 + SequenceFlow_11 + SequenceFlow_47 + + + SequenceFlow_47 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dunning]]> + + + + + + + + + + + + false + + + + SequenceFlow_21 + + + + + + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + SequenceFlow_27 + SequenceFlow_4 + SequenceFlow_18 + SequenceFlow_34 + sequenceFlow_zhcE7Q + + + SequenceFlow_61 + + + + SequenceFlow_39 + SequenceFlow_17 + SequenceFlow_65 + SequenceFlow_13 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + false + + + + SequenceFlow_52 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + mahnlauf-de-1.0 +1000 +100 +/pages/workitems/workitem.jsf?id=dunning.ref +]]> + + + + + + + + + + + + false + + + + + + + + + + + + SequenceFlow_4 + + + DataOutput_1 + + + DataOutput_1 + + + + + SequenceFlow_18 + SequenceFlow_25 + SequenceFlow_64 + SequenceFlow_9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + false + + + + + + + SequenceFlow_42 + SequenceFlow_8 + + + + + + + + + + SequenceFlow_42 + + + + + SequenceFlow_8 + SequenceFlow_10 + SequenceFlow_12 + + + + + + + + + invoice.saldo]]> + + + + + + SequenceFlow_10 + SequenceFlow_11 + + + + + + + + + + + + + invoice.saldo]]> + + + + + + SequenceFlow_12 + SequenceFlow_14 + + + + + + + + + + SequenceFlow_17 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + home]]> + + + + + + + + + + + + false + + + + + SequenceFlow_34 + SequenceFlow_15 + + + + + + SequenceFlow_25 + + + + + + + + + + + + + + + + + + + cdtr.name invoice.number]]> + + + + + + + + + cdtr.name +Rechnungsnummer: invoice.number +Betrag: invoice.total invoice.currency + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + 7 +1. Dunning + +]]> + + + + + + + + + + + + false + + + + + + + + + + sequenceFlow_zhcE7Q + association_P36Fzw + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + SequenceFlow_60 + SequenceFlow_61 + + + + + invoice.saldo]]> + + + + + + SequenceFlow_60 + + + + + + + + + + SequenceFlow_64 + + + + + + + + SequenceFlow_65 + + + + + + + + + + + + + + + + _subject]]> + + + + + + + + + _amount (Brutto € _amount_brutto) +_description]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + home]]> + + + + + + + + + + + + false + + + + + + + SequenceFlow_9 + SequenceFlow_13 + SequenceFlow_5 + + + + + + + + + + + + + + + + + txtlastcomment]]> + + + + + + + + + _imginvoice.number dbtr.number dbtr.name (invoice.currency invoice.total) ]]> + + + true + + + + + + + + + + + + + + + $workflowgroup - $workflowstatus]]> + SequenceFlow_5 + SequenceFlow_19 + + + SequenceFlow_19 + + + + + + + + + + + + + Trigger über ZahlungseingangsSaldoAdapter + + + + + + + Triggered by MahnlaufExecuteAdapter + +Wiedervorlage wird um 7 bzw. 5 Tage erhöht + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +