diff --git a/application/pom.xml b/application/pom.xml
index 2d6dda0bbe..6d1e009d00 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -69,6 +69,10 @@
org.thingsboard.common
cluster-api
+
+ org.thingsboard.common
+ version-control
+
org.thingsboard.rule-engine
rule-engine-components
diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json
index 2772c696bf..de32604dbb 100644
--- a/application/src/main/data/json/system/widget_bundles/charts.json
+++ b/application/src/main/data/json/system/widget_bundles/charts.json
@@ -54,7 +54,7 @@
},
{
"alias": "pie",
- "name": "Pie- Flot",
+ "name": "Pie - Flot",
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAZTElEQVR42u2deXwUVbbH/e+tM/Pe5/Oe82beNjo6znvjvFFnRkFxF1f2RREQWQQEEZFNFDUCOjAoCKjsq+xG1hBCWAJkJSEJCQGyJyQkZN+7urq7lvt+1RU61dWd0Emqqm911/2cPzq9pdP1zT3nnnvO795BrGENHcYd1ldgDQss3YYoiDcr+NQkLvqQc/sGx5eL2Q9n2aePt49/lRk1gBn0jNeTk+4kKb8iab8jGX1JzkCS9xYp/YxUriMNsYQtJUSwvs4wBovjhGtXXAf2OZZF2KeNs738hO3Zh7swr9ee/5uuLOFnJP1PJG8iqVxLWtOIwFpghfrgef7yJefWdex7k20v9euapJ6DpbL4f5AmtpKFpPFMWEEW+mCJba3ciShHxAfwaN2CSRuwvCazfyJXhpHq3YRrssAyLU92O3fmBPvJHNuL/XrMk8ZgdUxj/0hyBpPaH0J4DgtBsISyUuemb5khz/aeJ73A8ljSz0nBdNKWaYFFdQjFnY6xz5igIU+6g+WxrKdJbSQRXRZYNA2Xkzt53P7mCD2QMggs2S7cI6UtQsI/mhwsp9MVuZsZ/oJ+SBkKlmwp/00qVhPBboEVlEhKwFqPeX2Q3kgFAaz22esucnMzEXkLLAOjqUvp9kmvG4NU0MCSLeMR0nTeAkv/JEJDvWPZZ7bnHjGSqmCCJdnfkrwJxFVrgaUTUyJ39AAz+FmDkaIALDkx8QtStRXfggWWxhMVu3B2UJCiBSzZsOftvGmBpc1AdkrbbKeJwZKnLqTsLbB6NRwOVLAEFynqwJIN1RMCY4HVI/dXU4WKKBqoohEsacHYh7AlFljdTChcSApWnG4asOStxsaTFlgB79Ac+sHWvw89VNELllQo8fek4jsLrNvnFFw7NlGFFO1gyVY0l7YcPU1guZxsxPxuXW/7lDH2ccP9PsQMec6x8i+OpREd9wx73rlulVRRM3pQqIEFuzaWquIIasByONC/EOBlZka+BESE4kLpdcsi/D6Hi4mSZsD6uvaXDHxKrLop5Ofy2RmoKWVee0W6//m+zIgXQwQsKcs1mJ6tazrAcrDsvBkBUoVnYgdanuE6A4ud+w68qhIs9qNZuEeqTu7fR2xscKxahjudG9YIuVdCByxYdn/C2yywblE1a0rgXol9f6pr/0601rj2bPcP1suPCxU3hMI8L7A+nYceL0xRuC1WVznXfs2MGUxYO/v+2yEFlsTWCzRUdAUbLI7DXNKzgLozsFz7vseUhhyYlysc+bLIMPCPgFJ6dMoYZDTwY+jEWEq7/AoRHGEMlig4Pl/Y45WaX7DsU8aiRhkNgzINHrCkh2ZM4M7EcvFxQNmx+EOxuZEZ2j80wZJi+dHBbZ0NJljONct7kwLwA1b/PgjPxdpqhOq+YHXE/oOeERvq5PIbrAMCyZmZDyxY8QfhCJaUBe1dbskXLOf61e57PsOmNUwui5A2sF941OuFRyL5zIv2qW9g16i9dGLm5BAEC4Zu7LACi8/OtL3wmOZgiS3Nfn8dInelQ8RyAZ0XfGYanxzPDHyaO3Uc81xogoW8fJD2fIIAltjUiOxl77Phfmasb1cg/+kx6XcxNtzoSKI+31coKoDsh0RhVaVzw2rccHy5RLS1hSZYcpmNJFUSHjOWkH8NSQHNwfKlQRVjIQcmlBbLvdHwhnziOUycXPRhoSg/ZMGCpf/Z+Bqb4AXvrN0+a6rBYEl2q+Me+Qg8Kj2npZmdMy2UwYLlTwmrdIPo2rsjmLvL/ftA/koV2ocmWLC6A6EJVp29flHKkjp7ndotFubZXnmKwoqGUAMLxVuOG6EGlkjEiOTFAw4Nee3Y6MTKJB+3yLJzpltgGZGRN6rPxyCwTpSeBFWyDTw0dEvONk7g1W4xco8Flu5WtSN0wGpgG1+NGu0BS7Y55+bXMDVqt1h+3Tb4GQssHS35F8b0vhoB1vKLK1RUyQbaEioS/RRmBVxCY4HVE8sdHwpgZdde9kuVxy1uyN7kElxqt9jrDR8LrK6sOd7cYAmiMP30zC7Akm3W2Tk3bVXq194otw3pb4GlT+vYw3rXPugLVuz1k7elSraRUa+fv+Hzb+RyOT6db4Gli1XvMitYTt45/sRbAYIl28r01Q5eXaEGtb6QAiv+J+TKSHJ9CSmaRzL7eVVQQY/U16CEKwXdvyTF8yVZ79R7NdLfulvXQlMdwYosONAtqmR7N25WRVuF2i1WVtj0l+0zAqzkfydtWV7vVrGm/SHmmr8EIC+JeCf+C2Gvk7YMKTaClDc02WQF+ZT/7NWH0bMhUS+wMPGMOf5mD8CCjYgaFVd+zo9bXLTA9GBV73TDtJok/iu5+EB7KhwnDMhTSOpvOyz7RSmZWX+kvf0Gt3HUCspgnDWkYIa7ju9D6eSLXmpS6tbVoxdYh4uO9owqpVtkefVczSecNTdYrgapGl32brDy5dIb5k/utEYv61npNlwnYm1MUbjNlkntqam/kbpxsp6jthJQF7A4gXszZlIvwYJhRVneWq52DjXV9tcGmBWspH+TJh5VHhyTk9pj/lIqdIH+u2dq4VulJ99YITlHlME0xEg/9j7SQsQmcqYBC46s91TJNuzoyKNFx3zI5RxLFpp+VZj5qDR7MXmSg1M9hNAeAwqRHU/uR2r2kbqDkltEmO+qk3LoFFc96ALW7HPztAJLthXpX7NcMN2i9mAhikc8LjrJpSd8jtz5Z4kbKPdh/ehnzruTOKskfSxok6b8lx8oe3B2gSnAym3I05Yq2aaeeqe0RV1iK9bV2kcPMh9YWOW1pkvxuMSHz6OF70m/pTSik6hoPWk6K6kpO9xBgrOaXHqqt59HhzNXtAdrVcY3eoAlu8WY0hPq38fzjuWLzQRWwk9JU5z0Pshj+W1/gJwaFmsIs/x4z35S7JV2v/QO9cekpWXNHikN0cuPVDiTdrCwjkMOXSewZPviwjKbSy1PwKcl6yqppRlY4EaOacq+6FQ0BgNHB/g9+tCWTa4vdq8NS0nxgvaaY6651zWAd2qed9AYrNNlZ3SlSraJsZPzGvLVbrGh3j52KO1gVax25+TqSPmXHVbykcIrZUguEikuP2V6LxHmqjTh4Ta8IVJcCMKg0Y2Ma++9s9aCuRqD9XFShAFgwYYeGXm0+JivW3SuXEo1WC0X/M3zpbf0PF6UfkQnYBdutN0nPtauy43EWPbzWkggDaEXrFZn6+DDw40BS7YlF/7S6mxT7/9kZdj6Pxr6m9DwqkjWe3KtvY/8+BZKwQq8lkFDm3Bi8rWGXLVbbG3R9rCdEKxu8DXkyegEC004xoMFG3Jk+KHCI6KqTUAQeik6EnZgSQI19IGFKtARR0cFBSzZFqd8AV+sDroyL2riFsMCLGTXtFMx1QysrNrsIFIl2/gTk67WX/Nxi60QzbLACmxtkUIdWNuu7Ag6WDCsHiLzD/hxixvXWGDd3q5/Th1YaOeiASzZFiZ+2sg2qVeL13IC6aYPa7CynqELLFQhDzkygh6wYCgzvFSTpXaLba32qWMtsDpPOvxM2henB6ycuitUUSXboMPD9uTuQ6eQN1yic9sGC6xOTdodpwasAwWHKASrK7eYd7VbAl1hBFblOorA+ip9JbVgwcYeH3+pVu0WCcPY351ogeVzEuIkisAKpCs1uIaW642XN/sqkTgDOxMqjMBCZwclYKHCHblvysGSbUH8wnq2Xu0WC3JtrzxpgdVRv6pFk7QGYKEN0BRUyTY6etzFap/4FLqVs9+2wGo3NALRABauk4nAkt0i0rm86CPQtX+nBZZkDbFUgIW6KHOBJdu88wtq7WqlKKGkyDbg6XAH6+YWKsDakrPdjGDJupUJFT66ldIZd9PDGiy5+SzoYKE3y6RgKVaLnNotHtwfvmDlT6UCLMPKkfWz98/NrWaq1W7xRpnt1gka4QVWziAqwII+jNnBcrvFMUmVyT6boE72o/fDDiz00NIA1qTYqd2v+Rzx+YWlu3P3brq81bdtesaZ9+Cevr+6a1HK5wMPD1VmCjbnbN1+5Xt06eiEF5RIsKGudotHIsMLrIt/oAIsXO9u1h2MK24uUb6DUpoGSt2iKCq3t+W6CbQrQmW5sKkI96CvUBYdQfUVtmu0ZWtm3OzKtptd/cEhD1bKr6gACw3K3bpyp8vi3DAdgWrytNPvymdVQIYUD70RMx7ppRZnyztnZo46Nja9Gk12ZG3Wejz0WfISlO9hHYeahSZH03dZ63Dn1ivb8xsLNJ+33LqVCeELFmqUaQBL6a0CMVSmo0DeswuEgk9yqzH/o8RPiCRWc1Z+CK4QP6KtHrfhOjGTye1lNUwtfOjE2ClovF6QsFA/t+irWykN1O+iWRRVAGhKhpTjleHk0uPkwq87mv5CYVfHhGBhKsLE4/nxVNlpvAmKW3B78slpmJYKGguRBZAnJE/R87iYCXbOjiejRAclVlgxIOOPH3WN6BHt3Wit6MZ3AVVPKC+0JJOGaKlNHgkhibxhkqILRPpk5TRTlPtRAdahoT2+crPOzsXshYsHByffsz9fipTh4HDkDtwibnjEIFD9DGVlrN3gFpelLW92tLwe/Ybeq8W/pn2lYVMU4RolrVGQV/sjqfhWEquF4AzIQ00BIpvzf0dHK+xPTDljKaN4xONITs49/4HnThADKSx4PflUgeiS477d1ZjwcIwKXBWYRljmgVJzmxn3vnpLUdcBHTaokrakkvoo94T3uSRpBJ3IS09KEx48lEEz1k+pAKtn7YSYhwqaCuH4wIdyRQY3h/vBHHhCGyreH1kJ1WtBGw68wJPlzT4UiM6LX6A5VXC+qtTDyjTnq4fsC8461mc6owq5tJt8YaNQw4iMi/DGHKoFVwvVNQiHeFxt0Ryp0bTd1WrUbg89SxrAAgQ9kPRAHyJeixhcef/e3P24c3Xmt55oDD+WtZapTndCTD311HS8Q2pVGpaWUKZEGkJbqoZHvYrFqfLP3HPVddfati7s7rVt/Xbaxh9jP09y7MhxnSrlMqv50mahkRUdHA7pMIQ8HMBku0Iaz0i6WTdWSdLwODkHMjXpD5Hk/whYAv4uKsDC6qy7PQ7ykYV78/arHkISC/fvurZH/nHa6RnEWyIQ01hJc+muXOkJ2ISR979XZ35jczFadmEcGlZp80plxV3n7lnf1jVYgdgTu5jJMezSFMfeq64TJRJ5ZS1ik0N08kaRJwd5IK96d0eQB/IuPugO8txgQdiNBrDkFFTghgwWXoXQGycMeExe+k05NQ0ZBBiYW5+9EXMVnolTnDyv/TgxAndiwsNteMOUmxeQPoUYCTKuGoKFHKzyD7xUzf/vRg2ouq09tLXt9cP2j887NmS6oou41Eq+qFGok1ytaJSrZaRVrS2bCrAikhd167JhBvJ9E0w/8qNIZV1vaa9gRIYdGzuqVadMlbxz3MA2ELd80kcJn2hF1YnrXmVuZS3Cn7fZDKAqEHvQTd4n8Y5v0p2H8rkLlVKQV2cX25wiJxCqhgZgKaNvDSulxsVMDMSrQq9Bw4p7TJzKPw3h0dO7GUqouq3dt77tlf3MO7Hs12nOfddc58v4q3VCRavY6pRcrWg6sLArHALVDVKmI3W5UvSB5cjwg3azUBWI/X5z24DIjiDvXBl/rV7wBHkibWBFl8SEAFWzz81X9kwjlH47hg0lqgKxR7YzWNVevMlTARa2is1O1fiYt1Qpq0UJjnCjymMplXSAZa72L7+p2jaXl5DpxkuusKUKVsuIVICFTQ/PSs10hpRVNVPj5dmLuF+vC1+q/m+zjZYYy9TVydjkVv4h2KX57Ya2cJ6uhh6wUwQW5aIgnVl8RaLyr0BO6MEttnCmCjYvzkERWEeKokxH1Z7cvco/AXvJj+9kwpwq2O6rLorAKm4uNhdVqzLWKD+/nSNwARZVsNx6gSKwkAFC+5RZqPowYaGyXwPbcJOPsxZSsPs32bTalNRM3BZVnaagasqp6aq+Z9RXWUjJNuaoZmeAaQYWKhTop2pU9FgUzis/9pp0p8WTx1amOqkDCwfa0J6yOjKs3l0N0bHmKODutnhSWEY1Tx1Y8C/BPfLktoYKQeUHTq7gf7PegqnD/rjNpmHVl5aHNOHsU2qpSqvyUvHLbxD+EPYpK5XNPcPSVd3gGWfK4+ikCi38ys9ZZRMf/d6iSm3HizhKwTL+IMxADCXO3h9SfGGflQhVG6ICfDOUgtWDMmW9DU36yto91O++cdRKhPqxt0+w2pKgMVhny8/TQ9XMuFnKdlPwNeeMlQj1b2hWoxosNNiMiKJibYhDmvBhlJ/trylWyqrT9aDmvRgag0WkdtNvgk4V6sMgdaT8VHuvuiyAOrPFiQ7NMdAeLBo2pMtby72Wq2g3XWcB1KmhmccEYGGg4y+IVKGRVflhcmqF322ykgudGhoVKW3/8h3Q7AsWVcdLTig/STlN7aZ0GqZz04CFQ7b005/twjZd3qJqN312j5Wy6srw/eikGaELWMRbr9YY+wJakoqUlYMnIw5aKavb2F6N6kWNAwviad1VU+5du+k8Vbvp9FgrZXUb67PDxnLEZGBhHCw06DxfSHOr2k2XJBpRu4d+npkn2VUXnRHxjs62iXDxVH36D221QUALSTWIaYVGebvRYDl5FxQ79KYKCmnYo1T+3i1ZRqSssI19vdlrjlQmYCF7NOsUe76cRyHKD7kuZe3vjVYBC1UIxbQ4xL47JLagvGX8CgNiJ7oK1OgIFrmlpK1ru2mVzesMHGxNGJOyinbXAnyb7vyfjW399zKVbZKE1ZO7GJkem0t0/2tJH0kJ1oRjdjyAplB8SMgPQQoLdy5NdmbV8AaDBalLXS+9vmAh7plxRsdeVpXUVnqVce2mQArikZ5ft/2y5FZkHRGA9WMeB4YmRrMqsKbGsJjbZHFACAzBZaPnDLpqow4bus4YGMnorWp0h87vT7LrLutE1dkb57wy/k0CwpdgxSuoR8VneHm/V6Q1LsquAuuR7dJkFpnr2nDJiRkOzz9bxkfmGrrdhGpsTfRkggwWxtLU5Xq3mzbYxaeCoZA2JYZFSAcpUdHtFlWP+oIl97AfLeBiiqUpDSJp+OQG/z98eM5hwEU3AixoskP/WEOqVqSvUr4/2k2HBandFLo0jDucQjLWN8HhFyyl/Abar1HJgynk4e02Y0JDQIyPGiJguaP4WK2o+iBe3W46JdgKaYjZ8+oFBE+DIpnAwdqZ44IDHfCDFPjjaRAPMiCjC+VSY664QWAhJ66J/uzkU9NU7aYLzwWn3fSBLTZMOZ4fITiLD7Mi1RkgWHCImGif2cMk3eBPl3K/32zDJUcaQtfPPP6YnRg17jDsN+EUiV624Y+KHqNqN/0uI2i1e/Bi2DW6/1bdBBpffSnvDCysCiH+CQla3MY2+V+SpVfNj3Og6ly/D/ynbbZaRgxBsDBwvlLv2k3rle92tDCY7aabsqT8QkYVj1wUwnZMPwi2VM0/nYE19qi9oEG4b0P7cjK2hEMvA3SOURel30rwdCln5LU2FCyMrzN6qN1d1FysfB/oZN4XVIW0e9e34UQdOXKXCKvmh/xoDzx493z4wT8y1bb28H/0Eb1irIh4h8EX2miwcAxOD+T/cAKF8k0QKSvjmyAaRCX7fm/rZSHhPe43uVe3tuyX9jMsR0IcLCJJnNV065zBHwsOKl+O/+/HrHbT7uQXcLiG8Vc5CGARt4J3gIcM4uxn5QtxtsdL+63avW60oaZW8kG5xMEBCyOqOPq2VH2c9Jmq3RRRi4ULJYUxlIKFseHypi6owgH3vGC1m/bcliQ5gnhxgwkWah+WpX3pv900+k3UoCqf/FWq1W7aDZt9mhVEEqZgub0bF5G82F+7abPyacjxWKx0S4iBD/Ypc0EGS05ALEhYqATLc16hPFBYco+lkNYdHVEHH/SrSgFYMlsfJ0XIVGXWZCkfstpNu0uVJ2drgSUNdENAAulYSbTyTpSHo57EwiVAe+s4S8NcRRdYUrwleqWHcTojasktXAI/qoQX6bmYNIHl7RzJq4eslFWghn4ykbIrSClYM09ZKatA98KDmAU1H1iJN/g/WmIeAZRYBWvHxqxgYdxsE30LUSzzmKem2QKrB0tFIrd0Wqaq2sPXYudovnR0gyWPY0VcEBsGKXR/BteChixYGPV2EdsUFlUo7pDLTS2wtBzoY3kgXM8pwVIGDdOieS6WmcAi7gN257o7PMMHKVQ/o3e5ySGa60qZDCx5QJslTE7axdIvs5o34zUyJVjEXfd3II/rG7rF75CiOFzACaJJr49pwZKHSyAH8zlZlSpkDP8t2y67nLypr4zJwfLgBakqCE2FwCy1I8fl4EPgmoQEWPJAqwVU9tDzacbQHh8bWoTmdXyhDJZnFDUJixIcpjhA9cEtNuTQc+uF0LsKIQiWPND7C3GzGbEsdGZp4wmypZBcO1nCuYRQ/fpDFyzPQKkuNoWgihb05CqSnJBSxlpP27NMLbCCPFBgmV0jrM1wojDcMA1cFOyjYhEiR/jVghg+X3Y4gaUcWMxfrhWwloSiFYpzNOQMJEGTGLlytKwheOKF8PyCwxUs38kM4tgomkPSFSdNoH58UjSLmQY6ETg/AusACKzBk3oM4sfP72NeO2yHvPYHZx1opoVWEV5ew4jWl2mBZQ0LLGuYbfw/UfikHjIsMFkAAAAASUVORK5CYII=",
"description": "Displays latest values of the attributes or timeseries data for multiple entities in a pie chart. Supports numeric values only.",
"descriptor": {
@@ -69,7 +69,7 @@
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-flot-pie-widget-settings",
"dataKeySettingsDirective": "tb-flot-pie-key-settings",
- "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"showPercentages\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie- Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"showPercentages\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
}
},
{
diff --git a/application/src/main/data/upgrade/3.3.4/schema_update.sql b/application/src/main/data/upgrade/3.3.4/schema_update.sql
index 7584d2f732..69df2afdc9 100644
--- a/application/src/main/data/upgrade/3.3.4/schema_update.sql
+++ b/application/src/main/data/upgrade/3.3.4/schema_update.sql
@@ -14,6 +14,40 @@
-- limitations under the License.
--
+ALTER TABLE device
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE device_profile
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE asset
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE rule_chain
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE rule_node
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE dashboard
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE customer
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE widgets_bundle
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE entity_view
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+
+CREATE INDEX IF NOT EXISTS idx_device_external_id ON device(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_device_profile_external_id ON device_profile(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_asset_external_id ON asset(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_rule_chain_external_id ON rule_chain(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_rule_node_external_id ON rule_node(rule_chain_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_dashboard_external_id ON dashboard(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_customer_external_id ON customer(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_widgets_bundle_external_id ON widgets_bundle(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_entity_view_external_id ON entity_view(tenant_id, external_id);
+
+CREATE INDEX IF NOT EXISTS idx_rule_node_type ON rule_node(type);
+
+ALTER TABLE admin_settings
+ ADD COLUMN IF NOT EXISTS tenant_id uuid NOT NULL DEFAULT '13814000-1dd2-11b2-8080-808080808080';
+
CREATE TABLE IF NOT EXISTS queue (
id uuid NOT NULL CONSTRAINT queue_pkey PRIMARY KEY,
created_time bigint NOT NULL,
@@ -35,3 +69,4 @@ CREATE TABLE IF NOT EXISTS user_auth_settings (
user_id uuid UNIQUE NOT NULL CONSTRAINT fk_user_auth_settings_user_id REFERENCES tb_user(id),
two_fa_settings varchar
);
+
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index 500ad60524..733cf75e64 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -48,7 +48,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
-import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
+import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
diff --git a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
index e2e6f8d982..86675a79a8 100644
--- a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
+++ b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
@@ -15,17 +15,21 @@
*/
package org.thingsboard.server.config;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
-import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
+import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.service.security.model.SecurityUser;
@@ -35,42 +39,40 @@ import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
+import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+@Slf4j
@Component
public class RateLimitProcessingFilter extends GenericFilterBean {
- @Value("${server.rest.limits.tenant.enabled:false}")
- private boolean perTenantLimitsEnabled;
- @Value("${server.rest.limits.tenant.configuration:}")
- private String perTenantLimitsConfiguration;
- @Value("${server.rest.limits.customer.enabled:false}")
- private boolean perCustomerLimitsEnabled;
- @Value("${server.rest.limits.customer.configuration:}")
- private String perCustomerLimitsConfiguration;
-
@Autowired
private ThingsboardErrorResponseHandler errorResponseHandler;
- private ConcurrentMap perTenantLimits = new ConcurrentHashMap<>();
- private ConcurrentMap perCustomerLimits = new ConcurrentHashMap<>();
+ @Autowired
+ @Lazy
+ private TbTenantProfileCache tenantProfileCache;
+
+ private final ConcurrentMap perTenantLimits = new ConcurrentHashMap<>();
+ private final ConcurrentMap perCustomerLimits = new ConcurrentHashMap<>();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
SecurityUser user = getCurrentUser();
if (user != null && !user.isSystemAdmin()) {
- if (perTenantLimitsEnabled) {
- TbRateLimits rateLimits = perTenantLimits.computeIfAbsent(user.getTenantId(), id -> new TbRateLimits(perTenantLimitsConfiguration));
- if (!rateLimits.tryConsume()) {
- errorResponseHandler.handle(new TbRateLimitsException(EntityType.TENANT), (HttpServletResponse) response);
- return;
- }
+ var profile = tenantProfileCache.get(user.getTenantId());
+ if (profile == null) {
+ log.debug("[{}] Failed to lookup tenant profile", user.getTenantId());
+ errorResponseHandler.handle(new BadCredentialsException("Failed to lookup tenant profile"), (HttpServletResponse) response);
+ return;
+ }
+ var profileConfiguration = profile.getDefaultProfileConfiguration();
+ if (!checkRateLimits(user.getTenantId(), profileConfiguration.getTenantServerRestLimitsConfiguration(), perTenantLimits, response)) {
+ return;
}
- if (perCustomerLimitsEnabled && user.isCustomerUser()) {
- TbRateLimits rateLimits = perCustomerLimits.computeIfAbsent(user.getCustomerId(), id -> new TbRateLimits(perCustomerLimitsConfiguration));
- if (!rateLimits.tryConsume()) {
- errorResponseHandler.handle(new TbRateLimitsException(EntityType.CUSTOMER), (HttpServletResponse) response);
+ if (user.isCustomerUser()) {
+ if (!checkRateLimits(user.getCustomerId(), profileConfiguration.getCustomerServerRestLimitsConfiguration(), perCustomerLimits, response)) {
return;
}
}
@@ -78,6 +80,25 @@ public class RateLimitProcessingFilter extends GenericFilterBean {
chain.doFilter(request, response);
}
+ private boolean checkRateLimits(I ownerId, String rateLimitConfig, Map rateLimitsMap, ServletResponse response) {
+ if (StringUtils.isNotEmpty(rateLimitConfig)) {
+ TbRateLimits rateLimits = rateLimitsMap.get(ownerId);
+ if (rateLimits == null || !rateLimits.getConfiguration().equals(rateLimitConfig)) {
+ rateLimits = new TbRateLimits(rateLimitConfig);
+ rateLimitsMap.put(ownerId, rateLimits);
+ }
+
+ if (!rateLimits.tryConsume()) {
+ errorResponseHandler.handle(new TbRateLimitsException(ownerId.getEntityType()), (HttpServletResponse) response);
+ return false;
+ }
+ } else {
+ rateLimitsMap.remove(ownerId);
+ }
+
+ return true;
+ }
+
protected SecurityUser getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof SecurityUser) {
diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
index e5934cbf26..54cbdd9b0f 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
@@ -16,16 +16,16 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-import org.springframework.web.bind.annotation.ResponseBody;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings;
@@ -34,14 +34,18 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
+import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
+import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
+import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
+import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsService;
import org.thingsboard.server.service.update.UpdateService;
-import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
+import static org.thingsboard.server.controller.ControllerConstants.*;
@RestController
@TbCoreComponent
@@ -60,6 +64,12 @@ public class AdminController extends BaseController {
@Autowired
private SystemSecurityService systemSecurityService;
+ @Autowired
+ private EntitiesVersionControlService versionControlService;
+
+ @Autowired
+ private TbAutoCommitSettingsService autoCommitSettingsService;
+
@Autowired
private UpdateService updateService;
@@ -96,6 +106,7 @@ public class AdminController extends BaseController {
@RequestBody AdminSettings adminSettings) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
+ adminSettings.setTenantId(getTenantId());
adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings));
if (adminSettings.getKey().equals("mail")) {
mailService.updateMailConfiguration();
@@ -180,6 +191,137 @@ public class AdminController extends BaseController {
}
}
+ @ApiOperation(value = "Get repository settings (getRepositorySettings)",
+ notes = "Get the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @GetMapping("/repositorySettings")
+ @ResponseBody
+ public RepositorySettings getRepositorySettings() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ RepositorySettings versionControlSettings = checkNotNull(versionControlService.getVersionControlSettings(getTenantId()));
+ versionControlSettings.setPassword(null);
+ versionControlSettings.setPrivateKey(null);
+ versionControlSettings.setPrivateKeyPassword(null);
+ return versionControlSettings;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "Check repository settings exists (repositorySettingsExists)",
+ notes = "Check whether the repository settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @GetMapping("/repositorySettings/exists")
+ @ResponseBody
+ public Boolean repositorySettingsExists() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ return versionControlService.getVersionControlSettings(getTenantId()) != null;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "Creates or Updates the repository settings (saveRepositorySettings)",
+ notes = "Creates or Updates the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @PostMapping("/repositorySettings")
+ public DeferredResult saveRepositorySettings(@RequestBody RepositorySettings settings) throws ThingsboardException {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE);
+ ListenableFuture future = versionControlService.saveVersionControlSettings(getTenantId(), settings);
+ return wrapFuture(Futures.transform(future, savedSettings -> {
+ savedSettings.setPassword(null);
+ savedSettings.setPrivateKey(null);
+ savedSettings.setPrivateKeyPassword(null);
+ return savedSettings;
+ }, MoreExecutors.directExecutor()));
+ }
+
+ @ApiOperation(value = "Delete repository settings (deleteRepositorySettings)",
+ notes = "Deletes the repository settings."
+ + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/repositorySettings", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public DeferredResult deleteRepositorySettings() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE);
+ return wrapFuture(versionControlService.deleteVersionControlSettings(getTenantId()));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+
+ @ApiOperation(value = "Check repository access (checkRepositoryAccess)",
+ notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/repositorySettings/checkAccess", method = RequestMethod.POST)
+ public DeferredResult checkRepositoryAccess(
+ @ApiParam(value = "A JSON value representing the Repository Settings.")
+ @RequestBody RepositorySettings settings) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ settings = checkNotNull(settings);
+ return wrapFuture(versionControlService.checkVersionControlAccess(getTenantId(), settings));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "Get auto commit settings (getAutoCommitSettings)",
+ notes = "Get the auto commit settings object. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @GetMapping("/autoCommitSettings")
+ @ResponseBody
+ public AutoCommitSettings getAutoCommitSettings() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ return checkNotNull(autoCommitSettingsService.get(getTenantId()));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "Check auto commit settings exists (autoCommitSettingsExists)",
+ notes = "Check whether the auto commit settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @GetMapping("/autoCommitSettings/exists")
+ @ResponseBody
+ public Boolean autoCommitSettingsExists() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ return autoCommitSettingsService.get(getTenantId()) != null;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "Creates or Updates the auto commit settings (saveAutoCommitSettings)",
+ notes = "Creates or Updates the auto commit settings object. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @PostMapping("/autoCommitSettings")
+ public AutoCommitSettings saveAutoCommitSettings(@RequestBody AutoCommitSettings settings) throws ThingsboardException {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE);
+ return autoCommitSettingsService.save(getTenantId(), settings);
+ }
+
+ @ApiOperation(value = "Delete auto commit settings (deleteAutoCommitSettings)",
+ notes = "Deletes the auto commit settings."
+ + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/autoCommitSettings", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deleteAutoCommitSettings() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE);
+ autoCommitSettingsService.delete(getTenantId());
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
@ApiOperation(value = "Check for new Platform Releases (checkUpdates)",
notes = "Check notifications about new platform releases. "
+ SYSTEM_AUTHORITY_PARAGRAPH)
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
index 4718feb774..2a3268146f 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -51,9 +51,9 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.asset.AssetBulkImportService;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportRequest;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.service.entitiy.asset.TbAssetService;
-import org.thingsboard.server.service.importing.BulkImportRequest;
-import org.thingsboard.server.service.importing.BulkImportResult;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index ebf9f1d37b..b269e66cad 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -18,6 +18,10 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -29,6 +33,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
@@ -138,6 +143,7 @@ import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.state.DeviceStateService;
+import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
@@ -281,6 +287,9 @@ public abstract class BaseController {
@Autowired
protected QueueService queueService;
+ @Autowired
+ protected EntitiesVersionControlService vcService;
+
@Value("${server.log_controller_error_stack_trace}")
@Getter
private boolean logControllerErrorStackTrace;
@@ -958,4 +967,20 @@ public abstract class BaseController {
return MediaType.APPLICATION_OCTET_STREAM;
}
}
+
+ protected DeferredResult wrapFuture(ListenableFuture future) {
+ final DeferredResult deferredResult = new DeferredResult<>();
+ Futures.addCallback(future, new FutureCallback<>() {
+ @Override
+ public void onSuccess(T result) {
+ deferredResult.setResult(result);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ deferredResult.setErrorResult(t);
+ }
+ }, MoreExecutors.directExecutor());
+ return deferredResult;
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
index a26493000a..63be9a437a 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
@@ -135,6 +135,8 @@ public class ControllerConstants {
protected static final String EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION = "Assignment works in async way - first, notification event pushed to edge service queue on platform. ";
protected static final String EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION = "(Edge will receive this instantly, if it's currently connected, or once it's going to be connected to platform). ";
+ protected static final String ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the entity version name.";
+
protected static final String MARKDOWN_CODE_BLOCK_START = "```json\n";
protected static final String MARKDOWN_CODE_BLOCK_END = "\n```";
protected static final String EVENT_ERROR_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START +
diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
index 47acdc64a4..d0f40a476b 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -631,7 +631,7 @@ public class DashboardController extends BaseController {
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
checkDashboardId(dashboardId, Operation.READ);
- return tbDashboardService.asignDashboardToEdge(dashboardId, edge, getCurrentUser());
+ return tbDashboardService.assignDashboardToEdge(dashboardId, edge, getCurrentUser());
}
@ApiOperation(value = "Unassign dashboard from edge (unassignDashboardFromEdge)",
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index 793ea64336..2881787d95 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -66,9 +66,9 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.device.DeviceBulkImportService;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportRequest;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.service.entitiy.device.TbDeviceService;
-import org.thingsboard.server.service.importing.BulkImportRequest;
-import org.thingsboard.server.service.importing.BulkImportResult;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java
index 5d9737db23..4dcffa605e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java
@@ -52,8 +52,8 @@ import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeBulkImportService;
import org.thingsboard.server.service.entitiy.edge.TbEdgeService;
-import org.thingsboard.server.service.importing.BulkImportRequest;
-import org.thingsboard.server.service.importing.BulkImportResult;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportRequest;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
new file mode 100644
index 0000000000..701719af58
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
@@ -0,0 +1,368 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.StringUtils;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.common.data.sync.vc.EntityDataDiff;
+import org.thingsboard.server.common.data.sync.vc.EntityDataInfo;
+import org.thingsboard.server.common.data.sync.vc.EntityVersion;
+import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
+import org.thingsboard.server.common.data.sync.vc.EntityTypeLoadResult;
+import org.thingsboard.server.common.data.sync.vc.VersionLoadResult;
+import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
+import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest;
+import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.permission.Operation;
+import org.thingsboard.server.service.security.permission.Resource;
+import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
+import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
+
+@RestController
+@TbCoreComponent
+@RequestMapping("/api/entities/vc")
+@PreAuthorize("hasAuthority('TENANT_ADMIN')")
+@RequiredArgsConstructor
+public class EntitiesVersionControlController extends BaseController {
+
+ private final EntitiesVersionControlService versionControlService;
+
+
+ @ApiOperation(value = "", notes = "" +
+ "SINGLE_ENTITY:" + NEW_LINE +
+ "```\n{\n" +
+ " \"type\": \"SINGLE_ENTITY\",\n" +
+ "\n" +
+ " \"versionName\": \"Version 1.0\",\n" +
+ " \"branch\": \"dev\",\n" +
+ "\n" +
+ " \"entityId\": {\n" +
+ " \"entityType\": \"DEVICE\",\n" +
+ " \"id\": \"b79448e0-d4f4-11ec-847b-0f432358ab48\"\n" +
+ " },\n" +
+ " \"config\": {\n" +
+ " \"saveRelations\": true\n" +
+ " }\n" +
+ "}\n```" + NEW_LINE +
+ "COMPLEX:" + NEW_LINE +
+ "```\n{\n" +
+ " \"type\": \"COMPLEX\",\n" +
+ "\n" +
+ " \"versionName\": \"Devices and profiles: release 2\",\n" +
+ " \"branch\": \"master\",\n" +
+ "\n" +
+ " \"syncStrategy\": \"OVERWRITE\",\n" +
+ " \"entityTypes\": {\n" +
+ " \"DEVICE\": {\n" +
+ " \"syncStrategy\": null,\n" +
+ " \"allEntities\": true,\n" +
+ " \"saveRelations\": true\n" +
+ " },\n" +
+ " \"DEVICE_PROFILE\": {\n" +
+ " \"syncStrategy\": \"MERGE\",\n" +
+ " \"allEntities\": false,\n" +
+ " \"entityIds\": [\n" +
+ " \"b79448e0-d4f4-11ec-847b-0f432358ab48\"\n" +
+ " ],\n" +
+ " \"saveRelations\": true\n" +
+ " }\n" +
+ " }\n" +
+ "}\n```")
+ @PostMapping("/version")
+ public DeferredResult saveEntitiesVersion(@RequestBody VersionCreateRequest request) throws ThingsboardException {
+ SecurityUser user = getCurrentUser();
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE);
+ return wrapFuture(versionControlService.saveEntitiesVersion(user, request));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "", notes = "" +
+ "```\n[\n" +
+ " {\n" +
+ " \"id\": \"c30c8bcaed3f0813649f0dee51a89d04d0a12b28\",\n" +
+ " \"name\": \"Device profile 1 version 1.0\"\n" +
+ " }\n" +
+ "]\n```")
+ @GetMapping(value = "/version/{branch}/{entityType}/{externalEntityUuid}", params = {"pageSize", "page"})
+ public DeferredResult> listEntityVersions(@PathVariable String branch,
+ @PathVariable EntityType entityType,
+ @PathVariable UUID externalEntityUuid,
+ @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
+ @RequestParam int pageSize,
+ @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
+ @RequestParam int page,
+ @ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION)
+ @RequestParam(required = false) String textSearch,
+ @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp")
+ @RequestParam(required = false) String sortProperty,
+ @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ EntityId externalEntityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid);
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return wrapFuture(versionControlService.listEntityVersions(getTenantId(), branch, externalEntityId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "", notes = "" +
+ "```\n[\n" +
+ " {\n" +
+ " \"id\": \"c30c8bcaed3f0813649f0dee51a89d04d0a12b28\",\n" +
+ " \"name\": \"Device profiles from dev\"\n" +
+ " }\n" +
+ "]\n```")
+ @GetMapping(value = "/version/{branch}/{entityType}", params = {"pageSize", "page"})
+ public DeferredResult> listEntityTypeVersions(@PathVariable String branch,
+ @PathVariable EntityType entityType,
+ @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
+ @RequestParam int pageSize,
+ @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
+ @RequestParam int page,
+ @ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION)
+ @RequestParam(required = false) String textSearch,
+ @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp")
+ @RequestParam(required = false) String sortProperty,
+ @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return wrapFuture(versionControlService.listEntityTypeVersions(getTenantId(), branch, entityType, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "", notes = "" +
+ "```\n[\n" +
+ " {\n" +
+ " \"id\": \"ba9baaca1742b730e7331f31a6a51da5fc7da7f7\",\n" +
+ " \"name\": \"Device 1 removed\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"id\": \"b3c28d722d328324c7c15b0b30047b0c40011cf7\",\n" +
+ " \"name\": \"Device profiles added\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"id\": \"c30c8bcaed3f0813649f0dee51a89d04d0a12b28\",\n" +
+ " \"name\": \"Devices added\"\n" +
+ " }\n" +
+ "]\n```")
+ @GetMapping(value = "/version/{branch}", params = {"pageSize", "page"})
+ public DeferredResult> listVersions(@PathVariable String branch,
+ @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
+ @RequestParam int pageSize,
+ @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
+ @RequestParam int page,
+ @ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION)
+ @RequestParam(required = false) String textSearch,
+ @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp")
+ @RequestParam(required = false) String sortProperty,
+ @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return wrapFuture(versionControlService.listVersions(getTenantId(), branch, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+
+ @GetMapping("/entity/{branch}/{entityType}/{versionId}")
+ public DeferredResult> listEntitiesAtVersion(@PathVariable String branch,
+ @PathVariable EntityType entityType,
+ @PathVariable String versionId) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ return wrapFuture(versionControlService.listEntitiesAtVersion(getTenantId(), branch, versionId, entityType));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @GetMapping("/entity/{branch}/{versionId}")
+ public DeferredResult> listAllEntitiesAtVersion(@PathVariable String branch,
+ @PathVariable String versionId) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ return wrapFuture(versionControlService.listAllEntitiesAtVersion(getTenantId(), branch, versionId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @GetMapping("/info/{versionId}/{entityType}/{externalEntityUuid}")
+ public DeferredResult getEntityDataInfo(@PathVariable String versionId,
+ @PathVariable EntityType entityType,
+ @PathVariable UUID externalEntityUuid) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid);
+ return wrapFuture(versionControlService.getEntityDataInfo(getCurrentUser(), entityId, versionId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @GetMapping("/diff/{branch}/{entityType}/{internalEntityUuid}")
+ public DeferredResult compareEntityDataToVersion(@PathVariable String branch,
+ @PathVariable EntityType entityType,
+ @PathVariable UUID internalEntityUuid,
+ @RequestParam String versionId) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, internalEntityUuid);
+ return wrapFuture(versionControlService.compareEntityDataToVersion(getCurrentUser(), branch, entityId, versionId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "", notes = "" +
+ "SINGLE_ENTITY:" + NEW_LINE +
+ "```\n{\n" +
+ " \"type\": \"SINGLE_ENTITY\",\n" +
+ " \n" +
+ " \"branch\": \"dev\",\n" +
+ " \"versionId\": \"b3c28d722d328324c7c15b0b30047b0c40011cf7\",\n" +
+ " \n" +
+ " \"externalEntityId\": {\n" +
+ " \"entityType\": \"DEVICE\",\n" +
+ " \"id\": \"b7944123-d4f4-11ec-847b-0f432358ab48\"\n" +
+ " },\n" +
+ " \"config\": {\n" +
+ " \"loadRelations\": false,\n" +
+ " \"findExistingEntityByName\": false\n" +
+ " }\n" +
+ "}\n```" + NEW_LINE +
+ "ENTITY_TYPE:" + NEW_LINE +
+ "```\n{\n" +
+ " \"type\": \"ENTITY_TYPE\",\n" +
+ "\n" +
+ " \"branch\": \"dev\",\n" +
+ " \"versionId\": \"b3c28d722d328324c7c15b0b30047b0c40011cf7\",\n" +
+ "\n" +
+ " \"entityTypes\": {\n" +
+ " \"DEVICE\": {\n" +
+ " \"loadRelations\": false,\n" +
+ " \"findExistingEntityByName\": false,\n" +
+ " \"removeOtherEntities\": true\n" +
+ " }\n" +
+ " }\n" +
+ "}\n```")
+ @PostMapping("/entity")
+ public DeferredResult loadEntitiesVersion(@RequestBody VersionLoadRequest request) throws ThingsboardException {
+ SecurityUser user = getCurrentUser();
+ try {
+ accessControlService.checkPermission(user, Resource.VERSION_CONTROL, Operation.READ);
+ return wrapFuture(versionControlService.loadEntitiesVersion(user, request));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+
+ @ApiOperation(value = "", notes = "" +
+ "```\n[\n" +
+ " {\n" +
+ " \"name\": \"master\",\n" +
+ " \"default\": true\n" +
+ " },\n" +
+ " {\n" +
+ " \"name\": \"dev\",\n" +
+ " \"default\": false\n" +
+ " },\n" +
+ " {\n" +
+ " \"name\": \"dev-2\",\n" +
+ " \"default\": false\n" +
+ " }\n" +
+ "]\n\n```")
+ @GetMapping("/branches")
+ public DeferredResult> listBranches() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ final TenantId tenantId = getTenantId();
+ ListenableFuture> branches = versionControlService.listBranches(tenantId);
+ return wrapFuture(Futures.transform(branches, remoteBranches -> {
+ List infos = new ArrayList<>();
+
+ String defaultBranch = versionControlService.getVersionControlSettings(tenantId).getDefaultBranch();
+ if (StringUtils.isNotEmpty(defaultBranch)) {
+ infos.add(new BranchInfo(defaultBranch, true));
+ }
+
+ remoteBranches.forEach(branch -> {
+ if (!branch.equals(defaultBranch)) {
+ infos.add(new BranchInfo(branch, false));
+ }
+ });
+ return infos;
+ }, MoreExecutors.directExecutor()));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @Data
+ public static class BranchInfo {
+ private final String name;
+ private final boolean isDefault;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
index 190d68e79e..a588004b79 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
@@ -233,7 +233,6 @@ public class RuleChainController extends BaseController {
public RuleChain saveRuleChain(
@ApiParam(value = "A JSON value representing the rule chain.")
@RequestBody RuleChain ruleChain) throws ThingsboardException {
-
ruleChain.setTenantId(getCurrentUser().getTenantId());
checkEntity(ruleChain.getId(), ruleChain, Resource.RULE_CHAIN);
return tbRuleChainService.save(ruleChain, getCurrentUser());
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
index 5f0ea800dd..578bfa5662 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
@@ -16,12 +16,12 @@
package org.thingsboard.server.controller.plugin;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanCreationNotAllowedException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
-import org.springframework.util.StringUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PongMessage;
import org.springframework.web.socket.TextMessage;
@@ -34,10 +34,10 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.config.WebSocketConfiguration;
+import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
-import org.thingsboard.server.service.telemetry.DefaultTelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.SessionEvent;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketMsgEndpoint;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
@@ -72,22 +72,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
@Autowired
private TelemetryWebSocketService webSocketService;
+ @Autowired
+ private TbTenantProfileCache tenantProfileCache;
+
@Value("${server.ws.send_timeout:5000}")
private long sendTimeout;
- @Value("${server.ws.limits.max_sessions_per_tenant:0}")
- private int maxSessionsPerTenant;
- @Value("${server.ws.limits.max_sessions_per_customer:0}")
- private int maxSessionsPerCustomer;
- @Value("${server.ws.limits.max_sessions_per_regular_user:0}")
- private int maxSessionsPerRegularUser;
- @Value("${server.ws.limits.max_sessions_per_public_user:0}")
- private int maxSessionsPerPublicUser;
- @Value("${server.ws.limits.max_queue_per_ws_session:1000}")
- private int maxMsgQueuePerSession;
-
- @Value("${server.ws.limits.max_updates_per_session:}")
- private String perSessionUpdatesConfiguration;
-
@Value("${server.ws.ping_timeout:30000}")
private long pingTimeout;
@@ -144,10 +133,13 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
String internalSessionId = session.getId();
TelemetryWebSocketSessionRef sessionRef = toRef(session);
String externalSessionId = sessionRef.getSessionId();
+
if (!checkLimits(session, sessionRef)) {
return;
}
- internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef, maxMsgQueuePerSession));
+ var tenantProfileConfiguration = tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()).getDefaultProfileConfiguration();
+ internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef, tenantProfileConfiguration.getWsMsgQueueLimitPerSession() > 0 ?
+ tenantProfileConfiguration.getWsMsgQueueLimitPerSession() : 500));
externalSessionMap.put(externalSessionId, internalSessionId);
processInWebSocketService(sessionRef, SessionEvent.onEstablished());
@@ -323,8 +315,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
if (internalId != null) {
SessionMetaData sessionMd = internalSessionMap.get(internalId);
if (sessionMd != null) {
- if (!StringUtils.isEmpty(perSessionUpdatesConfiguration)) {
- TbRateLimits rateLimits = perSessionUpdateLimits.computeIfAbsent(sessionRef.getSessionId(), sid -> new TbRateLimits(perSessionUpdatesConfiguration));
+ var tenantProfileConfiguration = tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()).getDefaultProfileConfiguration();
+ if (StringUtils.isNotEmpty(tenantProfileConfiguration.getWsUpdatesPerSessionRateLimit())) {
+ TbRateLimits rateLimits = perSessionUpdateLimits.computeIfAbsent(sessionRef.getSessionId(), sid -> new TbRateLimits(tenantProfileConfiguration.getWsUpdatesPerSessionRateLimit()));
if (!rateLimits.tryConsume()) {
if (blacklistedSessions.putIfAbsent(externalId, sessionRef) == null) {
log.info("[{}][{}][{}] Failed to process session update. Max session updates limit reached"
@@ -336,6 +329,8 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
log.debug("[{}][{}][{}] Session is no longer blacklisted.", sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), externalId);
blacklistedSessions.remove(externalId);
}
+ } else {
+ perSessionUpdateLimits.remove(sessionRef.getSessionId());
}
sessionMd.sendMsg(msg);
} else {
@@ -380,11 +375,17 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
private boolean checkLimits(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) throws Exception {
+ var tenantProfileConfiguration =
+ tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()).getDefaultProfileConfiguration();
+ if (tenantProfileConfiguration == null) {
+ return true;
+ }
+
String sessionId = session.getId();
- if (maxSessionsPerTenant > 0) {
+ if (tenantProfileConfiguration.getMaxWsSessionsPerTenant() > 0) {
Set tenantSessions = tenantSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (tenantSessions) {
- if (tenantSessions.size() < maxSessionsPerTenant) {
+ if (tenantSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerTenant()) {
tenantSessions.add(sessionId);
} else {
log.info("[{}][{}][{}] Failed to start session. Max tenant sessions limit reached"
@@ -396,10 +397,10 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
if (sessionRef.getSecurityCtx().isCustomerUser()) {
- if (maxSessionsPerCustomer > 0) {
+ if (tenantProfileConfiguration.getMaxWsSessionsPerCustomer() > 0) {
Set customerSessions = customerSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet());
synchronized (customerSessions) {
- if (customerSessions.size() < maxSessionsPerCustomer) {
+ if (customerSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerCustomer()) {
customerSessions.add(sessionId);
} else {
log.info("[{}][{}][{}] Failed to start session. Max customer sessions limit reached"
@@ -409,10 +410,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
}
}
- if (maxSessionsPerRegularUser > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
+ if (tenantProfileConfiguration.getMaxWsSessionsPerRegularUser() > 0
+ && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
Set regularUserSessions = regularUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
synchronized (regularUserSessions) {
- if (regularUserSessions.size() < maxSessionsPerRegularUser) {
+ if (regularUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerRegularUser()) {
regularUserSessions.add(sessionId);
} else {
log.info("[{}][{}][{}] Failed to start session. Max regular user sessions limit reached"
@@ -422,10 +424,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
}
}
- if (maxSessionsPerPublicUser > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
+ if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0
+ && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
Set publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
- if (publicUserSessions.size() < maxSessionsPerPublicUser) {
+ if (publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerPublicUser()) {
publicUserSessions.add(sessionId);
} else {
log.info("[{}][{}][{}] Failed to start session. Max public user sessions limit reached"
@@ -440,29 +443,31 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
private void cleanupLimits(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) {
+ var tenantProfileConfiguration = tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()).getDefaultProfileConfiguration();
+
String sessionId = session.getId();
perSessionUpdateLimits.remove(sessionRef.getSessionId());
blacklistedSessions.remove(sessionRef.getSessionId());
- if (maxSessionsPerTenant > 0) {
+ if (tenantProfileConfiguration.getMaxWsSessionsPerTenant() > 0) {
Set tenantSessions = tenantSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (tenantSessions) {
tenantSessions.remove(sessionId);
}
}
if (sessionRef.getSecurityCtx().isCustomerUser()) {
- if (maxSessionsPerCustomer > 0) {
+ if (tenantProfileConfiguration.getMaxWsSessionsPerCustomer() > 0) {
Set customerSessions = customerSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet());
synchronized (customerSessions) {
customerSessions.remove(sessionId);
}
}
- if (maxSessionsPerRegularUser > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
+ if (tenantProfileConfiguration.getMaxWsSessionsPerRegularUser() > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
Set regularUserSessions = regularUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
synchronized (regularUserSessions) {
regularUserSessions.remove(sessionId);
}
}
- if (maxSessionsPerPublicUser > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
+ if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
Set publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
publicUserSessions.remove(sessionId);
diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
index b8c0656bd2..2a64db7385 100644
--- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
@@ -22,13 +22,14 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
+import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
-import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -41,7 +42,6 @@ import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.util.TbCoreComponent;
-import org.thingsboard.server.cluster.TbClusterService;
import java.util.List;
import java.util.Map;
@@ -212,7 +212,7 @@ public class EntityActionService {
}
}
- public void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
+ public void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
ActionType actionType, Exception e, Object... additionalInfo) {
if (customerId == null || customerId.isNullUid()) {
customerId = user.getCustomerId();
@@ -223,6 +223,9 @@ public class EntityActionService {
auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
}
+ public void sendEntityNotificationMsgToEdgeService(TenantId tenantId, EntityId entityId, EdgeEventActionType action) {
+ tbClusterService.sendNotificationMsgToEdgeService(tenantId, null, entityId, null, null, action);
+ }
private T extractParameter(Class clazz, int index, Object... additionalInfo) {
T result = null;
@@ -267,4 +270,5 @@ public class EntityActionService {
entityNode.put(kvEntry.getKey(), kvEntry.getValueAsString());
}
}
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java
new file mode 100644
index 0000000000..55b23a99cc
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.apiusage;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
+import org.thingsboard.server.common.msg.tools.TbRateLimits;
+import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+@Service
+@RequiredArgsConstructor
+public class DefaultRateLimitService implements RateLimitService {
+
+ private final TbTenantProfileCache tenantProfileCache;
+
+ private final Map> rateLimits = new ConcurrentHashMap<>();
+
+ @Override
+ public boolean checkEntityExportLimit(TenantId tenantId) {
+ return checkLimit(tenantId, "entityExport", DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit);
+ }
+
+ @Override
+ public boolean checkEntityImportLimit(TenantId tenantId) {
+ return checkLimit(tenantId, "entityImport", DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit);
+ }
+
+ private boolean checkLimit(TenantId tenantId, String rateLimitsKey, Function rateLimitConfigExtractor) {
+ String rateLimitConfig = tenantProfileCache.get(tenantId).getProfileConfiguration()
+ .map(rateLimitConfigExtractor).orElse(null);
+
+ Map rateLimits = this.rateLimits.get(rateLimitsKey);
+ if (StringUtils.isEmpty(rateLimitConfig)) {
+ if (rateLimits != null) {
+ rateLimits.remove(tenantId);
+ if (rateLimits.isEmpty()) {
+ this.rateLimits.remove(rateLimitsKey);
+ }
+ }
+ return true;
+ }
+
+ if (rateLimits == null) {
+ rateLimits = new ConcurrentHashMap<>();
+ this.rateLimits.put(rateLimitsKey, rateLimits);
+ }
+ TbRateLimits rateLimit = rateLimits.get(tenantId);
+ if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) {
+ rateLimit = new TbRateLimits(rateLimitConfig);
+ rateLimits.put(tenantId, rateLimit);
+ }
+
+ return rateLimit.tryConsume();
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java
new file mode 100644
index 0000000000..d3d4244ca1
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.apiusage;
+
+import org.thingsboard.server.common.data.id.TenantId;
+
+public interface RateLimitService {
+
+ boolean checkEntityExportLimit(TenantId tenantId);
+
+ boolean checkEntityImportLimit(TenantId tenantId);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java
index e9999d2dc1..5eaeec0b73 100644
--- a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java
@@ -26,9 +26,9 @@ import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.ie.importing.csv.AbstractBulkImportService;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.service.entitiy.asset.TbAssetService;
-import org.thingsboard.server.service.importing.AbstractBulkImportService;
-import org.thingsboard.server.service.importing.BulkImportColumnType;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Map;
diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
index 34c30d5225..f0a7b6231e 100644
--- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
@@ -49,9 +49,9 @@ import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.ie.importing.csv.AbstractBulkImportService;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.service.entitiy.device.TbDeviceService;
-import org.thingsboard.server.service.importing.AbstractBulkImportService;
-import org.thingsboard.server.service.importing.BulkImportColumnType;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Collection;
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java
index fd55e9b321..404e5af5fd 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java
@@ -28,9 +28,9 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.ie.importing.csv.AbstractBulkImportService;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.service.entitiy.edge.TbEdgeService;
-import org.thingsboard.server.service.importing.AbstractBulkImportService;
-import org.thingsboard.server.service.importing.BulkImportColumnType;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Map;
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java
index 77dc121e38..7943ec0595 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java
@@ -20,7 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceProfileId;
-import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
+import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java
index 0f4ecf7e30..ee10c7859b 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java
@@ -83,7 +83,7 @@ public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher {
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, mapper.valueToTree(systemMailSettings)));
- AdminSettings tenantMailSettings = convertToTenantAdminSettings(systemMailSettings.getKey(), (ObjectNode) systemMailSettings.getJsonValue());
+ AdminSettings tenantMailSettings = convertToTenantAdminSettings(tenantId, systemMailSettings.getKey(), (ObjectNode) systemMailSettings.getJsonValue());
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, mapper.valueToTree(tenantMailSettings)));
@@ -91,7 +91,7 @@ public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher {
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, mapper.valueToTree(systemMailTemplates)));
- AdminSettings tenantMailTemplates = convertToTenantAdminSettings(systemMailTemplates.getKey(), (ObjectNode) systemMailTemplates.getJsonValue());
+ AdminSettings tenantMailTemplates = convertToTenantAdminSettings(tenantId, systemMailTemplates.getKey(), (ObjectNode) systemMailTemplates.getJsonValue());
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, mapper.valueToTree(tenantMailTemplates)));
@@ -151,8 +151,9 @@ public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher {
}
}
- private AdminSettings convertToTenantAdminSettings(String key, ObjectNode jsonValue) {
+ private AdminSettings convertToTenantAdminSettings(TenantId tenantId, String key, ObjectNode jsonValue) {
AdminSettings tenantMailSettings = new AdminSettings();
+ tenantMailSettings.setTenantId(tenantId);
jsonValue.put("useSystemMailSettings", true);
tenantMailSettings.setJsonValue(jsonValue);
tenantMailSettings.setKey(key);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
index 395ebdeaac..8439c1780b 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
@@ -64,6 +64,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.edge.EdgeNotificationService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
+import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.resource.TbResourceService;
@@ -125,6 +126,8 @@ public abstract class AbstractTbEntityService {
@Autowired
protected DashboardService dashboardService;
@Autowired
+ protected EntitiesVersionControlService vcService;
+ @Autowired
protected EntityViewService entityViewService;
@Autowired
protected TelemetrySubscriptionService tsSubService;
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
index 4ec70fd115..125cf9be90 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
@@ -334,7 +334,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS
return null;
}
- private EdgeEventActionType edgeTypeByActionType(ActionType actionType) {
+ public static EdgeEventActionType edgeTypeByActionType(ActionType actionType) {
switch (actionType) {
case ADDED:
return EdgeEventActionType.ADDED;
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
index 492464f147..4ee8554c4a 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
@@ -46,6 +46,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb
TenantId tenantId = asset.getTenantId();
try {
Asset savedAsset = checkNotNull(assetService.saveAsset(asset));
+ vcService.autoCommit(user, savedAsset.getId());
notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedAsset.getId(), asset, savedAsset.getCustomerId(), actionType, user);
return savedAsset;
} catch (Exception e) {
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
index d35cb06a85..536e1be976 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
@@ -40,10 +40,10 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements
public Customer save(Customer customer, SecurityUser user) throws ThingsboardException {
ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = customer.getTenantId();
- CustomerId customerId = customer.getId();
try {
Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer));
- notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedCustomer.getId(), savedCustomer, customerId, actionType, user);
+ vcService.autoCommit(user, savedCustomer.getId());
+ notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedCustomer.getId(), savedCustomer, null, actionType, user);
return savedCustomer;
} catch (Exception e) {
notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.CUSTOMER), customer, null, actionType, user, e);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
index f7110c4da7..a475ff77da 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
@@ -48,6 +48,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
TenantId tenantId = dashboard.getTenantId();
try {
Dashboard savedDashboard = checkNotNull(dashboardService.saveDashboard(dashboard));
+ vcService.autoCommit(user, savedDashboard.getId());
notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedDashboard.getId(), savedDashboard,
null, actionType, user);
return savedDashboard;
@@ -219,7 +220,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
}
@Override
- public Dashboard asignDashboardToEdge(DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException {
+ public Dashboard assignDashboardToEdge(DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException {
ActionType actionType = ActionType.ASSIGNED_TO_EDGE;
TenantId tenantId = user.getTenantId();
EdgeId edgeId = edge.getId();
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java
index 84dba247ff..313fd737f9 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java
@@ -26,7 +26,7 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Set;
-public interface TbDashboardService extends SimpleTbEntityService {
+public interface TbDashboardService extends SimpleTbEntityService {
Dashboard assignDashboardToCustomer(DashboardId dashboardId, Customer customer, SecurityUser user) throws ThingsboardException;
@@ -40,7 +40,7 @@ public interface TbDashboardService extends SimpleTbEntityService {
Dashboard removeDashboardCustomers(Dashboard dashboard, Set customerIds, SecurityUser user) throws ThingsboardException;
- Dashboard asignDashboardToEdge(DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException;
+ Dashboard assignDashboardToEdge(DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException;
Dashboard unassignDashboardFromEdge(Dashboard dashboard, Edge edge, SecurityUser user) throws ThingsboardException;
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
index d74610a462..5199887940 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
@@ -54,6 +54,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
try {
Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken));
+ vcService.autoCommit(user, savedDevice.getId());
notificationEntityService.notifyCreateOrUpdateDevice(tenantId, savedDevice.getId(), savedDevice.getCustomerId(),
savedDevice, oldDevice, actionType, user);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/deviceProfile/DefaultTbDeviceProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/deviceProfile/DefaultTbDeviceProfileService.java
index ca73b0cb83..3a0c1f1a23 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/deviceProfile/DefaultTbDeviceProfileService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/deviceProfile/DefaultTbDeviceProfileService.java
@@ -54,7 +54,7 @@ public class DefaultTbDeviceProfileService extends AbstractTbEntityService imple
}
}
DeviceProfile savedDeviceProfile = checkNotNull(deviceProfileService.saveDeviceProfile(deviceProfile));
-
+ vcService.autoCommit(user, savedDeviceProfile.getId());
tbClusterService.onDeviceProfileChange(savedDeviceProfile, null);
tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedDeviceProfile.getId(),
actionType.equals(ActionType.ADDED) ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/DefaultTbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/DefaultTbEntityViewService.java
index 878c6d32bb..2eee167e75 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/DefaultTbEntityViewService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/DefaultTbEntityViewService.java
@@ -66,19 +66,33 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen
@Override
public EntityView save(EntityView entityView, EntityView existingEntityView, SecurityUser user) throws ThingsboardException {
ActionType actionType = entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
+ try {
+ EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView));
+ this.updateEntityViewAttributes(user, savedEntityView, existingEntityView);
+ notificationEntityService.notifyCreateOrUpdateEntity(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView,
+ null, actionType, user);
+ return savedEntityView;
+ } catch (Exception e) {
+ notificationEntityService.notifyEntity(user.getTenantId(), emptyId(EntityType.ENTITY_VIEW), entityView, null, actionType, user, e);
+ throw handleException(e);
+ }
+ }
+
+ @Override
+ public void updateEntityViewAttributes(SecurityUser user, EntityView savedEntityView, EntityView oldEntityView) throws ThingsboardException {
try {
List> futures = new ArrayList<>();
- if (existingEntityView != null) {
- if (existingEntityView.getKeys() != null && existingEntityView.getKeys().getAttributes() != null) {
- futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.CLIENT_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), user));
- futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.SERVER_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), user));
- futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.SHARED_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), user));
+
+ if (oldEntityView != null) {
+ if (oldEntityView.getKeys() != null && oldEntityView.getKeys().getAttributes() != null) {
+ futures.add(deleteAttributesFromEntityView(oldEntityView, DataConstants.CLIENT_SCOPE, oldEntityView.getKeys().getAttributes().getCs(), user));
+ futures.add(deleteAttributesFromEntityView(oldEntityView, DataConstants.SERVER_SCOPE, oldEntityView.getKeys().getAttributes().getSs(), user));
+ futures.add(deleteAttributesFromEntityView(oldEntityView, DataConstants.SHARED_SCOPE, oldEntityView.getKeys().getAttributes().getSh(), user));
}
- List tsKeys = existingEntityView.getKeys() != null && existingEntityView.getKeys().getTimeseries() != null ?
- existingEntityView.getKeys().getTimeseries() : Collections.emptyList();
- futures.add(deleteLatestFromEntityView(existingEntityView, tsKeys, user));
+ List tsKeys = oldEntityView.getKeys() != null && oldEntityView.getKeys().getTimeseries() != null ?
+ oldEntityView.getKeys().getTimeseries() : Collections.emptyList();
+ futures.add(deleteLatestFromEntityView(oldEntityView, tsKeys, user));
}
- EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView));
if (savedEntityView.getKeys() != null) {
if (savedEntityView.getKeys().getAttributes() != null) {
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.CLIENT_SCOPE, savedEntityView.getKeys().getAttributes().getCs(), user));
@@ -94,13 +108,7 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen
throw new RuntimeException("Failed to copy attributes to entity view", e);
}
}
-
- notificationEntityService.notifyCreateOrUpdateEntity(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView,
- null, actionType, user);
-
- return savedEntityView;
} catch (Exception e) {
- notificationEntityService.notifyEntity(user.getTenantId(), emptyId(EntityType.ENTITY_VIEW), entityView, null, actionType, user, e);
throw handleException(e);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/TbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/TbEntityViewService.java
index dd5db9391b..e7e150531b 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/TbEntityViewService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/TbEntityViewService.java
@@ -28,6 +28,8 @@ public interface TbEntityViewService {
EntityView save(EntityView entityView, EntityView existingEntityView, SecurityUser user) throws ThingsboardException;
+ void updateEntityViewAttributes(SecurityUser user, EntityView savedEntityView, EntityView oldEntityView) throws ThingsboardException;
+
void delete (EntityView entity, SecurityUser user) throws ThingsboardException;
EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer,
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java
index 2a8fc1bf0a..22baed8366 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java
@@ -27,8 +27,10 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.entitiy.queue.TbQueueService;
import org.thingsboard.server.service.install.InstallScripts;
+import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import java.util.Collections;
+import java.util.concurrent.TimeUnit;
@Service
@TbCoreComponent
@@ -38,6 +40,7 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T
private final InstallScripts installScripts;
private final TbQueueService tbQueueService;
private final TenantProfileService tenantProfileService;
+ private final EntitiesVersionControlService versionControlService;
@Override
public Tenant save(Tenant tenant) throws ThingsboardException {
@@ -70,6 +73,7 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T
tenantService.deleteTenant(tenantId);
tenantProfileCache.evict(tenantId);
notificationEntityService.notifyDeleteTenant(tenant);
+ versionControlService.deleteVersionControlSettings(tenantId).get(1, TimeUnit.MINUTES);
} catch (Exception e) {
throw handleException(e);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
index cece3377b0..50fcb28387 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
@@ -265,6 +265,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Override
public void createAdminSettings() throws Exception {
AdminSettings generalSettings = new AdminSettings();
+ generalSettings.setTenantId(TenantId.SYS_TENANT_ID);
generalSettings.setKey("general");
ObjectNode node = objectMapper.createObjectNode();
node.put("baseUrl", "http://localhost:8080");
@@ -273,6 +274,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, generalSettings);
AdminSettings mailSettings = new AdminSettings();
+ mailSettings.setTenantId(TenantId.SYS_TENANT_ID);
mailSettings.setKey("mail");
node = objectMapper.createObjectNode();
node.put("mailFrom", "ThingsBoard ");
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
index d0d5f37e00..306102c66c 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
@@ -571,6 +571,22 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.4", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
+ log.info("Loading queues...");
+ try {
+ if (!CollectionUtils.isEmpty(queueConfig.getQueues())) {
+ queueConfig.getQueues().forEach(queueSettings -> {
+ Queue queue = queueConfigToQueue(queueSettings);
+ Queue existing = queueService.findQueueByTenantIdAndName(queue.getTenantId(), queue.getName());
+ if (existing == null) {
+ queueService.saveQueue(queue);
+ }
+ });
+ } else {
+ systemDataLoaderService.createQueues();
+ }
+ } catch (Exception e) {
+ }
+
log.info("Updating device profiles...");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.4", "schema_update_device_profile.sql");
loadSql(schemaUpdateFile, conn);
@@ -628,4 +644,29 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
return isOldSchema;
}
+ private Queue queueConfigToQueue(TbRuleEngineQueueConfiguration queueSettings) {
+ Queue queue = new Queue();
+ queue.setTenantId(TenantId.SYS_TENANT_ID);
+ queue.setName(queueSettings.getName());
+ queue.setTopic(queueSettings.getTopic());
+ queue.setPollInterval(queueSettings.getPollInterval());
+ queue.setPartitions(queueSettings.getPartitions());
+ queue.setPackProcessingTimeout(queueSettings.getPackProcessingTimeout());
+ SubmitStrategy submitStrategy = new SubmitStrategy();
+ submitStrategy.setBatchSize(queueSettings.getSubmitStrategy().getBatchSize());
+ submitStrategy.setType(SubmitStrategyType.valueOf(queueSettings.getSubmitStrategy().getType()));
+ queue.setSubmitStrategy(submitStrategy);
+ ProcessingStrategy processingStrategy = new ProcessingStrategy();
+ processingStrategy.setType(ProcessingStrategyType.valueOf(queueSettings.getProcessingStrategy().getType()));
+ processingStrategy.setRetries(queueSettings.getProcessingStrategy().getRetries());
+ processingStrategy.setFailurePercentage(queueSettings.getProcessingStrategy().getFailurePercentage());
+ processingStrategy.setPauseBetweenRetries(queueSettings.getProcessingStrategy().getPauseBetweenRetries());
+ processingStrategy.setMaxPauseBetweenRetries(queueSettings.getProcessingStrategy().getMaxPauseBetweenRetries());
+ queue.setProcessingStrategy(processingStrategy);
+ queue.setConsumerPerPartition(queueSettings.isConsumerPerPartition());
+ return queue;
+ }
+
+
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java
similarity index 88%
rename from application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java
rename to application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java
index eaa9d708be..7ae155402a 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java
@@ -18,19 +18,17 @@ package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
-import org.thingsboard.server.dao.util.PsqlDao;
@Service
-@PsqlDao
@Profile("install")
@Slf4j
-public class PsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService
+public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService
implements EntityDatabaseSchemaService {
public static final String SCHEMA_ENTITIES_SQL = "schema-entities.sql";
public static final String SCHEMA_ENTITIES_IDX_SQL = "schema-entities-idx.sql";
public static final String SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL = "schema-entities-idx-psql-addon.sql";
- public PsqlEntityDatabaseSchemaService() {
+ public SqlEntityDatabaseSchemaService() {
super(SCHEMA_ENTITIES_SQL, SCHEMA_ENTITIES_IDX_SQL);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseSchemaService.java
similarity index 85%
rename from application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseSchemaService.java
rename to application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseSchemaService.java
index 0e7cff7d63..f3ede624ff 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseSchemaService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseSchemaService.java
@@ -18,19 +18,17 @@ package org.thingsboard.server.service.install;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
-import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.dao.util.SqlTsDao;
@Service
@SqlTsDao
-@PsqlDao
@Profile("install")
-public class PsqlTsDatabaseSchemaService extends SqlAbstractDatabaseSchemaService implements TsDatabaseSchemaService {
+public class SqlTsDatabaseSchemaService extends SqlAbstractDatabaseSchemaService implements TsDatabaseSchemaService {
@Value("${sql.postgres.ts_key_value_partitioning:MONTHS}")
private String partitionType;
- public PsqlTsDatabaseSchemaService() {
+ public SqlTsDatabaseSchemaService() {
super("schema-ts-psql.sql", null);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseUpgradeService.java
similarity index 98%
rename from application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java
rename to application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseUpgradeService.java
index 55fa73fb27..c1d41a6f6e 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseUpgradeService.java
@@ -21,7 +21,6 @@ import org.apache.commons.lang3.SystemUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
-import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.dao.util.SqlTsDao;
import java.io.File;
@@ -36,8 +35,7 @@ import java.sql.DriverManager;
@Profile("install")
@Slf4j
@SqlTsDao
-@PsqlDao
-public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeService implements DatabaseTsUpgradeService {
+public class SqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeService implements DatabaseTsUpgradeService {
@Value("${sql.postgres.ts_key_value_partitioning:MONTHS}")
private String partitionType;
diff --git a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseSchemaService.java
index a0257ba0e4..409498cfd3 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseSchemaService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseSchemaService.java
@@ -19,16 +19,10 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
-import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.dao.util.TimescaleDBTsDao;
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.SQLException;
-
@Service
@TimescaleDBTsDao
-@PsqlDao
@Profile("install")
@Slf4j
public class TimescaleTsDatabaseSchemaService extends SqlAbstractDatabaseSchemaService implements TsDatabaseSchemaService {
diff --git a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java
index 1834ca6552..245dc4503d 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java
@@ -22,7 +22,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
-import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.dao.util.TimescaleDBTsDao;
import java.io.File;
@@ -37,7 +36,6 @@ import java.sql.DriverManager;
@Profile("install")
@Slf4j
@TimescaleDBTsDao
-@PsqlDao
public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeService implements DatabaseTsUpgradeService {
@Value("${sql.timescale.chunk_time_interval:86400000}")
diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
index 7b87668319..c6576097ee 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
@@ -21,7 +21,6 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
@@ -56,12 +55,9 @@ import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.alarm.AlarmDao;
-import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.model.sql.DeviceProfileEntity;
-import org.thingsboard.server.dao.model.sql.RelationEntity;
-import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
@@ -69,7 +65,6 @@ import org.thingsboard.server.dao.sql.device.DeviceProfileRepository;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
-import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.install.SystemDataLoaderService;
import org.thingsboard.server.service.install.TbRuleEngineQueueConfigService;
@@ -107,9 +102,6 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Autowired
private TimeseriesService tsService;
- @Autowired
- private AlarmService alarmService;
-
@Autowired
private EntityService entityService;
@@ -120,7 +112,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
private DeviceProfileRepository deviceProfileRepository;
@Autowired
- private OAuth2Service oAuth2Service;
+ private RateLimitsUpdater rateLimitsUpdater;
@Autowired
private TenantProfileService tenantProfileService;
@@ -162,23 +154,9 @@ public class DefaultDataUpdateService implements DataUpdateService {
break;
case "3.3.4":
log.info("Updating data from version 3.3.4 to 3.4.0 ...");
- log.info("Loading queues...");
- try {
- if (!CollectionUtils.isEmpty(queueConfig.getQueues())) {
- queueConfig.getQueues().forEach(queueSettings -> {
- Queue queue = queueConfigToQueue(queueSettings);
- Queue existing = queueService.findQueueByTenantIdAndName(queue.getTenantId(), queue.getName());
- if (existing == null) {
- queueService.saveQueue(queue);
- }
- });
- } else {
- systemDataLoaderService.createQueues();
- }
- } catch (Exception e) {
- }
- tenantsProfileQueueConfigurationUpdater.updateEntities(null);
- checkPointRuleNodesUpdater.updateEntities(null);
+ rateLimitsUpdater.updateEntities();
+ tenantsProfileQueueConfigurationUpdater.updateEntities();
+ checkPointRuleNodesUpdater.updateEntities();
break;
default:
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
@@ -649,29 +627,6 @@ public class DefaultDataUpdateService implements DataUpdateService {
return mainQueueConfiguration;
}
- private Queue queueConfigToQueue(TbRuleEngineQueueConfiguration queueSettings) {
- Queue queue = new Queue();
- queue.setTenantId(TenantId.SYS_TENANT_ID);
- queue.setName(queueSettings.getName());
- queue.setTopic(queueSettings.getTopic());
- queue.setPollInterval(queueSettings.getPollInterval());
- queue.setPartitions(queueSettings.getPartitions());
- queue.setPackProcessingTimeout(queueSettings.getPackProcessingTimeout());
- SubmitStrategy submitStrategy = new SubmitStrategy();
- submitStrategy.setBatchSize(queueSettings.getSubmitStrategy().getBatchSize());
- submitStrategy.setType(SubmitStrategyType.valueOf(queueSettings.getSubmitStrategy().getType()));
- queue.setSubmitStrategy(submitStrategy);
- ProcessingStrategy processingStrategy = new ProcessingStrategy();
- processingStrategy.setType(ProcessingStrategyType.valueOf(queueSettings.getProcessingStrategy().getType()));
- processingStrategy.setRetries(queueSettings.getProcessingStrategy().getRetries());
- processingStrategy.setFailurePercentage(queueSettings.getProcessingStrategy().getFailurePercentage());
- processingStrategy.setPauseBetweenRetries(queueSettings.getProcessingStrategy().getPauseBetweenRetries());
- processingStrategy.setMaxPauseBetweenRetries(queueSettings.getProcessingStrategy().getMaxPauseBetweenRetries());
- queue.setProcessingStrategy(processingStrategy);
- queue.setConsumerPerPartition(queueSettings.isConsumerPerPartition());
- return queue;
- }
-
private final PaginatedUpdater checkPointRuleNodesUpdater =
new PaginatedUpdater<>() {
diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java
index 914a136a4c..b2fb707791 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java
@@ -49,6 +49,10 @@ public abstract class PaginatedUpdater {
}
}
+ public void updateEntities() {
+ updateEntities(null);
+ }
+
protected boolean forceReportTotal() {
return false;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java
new file mode 100644
index 0000000000..c136f97b52
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java
@@ -0,0 +1,115 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.install.update;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.TenantProfile;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.dao.tenant.TenantProfileService;
+
+@Component
+class RateLimitsUpdater extends PaginatedUpdater {
+
+ @Value("#{ environment.getProperty('TB_SERVER_REST_LIMITS_TENANT_ENABLED') ?: environment.getProperty('server.rest.limits.tenant.enabled') ?: 'false' }")
+ boolean tenantServerRestLimitsEnabled;
+ @Value("#{ environment.getProperty('TB_SERVER_REST_LIMITS_TENANT_CONFIGURATION') ?: environment.getProperty('server.rest.limits.tenant.configuration') ?: '100:1,2000:60' }")
+ String tenantServerRestLimitsConfiguration;
+ @Value("#{ environment.getProperty('TB_SERVER_REST_LIMITS_CUSTOMER_ENABLED') ?: environment.getProperty('server.rest.limits.customer.enabled') ?: 'false' }")
+ boolean customerServerRestLimitsEnabled;
+ @Value("#{ environment.getProperty('TB_SERVER_REST_LIMITS_CUSTOMER_CONFIGURATION') ?: environment.getProperty('server.rest.limits.customer.configuration') ?: '50:1,1000:60' }")
+ String customerServerRestLimitsConfiguration;
+
+ @Value("#{ environment.getProperty('TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SESSIONS_PER_TENANT') ?: environment.getProperty('server.ws.limits.max_sessions_per_tenant') ?: '0' }")
+ private int maxWsSessionsPerTenant;
+ @Value("#{ environment.getProperty('TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SESSIONS_PER_CUSTOMER') ?: environment.getProperty('server.ws.limits.max_sessions_per_customer') ?: '0' }")
+ private int maxWsSessionsPerCustomer;
+ @Value("#{ environment.getProperty('TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SESSIONS_PER_REGULAR_USER') ?: environment.getProperty('server.ws.limits.max_sessions_per_regular_user') ?: '0' }")
+ private int maxWsSessionsPerRegularUser;
+ @Value("#{ environment.getProperty('TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SESSIONS_PER_PUBLIC_USER') ?: environment.getProperty('server.ws.limits.max_sessions_per_public_user') ?: '0' }")
+ private int maxWsSessionsPerPublicUser;
+ @Value("#{ environment.getProperty('TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_QUEUE_PER_WS_SESSION') ?: environment.getProperty('server.ws.limits.max_queue_per_ws_session') ?: '500' }")
+ private int wsMsgQueueLimitPerSession;
+ @Value("#{ environment.getProperty('TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SUBSCRIPTIONS_PER_TENANT') ?: environment.getProperty('server.ws.limits.max_subscriptions_per_tenant') ?: '0' }")
+ private long maxWsSubscriptionsPerTenant;
+ @Value("#{ environment.getProperty('TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SUBSCRIPTIONS_PER_CUSTOMER') ?: environment.getProperty('server.ws.limits.max_subscriptions_per_customer') ?: '0' }")
+ private long maxWsSubscriptionsPerCustomer;
+ @Value("#{ environment.getProperty('TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SUBSCRIPTIONS_PER_REGULAR_USER') ?: environment.getProperty('server.ws.limits.max_subscriptions_per_regular_user') ?: '0' }")
+ private long maxWsSubscriptionsPerRegularUser;
+ @Value("#{ environment.getProperty('TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SUBSCRIPTIONS_PER_PUBLIC_USER') ?: environment.getProperty('server.ws.limits.max_subscriptions_per_public_user') ?: '0' }")
+ private long maxWsSubscriptionsPerPublicUser;
+ @Value("#{ environment.getProperty('TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_UPDATES_PER_SESSION') ?: environment.getProperty('server.ws.limits.max_updates_per_session') ?: '300:1,3000:60' }")
+ private String wsUpdatesPerSessionRateLimit;
+
+ @Value("#{ environment.getProperty('CASSANDRA_QUERY_TENANT_RATE_LIMITS_ENABLED') ?: environment.getProperty('cassandra.query.tenant_rate_limits.enabled') ?: 'false' }")
+ private boolean cassandraQueryTenantRateLimitsEnabled;
+ @Value("#{ environment.getProperty('CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION') ?: environment.getProperty('cassandra.query.tenant_rate_limits.configuration') ?: '1000:1,30000:60' }")
+ private String cassandraQueryTenantRateLimitsConfiguration;
+
+ @Autowired
+ private TenantProfileService tenantProfileService;
+
+ @Override
+ protected boolean forceReportTotal() {
+ return true;
+ }
+
+ @Override
+ protected String getName() {
+ return "Rate limits updater";
+ }
+
+ @Override
+ protected PageData findEntities(String id, PageLink pageLink) {
+ return tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, pageLink);
+ }
+
+ @Override
+ protected void updateEntity(TenantProfile tenantProfile) {
+ var profileConfiguration = tenantProfile.getDefaultProfileConfiguration();
+
+ if (tenantServerRestLimitsEnabled && StringUtils.isNotEmpty(tenantServerRestLimitsConfiguration)) {
+ profileConfiguration.setTenantServerRestLimitsConfiguration(tenantServerRestLimitsConfiguration);
+ }
+ if (customerServerRestLimitsEnabled && StringUtils.isNotEmpty(customerServerRestLimitsConfiguration)) {
+ profileConfiguration.setCustomerServerRestLimitsConfiguration(customerServerRestLimitsConfiguration);
+ }
+
+ profileConfiguration.setMaxWsSessionsPerTenant(maxWsSessionsPerTenant);
+ profileConfiguration.setMaxWsSessionsPerCustomer(maxWsSessionsPerCustomer);
+ profileConfiguration.setMaxWsSessionsPerPublicUser(maxWsSessionsPerPublicUser);
+ profileConfiguration.setMaxWsSessionsPerRegularUser(maxWsSessionsPerRegularUser);
+ profileConfiguration.setMaxWsSubscriptionsPerTenant(maxWsSubscriptionsPerTenant);
+ profileConfiguration.setMaxWsSubscriptionsPerCustomer(maxWsSubscriptionsPerCustomer);
+ profileConfiguration.setMaxWsSubscriptionsPerPublicUser(maxWsSubscriptionsPerPublicUser);
+ profileConfiguration.setMaxWsSubscriptionsPerRegularUser(maxWsSubscriptionsPerRegularUser);
+ profileConfiguration.setWsMsgQueueLimitPerSession(wsMsgQueueLimitPerSession);
+ if (StringUtils.isNotEmpty(wsUpdatesPerSessionRateLimit)) {
+ profileConfiguration.setWsUpdatesPerSessionRateLimit(wsUpdatesPerSessionRateLimit);
+ }
+
+ if (cassandraQueryTenantRateLimitsEnabled && StringUtils.isNotEmpty(cassandraQueryTenantRateLimitsConfiguration)) {
+ profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(cassandraQueryTenantRateLimitsConfiguration);
+ }
+
+ tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
index d951a06b64..01adab5f6f 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
@@ -53,7 +53,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
-import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
+import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
@@ -135,6 +135,15 @@ public class DefaultTbClusterService implements TbClusterService {
toCoreMsgs.incrementAndGet();
}
+ @Override
+ public void pushMsgToVersionControl(TenantId tenantId, TransportProtos.ToVersionControlServiceMsg msg, TbQueueCallback callback) {
+ TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_VC_EXECUTOR, tenantId, tenantId);
+ log.trace("PUSHING msg: {} to:{}", msg, tpi);
+ producerProvider.getTbVersionControlMsgProducer().send(tpi, new TbProtoQueueMsg<>(tenantId.getId(), msg), callback);
+ //TODO: ashvayka
+ toCoreMsgs.incrementAndGet();
+ }
+
@Override
public void pushNotificationToCore(String serviceId, FromDeviceRpcResponse response, TbQueueCallback callback) {
TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceId);
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
index 2ca47d689f..d071d591a1 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
@@ -36,7 +36,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.stats.StatsFactory;
-import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
+import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto;
@@ -75,6 +75,8 @@ import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.subscription.SubscriptionManagerService;
import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
+import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
+import org.thingsboard.server.service.sync.vc.GitVersionControlQueueService;
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
import javax.annotation.PostConstruct;
@@ -117,6 +119,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> usageStatsConsumer;
private final TbQueueConsumer> firmwareStatesConsumer;
@@ -138,7 +141,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService getRuleChainOutputLabels(TenantId tenantId, RuleChainId ruleChainId) {
RuleChainMetaData metaData = ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId);
@@ -169,6 +166,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
ActionType actionType = ruleChain.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
try {
RuleChain savedRuleChain = checkNotNull(ruleChainService.saveRuleChain(ruleChain));
+ vcService.autoCommit(user, savedRuleChain.getId());
if (RuleChainType.CORE.equals(savedRuleChain.getType())) {
tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedRuleChain.getId(),
@@ -222,6 +220,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
public RuleChain saveDefaultByName(TenantId tenantId, DefaultRuleChainCreateRequest request, SecurityUser user) throws ThingsboardException {
try {
RuleChain savedRuleChain = installScripts.createDefaultRuleChain(tenantId, request.getName());
+ vcService.autoCommit(user, savedRuleChain.getId());
tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedRuleChain.getId(), ComponentLifecycleEvent.CREATED);
notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, savedRuleChain.getId(),
savedRuleChain, user, ActionType.ADDED, false, null);
@@ -281,6 +280,15 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
updatedRuleChains = Collections.emptyList();
}
+ if (updatedRuleChains.isEmpty()) {
+ vcService.autoCommit(user, ruleChainMetaData.getRuleChainId());
+ } else {
+ List uuids = new ArrayList<>(updatedRuleChains.size() + 1);
+ uuids.add(ruleChainMetaData.getRuleChainId().getId());
+ updatedRuleChains.forEach(rc -> uuids.add(rc.getId().getId()));
+ vcService.autoCommit(user, EntityType.RULE_CHAIN, uuids);
+ }
+
RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.loadRuleChainMetaData(tenantId, ruleChainMetaDataId));
if (RuleChainType.CORE.equals(ruleChain.getType())) {
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
index af78d307d3..0972b378eb 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
@@ -149,7 +149,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
ConcurrentMap providersRateLimits = rateLimits.computeIfAbsent(userId, i -> new ConcurrentHashMap<>());
TbRateLimits rateLimit = providersRateLimits.get(providerType);
- if (rateLimit == null || !rateLimit.getConfig().equals(rateLimitConfig)) {
+ if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) {
rateLimit = new TbRateLimits(rateLimitConfig, true);
providersRateLimits.put(providerType, rateLimit);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java
index 23e6dc065f..ad2ecebd2c 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java
@@ -56,12 +56,31 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
@Override
public Optional getAccountTwoFaSettings(TenantId tenantId, UserId userId) {
+ PlatformTwoFaSettings platformTwoFaSettings = getPlatformTwoFaSettings(tenantId, true).orElse(null);
return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
- .flatMap(userAuthSettings -> Optional.ofNullable(userAuthSettings.getTwoFaSettings()))
- .map(twoFaSettings -> {
- twoFaSettings.getConfigs().keySet().removeIf(providerType -> {
- return getTwoFaProviderConfig(tenantId, providerType).isEmpty();
+ .map(userAuthSettings -> {
+ AccountTwoFaSettings twoFaSettings = userAuthSettings.getTwoFaSettings();
+ if (twoFaSettings == null) return null;
+ boolean updateNeeded;
+
+ Map configs = twoFaSettings.getConfigs();
+ updateNeeded = configs.keySet().removeIf(providerType -> {
+ return platformTwoFaSettings == null || platformTwoFaSettings.getProviderConfig(providerType).isEmpty();
});
+ if (configs.size() == 1 && configs.containsKey(TwoFaProviderType.BACKUP_CODE)) {
+ configs.remove(TwoFaProviderType.BACKUP_CODE);
+ updateNeeded = true;
+ }
+ if (!configs.isEmpty() && configs.values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) {
+ configs.values().stream()
+ .filter(config -> config.getProviderType() != TwoFaProviderType.BACKUP_CODE)
+ .findFirst().ifPresent(config -> config.setUseByDefault(true));
+ updateNeeded = true;
+ }
+
+ if (updateNeeded) {
+ twoFaSettings = saveAccountTwoFaSettings(tenantId, userId, twoFaSettings);
+ }
return twoFaSettings;
});
}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java
index 63d3233928..419a60b3b9 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java
@@ -63,7 +63,7 @@ public class SmsTwoFaProvider extends OtpBasedTwoFaProvider {
public SessionRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
- super(CacheConstants.ASSET_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() {
+ super(CacheConstants.SESSIONS_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() {
@Override
public byte[] serialize(TransportProtos.DeviceSessionsCacheEntry deviceSessionsCacheEntry) throws SerializationException {
return deviceSessionsCacheEntry.toByteArray();
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
new file mode 100644
index 0000000000..b2abe2a581
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
@@ -0,0 +1,166 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.sync.ThrowingRunnable;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.action.EntityActionService;
+import org.thingsboard.server.service.apiusage.RateLimitService;
+import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;
+import org.thingsboard.server.service.sync.ie.exporting.impl.BaseEntityExportService;
+import org.thingsboard.server.service.sync.ie.exporting.impl.DefaultEntityExportService;
+import org.thingsboard.server.service.sync.ie.importing.EntityImportService;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultEntitiesExportImportService implements EntitiesExportImportService {
+
+ private final Map> exportServices = new HashMap<>();
+ private final Map> importServices = new HashMap<>();
+
+ private final EntityActionService entityActionService;
+ private final RelationService relationService;
+ private final RateLimitService rateLimitService;
+
+ protected static final List SUPPORTED_ENTITY_TYPES = List.of(
+ EntityType.CUSTOMER, EntityType.ASSET, EntityType.RULE_CHAIN,
+ EntityType.DASHBOARD, EntityType.DEVICE_PROFILE, EntityType.DEVICE,
+ EntityType.ENTITY_VIEW, EntityType.WIDGETS_BUNDLE
+ );
+
+
+ @Override
+ public , I extends EntityId> EntityExportData exportEntity(EntitiesExportCtx> ctx, I entityId) throws ThingsboardException {
+ if (!rateLimitService.checkEntityExportLimit(ctx.getTenantId())) {
+ throw new ThingsboardException("Rate limit for entities export is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS);
+ }
+
+ EntityType entityType = entityId.getEntityType();
+ EntityExportService> exportService = getExportService(entityType);
+
+ return exportService.getExportData(ctx, entityId);
+ }
+
+ @Override
+ public , I extends EntityId> EntityImportResult importEntity(EntitiesImportCtx ctx, EntityExportData exportData) throws ThingsboardException {
+ if (!rateLimitService.checkEntityImportLimit(ctx.getTenantId())) {
+ throw new ThingsboardException("Rate limit for entities import is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS);
+ }
+ if (exportData.getEntity() == null || exportData.getEntity().getId() == null) {
+ throw new DataValidationException("Invalid entity data");
+ }
+
+ EntityType entityType = exportData.getEntityType();
+ EntityImportService> importService = getImportService(entityType);
+
+ EntityImportResult importResult = importService.importEntity(ctx, exportData);
+ ctx.putInternalId(exportData.getExternalId(), importResult.getSavedEntity().getId());
+
+ ctx.addReferenceCallback(importResult.getSaveReferencesCallback());
+ ctx.addEventCallback(importResult.getSendEventsCallback());
+ return importResult;
+ }
+
+ @Override
+ public void saveReferencesAndRelations(EntitiesImportCtx ctx) throws ThingsboardException {
+ for (ThrowingRunnable saveReferencesCallback : ctx.getReferenceCallbacks()) {
+ saveReferencesCallback.run();
+ }
+
+ relationService.saveRelations(ctx.getTenantId(), new ArrayList<>(ctx.getRelations()));
+
+ for (EntityRelation relation : ctx.getRelations()) {
+ entityActionService.logEntityAction(ctx.getUser(), relation.getFrom(), null, null,
+ ActionType.RELATION_ADD_OR_UPDATE, null, relation);
+ entityActionService.logEntityAction(ctx.getUser(), relation.getTo(), null, null,
+ ActionType.RELATION_ADD_OR_UPDATE, null, relation);
+ }
+ }
+
+
+ @Override
+ public Comparator getEntityTypeComparatorForImport() {
+ return Comparator.comparing(SUPPORTED_ENTITY_TYPES::indexOf);
+ }
+
+
+ @SuppressWarnings("unchecked")
+ private , D extends EntityExportData> EntityExportService getExportService(EntityType entityType) {
+ EntityExportService, ?, ?> exportService = exportServices.get(entityType);
+ if (exportService == null) {
+ throw new IllegalArgumentException("Export for entity type " + entityType + " is not supported");
+ }
+ return (EntityExportService) exportService;
+ }
+
+ @SuppressWarnings("unchecked")
+ private , D extends EntityExportData> EntityImportService getImportService(EntityType entityType) {
+ EntityImportService, ?, ?> importService = importServices.get(entityType);
+ if (importService == null) {
+ throw new IllegalArgumentException("Import for entity type " + entityType + " is not supported");
+ }
+ return (EntityImportService) importService;
+ }
+
+ @Autowired
+ private void setExportServices(DefaultEntityExportService, ?, ?> defaultExportService,
+ Collection> exportServices) {
+ exportServices.stream()
+ .sorted(Comparator.comparing(exportService -> exportService.getSupportedEntityTypes().size(), Comparator.reverseOrder()))
+ .forEach(exportService -> {
+ exportService.getSupportedEntityTypes().forEach(entityType -> {
+ this.exportServices.put(entityType, exportService);
+ });
+ });
+ SUPPORTED_ENTITY_TYPES.forEach(entityType -> {
+ this.exportServices.putIfAbsent(entityType, defaultExportService);
+ });
+ }
+
+ @Autowired
+ private void setImportServices(Collection> importServices) {
+ importServices.forEach(entityImportService -> {
+ this.importServices.put(entityImportService.getEntityType(), entityImportService);
+ });
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java
new file mode 100644
index 0000000000..8657528dac
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie;
+
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+import java.util.Comparator;
+
+public interface EntitiesExportImportService {
+
+ , I extends EntityId> EntityExportData exportEntity(EntitiesExportCtx> ctx, I entityId) throws ThingsboardException;
+
+ , I extends EntityId> EntityImportResult importEntity(EntitiesImportCtx ctx, EntityExportData exportData) throws ThingsboardException;
+
+
+ void saveReferencesAndRelations(EntitiesImportCtx ctx) throws ThingsboardException;
+
+ Comparator getEntityTypeComparatorForImport();
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java
new file mode 100644
index 0000000000..6e06144ae2
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java
@@ -0,0 +1,210 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.exporting;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.HasTenantId;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DashboardId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.HasId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetsBundleId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.ExportableEntityDao;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.dashboard.DashboardService;
+import org.thingsboard.server.dao.device.DeviceProfileService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.widget.WidgetsBundleService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.permission.AccessControlService;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultExportableEntitiesService implements ExportableEntitiesService {
+
+ private final Map> daos = new HashMap<>();
+ private final Map> removers = new HashMap<>();
+
+ private final AccessControlService accessControlService;
+
+
+ @Override
+ public , I extends EntityId> E findEntityByTenantIdAndExternalId(TenantId tenantId, I externalId) {
+ EntityType entityType = externalId.getEntityType();
+ Dao dao = getDao(entityType);
+
+ E entity = null;
+
+ if (dao instanceof ExportableEntityDao) {
+ ExportableEntityDao exportableEntityDao = (ExportableEntityDao) dao;
+ entity = exportableEntityDao.findByTenantIdAndExternalId(tenantId.getId(), externalId.getId());
+ }
+ if (entity == null || !belongsToTenant(entity, tenantId)) {
+ return null;
+ }
+
+ return entity;
+ }
+
+ @Override
+ public , I extends EntityId> E findEntityByTenantIdAndId(TenantId tenantId, I id) {
+ E entity = findEntityById(id);
+
+ if (entity == null || !belongsToTenant(entity, tenantId)) {
+ return null;
+ }
+ return entity;
+ }
+
+ @Override
+ public , I extends EntityId> E findEntityById(I id) {
+ EntityType entityType = id.getEntityType();
+ Dao dao = getDao(entityType);
+ if (dao == null) {
+ throw new IllegalArgumentException("Unsupported entity type " + entityType);
+ }
+
+ return dao.findById(TenantId.SYS_TENANT_ID, id.getId());
+ }
+
+ @Override
+ public , I extends EntityId> E findEntityByTenantIdAndName(TenantId tenantId, EntityType entityType, String name) {
+ Dao dao = getDao(entityType);
+
+ E entity = null;
+
+ if (dao instanceof ExportableEntityDao) {
+ ExportableEntityDao exportableEntityDao = (ExportableEntityDao) dao;
+ try {
+ entity = exportableEntityDao.findByTenantIdAndName(tenantId.getId(), name);
+ } catch (UnsupportedOperationException ignored) {
+ }
+ }
+ if (entity == null || !belongsToTenant(entity, tenantId)) {
+ return null;
+ }
+
+ return entity;
+ }
+
+ @Override
+ public , I extends EntityId> PageData findEntitiesByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink) {
+ ExportableEntityDao dao = getExportableEntityDao(entityType);
+ if (dao != null) {
+ return dao.findByTenantId(tenantId.getId(), pageLink);
+ } else {
+ return new PageData<>();
+ }
+ }
+
+ @Override
+ public I getExternalIdByInternal(I internalId) {
+ ExportableEntityDao dao = getExportableEntityDao(internalId.getEntityType());
+ if (dao != null) {
+ return dao.getExternalIdByInternal(internalId);
+ } else {
+ return null;
+ }
+ }
+
+ private boolean belongsToTenant(HasId extends EntityId> entity, TenantId tenantId) {
+ return tenantId.equals(((HasTenantId) entity).getTenantId());
+ }
+
+
+ @Override
+ public void removeById(TenantId tenantId, I id) {
+ EntityType entityType = id.getEntityType();
+ BiConsumer entityRemover = removers.get(entityType);
+ if (entityRemover == null) {
+ throw new IllegalArgumentException("Unsupported entity type " + entityType);
+ }
+ entityRemover.accept(tenantId, id);
+ }
+
+ private > ExportableEntityDao getExportableEntityDao(EntityType entityType) {
+ Dao dao = getDao(entityType);
+ if (dao instanceof ExportableEntityDao) {
+ return (ExportableEntityDao) dao;
+ } else {
+ return null;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private Dao getDao(EntityType entityType) {
+ return (Dao) daos.get(entityType);
+ }
+
+ @Autowired
+ private void setDaos(Collection> daos) {
+ daos.forEach(dao -> {
+ if (dao.getEntityType() != null) {
+ this.daos.put(dao.getEntityType(), dao);
+ }
+ });
+ }
+
+ @Autowired
+ private void setRemovers(CustomerService customerService, AssetService assetService, RuleChainService ruleChainService,
+ DashboardService dashboardService, DeviceProfileService deviceProfileService,
+ DeviceService deviceService, WidgetsBundleService widgetsBundleService) {
+ removers.put(EntityType.CUSTOMER, (tenantId, entityId) -> {
+ customerService.deleteCustomer(tenantId, (CustomerId) entityId);
+ });
+ removers.put(EntityType.ASSET, (tenantId, entityId) -> {
+ assetService.deleteAsset(tenantId, (AssetId) entityId);
+ });
+ removers.put(EntityType.RULE_CHAIN, (tenantId, entityId) -> {
+ ruleChainService.deleteRuleChainById(tenantId, (RuleChainId) entityId);
+ });
+ removers.put(EntityType.DASHBOARD, (tenantId, entityId) -> {
+ dashboardService.deleteDashboard(tenantId, (DashboardId) entityId);
+ });
+ removers.put(EntityType.DEVICE_PROFILE, (tenantId, entityId) -> {
+ deviceProfileService.deleteDeviceProfile(tenantId, (DeviceProfileId) entityId);
+ });
+ removers.put(EntityType.DEVICE, (tenantId, entityId) -> {
+ deviceService.deleteDevice(tenantId, (DeviceId) entityId);
+ });
+ removers.put(EntityType.WIDGETS_BUNDLE, (tenantId, entityId) -> {
+ widgetsBundleService.deleteWidgetsBundle(tenantId, (WidgetsBundleId) entityId);
+ });
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/EntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/EntityExportService.java
new file mode 100644
index 0000000000..505d1b2a3f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/EntityExportService.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.exporting;
+
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+
+public interface EntityExportService, D extends EntityExportData> {
+
+ D getExportData(EntitiesExportCtx> ctx, I entityId) throws ThingsboardException;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java
new file mode 100644
index 0000000000..0a99979e8c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.exporting;
+
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.HasId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+
+public interface ExportableEntitiesService {
+
+ , I extends EntityId> E findEntityByTenantIdAndExternalId(TenantId tenantId, I externalId);
+
+ , I extends EntityId> E findEntityByTenantIdAndId(TenantId tenantId, I id);
+
+ , I extends EntityId> E findEntityById(I id);
+
+ , I extends EntityId> E findEntityByTenantIdAndName(TenantId tenantId, EntityType entityType, String name);
+
+ , I extends EntityId> PageData findEntitiesByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink);
+
+ I getExternalIdByInternal(I internalId);
+
+ void removeById(TenantId tenantId, I id);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java
new file mode 100644
index 0000000000..8875491d42
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.exporting.impl;
+
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+
+import java.util.Set;
+
+@Service
+@TbCoreComponent
+public class AssetExportService extends BaseEntityExportService> {
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, Asset asset, EntityExportData exportData) {
+ asset.setCustomerId(getExternalIdOrElseInternal(ctx, asset.getCustomerId()));
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.ASSET);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java
new file mode 100644
index 0000000000..169b7f273c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.exporting.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+
+import java.util.Set;
+
+public abstract class BaseEntityExportService, D extends EntityExportData> extends DefaultEntityExportService {
+
+ @Override
+ protected void setAdditionalExportData(EntitiesExportCtx> ctx, E entity, D exportData) throws ThingsboardException {
+ setRelatedEntities(ctx, entity, (D) exportData);
+ super.setAdditionalExportData(ctx, entity, exportData);
+ }
+
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, E mainEntity, D exportData) {
+ }
+
+ protected D newExportData() {
+ return (D) new EntityExportData();
+ }
+
+ ;
+
+ public abstract Set getSupportedEntityTypes();
+
+ protected void replaceUuidsRecursively(EntitiesExportCtx> ctx, JsonNode node, Set skipFieldsSet) {
+ JacksonUtil.replaceUuidsRecursively(node, skipFieldsSet, uuid -> getExternalIdOrElseInternalByUuid(ctx, uuid));
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java
new file mode 100644
index 0000000000..9a3b195b57
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.exporting.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.Lists;
+import org.apache.commons.collections.CollectionUtils;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.DashboardId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+import org.thingsboard.common.util.RegexUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
+import java.util.UUID;
+
+@Service
+@TbCoreComponent
+public class DashboardExportService extends BaseEntityExportService> {
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, Dashboard dashboard, EntityExportData exportData) {
+ if (CollectionUtils.isNotEmpty(dashboard.getAssignedCustomers())) {
+ dashboard.getAssignedCustomers().forEach(customerInfo -> {
+ customerInfo.setCustomerId(getExternalIdOrElseInternal(ctx, customerInfo.getCustomerId()));
+ });
+ }
+ for (JsonNode entityAlias : dashboard.getEntityAliasesConfig()) {
+ replaceUuidsRecursively(ctx, entityAlias, Collections.emptySet());
+ }
+ for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) {
+ replaceUuidsRecursively(ctx, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.singleton("id"));
+ }
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.DASHBOARD);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
new file mode 100644
index 0000000000..7e236370df
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
@@ -0,0 +1,184 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.exporting.impl;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.context.annotation.Primary;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.sync.ie.AttributeExportData;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;
+import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+@Service
+@TbCoreComponent
+@Primary
+public class DefaultEntityExportService, D extends EntityExportData> implements EntityExportService {
+
+ @Autowired
+ @Lazy
+ protected ExportableEntitiesService exportableEntitiesService;
+ @Autowired
+ private RelationService relationService;
+ @Autowired
+ private AttributesService attributesService;
+
+ @Override
+ public final D getExportData(EntitiesExportCtx> ctx, I entityId) throws ThingsboardException {
+ D exportData = newExportData();
+
+ E entity = exportableEntitiesService.findEntityByTenantIdAndId(ctx.getTenantId(), entityId);
+ if (entity == null) {
+ throw new IllegalArgumentException(entityId.getEntityType() + " [" + entityId.getId() + "] not found");
+ }
+
+ exportData.setEntity(entity);
+ exportData.setEntityType(entityId.getEntityType());
+ setAdditionalExportData(ctx, entity, exportData);
+
+ var externalId = entity.getExternalId() != null ? entity.getExternalId() : entity.getId();
+ ctx.putExternalId(entityId, externalId);
+ entity.setId(externalId);
+ entity.setTenantId(null);
+
+ return exportData;
+ }
+
+ protected void setAdditionalExportData(EntitiesExportCtx> ctx, E entity, D exportData) throws ThingsboardException {
+ var exportSettings = ctx.getSettings();
+ if (exportSettings.isExportRelations()) {
+ List relations = exportRelations(ctx, entity);
+ relations.forEach(relation -> {
+ relation.setFrom(getExternalIdOrElseInternal(ctx, relation.getFrom()));
+ relation.setTo(getExternalIdOrElseInternal(ctx, relation.getTo()));
+ });
+ exportData.setRelations(relations);
+ }
+ if (exportSettings.isExportAttributes()) {
+ Map> attributes = exportAttributes(ctx, entity);
+ exportData.setAttributes(attributes);
+ }
+ }
+
+ private List exportRelations(EntitiesExportCtx> ctx, E entity) throws ThingsboardException {
+ List relations = new ArrayList<>();
+
+ List inboundRelations = relationService.findByTo(ctx.getTenantId(), entity.getId(), RelationTypeGroup.COMMON);
+ relations.addAll(inboundRelations);
+
+ List outboundRelations = relationService.findByFrom(ctx.getTenantId(), entity.getId(), RelationTypeGroup.COMMON);
+ relations.addAll(outboundRelations);
+ return relations;
+ }
+
+ private Map> exportAttributes(EntitiesExportCtx> ctx, E entity) throws ThingsboardException {
+ List scopes;
+ if (entity.getId().getEntityType() == EntityType.DEVICE) {
+ scopes = List.of(DataConstants.SERVER_SCOPE, DataConstants.SHARED_SCOPE);
+ } else {
+ scopes = Collections.singletonList(DataConstants.SERVER_SCOPE);
+ }
+ Map> attributes = new LinkedHashMap<>();
+ scopes.forEach(scope -> {
+ try {
+ attributes.put(scope, attributesService.findAll(ctx.getTenantId(), entity.getId(), scope).get().stream()
+ .map(attribute -> {
+ AttributeExportData attributeExportData = new AttributeExportData();
+ attributeExportData.setKey(attribute.getKey());
+ attributeExportData.setLastUpdateTs(attribute.getLastUpdateTs());
+ attributeExportData.setStrValue(attribute.getStrValue().orElse(null));
+ attributeExportData.setDoubleValue(attribute.getDoubleValue().orElse(null));
+ attributeExportData.setLongValue(attribute.getLongValue().orElse(null));
+ attributeExportData.setBooleanValue(attribute.getBooleanValue().orElse(null));
+ attributeExportData.setJsonValue(attribute.getJsonValue().orElse(null));
+ return attributeExportData;
+ })
+ .collect(Collectors.toList()));
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ return attributes;
+ }
+
+ protected ID getExternalIdOrElseInternal(EntitiesExportCtx> ctx, ID internalId) {
+ if (internalId == null || internalId.isNullUid()) return internalId;
+ var result = ctx.getExternalId(internalId);
+ if (result == null) {
+ result = Optional.ofNullable(exportableEntitiesService.getExternalIdByInternal(internalId))
+ .orElse(internalId);
+ ctx.putExternalId(internalId, result);
+ }
+ return result;
+ }
+
+ protected UUID getExternalIdOrElseInternalByUuid(EntitiesExportCtx> ctx, UUID internalUuid) {
+ for (EntityType entityType : EntityType.values()) {
+ EntityId internalId;
+ try {
+ internalId = EntityIdFactory.getByTypeAndUuid(entityType, internalUuid);
+ } catch (Exception e) {
+ continue;
+ }
+ EntityId externalId = ctx.getExternalId(internalId);
+ if (externalId != null) {
+ return externalId.getId();
+ }
+ }
+ for (EntityType entityType : EntityType.values()) {
+ EntityId internalId;
+ try {
+ internalId = EntityIdFactory.getByTypeAndUuid(entityType, internalUuid);
+ } catch (Exception e) {
+ continue;
+ }
+ EntityId externalId = exportableEntitiesService.getExternalIdByInternal(internalId);
+ if (externalId != null) {
+ ctx.putExternalId(internalId, externalId);
+ return externalId.getId();
+ }
+ }
+ return internalUuid;
+ }
+
+ protected D newExportData() {
+ return (D) new EntityExportData();
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java
new file mode 100644
index 0000000000..bc5136bee6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.exporting.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.sync.ie.DeviceExportData;
+import org.thingsboard.server.dao.device.DeviceCredentialsService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+
+import java.util.Set;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class DeviceExportService extends BaseEntityExportService {
+
+ private final DeviceCredentialsService deviceCredentialsService;
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, Device device, DeviceExportData exportData) {
+ device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId()));
+ device.setDeviceProfileId(getExternalIdOrElseInternal(ctx, device.getDeviceProfileId()));
+ if (ctx.getSettings().isExportCredentials()) {
+ var credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), device.getId());
+ credentials.setId(null);
+ credentials.setDeviceId(null);
+ exportData.setCredentials(credentials);
+ }
+ }
+
+ @Override
+ protected DeviceExportData newExportData() {
+ return new DeviceExportData();
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.DEVICE);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java
new file mode 100644
index 0000000000..46423c66ef
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.exporting.impl;
+
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+
+import java.util.Set;
+
+@Service
+@TbCoreComponent
+public class DeviceProfileExportService extends BaseEntityExportService> {
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, DeviceProfile deviceProfile, EntityExportData exportData) {
+ deviceProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultDashboardId()));
+ deviceProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId()));
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.DEVICE_PROFILE);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/EntityViewExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/EntityViewExportService.java
new file mode 100644
index 0000000000..3fff890d3a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/EntityViewExportService.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.exporting.impl;
+
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.EntityView;
+import org.thingsboard.server.common.data.id.EntityViewId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+
+import java.util.Set;
+
+@Service
+@TbCoreComponent
+public class EntityViewExportService extends BaseEntityExportService> {
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, EntityView entityView, EntityExportData exportData) {
+ entityView.setEntityId(getExternalIdOrElseInternal(ctx, entityView.getEntityId()));
+ entityView.setCustomerId(getExternalIdOrElseInternal(ctx, entityView.getCustomerId()));
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.ENTITY_VIEW);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java
new file mode 100644
index 0000000000..802a03ab94
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.exporting.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.sync.ie.RuleChainExportData;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+import org.thingsboard.common.util.RegexUtils;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class RuleChainExportService extends BaseEntityExportService {
+
+ private final RuleChainService ruleChainService;
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, RuleChain ruleChain, RuleChainExportData exportData) {
+ RuleChainMetaData metaData = ruleChainService.loadRuleChainMetaData(ctx.getTenantId(), ruleChain.getId());
+ Optional.ofNullable(metaData.getNodes()).orElse(Collections.emptyList())
+ .forEach(ruleNode -> {
+ ruleNode.setRuleChainId(null);
+ ctx.putExternalId(ruleNode.getId(), ruleNode.getExternalId());
+ ruleNode.setId(ctx.getExternalId(ruleNode.getId()));
+ ruleNode.setCreatedTime(0);
+ ruleNode.setExternalId(null);
+ replaceUuidsRecursively(ctx, ruleNode.getConfiguration(), Collections.emptySet());
+ });
+ Optional.ofNullable(metaData.getRuleChainConnections()).orElse(Collections.emptyList())
+ .forEach(ruleChainConnectionInfo -> {
+ ruleChainConnectionInfo.setTargetRuleChainId(getExternalIdOrElseInternal(ctx, ruleChainConnectionInfo.getTargetRuleChainId()));
+ });
+ exportData.setMetaData(metaData);
+ if (ruleChain.getFirstRuleNodeId() != null) {
+ ruleChain.setFirstRuleNodeId(ctx.getExternalId(ruleChain.getFirstRuleNodeId()));
+ }
+ }
+
+ @Override
+ protected RuleChainExportData newExportData() {
+ return new RuleChainExportData();
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.RULE_CHAIN);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java
new file mode 100644
index 0000000000..d3dd23910f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.exporting.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.WidgetsBundleId;
+import org.thingsboard.server.common.data.sync.ie.WidgetsBundleExportData;
+import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.widget.WidgetTypeService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+
+import java.util.List;
+import java.util.Set;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class WidgetsBundleExportService extends BaseEntityExportService {
+
+ private final WidgetTypeService widgetTypeService;
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, WidgetsBundle widgetsBundle, WidgetsBundleExportData exportData) {
+ if (widgetsBundle.getTenantId() == null || widgetsBundle.getTenantId().isNullUid()) {
+ throw new IllegalArgumentException("Export of system Widget Bundles is not allowed");
+ }
+
+ List widgets = widgetTypeService.findWidgetTypesDetailsByTenantIdAndBundleAlias(ctx.getTenantId(), widgetsBundle.getAlias());
+ exportData.setWidgets(widgets);
+ }
+
+ @Override
+ protected WidgetsBundleExportData newExportData() {
+ return new WidgetsBundleExportData();
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.WIDGETS_BUNDLE);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/EntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/EntityImportService.java
new file mode 100644
index 0000000000..9437f21e60
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/EntityImportService.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.importing;
+
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+public interface EntityImportService, D extends EntityExportData> {
+
+ EntityImportResult importEntity(EntitiesImportCtx ctx, D exportData) throws ThingsboardException;
+
+ EntityType getEntityType();
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
similarity index 97%
rename from application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java
rename to application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
index 81a0068a4d..b92e100401 100644
--- a/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.importing;
+package org.thingsboard.server.service.sync.ie.importing.csv;
import com.google.common.util.concurrent.FutureCallback;
import com.google.gson.JsonObject;
@@ -44,7 +44,6 @@ import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.controller.BaseController;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.action.EntityActionService;
-import org.thingsboard.server.service.importing.BulkImportRequest.ColumnMapping;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.AccessControlService;
@@ -67,7 +66,6 @@ import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -165,7 +163,7 @@ public abstract class AbstractBulkImportService data) {
+ private void saveKvs(SecurityUser user, E entity, Map data) {
Arrays.stream(BulkImportColumnType.values())
.filter(BulkImportColumnType::isKv)
.map(kvType -> {
@@ -249,7 +247,7 @@ public abstract class AbstractBulkImportService columnsMappings = request.getMapping().getColumns();
+ List columnsMappings = request.getMapping().getColumns();
return records.stream()
.map(record -> {
EntityData entityData = new EntityData();
@@ -280,7 +278,7 @@ public abstract class AbstractBulkImportService fields = new LinkedHashMap<>();
- private final Map kvs = new LinkedHashMap<>();
+ private final Map kvs = new LinkedHashMap<>();
private int lineNumber;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportColumnType.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportColumnType.java
similarity index 97%
rename from application/src/main/java/org/thingsboard/server/service/importing/BulkImportColumnType.java
rename to application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportColumnType.java
index 96075eb4c8..24b566e631 100644
--- a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportColumnType.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportColumnType.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.importing;
+package org.thingsboard.server.service.sync.ie.importing.csv;
import lombok.Getter;
import org.thingsboard.server.common.data.DataConstants;
diff --git a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportRequest.java
similarity index 94%
rename from application/src/main/java/org/thingsboard/server/service/importing/BulkImportRequest.java
rename to application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportRequest.java
index 9f8195ca45..e8eac6a9ed 100644
--- a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportRequest.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportRequest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.importing;
+package org.thingsboard.server.service.sync.ie.importing.csv;
import lombok.Data;
diff --git a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportResult.java
similarity index 94%
rename from application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java
rename to application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportResult.java
index 651aedeb0b..0626c8e690 100644
--- a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportResult.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.importing;
+package org.thingsboard.server.service.sync.ie.importing.csv;
import lombok.Data;
diff --git a/application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/ImportedEntityInfo.java
similarity index 92%
rename from application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java
rename to application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/ImportedEntityInfo.java
index 45c2551be2..d48e9a3d23 100644
--- a/application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/ImportedEntityInfo.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.importing;
+package org.thingsboard.server.service.sync.ie.importing.csv;
import lombok.Data;
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java
new file mode 100644
index 0000000000..cd9dc17d60
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.importing.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class AssetImportService extends BaseEntityImportService> {
+
+ private final AssetService assetService;
+
+ @Override
+ protected void setOwner(TenantId tenantId, Asset asset, IdProvider idProvider) {
+ asset.setTenantId(tenantId);
+ asset.setCustomerId(idProvider.getInternalId(asset.getCustomerId()));
+ }
+
+ @Override
+ protected Asset prepare(EntitiesImportCtx ctx, Asset asset, Asset old, EntityExportData exportData, IdProvider idProvider) {
+ return asset;
+ }
+
+ @Override
+ protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData exportData, IdProvider idProvider) {
+ return assetService.saveAsset(asset);
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, Asset savedAsset, Asset oldAsset) throws ThingsboardException {
+ super.onEntitySaved(user, savedAsset, oldAsset);
+ if (oldAsset != null) {
+ entityActionService.sendEntityNotificationMsgToEdgeService(user.getTenantId(), savedAsset.getId(), EdgeEventActionType.UPDATED);
+ }
+ }
+
+ @Override
+ protected Asset deepCopy(Asset asset) {
+ return new Asset(asset);
+ }
+
+ @Override
+ protected void cleanupForComparison(Asset e) {
+ super.cleanupForComparison(e);
+ if (e.getCustomerId() != null && e.getCustomerId().isNullUid()) {
+ e.setCustomerId(null);
+ }
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.ASSET;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
new file mode 100644
index 0000000000..26c5a869f9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
@@ -0,0 +1,398 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.importing.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.api.client.util.Objects;
+import com.google.common.util.concurrent.FutureCallback;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.transaction.annotation.Transactional;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.cluster.TbClusterService;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.HasCustomerId;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.HasId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.JsonDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.sync.ie.AttributeExportData;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.service.action.EntityActionService;
+import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService;
+import org.thingsboard.server.service.sync.ie.importing.EntityImportService;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Slf4j
+public abstract class BaseEntityImportService, D extends EntityExportData> implements EntityImportService {
+
+ @Autowired
+ @Lazy
+ private ExportableEntitiesService exportableEntitiesService;
+ @Autowired
+ private RelationService relationService;
+ @Autowired
+ private TelemetrySubscriptionService tsSubService;
+ @Autowired
+ protected EntityActionService entityActionService;
+ @Autowired
+ protected TbClusterService clusterService;
+ @Autowired
+ protected TbNotificationEntityService entityNotificationService;
+
+ @Transactional(rollbackFor = Exception.class)
+ @Override
+ public EntityImportResult importEntity(EntitiesImportCtx ctx, D exportData) throws ThingsboardException {
+ EntityImportResult importResult = new EntityImportResult<>();
+ ctx.setCurrentImportResult(importResult);
+ importResult.setEntityType(getEntityType());
+ IdProvider idProvider = new IdProvider(ctx, importResult);
+
+ E entity = exportData.getEntity();
+ entity.setExternalId(entity.getId());
+
+ E existingEntity = findExistingEntity(ctx, entity, idProvider);
+ importResult.setOldEntity(existingEntity);
+
+ setOwner(ctx.getTenantId(), entity, idProvider);
+ if (existingEntity == null) {
+ entity.setId(null);
+ } else {
+ entity.setId(existingEntity.getId());
+ entity.setCreatedTime(existingEntity.getCreatedTime());
+ }
+
+ E prepared = prepare(ctx, entity, existingEntity, exportData, idProvider);
+
+ boolean saveOrUpdate = existingEntity == null || compare(ctx, exportData, prepared, existingEntity);
+
+ if (saveOrUpdate) {
+ E savedEntity = saveOrUpdate(ctx, prepared, exportData, idProvider);
+ boolean created = existingEntity == null;
+ importResult.setCreated(created);
+ importResult.setUpdated(!created);
+ importResult.setSavedEntity(savedEntity);
+ ctx.putInternalId(exportData.getExternalId(), savedEntity.getId());
+ } else {
+ importResult.setSavedEntity(existingEntity);
+ ctx.putInternalId(exportData.getExternalId(), existingEntity.getId());
+ importResult.setUpdatedRelatedEntities(updateRelatedEntitiesIfUnmodified(ctx, prepared, exportData, idProvider));
+ }
+
+ processAfterSaved(ctx, importResult, exportData, idProvider);
+
+ return importResult;
+ }
+
+ protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) {
+ return false;
+ }
+
+ @Override
+ public abstract EntityType getEntityType();
+
+ protected abstract void setOwner(TenantId tenantId, E entity, IdProvider idProvider);
+
+ protected abstract E prepare(EntitiesImportCtx ctx, E entity, E oldEntity, D exportData, IdProvider idProvider);
+
+ protected boolean compare(EntitiesImportCtx ctx, D exportData, E prepared, E existing) {
+ var newCopy = deepCopy(prepared);
+ var existingCopy = deepCopy(existing);
+ cleanupForComparison(newCopy);
+ cleanupForComparison(existingCopy);
+ var result = !newCopy.equals(existingCopy);
+ if (result) {
+ log.debug("[{}] Found update.", prepared.getId());
+ log.debug("[{}] From: {}", prepared.getId(), newCopy);
+ log.debug("[{}] To: {}", prepared.getId(), existingCopy);
+ }
+ return result;
+ }
+
+ protected abstract E deepCopy(E e);
+
+ protected void cleanupForComparison(E e) {
+ e.setTenantId(null);
+ e.setCreatedTime(0);
+ }
+
+ protected abstract E saveOrUpdate(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider);
+
+
+ protected void processAfterSaved(EntitiesImportCtx ctx, EntityImportResult importResult, D exportData, IdProvider idProvider) throws ThingsboardException {
+ E savedEntity = importResult.getSavedEntity();
+ E oldEntity = importResult.getOldEntity();
+
+ if (importResult.isCreated() || importResult.isUpdated()) {
+ importResult.addSendEventsCallback(() -> onEntitySaved(ctx.getUser(), savedEntity, oldEntity));
+ }
+
+ if (ctx.isUpdateRelations() && exportData.getRelations() != null) {
+ importRelations(ctx, exportData.getRelations(), importResult, idProvider);
+ }
+ if (ctx.isSaveAttributes() && exportData.getAttributes() != null) {
+ if (exportData.getAttributes().values().stream().anyMatch(d -> !d.isEmpty())) {
+ importResult.setUpdatedRelatedEntities(true);
+ }
+ importAttributes(ctx.getUser(), exportData.getAttributes(), importResult);
+ }
+ }
+
+ private void importRelations(EntitiesImportCtx ctx, List relations, EntityImportResult importResult, IdProvider idProvider) {
+ var tenantId = ctx.getTenantId();
+ E entity = importResult.getSavedEntity();
+ importResult.addSaveReferencesCallback(() -> {
+ for (EntityRelation relation : relations) {
+ if (!relation.getTo().equals(entity.getId())) {
+ relation.setTo(idProvider.getInternalId(relation.getTo()));
+ }
+ if (!relation.getFrom().equals(entity.getId())) {
+ relation.setFrom(idProvider.getInternalId(relation.getFrom()));
+ }
+ }
+
+ Map relationsMap = new LinkedHashMap<>();
+ relations.forEach(r -> relationsMap.put(r, r));
+
+ if (importResult.getOldEntity() != null) {
+ List existingRelations = new ArrayList<>();
+ existingRelations.addAll(relationService.findByTo(tenantId, entity.getId(), RelationTypeGroup.COMMON));
+ existingRelations.addAll(relationService.findByFrom(tenantId, entity.getId(), RelationTypeGroup.COMMON));
+
+ for (EntityRelation existingRelation : existingRelations) {
+ EntityRelation relation = relationsMap.get(existingRelation);
+ if (relation == null) {
+ importResult.setUpdatedRelatedEntities(true);
+ relationService.deleteRelation(tenantId, existingRelation);
+ importResult.addSendEventsCallback(() -> {
+ entityActionService.logEntityAction(ctx.getUser(), existingRelation.getFrom(), null, null,
+ ActionType.RELATION_DELETED, null, existingRelation);
+ entityActionService.logEntityAction(ctx.getUser(), existingRelation.getTo(), null, null,
+ ActionType.RELATION_DELETED, null, existingRelation);
+ });
+ } else if (Objects.equal(relation.getAdditionalInfo(), existingRelation.getAdditionalInfo())) {
+ relationsMap.remove(relation);
+ }
+ }
+ }
+ if (!relationsMap.isEmpty()) {
+ importResult.setUpdatedRelatedEntities(true);
+ ctx.addRelations(relationsMap.values());
+ }
+ });
+ }
+
+ private void importAttributes(SecurityUser user, Map> attributes, EntityImportResult importResult) {
+ E entity = importResult.getSavedEntity();
+ importResult.addSaveReferencesCallback(() -> {
+ attributes.forEach((scope, attributesExportData) -> {
+ List attributeKvEntries = attributesExportData.stream()
+ .map(attributeExportData -> {
+ KvEntry kvEntry;
+ String key = attributeExportData.getKey();
+ if (attributeExportData.getStrValue() != null) {
+ kvEntry = new StringDataEntry(key, attributeExportData.getStrValue());
+ } else if (attributeExportData.getBooleanValue() != null) {
+ kvEntry = new BooleanDataEntry(key, attributeExportData.getBooleanValue());
+ } else if (attributeExportData.getDoubleValue() != null) {
+ kvEntry = new DoubleDataEntry(key, attributeExportData.getDoubleValue());
+ } else if (attributeExportData.getLongValue() != null) {
+ kvEntry = new LongDataEntry(key, attributeExportData.getLongValue());
+ } else if (attributeExportData.getJsonValue() != null) {
+ kvEntry = new JsonDataEntry(key, attributeExportData.getJsonValue());
+ } else {
+ throw new IllegalArgumentException("Invalid attribute export data");
+ }
+ return new BaseAttributeKvEntry(kvEntry, attributeExportData.getLastUpdateTs());
+ })
+ .collect(Collectors.toList());
+ // fixme: attributes are saved outside the transaction
+ tsSubService.saveAndNotify(user.getTenantId(), entity.getId(), scope, attributeKvEntries, new FutureCallback() {
+ @Override
+ public void onSuccess(@Nullable Void unused) {
+ }
+
+ @Override
+ public void onFailure(Throwable thr) {
+ log.error("Failed to import attributes for {} {}", entity.getId().getEntityType(), entity.getId(), thr);
+ }
+ });
+ });
+ });
+ }
+
+ protected void onEntitySaved(SecurityUser user, E savedEntity, E oldEntity) throws ThingsboardException {
+ entityActionService.logEntityAction(user, savedEntity.getId(), savedEntity,
+ savedEntity instanceof HasCustomerId ? ((HasCustomerId) savedEntity).getCustomerId() : user.getCustomerId(),
+ oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, null);
+ }
+
+
+ @SuppressWarnings("unchecked")
+ protected E findExistingEntity(EntitiesImportCtx ctx, E entity, IdProvider idProvider) {
+ return (E) Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndExternalId(ctx.getTenantId(), entity.getId()))
+ .or(() -> Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndId(ctx.getTenantId(), entity.getId())))
+ .or(() -> {
+ if (ctx.isFindExistingByName()) {
+ return Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndName(ctx.getTenantId(), getEntityType(), entity.getName()));
+ } else {
+ return Optional.empty();
+ }
+ })
+ .orElse(null);
+ }
+
+ @SuppressWarnings("unchecked")
+ private HasId findInternalEntity(TenantId tenantId, ID externalId) {
+ return (HasId) Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndExternalId(tenantId, externalId))
+ .or(() -> Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndId(tenantId, externalId)))
+ .orElseThrow(() -> new MissingEntityException(externalId));
+ }
+
+
+ @SuppressWarnings("unchecked")
+ @RequiredArgsConstructor
+ protected class IdProvider {
+ private final EntitiesImportCtx ctx;
+ private final EntityImportResult importResult;
+
+ public ID getInternalId(ID externalId) {
+ return getInternalId(externalId, true);
+ }
+
+ public ID getInternalId(ID externalId, boolean throwExceptionIfNotFound) {
+ if (externalId == null || externalId.isNullUid()) return null;
+
+ EntityId localId = ctx.getInternalId(externalId);
+ if (localId != null) {
+ return (ID) localId;
+ }
+
+ HasId entity;
+ try {
+ entity = findInternalEntity(ctx.getTenantId(), externalId);
+ } catch (Exception e) {
+ if (throwExceptionIfNotFound) {
+ throw e;
+ } else {
+ importResult.setUpdatedAllExternalIds(false);
+ return null;
+ }
+ }
+ ctx.putInternalId(externalId, entity.getId());
+ return entity.getId();
+ }
+
+ public Optional getInternalIdByUuid(UUID externalUuid, boolean fetchAllUUIDs, Set hints) {
+ if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty();
+
+ for (EntityType entityType : EntityType.values()) {
+ Optional externalIdOpt = buildEntityId(entityType, externalUuid);
+ if (!externalIdOpt.isPresent()) {
+ continue;
+ }
+ EntityId internalId = ctx.getInternalId(externalIdOpt.get());
+ if (internalId != null) {
+ return Optional.of(internalId);
+ }
+ }
+
+ if (fetchAllUUIDs) {
+ for (EntityType entityType : hints) {
+ Optional internalId = lookupInDb(externalUuid, entityType);
+ if (internalId.isPresent()) return internalId;
+ }
+ for (EntityType entityType : EntityType.values()) {
+ if (hints.contains(entityType)) {
+ continue;
+ }
+ Optional internalId = lookupInDb(externalUuid, entityType);
+ if (internalId.isPresent()) return internalId;
+ }
+ }
+
+ importResult.setUpdatedAllExternalIds(false);
+ return Optional.empty();
+ }
+
+ private Optional lookupInDb(UUID externalUuid, EntityType entityType) {
+ Optional externalIdOpt = buildEntityId(entityType, externalUuid);
+ if (externalIdOpt.isEmpty() || ctx.isNotFound(externalIdOpt.get())) {
+ return Optional.empty();
+ }
+ EntityId internalId = getInternalId(externalIdOpt.get(), false);
+ if (internalId != null) {
+ return Optional.of(internalId);
+ } else {
+ ctx.registerNotFound(externalIdOpt.get());
+ }
+ return Optional.empty();
+ }
+
+ private Optional buildEntityId(EntityType entityType, UUID externalUuid) {
+ try {
+ return Optional.of(EntityIdFactory.getByTypeAndUuid(entityType, externalUuid));
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+ }
+
+ }
+
+ protected T getOldEntityField(O oldEntity, Function getter) {
+ return oldEntity == null ? null : getter.apply(oldEntity);
+ }
+
+ protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode entityAlias, Set skipFieldsSet, LinkedHashSet hints) {
+ JacksonUtil.replaceUuidsRecursively(entityAlias, skipFieldsSet,
+ uuid -> idProvider.getInternalIdByUuid(uuid, ctx.isFinalImportAttempt(), hints).map(EntityId::getId).orElse(uuid));
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java
new file mode 100644
index 0000000000..1dc476340f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.importing.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.dao.customer.CustomerDao;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class CustomerImportService extends BaseEntityImportService> {
+
+ private final CustomerService customerService;
+ private final CustomerDao customerDao;
+
+ @Override
+ protected void setOwner(TenantId tenantId, Customer customer, IdProvider idProvider) {
+ customer.setTenantId(tenantId);
+ }
+
+ @Override
+ protected Customer prepare(EntitiesImportCtx ctx, Customer customer, Customer old, EntityExportData exportData, IdProvider idProvider) {
+ if (customer.isPublic()) {
+ Customer publicCustomer = customerService.findOrCreatePublicCustomer(ctx.getTenantId());
+ publicCustomer.setExternalId(customer.getExternalId());
+ return publicCustomer;
+ } else {
+ return customer;
+ }
+ }
+
+ @Override
+ protected Customer saveOrUpdate(EntitiesImportCtx ctx, Customer customer, EntityExportData exportData, IdProvider idProvider) {
+ if (!customer.isPublic()) {
+ return customerService.saveCustomer(customer);
+ } else {
+ return customerDao.save(ctx.getTenantId(), customer);
+ }
+ }
+
+ @Override
+ protected Customer deepCopy(Customer customer) {
+ return new Customer(customer);
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, Customer savedCustomer, Customer oldCustomer) throws ThingsboardException {
+ super.onEntitySaved(user, savedCustomer, oldCustomer);
+ if (oldCustomer != null) {
+ entityActionService.sendEntityNotificationMsgToEdgeService(user.getTenantId(), savedCustomer.getId(), EdgeEventActionType.UPDATED);
+ }
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.CUSTOMER;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java
new file mode 100644
index 0000000000..b6de84bfff
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java
@@ -0,0 +1,139 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.importing.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.Lists;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ShortCustomerInfo;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DashboardId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.dao.dashboard.DashboardService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+import org.thingsboard.common.util.RegexUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class DashboardImportService extends BaseEntityImportService> {
+
+ private static final LinkedHashSet HINTS = new LinkedHashSet<>(Arrays.asList(EntityType.DASHBOARD, EntityType.DEVICE, EntityType.ASSET));
+
+ private final DashboardService dashboardService;
+
+
+ @Override
+ protected void setOwner(TenantId tenantId, Dashboard dashboard, IdProvider idProvider) {
+ dashboard.setTenantId(tenantId);
+ }
+
+ @Override
+ protected Dashboard findExistingEntity(EntitiesImportCtx ctx, Dashboard dashboard, IdProvider idProvider) {
+ Dashboard existingDashboard = super.findExistingEntity(ctx, dashboard, idProvider);
+ if (existingDashboard == null && ctx.isFindExistingByName()) {
+ existingDashboard = dashboardService.findTenantDashboardsByTitle(ctx.getTenantId(), dashboard.getName()).stream().findFirst().orElse(null);
+ }
+ return existingDashboard;
+ }
+
+ @Override
+ protected Dashboard prepare(EntitiesImportCtx ctx, Dashboard dashboard, Dashboard old, EntityExportData exportData, IdProvider idProvider) {
+ for (JsonNode entityAlias : dashboard.getEntityAliasesConfig()) {
+ replaceIdsRecursively(ctx, idProvider, entityAlias, Collections.emptySet(), HINTS);
+ }
+ for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) {
+ replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.singleton("id"), HINTS);
+ }
+ return dashboard;
+ }
+
+ @Override
+ protected Dashboard saveOrUpdate(EntitiesImportCtx ctx, Dashboard dashboard, EntityExportData exportData, IdProvider idProvider) {
+ var tenantId = ctx.getTenantId();
+
+ Set assignedCustomers = Optional.ofNullable(dashboard.getAssignedCustomers()).orElse(Collections.emptySet()).stream()
+ .peek(customerInfo -> customerInfo.setCustomerId(idProvider.getInternalId(customerInfo.getCustomerId())))
+ .collect(Collectors.toSet());
+
+ if (dashboard.getId() == null) {
+ dashboard.setAssignedCustomers(assignedCustomers);
+ dashboard = dashboardService.saveDashboard(dashboard);
+ for (ShortCustomerInfo customerInfo : assignedCustomers) {
+ dashboard = dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), customerInfo.getCustomerId());
+ }
+ } else {
+ Set existingAssignedCustomers = Optional.ofNullable(dashboardService.findDashboardById(tenantId, dashboard.getId()).getAssignedCustomers())
+ .orElse(Collections.emptySet()).stream().map(ShortCustomerInfo::getCustomerId).collect(Collectors.toSet());
+ Set newAssignedCustomers = assignedCustomers.stream().map(ShortCustomerInfo::getCustomerId).collect(Collectors.toSet());
+
+ Set toUnassign = new HashSet<>(existingAssignedCustomers);
+ toUnassign.removeAll(newAssignedCustomers);
+ for (CustomerId customerId : toUnassign) {
+ assignedCustomers = dashboardService.unassignDashboardFromCustomer(tenantId, dashboard.getId(), customerId).getAssignedCustomers();
+ }
+
+ Set toAssign = new HashSet<>(newAssignedCustomers);
+ toAssign.removeAll(existingAssignedCustomers);
+ for (CustomerId customerId : toAssign) {
+ assignedCustomers = dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), customerId).getAssignedCustomers();
+ }
+ dashboard.setAssignedCustomers(assignedCustomers);
+ dashboard = dashboardService.saveDashboard(dashboard);
+ }
+ return dashboard;
+ }
+
+ @Override
+ protected Dashboard deepCopy(Dashboard dashboard) {
+ return new Dashboard(dashboard);
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, Dashboard savedDashboard, Dashboard oldDashboard) throws ThingsboardException {
+ super.onEntitySaved(user, savedDashboard, oldDashboard);
+ if (oldDashboard != null) {
+ entityActionService.sendEntityNotificationMsgToEdgeService(user.getTenantId(), savedDashboard.getId(), EdgeEventActionType.UPDATED);
+ }
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.DASHBOARD;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java
new file mode 100644
index 0000000000..c699f466d7
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.importing.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.sync.ie.DeviceExportData;
+import org.thingsboard.server.dao.device.DeviceCredentialsService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class DeviceImportService extends BaseEntityImportService {
+
+ private final DeviceService deviceService;
+ private final DeviceCredentialsService credentialsService;
+
+ @Override
+ protected void setOwner(TenantId tenantId, Device device, IdProvider idProvider) {
+ device.setTenantId(tenantId);
+ device.setCustomerId(idProvider.getInternalId(device.getCustomerId()));
+ }
+
+ @Override
+ protected Device prepare(EntitiesImportCtx ctx, Device device, Device old, DeviceExportData exportData, IdProvider idProvider) {
+ device.setDeviceProfileId(idProvider.getInternalId(device.getDeviceProfileId()));
+ device.setFirmwareId(getOldEntityField(old, Device::getFirmwareId));
+ device.setSoftwareId(getOldEntityField(old, Device::getSoftwareId));
+ return device;
+ }
+
+ @Override
+ protected Device deepCopy(Device d) {
+ return new Device(d);
+ }
+
+ @Override
+ protected void cleanupForComparison(Device e) {
+ super.cleanupForComparison(e);
+ if (e.getCustomerId() != null && e.getCustomerId().isNullUid()) {
+ e.setCustomerId(null);
+ }
+ }
+
+ @Override
+ protected Device saveOrUpdate(EntitiesImportCtx ctx, Device device, DeviceExportData exportData, IdProvider idProvider) {
+ if (exportData.getCredentials() != null && ctx.isSaveCredentials()) {
+ exportData.getCredentials().setId(null);
+ exportData.getCredentials().setDeviceId(null);
+ return deviceService.saveDeviceWithCredentials(device, exportData.getCredentials());
+ } else {
+ return deviceService.saveDevice(device);
+ }
+ }
+
+ @Override
+ protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, Device prepared, DeviceExportData exportData, IdProvider idProvider) {
+ boolean updated = super.updateRelatedEntitiesIfUnmodified(ctx, prepared, exportData, idProvider);
+ var credentials = exportData.getCredentials();
+ if (credentials != null && ctx.isSaveCredentials()) {
+ var existing = credentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), prepared.getId());
+ credentials.setId(existing.getId());
+ credentials.setDeviceId(prepared.getId());
+ if (!existing.equals(credentials)) {
+ credentialsService.updateDeviceCredentials(ctx.getTenantId(), credentials);
+ updated = true;
+ }
+ }
+ return updated;
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, Device savedDevice, Device oldDevice) throws ThingsboardException {
+ super.onEntitySaved(user, savedDevice, oldDevice);
+ clusterService.onDeviceUpdated(savedDevice, oldDevice);
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.DEVICE;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java
new file mode 100644
index 0000000000..0944792751
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.importing.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.dao.device.DeviceProfileService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.ota.OtaPackageStateService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+import java.util.Objects;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class DeviceProfileImportService extends BaseEntityImportService> {
+
+ private final DeviceProfileService deviceProfileService;
+ private final OtaPackageStateService otaPackageStateService;
+
+ @Override
+ protected void setOwner(TenantId tenantId, DeviceProfile deviceProfile, IdProvider idProvider) {
+ deviceProfile.setTenantId(tenantId);
+ }
+
+ @Override
+ protected DeviceProfile prepare(EntitiesImportCtx ctx, DeviceProfile deviceProfile, DeviceProfile old, EntityExportData exportData, IdProvider idProvider) {
+ deviceProfile.setDefaultRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultRuleChainId()));
+ deviceProfile.setDefaultDashboardId(idProvider.getInternalId(deviceProfile.getDefaultDashboardId()));
+ deviceProfile.setFirmwareId(getOldEntityField(old, DeviceProfile::getFirmwareId));
+ deviceProfile.setSoftwareId(getOldEntityField(old, DeviceProfile::getSoftwareId));
+ return deviceProfile;
+ }
+
+ @Override
+ protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData exportData, IdProvider idProvider) {
+ return deviceProfileService.saveDeviceProfile(deviceProfile);
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, DeviceProfile savedDeviceProfile, DeviceProfile oldDeviceProfile) throws ThingsboardException {
+ super.onEntitySaved(user, savedDeviceProfile, oldDeviceProfile);
+ clusterService.onDeviceProfileChange(savedDeviceProfile, null);
+ clusterService.broadcastEntityStateChangeEvent(user.getTenantId(), savedDeviceProfile.getId(),
+ oldDeviceProfile == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+ entityActionService.sendEntityNotificationMsgToEdgeService(user.getTenantId(), savedDeviceProfile.getId(),
+ oldDeviceProfile == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED);
+ otaPackageStateService.update(savedDeviceProfile,
+ oldDeviceProfile != null && !Objects.equals(oldDeviceProfile.getFirmwareId(), savedDeviceProfile.getFirmwareId()),
+ oldDeviceProfile != null && !Objects.equals(oldDeviceProfile.getSoftwareId(), savedDeviceProfile.getSoftwareId()));
+ }
+
+ @Override
+ protected DeviceProfile deepCopy(DeviceProfile deviceProfile) {
+ return new DeviceProfile(deviceProfile);
+ }
+
+ @Override
+ protected void cleanupForComparison(DeviceProfile deviceProfile) {
+ super.cleanupForComparison(deviceProfile);
+ deviceProfile.setFirmwareId(null);
+ deviceProfile.setSoftwareId(null);
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.DEVICE_PROFILE;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java
new file mode 100644
index 0000000000..57d22cf7be
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.importing.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.EntityView;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityViewId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.dao.entityview.EntityViewService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.entitiy.entityView.TbEntityViewService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class EntityViewImportService extends BaseEntityImportService> {
+
+ private final EntityViewService entityViewService;
+
+ @Lazy
+ @Autowired
+ private TbEntityViewService tbEntityViewService;
+
+ @Override
+ protected void setOwner(TenantId tenantId, EntityView entityView, IdProvider idProvider) {
+ entityView.setTenantId(tenantId);
+ entityView.setCustomerId(idProvider.getInternalId(entityView.getCustomerId()));
+ }
+
+ @Override
+ protected EntityView prepare(EntitiesImportCtx ctx, EntityView entityView, EntityView old, EntityExportData exportData, IdProvider idProvider) {
+ entityView.setEntityId(idProvider.getInternalId(entityView.getEntityId()));
+ return entityView;
+ }
+
+ @Override
+ protected EntityView saveOrUpdate(EntitiesImportCtx ctx, EntityView entityView, EntityExportData exportData, IdProvider idProvider) {
+ return entityViewService.saveEntityView(entityView);
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, EntityView savedEntityView, EntityView oldEntityView) throws ThingsboardException {
+ tbEntityViewService.updateEntityViewAttributes(user, savedEntityView, oldEntityView);
+ super.onEntitySaved(user, savedEntityView, oldEntityView);
+ if (oldEntityView != null) {
+ entityActionService.sendEntityNotificationMsgToEdgeService(user.getTenantId(), savedEntityView.getId(), EdgeEventActionType.UPDATED);
+ }
+ }
+
+ @Override
+ protected EntityView deepCopy(EntityView entityView) {
+ return new EntityView(entityView);
+ }
+
+ @Override
+ protected void cleanupForComparison(EntityView e) {
+ super.cleanupForComparison(e);
+ if (e.getCustomerId() != null && e.getCustomerId().isNullUid()) {
+ e.setCustomerId(null);
+ }
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.ENTITY_VIEW;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ImportServiceException.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ImportServiceException.java
new file mode 100644
index 0000000000..1e869429d8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ImportServiceException.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.importing.impl;
+
+public class ImportServiceException extends RuntimeException{
+ private static final long serialVersionUID = -4932715239522125041L;
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/MissingEntityException.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/MissingEntityException.java
new file mode 100644
index 0000000000..a0a961bfef
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/MissingEntityException.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.importing.impl;
+
+import lombok.Getter;
+import org.thingsboard.server.common.data.id.EntityId;
+
+public class MissingEntityException extends ImportServiceException {
+
+ private static final long serialVersionUID = 3669135386955906022L;
+ @Getter
+ private final EntityId entityId;
+
+ public MissingEntityException(EntityId entityId) {
+ this.entityId = entityId;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java
new file mode 100644
index 0000000000..916c10844f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java
@@ -0,0 +1,152 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.importing.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleChainType;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.data.sync.ie.RuleChainExportData;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.rule.RuleNodeDao;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+import org.thingsboard.common.util.RegexUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class RuleChainImportService extends BaseEntityImportService {
+
+ private static final LinkedHashSet HINTS = new LinkedHashSet<>(Arrays.asList(EntityType.RULE_CHAIN, EntityType.DEVICE, EntityType.ASSET));
+
+ private final RuleChainService ruleChainService;
+ private final RuleNodeDao ruleNodeDao;
+
+ @Override
+ protected void setOwner(TenantId tenantId, RuleChain ruleChain, IdProvider idProvider) {
+ ruleChain.setTenantId(tenantId);
+ }
+
+ @Override
+ protected RuleChain findExistingEntity(EntitiesImportCtx ctx, RuleChain ruleChain, IdProvider idProvider) {
+ RuleChain existingRuleChain = super.findExistingEntity(ctx, ruleChain, idProvider);
+ if (existingRuleChain == null && ctx.isFindExistingByName()) {
+ existingRuleChain = ruleChainService.findTenantRuleChainsByTypeAndName(ctx.getTenantId(), ruleChain.getType(), ruleChain.getName()).stream().findFirst().orElse(null);
+ }
+ return existingRuleChain;
+ }
+
+ @Override
+ protected RuleChain prepare(EntitiesImportCtx ctx, RuleChain ruleChain, RuleChain old, RuleChainExportData exportData, IdProvider idProvider) {
+ RuleChainMetaData metaData = exportData.getMetaData();
+ List ruleNodes = Optional.ofNullable(metaData.getNodes()).orElse(Collections.emptyList());
+ if (old != null) {
+ List nodeIds = ruleNodes.stream().map(RuleNode::getId).collect(Collectors.toList());
+ List existing = ruleNodeDao.findByExternalIds(old.getId(), nodeIds);
+ existing.forEach(node -> ctx.putInternalId(node.getExternalId(), node.getId()));
+ ruleNodes.forEach(node -> {
+ node.setRuleChainId(old.getId());
+ node.setExternalId(node.getId());
+ node.setId((RuleNodeId) ctx.getInternalId(node.getId()));
+ });
+ } else {
+ ruleNodes.forEach(node -> {
+ node.setRuleChainId(null);
+ node.setExternalId(node.getId());
+ node.setId(null);
+ });
+ }
+
+ ruleNodes.forEach(ruleNode -> replaceIdsRecursively(ctx, idProvider, ruleNode.getConfiguration(), Collections.emptySet(), HINTS));
+ Optional.ofNullable(metaData.getRuleChainConnections()).orElse(Collections.emptyList())
+ .forEach(ruleChainConnectionInfo -> {
+ ruleChainConnectionInfo.setTargetRuleChainId(idProvider.getInternalId(ruleChainConnectionInfo.getTargetRuleChainId(), false));
+ });
+ if (ruleChain.getFirstRuleNodeId() != null) {
+ ruleChain.setFirstRuleNodeId((RuleNodeId) ctx.getInternalId(ruleChain.getFirstRuleNodeId()));
+ }
+ return ruleChain;
+ }
+
+ @Override
+ protected RuleChain saveOrUpdate(EntitiesImportCtx ctx, RuleChain ruleChain, RuleChainExportData exportData, IdProvider idProvider) {
+ ruleChain = ruleChainService.saveRuleChain(ruleChain);
+ if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
+ exportData.getMetaData().setRuleChainId(ruleChain.getId());
+ ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), exportData.getMetaData());
+ return ruleChainService.findRuleChainById(ctx.getTenantId(), ruleChain.getId());
+ } else {
+ return ruleChain;
+ }
+ }
+
+ @Override
+ protected boolean compare(EntitiesImportCtx ctx, RuleChainExportData exportData, RuleChain prepared, RuleChain existing) {
+ boolean different = super.compare(ctx, exportData, prepared, existing);
+ if (!different) {
+ RuleChainMetaData newMD = exportData.getMetaData();
+ RuleChainMetaData existingMD = ruleChainService.loadRuleChainMetaData(ctx.getTenantId(), prepared.getId());
+ existingMD.setRuleChainId(null);
+ different = newMD.equals(existingMD);
+ }
+ return different;
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, RuleChain savedRuleChain, RuleChain oldRuleChain) throws ThingsboardException {
+ super.onEntitySaved(user, savedRuleChain, oldRuleChain);
+ if (savedRuleChain.getType() == RuleChainType.CORE) {
+ clusterService.broadcastEntityStateChangeEvent(user.getTenantId(), savedRuleChain.getId(),
+ oldRuleChain == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+ } else if (savedRuleChain.getType() == RuleChainType.EDGE && oldRuleChain != null) {
+ entityActionService.sendEntityNotificationMsgToEdgeService(user.getTenantId(), savedRuleChain.getId(), EdgeEventActionType.UPDATED);
+ }
+ }
+
+ @Override
+ protected RuleChain deepCopy(RuleChain ruleChain) {
+ return new RuleChain(ruleChain);
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.RULE_CHAIN;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetsBundleImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetsBundleImportService.java
new file mode 100644
index 0000000000..550f00952a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetsBundleImportService.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.ie.importing.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetsBundleId;
+import org.thingsboard.server.common.data.sync.ie.WidgetsBundleExportData;
+import org.thingsboard.server.common.data.widget.BaseWidgetType;
+import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
+import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.widget.WidgetTypeService;
+import org.thingsboard.server.dao.widget.WidgetsBundleService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class WidgetsBundleImportService extends BaseEntityImportService {
+
+ private final WidgetsBundleService widgetsBundleService;
+ private final WidgetTypeService widgetTypeService;
+
+ @Override
+ protected void setOwner(TenantId tenantId, WidgetsBundle widgetsBundle, IdProvider idProvider) {
+ widgetsBundle.setTenantId(tenantId);
+ }
+
+ @Override
+ protected WidgetsBundle prepare(EntitiesImportCtx ctx, WidgetsBundle widgetsBundle, WidgetsBundle old, WidgetsBundleExportData exportData, IdProvider idProvider) {
+ return widgetsBundle;
+ }
+
+ @Override
+ protected WidgetsBundle saveOrUpdate(EntitiesImportCtx ctx, WidgetsBundle widgetsBundle, WidgetsBundleExportData exportData, IdProvider idProvider) {
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+ if (widgetsBundle.getId() == null) {
+ for (WidgetTypeDetails widget : exportData.getWidgets()) {
+ widget.setId(null);
+ widget.setTenantId(ctx.getTenantId());
+ widget.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetTypeService.saveWidgetType(widget);
+ }
+ } else {
+ Map existingWidgets = widgetTypeService.findWidgetTypesInfosByTenantIdAndBundleAlias(ctx.getTenantId(), savedWidgetsBundle.getAlias()).stream()
+ .collect(Collectors.toMap(BaseWidgetType::getAlias, w -> w));
+ for (WidgetTypeDetails widget : exportData.getWidgets()) {
+ WidgetTypeInfo existingWidget;
+ if ((existingWidget = existingWidgets.remove(widget.getAlias())) != null) {
+ widget.setId(existingWidget.getId());
+ widget.setCreatedTime(existingWidget.getCreatedTime());
+ } else {
+ widget.setId(null);
+ }
+ widget.setTenantId(ctx.getTenantId());
+ widget.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetTypeService.saveWidgetType(widget);
+ }
+ existingWidgets.values().stream()
+ .map(BaseWidgetType::getId)
+ .forEach(widgetTypeId -> widgetTypeService.deleteWidgetType(ctx.getTenantId(), widgetTypeId));
+ }
+ return savedWidgetsBundle;
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, WidgetsBundle savedWidgetsBundle, WidgetsBundle oldWidgetsBundle) throws ThingsboardException {
+ super.onEntitySaved(user, savedWidgetsBundle, oldWidgetsBundle);
+ entityNotificationService.notifySendMsgToEdgeService(user.getTenantId(), savedWidgetsBundle.getId(),
+ oldWidgetsBundle == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED);
+ }
+
+ @Override
+ protected WidgetsBundle deepCopy(WidgetsBundle widgetsBundle) {
+ return new WidgetsBundle(widgetsBundle);
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.WIDGETS_BUNDLE;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
new file mode 100644
index 0000000000..1f6c174fdc
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
@@ -0,0 +1,554 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.sync.vc;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.support.TransactionTemplate;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.common.util.TbStopWatch;
+import org.thingsboard.common.util.ThingsBoardExecutors;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.StringUtils;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.HasId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.common.data.sync.ThrowingRunnable;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.common.data.sync.ie.EntityExportSettings;
+import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
+import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
+import org.thingsboard.server.common.data.sync.vc.EntityDataDiff;
+import org.thingsboard.server.common.data.sync.vc.EntityDataInfo;
+import org.thingsboard.server.common.data.sync.vc.EntityLoadError;
+import org.thingsboard.server.common.data.sync.vc.EntityTypeLoadResult;
+import org.thingsboard.server.common.data.sync.vc.EntityVersion;
+import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
+import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
+import org.thingsboard.server.common.data.sync.vc.VersionLoadResult;
+import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
+import org.thingsboard.server.common.data.sync.vc.request.create.AutoVersionCreateConfig;
+import org.thingsboard.server.common.data.sync.vc.request.create.ComplexVersionCreateRequest;
+import org.thingsboard.server.common.data.sync.vc.request.create.EntityTypeVersionCreateConfig;
+import org.thingsboard.server.common.data.sync.vc.request.create.SingleEntityVersionCreateRequest;
+import org.thingsboard.server.common.data.sync.vc.request.create.SyncStrategy;
+import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest;
+import org.thingsboard.server.common.data.sync.vc.request.load.EntityTypeVersionLoadRequest;
+import org.thingsboard.server.common.data.sync.vc.request.load.SingleEntityVersionLoadRequest;
+import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadConfig;
+import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest;
+import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.ie.EntitiesExportImportService;
+import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService;
+import org.thingsboard.server.service.sync.ie.importing.impl.MissingEntityException;
+import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsService;
+import org.thingsboard.server.service.sync.vc.data.ComplexEntitiesExportCtx;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+import org.thingsboard.server.service.sync.vc.data.EntityTypeExportCtx;
+import org.thingsboard.server.service.sync.vc.data.ReimportTask;
+import org.thingsboard.server.service.sync.vc.data.SimpleEntitiesExportCtx;
+import org.thingsboard.server.service.sync.vc.repository.TbRepositorySettingsService;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static com.google.common.util.concurrent.Futures.transform;
+import static com.google.common.util.concurrent.Futures.transformAsync;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultEntitiesVersionControlService implements EntitiesVersionControlService {
+
+ private final TbRepositorySettingsService repositorySettingsService;
+ private final TbAutoCommitSettingsService autoCommitSettingsService;
+ private final GitVersionControlQueueService gitServiceQueue;
+ private final EntitiesExportImportService exportImportService;
+ private final ExportableEntitiesService exportableEntitiesService;
+ private final TbNotificationEntityService entityNotificationService;
+ private final TransactionTemplate transactionTemplate;
+
+ private ListeningExecutorService executor;
+
+ @Value("${vc.thread_pool_size:4}")
+ private int threadPoolSize;
+
+ @PostConstruct
+ public void init() {
+ executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, DefaultEntitiesVersionControlService.class));
+ }
+
+ @PreDestroy
+ public void shutdown() {
+ if (executor != null) {
+ executor.shutdownNow();
+ }
+ }
+
+ @SuppressWarnings("UnstableApiUsage")
+ @Override
+ public ListenableFuture saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception {
+ var pendingCommit = gitServiceQueue.prepareCommit(user, request);
+
+ return transformAsync(pendingCommit, commit -> {
+ List> gitFutures = new ArrayList<>();
+ switch (request.getType()) {
+ case SINGLE_ENTITY: {
+ handleSingleEntityRequest(new SimpleEntitiesExportCtx(user, commit, (SingleEntityVersionCreateRequest) request));
+ break;
+ }
+ case COMPLEX: {
+ handleComplexRequest(new ComplexEntitiesExportCtx(user, commit, (ComplexVersionCreateRequest) request));
+ break;
+ }
+ }
+ return transformAsync(Futures.allAsList(gitFutures), success -> gitServiceQueue.push(commit), executor);
+ }, executor);
+ }
+
+ private void handleSingleEntityRequest(SimpleEntitiesExportCtx ctx) throws Exception {
+ ctx.add(saveEntityData(ctx, ctx.getRequest().getEntityId()));
+ }
+
+ private void handleComplexRequest(ComplexEntitiesExportCtx parentCtx) {
+ ComplexVersionCreateRequest request = parentCtx.getRequest();
+ request.getEntityTypes().forEach((entityType, config) -> {
+ EntityTypeExportCtx ctx = new EntityTypeExportCtx(parentCtx, config, request.getSyncStrategy(), entityType);
+ if (ctx.isOverwrite()) {
+ ctx.add(gitServiceQueue.deleteAll(ctx.getCommit(), entityType));
+ }
+
+ if (config.isAllEntities()) {
+ DaoUtil.processInBatches(pageLink -> exportableEntitiesService.findEntitiesByTenantId(ctx.getTenantId(), entityType, pageLink)
+ , 100, entity -> {
+ try {
+ ctx.add(saveEntityData(ctx, entity.getId()));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ } else {
+ for (UUID entityId : config.getEntityIds()) {
+ try {
+ ctx.add(saveEntityData(ctx, EntityIdFactory.getByTypeAndUuid(entityType, entityId)));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ });
+ }
+
+ private ListenableFuture saveEntityData(EntitiesExportCtx> ctx, EntityId entityId) throws Exception {
+ EntityExportData> entityData = exportImportService.exportEntity(ctx, entityId);
+ return gitServiceQueue.addToCommit(ctx.getCommit(), entityData);
+ }
+
+ @Override
+ public ListenableFuture> listEntityVersions(TenantId tenantId, String branch, EntityId externalId, PageLink pageLink) throws Exception {
+ return gitServiceQueue.listVersions(tenantId, branch, externalId, pageLink);
+ }
+
+ @Override
+ public ListenableFuture> listEntityTypeVersions(TenantId tenantId, String branch, EntityType entityType, PageLink pageLink) throws Exception {
+ return gitServiceQueue.listVersions(tenantId, branch, entityType, pageLink);
+ }
+
+ @Override
+ public ListenableFuture> listVersions(TenantId tenantId, String branch, PageLink pageLink) throws Exception {
+ return gitServiceQueue.listVersions(tenantId, branch, pageLink);
+ }
+
+ @Override
+ public ListenableFuture> listEntitiesAtVersion(TenantId tenantId, String branch, String versionId, EntityType entityType) throws Exception {
+ return gitServiceQueue.listEntitiesAtVersion(tenantId, branch, versionId, entityType);
+ }
+
+ @Override
+ public ListenableFuture> listAllEntitiesAtVersion(TenantId tenantId, String branch, String versionId) throws Exception {
+ return gitServiceQueue.listEntitiesAtVersion(tenantId, branch, versionId);
+ }
+
+ @SuppressWarnings({"UnstableApiUsage", "rawtypes"})
+ @Override
+ public ListenableFuture loadEntitiesVersion(SecurityUser user, VersionLoadRequest request) throws Exception {
+ switch (request.getType()) {
+ case SINGLE_ENTITY: {
+ SingleEntityVersionLoadRequest versionLoadRequest = (SingleEntityVersionLoadRequest) request;
+ VersionLoadConfig config = versionLoadRequest.getConfig();
+ ListenableFuture future = gitServiceQueue.getEntity(user.getTenantId(), request.getVersionId(), versionLoadRequest.getExternalEntityId());
+ return Futures.transform(future, entityData -> doInTemplate(user, request, ctx -> loadSingleEntity(ctx, config, entityData)), executor);
+ }
+ case ENTITY_TYPE: {
+ EntityTypeVersionLoadRequest versionLoadRequest = (EntityTypeVersionLoadRequest) request;
+ return executor.submit(() -> doInTemplate(user, request, ctx -> loadMultipleEntities(ctx, versionLoadRequest)));
+ }
+ default:
+ throw new IllegalArgumentException("Unsupported version load request");
+ }
+ }
+
+ private VersionLoadResult doInTemplate(SecurityUser user, VersionLoadRequest request, Function function) {
+ try {
+ EntitiesImportCtx ctx = new EntitiesImportCtx(user, request.getVersionId());
+ VersionLoadResult result = transactionTemplate.execute(status -> function.apply(ctx));
+ try {
+ for (ThrowingRunnable throwingRunnable : ctx.getEventCallbacks()) {
+ throwingRunnable.run();
+ }
+ } catch (ThingsboardException e) {
+ throw new RuntimeException(e);
+ }
+ return result;
+ } catch (LoadEntityException e) {
+ return onError(e.getData(), e.getCause());
+ } catch (Exception e) {
+ log.info("[{}] Failed to process request [{}] due to: ", user.getTenantId(), request, e);
+ throw e;
+ }
+ }
+
+ private VersionLoadResult loadSingleEntity(EntitiesImportCtx ctx, VersionLoadConfig config, EntityExportData entityData) {
+ try {
+ ctx.setSettings(EntityImportSettings.builder()
+ .updateRelations(config.isLoadRelations())
+ .saveAttributes(config.isLoadAttributes())
+ .saveCredentials(config.isLoadCredentials())
+ .findExistingByName(false)
+ .build());
+ ctx.setFinalImportAttempt(true);
+ EntityImportResult> importResult = exportImportService.importEntity(ctx, entityData);
+
+ exportImportService.saveReferencesAndRelations(ctx);
+
+ return VersionLoadResult.success(EntityTypeLoadResult.builder()
+ .entityType(importResult.getEntityType())
+ .created(importResult.getOldEntity() == null ? 1 : 0)
+ .updated(importResult.getOldEntity() != null ? 1 : 0)
+ .deleted(0)
+ .build());
+ } catch (Exception e) {
+ throw new LoadEntityException(entityData, e);
+ }
+ }
+
+ @SneakyThrows
+ private VersionLoadResult loadMultipleEntities(EntitiesImportCtx ctx, EntityTypeVersionLoadRequest request) {
+ var sw = TbStopWatch.create("before");
+
+ List entityTypes = request.getEntityTypes().keySet().stream()
+ .sorted(exportImportService.getEntityTypeComparatorForImport()).collect(Collectors.toList());
+ for (EntityType entityType : entityTypes) {
+ log.debug("[{}] Loading {} entities", ctx.getTenantId(), entityType);
+ sw.startNew("Entities " + entityType.name());
+ ctx.setSettings(getEntityImportSettings(request, entityType));
+ importEntities(ctx, entityType);
+ }
+
+ sw.startNew("Reimport");
+ reimport(ctx);
+
+ sw.startNew("Remove Others");
+ request.getEntityTypes().keySet().stream()
+ .filter(entityType -> request.getEntityTypes().get(entityType).isRemoveOtherEntities())
+ .sorted(exportImportService.getEntityTypeComparatorForImport().reversed())
+ .forEach(entityType -> removeOtherEntities(ctx, entityType));
+
+ sw.startNew("References and Relations");
+
+ exportImportService.saveReferencesAndRelations(ctx);
+
+ sw.stop();
+ for (var task : sw.getTaskInfo()) {
+ log.info("[{}] Executed: {} in {}ms", ctx.getTenantId(), task.getTaskName(), task.getTimeMillis());
+ }
+ log.info("[{}] Total time: {}ms", ctx.getTenantId(), sw.getTotalTimeMillis());
+ return VersionLoadResult.success(new ArrayList<>(ctx.getResults().values()));
+ }
+
+ private EntityImportSettings getEntityImportSettings(EntityTypeVersionLoadRequest request, EntityType entityType) {
+ var config = request.getEntityTypes().get(entityType);
+ return EntityImportSettings.builder()
+ .updateRelations(config.isLoadRelations())
+ .saveAttributes(config.isLoadAttributes())
+ .findExistingByName(config.isFindExistingEntityByName())
+ .build();
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void importEntities(EntitiesImportCtx ctx, EntityType entityType) {
+ int limit = 100;
+ int offset = 0;
+ List entityDataList;
+ do {
+ try {
+ entityDataList = gitServiceQueue.getEntities(ctx.getTenantId(), ctx.getVersionId(), entityType, offset, limit).get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ for (EntityExportData entityData : entityDataList) {
+ EntityExportData reimportBackup = JacksonUtil.clone(entityData);
+ log.debug("[{}] Loading {} entities", ctx.getTenantId(), entityType);
+ EntityImportResult> importResult;
+ try {
+ importResult = exportImportService.importEntity(ctx, entityData);
+ } catch (Exception e) {
+ throw new LoadEntityException(entityData, e);
+ }
+ if (!importResult.isUpdatedAllExternalIds()) {
+ ctx.getToReimport().put(entityData.getEntity().getExternalId(), new ReimportTask(reimportBackup, ctx.getSettings()));
+ continue;
+ }
+
+ registerResult(ctx, entityType, importResult);
+ ctx.getImportedEntities().computeIfAbsent(entityType, t -> new HashSet<>())
+ .add(importResult.getSavedEntity().getId());
+ }
+ offset += limit;
+ } while (entityDataList.size() == limit);
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void reimport(EntitiesImportCtx ctx) {
+ ctx.setFinalImportAttempt(true);
+ ctx.getToReimport().forEach((externalId, task) -> {
+ try {
+ EntityExportData entityData = task.getData();
+ var settings = task.getSettings();
+ ctx.setSettings(settings);
+ EntityImportResult> importResult = exportImportService.importEntity(ctx, entityData);
+
+ registerResult(ctx, externalId.getEntityType(), importResult);
+ ctx.getImportedEntities().computeIfAbsent(externalId.getEntityType(), t -> new HashSet<>())
+ .add(importResult.getSavedEntity().getId());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ private void removeOtherEntities(EntitiesImportCtx ctx, EntityType entityType) {
+ DaoUtil.processInBatches(pageLink -> {
+ return exportableEntitiesService.findEntitiesByTenantId(ctx.getTenantId(), entityType, pageLink);
+ }, 100, entity -> {
+ if (ctx.getImportedEntities().get(entityType) == null || !ctx.getImportedEntities().get(entityType).contains(entity.getId())) {
+ exportableEntitiesService.removeById(ctx.getTenantId(), entity.getId());
+
+ ctx.addEventCallback(() -> {
+ entityNotificationService.notifyDeleteEntity(ctx.getTenantId(), entity.getId(),
+ entity, null, ActionType.DELETED, null, ctx.getUser());
+ });
+ ctx.registerDeleted(entityType);
+ }
+ });
+ }
+
+ private VersionLoadResult onError(EntityExportData> entityData, Throwable e) {
+ return analyze(e, entityData).orElseThrow(() -> new RuntimeException(e));
+ }
+
+ private Optional