From eaf1e4504fb4830d3d1c12021a217548200ec39c Mon Sep 17 00:00:00 2001 From: MarkBryanMilligan Date: Sat, 29 Jan 2022 18:25:19 -0600 Subject: [PATCH] Allow exporting all data in bson, json, or csv formats. --- case/Z2/LPM_Case_Z2_Base_Flange.blend | Bin 1000960 -> 1002056 bytes case/Z2/LPM_Case_Z2_Base_Flange.stl | Bin 187684 -> 188684 bytes .../currentmonitor/led/LEDFlasher.java | 8 +- .../currentmonitor/CreateConfig.java | 4 +- .../CurrentMonitorAppSerializers.java | 4 +- .../currentmonitor/ReleaseCurrentMonitor.java | 13 +- .../dataaccess/currentmonitor/Backup.java | 6 +- .../currentmonitor/BackupMinutes.java | 6 +- .../currentmonitor/CurrentMonitorDao.java | 12 + .../MongoCurrentMonitorDao.java | 246 +- .../datamodel/currentmonitor/Breaker.java | 16 + .../currentmonitor/BreakerPowerMinute.java | 4 + .../currentmonitor/archive/ArchiveStatus.java | 50 + .../archive/BreakerEnergyArchive.java | 34 + .../archive/DailyEnergyArchive.java | 18 + .../archive/MonthlyEnergyArchive.java | 53 + .../archive/dao/ArchiveStatusSerializer.java | 44 + .../dao/BreakerEnergyArchiveSerializer.java | 43 + .../dao/DailyEnergyArchiveSerializer.java | 40 + .../dao/MonthlyEnergyArchiveSerializer.java | 45 + ...om.lanternsoftware.util.dao.IDaoSerializer | 4 + .../currentmonitor/context/Globals.java | 4 +- .../currentmonitor/servlet/AuthServlet.java | 28 +- .../currentmonitor/servlet/ChargeServlet.java | 2 +- .../servlet/CommandServlet.java | 2 +- .../currentmonitor/servlet/ConfigServlet.java | 2 +- .../currentmonitor/servlet/EnergyServlet.java | 2 +- .../servlet/FreemarkerCMServlet.java | 23 + .../servlet/GenerateBomServlet.java | 2 +- .../servlet/GroupEnergyServlet.java | 2 +- .../servlet/GroupPowerServlet.java | 2 +- .../currentmonitor/servlet/PowerServlet.java | 2 +- .../servlet/RebuildSummariesServlet.java | 3 +- .../servlet/ResetPasswordServlet.java | 7 +- ...Servlet.java => SecureServiceServlet.java} | 2 +- .../currentmonitor/servlet/UpdateServlet.java | 6 +- .../servlet/console/ConsoleServlet.java | 19 + .../servlet/console/ExportServlet.java | 140 + .../servlet/console/GsoServlet.java | 33 + .../servlet/console/LoginServlet.java | 42 + .../servlet/console/LogoutServlet.java | 27 + .../servlet/console/MonthDisplay.java | 29 + .../servlet/console/SecureConsoleServlet.java | 48 + .../currentmonitor/util/GoogleAuthHelper.java | 42 + .../src/main/resources/templates/export.ftl | 50 + .../src/main/resources/templates/frame.ftl | 34 + .../src/main/resources/templates/login.ftl | 97 + .../resources/templates/passwordReset.ftl | 2 +- .../webapp/bootstrap/css/bootstrap-grid.css | 7591 ++++---- .../bootstrap/css/bootstrap-grid.css.map | 2 +- .../bootstrap/css/bootstrap-grid.min.css | 10 +- .../bootstrap/css/bootstrap-grid.min.css.map | 2 +- .../bootstrap/css/bootstrap-grid.rtl.css | 5050 +++++ .../bootstrap/css/bootstrap-grid.rtl.css.map | 1 + .../bootstrap/css/bootstrap-grid.rtl.min.css | 7 + .../css/bootstrap-grid.rtl.min.css.map | 1 + .../webapp/bootstrap/css/bootstrap-reboot.css | 364 +- .../bootstrap/css/bootstrap-reboot.css.map | 2 +- .../bootstrap/css/bootstrap-reboot.min.css | 10 +- .../css/bootstrap-reboot.min.css.map | 2 +- .../bootstrap/css/bootstrap-reboot.rtl.css | 478 + .../css/bootstrap-reboot.rtl.css.map | 1 + .../css/bootstrap-reboot.rtl.min.css | 8 + .../css/bootstrap-reboot.rtl.min.css.map | 1 + .../bootstrap/css/bootstrap-utilities.css | 4866 +++++ .../bootstrap/css/bootstrap-utilities.css.map | 1 + .../bootstrap/css/bootstrap-utilities.min.css | 7 + .../css/bootstrap-utilities.min.css.map | 1 + .../bootstrap/css/bootstrap-utilities.rtl.css | 4857 +++++ .../css/bootstrap-utilities.rtl.css.map | 1 + .../css/bootstrap-utilities.rtl.min.css | 7 + .../css/bootstrap-utilities.rtl.min.css.map | 1 + .../main/webapp/bootstrap/css/bootstrap.css | 15308 +++++++++------- .../webapp/bootstrap/css/bootstrap.css.map | 2 +- .../webapp/bootstrap/css/bootstrap.min.css | 13 +- .../bootstrap/css/bootstrap.min.css.map | 2 +- .../webapp/bootstrap/css/bootstrap.rtl.css | 11198 +++++++++++ .../bootstrap/css/bootstrap.rtl.css.map | 1 + .../bootstrap/css/bootstrap.rtl.min.css | 7 + .../bootstrap/css/bootstrap.rtl.min.css.map | 1 + .../src/main/webapp/bootstrap/css/style.css | 229 + .../main/webapp/bootstrap/css/style.min.css | 1 + .../src/main/webapp/img/favicon.png | Bin 0 -> 1802 bytes .../src/main/webapp/img/gso.png | Bin 0 -> 9241 bytes .../src/main/webapp/img/gso_pressed.png | Bin 0 -> 6228 bytes .../src/main/webapp/img/lantern_cm.png | Bin 0 -> 171222 bytes .../src/main/webapp/img/pcb.png | Bin 0 -> 301681 bytes .../currentmonitor/CreateAccount.java | 6 +- .../currentmonitor/CreateAuthCode.java | 4 +- .../currentmonitor/CreateAuthKey.java | 4 +- .../currentmonitor/CreateBreakers.java | 47 +- .../currentmonitor/CreateMongoConfig.java | 4 +- .../CurrentMonitorSerializers.java | 6 +- .../currentmonitor/MigrateSummaries.java | 10 +- .../currentmonitor/RebuildSummaries.java | 24 +- .../lanternsoftware/rules/RulesEngine.java | 6 +- .../rules/actions/AbstractAlertAction.java | 4 +- .../com/lanternsoftware/TestSendAlert.java | 8 +- .../lanternsoftware/util/CollectionUtils.java | 30 +- .../com/lanternsoftware/util/DateRange.java | 32 + .../lanternsoftware/util/LanternFiles.java | 10 - .../lanternsoftware/util/ResourceLoader.java | 2 + .../com/lanternsoftware/util/ZipUtils.java | 3 +- .../util/cryptography/AESTool.java | 5 +- .../util/external/DevFiles.java | 10 + .../util/external/LanternFiles.java | 12 + .../util/external/ProdConsoleFiles.java | 10 + .../util/external/ProdFiles.java | 10 + .../util/external/ProdSupportFiles.java | 10 + .../util/dao/mongo/MongoProxy.java | 88 +- .../util/dao/DaoSerializer.java | 18 +- .../util/servlet/FreemarkerServlet.java | 4 - .../util/servlet/LanternServlet.java | 10 + .../thermometer/context/Globals.java | 4 +- .../GenerateEnvironmentSerializers.java | 4 +- .../zwave/context/ZWaveApp.java | 8 +- .../zwave/GenerateSerializers.java | 4 +- 117 files changed, 41205 insertions(+), 10527 deletions(-) create mode 100644 currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/ArchiveStatus.java create mode 100644 currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/BreakerEnergyArchive.java create mode 100644 currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/DailyEnergyArchive.java create mode 100644 currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/MonthlyEnergyArchive.java create mode 100644 currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/ArchiveStatusSerializer.java create mode 100644 currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/BreakerEnergyArchiveSerializer.java create mode 100644 currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/DailyEnergyArchiveSerializer.java create mode 100644 currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/MonthlyEnergyArchiveSerializer.java create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/FreemarkerCMServlet.java rename currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/{SecureServlet.java => SecureServiceServlet.java} (93%) create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/ConsoleServlet.java create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/ExportServlet.java create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/GsoServlet.java create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/LoginServlet.java create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/LogoutServlet.java create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/MonthDisplay.java create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/SecureConsoleServlet.java create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/util/GoogleAuthHelper.java create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/resources/templates/export.ftl create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/resources/templates/frame.ftl create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/resources/templates/login.ftl create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-grid.rtl.css create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-grid.rtl.css.map create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-grid.rtl.min.css create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-grid.rtl.min.css.map create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-reboot.rtl.css create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-reboot.rtl.css.map create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-reboot.rtl.min.css create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-reboot.rtl.min.css.map create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-utilities.css create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-utilities.css.map create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-utilities.min.css create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-utilities.min.css.map create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-utilities.rtl.css create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-utilities.rtl.css.map create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-utilities.rtl.min.css create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap-utilities.rtl.min.css.map create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap.rtl.css create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap.rtl.css.map create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap.rtl.min.css create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/bootstrap.rtl.min.css.map create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/style.css create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/bootstrap/css/style.min.css create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/img/favicon.png create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/img/gso.png create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/img/gso_pressed.png create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/img/lantern_cm.png create mode 100644 currentmonitor/lantern-service-currentmonitor/src/main/webapp/img/pcb.png create mode 100644 util/lantern-util-common/src/main/java/com/lanternsoftware/util/DateRange.java delete mode 100644 util/lantern-util-common/src/main/java/com/lanternsoftware/util/LanternFiles.java create mode 100644 util/lantern-util-common/src/main/java/com/lanternsoftware/util/external/DevFiles.java create mode 100644 util/lantern-util-common/src/main/java/com/lanternsoftware/util/external/LanternFiles.java create mode 100644 util/lantern-util-common/src/main/java/com/lanternsoftware/util/external/ProdConsoleFiles.java create mode 100644 util/lantern-util-common/src/main/java/com/lanternsoftware/util/external/ProdFiles.java create mode 100644 util/lantern-util-common/src/main/java/com/lanternsoftware/util/external/ProdSupportFiles.java diff --git a/case/Z2/LPM_Case_Z2_Base_Flange.blend b/case/Z2/LPM_Case_Z2_Base_Flange.blend index f3e3f3a2acbdd314e39ea570fdb70a525dc117cf..ca454bf7c78f168dc29adbaa0858c541829247cd 100644 GIT binary patch delta 63199 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

IX3f*08cRyJ%qOu`-UZ`g)aFq>cYjtt~3 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 M*xvFz16Nf49|lvX^Z)<= delta 62013 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 rT+^@ummc5(HkZWU0u+~A-~tSj9MKMw{ /sys/class/leds/led0/trigger"}); Runtime.getRuntime().exec(new String[]{"sh", "-c", "echo default-on > /sys/class/leds/led1/trigger"}); - else + } + else { + Runtime.getRuntime().exec(new String[]{"sh", "-c", "echo none > /sys/class/leds/led0/trigger"}); Runtime.getRuntime().exec(new String[]{"sh", "-c", "echo none > /sys/class/leds/led1/trigger"}); + } } catch (Exception _e) { LOG.error("Failed to change LED state", _e); diff --git a/currentmonitor/lantern-currentmonitor/src/test/java/com/lanternsoftware/currentmonitor/CreateConfig.java b/currentmonitor/lantern-currentmonitor/src/test/java/com/lanternsoftware/currentmonitor/CreateConfig.java index cd9b95e..6005d9d 100644 --- a/currentmonitor/lantern-currentmonitor/src/test/java/com/lanternsoftware/currentmonitor/CreateConfig.java +++ b/currentmonitor/lantern-currentmonitor/src/test/java/com/lanternsoftware/currentmonitor/CreateConfig.java @@ -3,7 +3,7 @@ package com.lanternsoftware.currentmonitor; import com.lanternsoftware.datamodel.currentmonitor.Breaker; import com.lanternsoftware.util.CollectionUtils; -import com.lanternsoftware.util.LanternFiles; +import com.lanternsoftware.util.external.LanternFiles; import com.lanternsoftware.util.ResourceLoader; import com.lanternsoftware.util.dao.DaoSerializer; @@ -23,6 +23,6 @@ public class CreateConfig { b1.setPort(1); b1.setSizeAmps(20); c.setMqttBreakers(CollectionUtils.asArrayList(b1)); - ResourceLoader.writeFile(LanternFiles.OPS_PATH + "mqtt1.json", DaoSerializer.toJson(c)); + ResourceLoader.writeFile(LanternFiles.CONFIG_PATH + "mqtt1.json", DaoSerializer.toJson(c)); } } diff --git a/currentmonitor/lantern-currentmonitor/src/test/java/com/lanternsoftware/currentmonitor/CurrentMonitorAppSerializers.java b/currentmonitor/lantern-currentmonitor/src/test/java/com/lanternsoftware/currentmonitor/CurrentMonitorAppSerializers.java index e7dc8fb..0dda6c1 100644 --- a/currentmonitor/lantern-currentmonitor/src/test/java/com/lanternsoftware/currentmonitor/CurrentMonitorAppSerializers.java +++ b/currentmonitor/lantern-currentmonitor/src/test/java/com/lanternsoftware/currentmonitor/CurrentMonitorAppSerializers.java @@ -1,11 +1,11 @@ package com.lanternsoftware.currentmonitor; -import com.lanternsoftware.util.LanternFiles; +import com.lanternsoftware.util.external.LanternFiles; import com.lanternsoftware.util.dao.generator.DaoSerializerGenerator; public class CurrentMonitorAppSerializers { public static void main(String[] args) { - DaoSerializerGenerator.generateSerializers(LanternFiles.SOURCE_PATH + "currentmonitor", true, null); + DaoSerializerGenerator.generateSerializers(LanternFiles.SOURCE_CODE_PATH + "currentmonitor", true, null); } } diff --git a/currentmonitor/lantern-currentmonitor/src/test/java/com/lanternsoftware/currentmonitor/ReleaseCurrentMonitor.java b/currentmonitor/lantern-currentmonitor/src/test/java/com/lanternsoftware/currentmonitor/ReleaseCurrentMonitor.java index bd3efdb..521f0fe 100644 --- a/currentmonitor/lantern-currentmonitor/src/test/java/com/lanternsoftware/currentmonitor/ReleaseCurrentMonitor.java +++ b/currentmonitor/lantern-currentmonitor/src/test/java/com/lanternsoftware/currentmonitor/ReleaseCurrentMonitor.java @@ -1,6 +1,6 @@ package com.lanternsoftware.currentmonitor; -import com.lanternsoftware.util.LanternFiles; +import com.lanternsoftware.util.external.LanternFiles; import com.lanternsoftware.util.ResourceLoader; import com.lanternsoftware.util.dao.DaoEntity; import com.lanternsoftware.util.dao.DaoSerializer; @@ -9,18 +9,17 @@ import com.lanternsoftware.util.xml.XmlParser; import org.apache.commons.codec.digest.DigestUtils; import java.io.File; -import java.util.Arrays; import java.util.Collections; public class ReleaseCurrentMonitor { public static void main(String[] args) { - XmlNode pom = XmlParser.loadXmlFile(LanternFiles.SOURCE_PATH + "currentmonitor" + File.separator + "lantern-currentmonitor" + File.separator + "pom.xml"); + XmlNode pom = XmlParser.loadXmlFile(LanternFiles.SOURCE_CODE_PATH + "currentmonitor" + File.separator + "lantern-currentmonitor" + File.separator + "pom.xml"); if (pom == null) return; XmlNode versionNode = pom.getChild(Collections.singletonList("version")); String version = versionNode.getContent(); ProcessBuilder builder = new ProcessBuilder(); - builder.directory(new File(LanternFiles.SOURCE_PATH)); + builder.directory(new File(LanternFiles.SOURCE_CODE_PATH)); builder.command("cmd.exe", "/c", "mvn clean install"); builder.redirectOutput(ProcessBuilder.Redirect.INHERIT); try { @@ -30,9 +29,9 @@ public class ReleaseCurrentMonitor { } catch (Exception _e) { _e.printStackTrace(); } - byte[] jar = ResourceLoader.loadFile(LanternFiles.SOURCE_PATH + "currentmonitor" + File.separator + "lantern-currentmonitor" + File.separator + "target" + File.separator + "lantern-currentmonitor.jar"); + byte[] jar = ResourceLoader.loadFile(LanternFiles.SOURCE_CODE_PATH + "currentmonitor" + File.separator + "lantern-currentmonitor" + File.separator + "target" + File.separator + "lantern-currentmonitor.jar"); DaoEntity meta = new DaoEntity("version", version).and("size", jar.length).and("checksum", DigestUtils.md5Hex(jar)); - ResourceLoader.writeFile(LanternFiles.OPS_PATH + "release" + File.separator + "lantern-currentmonitor.jar", jar); - ResourceLoader.writeFile(LanternFiles.OPS_PATH + "release" + File.separator + "version.json", DaoSerializer.toJson(meta)); + ResourceLoader.writeFile(LanternFiles.CONFIG_PATH + "release" + File.separator + "lantern-currentmonitor.jar", jar); + ResourceLoader.writeFile(LanternFiles.CONFIG_PATH + "release" + File.separator + "version.json", DaoSerializer.toJson(meta)); } } diff --git a/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/Backup.java b/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/Backup.java index a382bdb..639b5fa 100644 --- a/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/Backup.java +++ b/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/Backup.java @@ -11,7 +11,7 @@ import com.lanternsoftware.datamodel.rules.Event; import com.lanternsoftware.datamodel.rules.FcmDevice; import com.lanternsoftware.datamodel.rules.Rule; import com.lanternsoftware.util.DebugTimer; -import com.lanternsoftware.util.LanternFiles; +import com.lanternsoftware.util.external.LanternFiles; import com.lanternsoftware.util.dao.DaoQuery; import com.lanternsoftware.util.dao.mongo.MongoConfig; @@ -19,8 +19,8 @@ import java.util.List; public class Backup { public static void main(String[] args) { - CurrentMonitorDao dao = new MongoCurrentMonitorDao(MongoConfig.fromDisk(LanternFiles.BACKUP_SOURCE + "mongo.cfg")); - CurrentMonitorDao backupDao = new MongoCurrentMonitorDao(MongoConfig.fromDisk(LanternFiles.BACKUP_DEST + "mongo.cfg")); + CurrentMonitorDao dao = new MongoCurrentMonitorDao(MongoConfig.fromDisk(LanternFiles.CONFIG_PATH + "mongo.cfg")); + CurrentMonitorDao backupDao = new MongoCurrentMonitorDao(MongoConfig.fromDisk(LanternFiles.BACKUP_DEST_PATH + "mongo.cfg")); DebugTimer t1 = new DebugTimer("Query Accounts"); List accounts = dao.getProxy().queryAll(Account.class); diff --git a/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/BackupMinutes.java b/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/BackupMinutes.java index 2565504..cc35958 100644 --- a/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/BackupMinutes.java +++ b/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/BackupMinutes.java @@ -4,7 +4,7 @@ import com.lanternsoftware.datamodel.currentmonitor.Account; import com.lanternsoftware.datamodel.currentmonitor.HubPowerMinute; import com.lanternsoftware.util.DateUtils; import com.lanternsoftware.util.DebugTimer; -import com.lanternsoftware.util.LanternFiles; +import com.lanternsoftware.util.external.LanternFiles; import com.lanternsoftware.util.NullUtils; import com.lanternsoftware.util.dao.DaoQuery; import com.lanternsoftware.util.dao.DaoSort; @@ -16,8 +16,8 @@ import java.util.TimeZone; public class BackupMinutes { public static void main(String[] args) { - CurrentMonitorDao dao = new MongoCurrentMonitorDao(MongoConfig.fromDisk(LanternFiles.BACKUP_SOURCE + "mongo.cfg")); - CurrentMonitorDao backupDao = new MongoCurrentMonitorDao(MongoConfig.fromDisk(LanternFiles.BACKUP_DEST + "mongo.cfg")); + CurrentMonitorDao dao = new MongoCurrentMonitorDao(MongoConfig.fromDisk(LanternFiles.CONFIG_PATH + "mongo.cfg")); + CurrentMonitorDao backupDao = new MongoCurrentMonitorDao(MongoConfig.fromDisk(LanternFiles.BACKUP_DEST_PATH + "mongo.cfg")); Date now = new Date(); for (Account a : dao.getProxy().queryAll(Account.class)) { if (a.getId() == 0) diff --git a/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/CurrentMonitorDao.java b/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/CurrentMonitorDao.java index 6c8171d..7f60be1 100644 --- a/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/CurrentMonitorDao.java +++ b/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/CurrentMonitorDao.java @@ -7,9 +7,12 @@ import com.lanternsoftware.datamodel.currentmonitor.EnergySummary; import com.lanternsoftware.datamodel.currentmonitor.EnergyViewMode; import com.lanternsoftware.datamodel.currentmonitor.HubCommand; import com.lanternsoftware.datamodel.currentmonitor.HubPowerMinute; +import com.lanternsoftware.datamodel.currentmonitor.archive.ArchiveStatus; +import com.lanternsoftware.util.DateRange; import com.lanternsoftware.util.dao.auth.AuthCode; import com.lanternsoftware.util.dao.mongo.MongoProxy; +import java.io.InputStream; import java.util.Date; import java.util.List; import java.util.TimeZone; @@ -26,6 +29,15 @@ public interface CurrentMonitorDao { void putEnergySummary(EnergySummary _energy); void putHubPowerMinute(HubPowerMinute _power); + Iterable streamHubPowerMinutes(int _accountId, Date _start, Date _end); + + void archiveMonth(int _accountId, Date _month); + InputStream streamArchive(int _accountId, Date _month); + void putArchiveStatus(ArchiveStatus _status); + void deleteArchiveStatus(int _accountId, Date _month); + List getArchiveStatus(int _accountId); + + DateRange getMonitoredDateRange(int _accountId); BreakerConfig getConfig(int _accountId); BreakerConfig getMergedConfig(AuthCode _authCode); diff --git a/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/MongoCurrentMonitorDao.java b/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/MongoCurrentMonitorDao.java index ad17f4f..779b8f1 100644 --- a/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/MongoCurrentMonitorDao.java +++ b/currentmonitor/lantern-dataaccess-currentmonitor/src/main/java/com/lanternsoftware/dataaccess/currentmonitor/MongoCurrentMonitorDao.java @@ -2,12 +2,13 @@ package com.lanternsoftware.dataaccess.currentmonitor; import com.lanternsoftware.datamodel.currentmonitor.Account; import com.lanternsoftware.datamodel.currentmonitor.BillingPlan; -import com.lanternsoftware.datamodel.currentmonitor.BillingRate; import com.lanternsoftware.datamodel.currentmonitor.Breaker; import com.lanternsoftware.datamodel.currentmonitor.BreakerConfig; import com.lanternsoftware.datamodel.currentmonitor.BreakerGroup; import com.lanternsoftware.datamodel.currentmonitor.BreakerHub; import com.lanternsoftware.datamodel.currentmonitor.BreakerPower; +import com.lanternsoftware.datamodel.currentmonitor.BreakerPowerMinute; +import com.lanternsoftware.datamodel.currentmonitor.BreakerType; import com.lanternsoftware.datamodel.currentmonitor.ChargeSummary; import com.lanternsoftware.datamodel.currentmonitor.ChargeTotal; import com.lanternsoftware.datamodel.currentmonitor.EnergySummary; @@ -16,7 +17,12 @@ import com.lanternsoftware.datamodel.currentmonitor.EnergyViewMode; import com.lanternsoftware.datamodel.currentmonitor.HubCommand; import com.lanternsoftware.datamodel.currentmonitor.HubPowerMinute; import com.lanternsoftware.datamodel.currentmonitor.Sequence; +import com.lanternsoftware.datamodel.currentmonitor.archive.ArchiveStatus; +import com.lanternsoftware.datamodel.currentmonitor.archive.BreakerEnergyArchive; +import com.lanternsoftware.datamodel.currentmonitor.archive.DailyEnergyArchive; +import com.lanternsoftware.datamodel.currentmonitor.archive.MonthlyEnergyArchive; import com.lanternsoftware.util.CollectionUtils; +import com.lanternsoftware.util.DateRange; import com.lanternsoftware.util.DateUtils; import com.lanternsoftware.util.DebugTimer; import com.lanternsoftware.util.NullUtils; @@ -28,24 +34,41 @@ import com.lanternsoftware.util.dao.DaoSort; import com.lanternsoftware.util.dao.auth.AuthCode; import com.lanternsoftware.util.dao.mongo.MongoConfig; import com.lanternsoftware.util.dao.mongo.MongoProxy; +import com.lanternsoftware.util.external.LanternFiles; import com.lanternsoftware.util.mutable.MutableDouble; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.mindrot.jbcrypt.BCrypt; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; +import java.util.Comparator; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.TimeZone; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.zip.Deflater; +import java.util.zip.GZIPOutputStream; public class MongoCurrentMonitorDao implements CurrentMonitorDao { private static final Logger logger = LoggerFactory.getLogger(MongoCurrentMonitorDao.class); @@ -65,6 +88,7 @@ public class MongoCurrentMonitorDao implements CurrentMonitorDao { proxy.ensureIndex(ChargeSummary.class, DaoSort.sort("account_id").then("plan_id").then("group_id").then("view_mode")); proxy.ensureIndex(ChargeTotal.class, DaoSort.sort("account_id").then("plan_id").then("group_id").then("view_mode").then("start")); proxy.ensureIndex(DirtyMinute.class, DaoSort.sort("posted")); + proxy.ensureIndex(ArchiveStatus.class, DaoSort.sort("account_id")); for (DirtyMinute minute : proxy.queryAll(DirtyMinute.class)) { updateEnergySummaries(minute); } @@ -100,6 +124,196 @@ public class MongoCurrentMonitorDao implements CurrentMonitorDao { }, 10000); } + @Override + public Iterable streamHubPowerMinutes(int _accountId, Date _start, Date _end) { + return proxy.queryIterator(HubPowerMinute.class, new DaoQuery("account_id", _accountId).andBetweenInclusiveExclusive("minute", DateUtils.toLong(_start)/60000, DateUtils.toLong(_end)/60000), null, DaoSort.sort("start"), 0, 0); + } + + @Override + public void archiveMonth(int _accountId, Date _month) { + ArchiveStatus status = new ArchiveStatus(); + status.setAccountId(_accountId); + status.setMonth(_month); + status.setProgress(1); + putArchiveStatus(status); + executor.submit(()->{ + synchronized (MongoCurrentMonitorDao.this) { + TimeZone tz = getTimeZoneForAccount(_accountId); + DebugTimer timer = new DebugTimer("Monthly Archive Generation for account " + _accountId + " month " + DateUtils.format("MMMM yyyy", tz, _month)); + Date start = _month; + Date end = DateUtils.getEndOfMonth(_month, tz); + BreakerConfig config = getConfig(_accountId); //TODO: get historical config for archive month in case it's changed since then. + List breakers = CollectionUtils.filter(config.getAllBreakers(), _b -> !NullUtils.isOneOf(_b.getType(), BreakerType.DOUBLE_POLE_BOTTOM, BreakerType.EMPTY)); + breakers.sort(Comparator.comparing(Breaker::getPanel).thenComparing(Breaker::getSpace)); + Map breakerKeys = CollectionUtils.transformToMap(config.getAllBreakers(), Breaker::getIntKey, _b -> Breaker.intKey(_b.getPanel(), _b.getType() == BreakerType.DOUBLE_POLE_BOTTOM ? _b.getSpace() - 2 : _b.getSpace())); + Map> minuteReadings = new HashMap<>(); + MonthlyEnergyArchive archive = new MonthlyEnergyArchive(); + archive.setAccountId(_accountId); + archive.setMonth(start); + List days = new ArrayList<>(); + archive.setDays(days); + while (start.before(end)) { + Map dayReadings = new HashMap<>(); + Date dayEnd = DateUtils.addDays(start, 1, tz); + int minute = 0; + int bytesInDay = (int) (4 * DateUtils.diffInSeconds(start, dayEnd)); + Iterator i = streamHubPowerMinutes(_accountId, start, dayEnd).iterator(); + HubPowerMinute m = null; + if (i.hasNext()) + m = i.next(); + DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + while (i.hasNext()) { + if (m == null) + break; + for (BreakerPowerMinute breaker : CollectionUtils.makeNotNull(m.getBreakers())) { + if (!breakerKeys.containsKey(breaker.breakerIntKey())) + continue; + int key = breakerKeys.get(breaker.breakerIntKey()); + List r = minuteReadings.get(key); + if (r == null) + minuteReadings.put(key, breaker.getReadings()); + else { + for (int idx = 0; idx < minuteReadings.size(); idx++) { + r.set(idx, r.get(idx) + breaker.getReadings().get(idx)); + } + } + } + HubPowerMinute cur = i.next(); + if (cur.getMinute() != m.getMinute()) { + addReadings(minute, bytesInDay, minuteReadings, dayReadings); + minute++; + } + m = cur; + } + if (m != null) + addReadings(minute, bytesInDay, minuteReadings, dayReadings); + List breakerEnergies = new ArrayList<>(); + for (Entry be : dayReadings.entrySet()) { + BreakerEnergyArchive breakerEnergy = new BreakerEnergyArchive(); + breakerEnergy.setPanel(Breaker.intKeyToPanel(be.getKey())); + breakerEnergy.setSpace(Breaker.intKeyToSpace(be.getKey())); + breakerEnergy.setReadings(be.getValue()); + breakerEnergies.add(breakerEnergy); + } + DailyEnergyArchive day = new DailyEnergyArchive(); + day.setBreakers(breakerEnergies); + days.add(day); + start = dayEnd; + status.setProgress(50f * (start.getTime() - _month.getTime()) / (end.getTime() - _month.getTime())); + putArchiveStatus(status); + } + timer.stop(); + DebugTimer t = new DebugTimer("Convert Archive to bson for account " + archive.getAccountId()); + byte[] bson = DaoSerializer.toBson(archive); + t.stop(); + + DebugTimer t2 = new DebugTimer("Zip Archive and write to disk for account" + archive.getAccountId()); + OutputStream os = null; + try { + File partialPath = new File(LanternFiles.BACKUP_DEST_PATH + archive.getAccountId()+File.separator + "partial"); + FileUtils.deleteDirectory(partialPath); + partialPath.mkdirs(); + String backupPath = LanternFiles.BACKUP_DEST_PATH + archive.getAccountId() + File.separator; + if (!archive.isComplete(tz)) + backupPath += "partial" + File.separator; + os = new GZIPOutputStream(new FileOutputStream(backupPath + archive.getMonth().getTime() + ".zip")) {{def.setLevel(Deflater.BEST_SPEED);}}; + int batchSize = bson.length / 50; + for (int offset = 0; offset < bson.length; offset += batchSize) { + os.write(bson, offset, Math.min(batchSize, bson.length - offset)); + status.setProgress(50 + (50f * offset / bson.length)); + putArchiveStatus(status); + } + } catch (Exception _e) { + logger.error("Failed to write export file", _e); + } finally { + IOUtils.closeQuietly(os); + } + t2.stop(); + deleteArchiveStatus(_accountId, _month); + } + }); + } + + private void addReadings(int _minuteInDay, int _bytesInDay, Map> _minuteReadings, Map _dayReadings) { + for (Entry> entry : _minuteReadings.entrySet()) { + byte[] dayBytes = _dayReadings.computeIfAbsent(entry.getKey(), _r->new byte[_bytesInDay]); + ByteBuffer bb = ByteBuffer.wrap(dayBytes); + for (int fl = 0; fl < CollectionUtils.size(entry.getValue()); fl++) { + bb.putFloat(_minuteInDay*240 + (fl*4), CollectionUtils.get(entry.getValue(), fl)); + } + } + _minuteReadings.clear(); + } + + @Override + public InputStream streamArchive(int _accountId, Date _month) { + try { + String complete = LanternFiles.BACKUP_DEST_PATH + _accountId + File.separator + _month.getTime() + ".zip"; + if (new File(complete).exists()) + return new FileInputStream(complete); + String partial = LanternFiles.BACKUP_DEST_PATH + _accountId + File.separator + "partial" + File.separator + _month.getTime() + ".zip"; + if (new File(partial).exists()) + return new FileInputStream(partial); + } + catch (Exception _e) { + logger.error("Failed to load archive", _e); + } + return null; + } + + @Override + public void putArchiveStatus(ArchiveStatus _status) { + proxy.save(_status); + } + + @Override + public void deleteArchiveStatus(int _accountId, Date _month) { + proxy.delete(ArchiveStatus.class, new DaoQuery("_id", MonthlyEnergyArchive.toId(_accountId, _month))); + } + + @Override + public List getArchiveStatus(int _accountId) { + Map statuses = CollectionUtils.transformToSortedMap(proxy.query(ArchiveStatus.class, new DaoQuery("account_id", _accountId)), ArchiveStatus::getMonth); + File folder = new File(LanternFiles.BACKUP_DEST_PATH + _accountId); + if (folder.exists()) { + for (File file : CollectionUtils.asArrayList(folder.listFiles())) { + if (file.isFile()) { + Date month = new Date(DaoSerializer.toLong(file.getName().replace(".zip", ""))); + statuses.computeIfAbsent(month, _m -> new ArchiveStatus(_accountId, _m, 100)); + } + } + } + File partial = new File(LanternFiles.BACKUP_DEST_PATH + _accountId + File.separator + "partial"); + if (partial.exists()) { + for (File file : CollectionUtils.asArrayList(partial.listFiles())) { + if (file.isFile() && (new Date().getTime() - file.lastModified() < 86400000)) { + Date month = new Date(DaoSerializer.toLong(file.getName().replace(".zip", ""))); + statuses.computeIfAbsent(month, _m -> new ArchiveStatus(_accountId, _m, 100)); + } + } + } + DateRange range = getMonitoredDateRange(_accountId); + TimeZone tz = getTimeZoneForAccount(_accountId); + Date month = DateUtils.getStartOfMonth(range.getStart(), tz); + Date end = DateUtils.getEndOfMonth(range.getEnd(), tz); + while (month.before(end)) { + statuses.computeIfAbsent(month, _m->new ArchiveStatus(_accountId, _m, 0)); + month = DateUtils.addMonths(month, 1, tz); + } + return new ArrayList<>(statuses.values()); + } + + @Override + public DateRange getMonitoredDateRange(int _accountId) { + DaoQuery query = new DaoQuery("account_id", _accountId).and("view_mode", EnergyViewMode.MONTH.name()); + EnergySummary first = proxy.queryOne(EnergySummary.class, query, DaoSort.sort("start")); + EnergySummary last = proxy.queryOne(EnergySummary.class, query, DaoSort.sortDesc("start")); + if ((first != null) && (last != null)) + return new DateRange(first.getStart(), last.getStart()); + return null; + } + private void updateEnergySummaries(DirtyMinute _minute) { DebugTimer timer = new DebugTimer("Updating summaries", logger); List minutes = proxy.query(HubPowerMinute.class, new DaoQuery("account_id", _minute.getAccountId()).and("minute", _minute.getMinute())); @@ -262,32 +476,6 @@ public class MongoCurrentMonitorDao implements CurrentMonitorDao { } } - private void rebuildSummaries(int _accountId, Collection _rates) { - logger.info("Rebuilding summaries due to a change in {} rates", CollectionUtils.size(_rates)); - HubPowerMinute firstMinute = proxy.queryOne(HubPowerMinute.class, new DaoQuery("account_id", _accountId), DaoSort.sort("minute")); - if (firstMinute == null) - return; - TimeZone tz = getTimeZoneForAccount(_accountId); - Map rates = CollectionUtils.transformToMap(_rates, _r -> String.format("%d%d", DaoSerializer.toLong(_r.getBeginEffective()), DaoSerializer.toLong(_r.getEndEffective()))); - for (BillingRate rate : rates.values()) { - Date start = rate.getBeginEffective(); - Date end = rate.getEndEffective(); - Date now = new Date(); - if ((start == null) || start.before(firstMinute.getMinuteAsDate())) - start = firstMinute.getMinuteAsDate(); - if ((end == null) || end.after(now)) - end = now; - rebuildSummaries(_accountId, start, end); - if (rate.isRecursAnnually()) { - while (end.before(now)) { - start = DateUtils.addYears(start, 1, tz); - end = DateUtils.addYears(end, 1, tz); - rebuildSummaries(_accountId, start, end); - } - } - } - } - @Override public void rebuildSummariesAsync(int _accountId) { executor.submit(() -> rebuildSummaries(_accountId)); @@ -488,13 +676,13 @@ public class MongoCurrentMonitorDao implements CurrentMonitorDao { public String getAuthCodeForEmail(String _email, TimeZone _tz) { _email = _email.toLowerCase().trim(); Account account = getAccountByUsername(_email); - if (account == null) { + if ((account == null) && (_tz != null)) { account = new Account(); account.setUsername(_email); account.setTimezone(_tz.getID()); putAccount(account); } - return toAuthCode(account.getId(), account.getAuxiliaryAccountIds()); + return (account == null)?null:toAuthCode(account.getId(), account.getAuxiliaryAccountIds()); } public String toAuthCode(int _acctId, List _auxAcctIds) { diff --git a/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/Breaker.java b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/Breaker.java index 2001613..8294e17 100644 --- a/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/Breaker.java +++ b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/Breaker.java @@ -194,10 +194,26 @@ public class Breaker implements IIdentical { return key; } + public int getIntKey() { + return intKey(panel, space); + } + + public static int intKeyToPanel(int _intKey) { + return _intKey/10000; + } + + public static int intKeyToSpace(int _intKey) { + return _intKey%10000; + } + public static String key(int _panel, int _space) { return String.format("%d-%d", _panel, _space); } + public static int intKey(int _panel, int _space) { + return 10000*_panel + _space; + } + public static int portToChip(int _port) { return (_port < 9) ? 1 : 0; } diff --git a/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/BreakerPowerMinute.java b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/BreakerPowerMinute.java index a995e2b..c529fd3 100644 --- a/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/BreakerPowerMinute.java +++ b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/BreakerPowerMinute.java @@ -30,6 +30,10 @@ public class BreakerPowerMinute { return Breaker.key(panel, space); } + public int breakerIntKey() { + return Breaker.intKey(panel, space); + } + public List getReadings() { return readings; } diff --git a/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/ArchiveStatus.java b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/ArchiveStatus.java new file mode 100644 index 0000000..b854ea2 --- /dev/null +++ b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/ArchiveStatus.java @@ -0,0 +1,50 @@ +package com.lanternsoftware.datamodel.currentmonitor.archive; + +import com.lanternsoftware.util.DateUtils; +import com.lanternsoftware.util.dao.annotations.DBSerializable; + +import java.util.Date; + +@DBSerializable(autogen = false) +public class ArchiveStatus { + private int accountId; + private Date month; + private float progress; + + public ArchiveStatus() { + } + + public ArchiveStatus(int _accountId, Date _month, float _progress) { + accountId = _accountId; + month = _month; + progress = _progress; + } + + public String getId() { + return String.format("%d-%d", accountId, DateUtils.toLong(month)); + } + + public int getAccountId() { + return accountId; + } + + public void setAccountId(int _accountId) { + accountId = _accountId; + } + + public Date getMonth() { + return month; + } + + public void setMonth(Date _month) { + month = _month; + } + + public float getProgress() { + return progress; + } + + public void setProgress(float _progress) { + progress = _progress; + } +} diff --git a/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/BreakerEnergyArchive.java b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/BreakerEnergyArchive.java new file mode 100644 index 0000000..f3d4835 --- /dev/null +++ b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/BreakerEnergyArchive.java @@ -0,0 +1,34 @@ +package com.lanternsoftware.datamodel.currentmonitor.archive; + +import com.lanternsoftware.util.dao.annotations.DBSerializable; + +@DBSerializable +public class BreakerEnergyArchive { + private int panel; + private int space; + private byte[] readings; + + public int getPanel() { + return panel; + } + + public void setPanel(int _panel) { + panel = _panel; + } + + public int getSpace() { + return space; + } + + public void setSpace(int _space) { + space = _space; + } + + public byte[] getReadings() { + return readings; + } + + public void setReadings(byte[] _readings) { + readings = _readings; + } +} diff --git a/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/DailyEnergyArchive.java b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/DailyEnergyArchive.java new file mode 100644 index 0000000..23198d4 --- /dev/null +++ b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/DailyEnergyArchive.java @@ -0,0 +1,18 @@ +package com.lanternsoftware.datamodel.currentmonitor.archive; + +import com.lanternsoftware.util.dao.annotations.DBSerializable; + +import java.util.List; + +@DBSerializable +public class DailyEnergyArchive { + private List breakers; + + public List getBreakers() { + return breakers; + } + + public void setBreakers(List _breakers) { + breakers = _breakers; + } +} diff --git a/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/MonthlyEnergyArchive.java b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/MonthlyEnergyArchive.java new file mode 100644 index 0000000..172434c --- /dev/null +++ b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/MonthlyEnergyArchive.java @@ -0,0 +1,53 @@ +package com.lanternsoftware.datamodel.currentmonitor.archive; + +import com.lanternsoftware.util.DateUtils; +import com.lanternsoftware.util.dao.annotations.DBSerializable; + +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +@DBSerializable(autogen = false) +public class MonthlyEnergyArchive { + private int accountId; + private Date month; + private List days; + + public String getId() { + return toId(accountId, month); + } + + public static String toId(int _accountId, Date _month) { + return String.format("%d-%d", _accountId, DateUtils.toLong(_month)); + } + + public int getAccountId() { + return accountId; + } + + public void setAccountId(int _accountId) { + accountId = _accountId; + } + + public Date getMonth() { + return month; + } + + public void setMonth(Date _month) { + month = _month; + } + + public List getDays() { + return days; + } + + public void setDays(List _days) { + days = _days; + } + + public boolean isComplete(TimeZone _tz) { + Date valid = DateUtils.addDays(new Date(), -7, _tz); + valid = DateUtils.addMonths(valid, -1, _tz); + return month.before(valid); + } +} diff --git a/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/ArchiveStatusSerializer.java b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/ArchiveStatusSerializer.java new file mode 100644 index 0000000..e1567f0 --- /dev/null +++ b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/ArchiveStatusSerializer.java @@ -0,0 +1,44 @@ +package com.lanternsoftware.datamodel.currentmonitor.archive.dao; + +import com.lanternsoftware.datamodel.currentmonitor.archive.ArchiveStatus; +import com.lanternsoftware.util.dao.AbstractDaoSerializer; +import com.lanternsoftware.util.dao.DaoEntity; +import com.lanternsoftware.util.dao.DaoProxyType; +import com.lanternsoftware.util.dao.DaoSerializer; +import java.util.Collections; +import java.util.List; + +public class ArchiveStatusSerializer extends AbstractDaoSerializer +{ + @Override + public Class getSupportedClass() + { + return ArchiveStatus.class; + } + + @Override + public List getSupportedProxies() { + return Collections.singletonList(DaoProxyType.MONGO); + } + + @Override + public DaoEntity toDaoEntity(ArchiveStatus _o) + { + DaoEntity d = new DaoEntity(); + d.put("_id", _o.getId()); + d.put("account_id", _o.getAccountId()); + d.put("month", DaoSerializer.toLong(_o.getMonth())); + d.put("progress", _o.getProgress()); + return d; + } + + @Override + public ArchiveStatus fromDaoEntity(DaoEntity _d) + { + ArchiveStatus o = new ArchiveStatus(); + o.setAccountId(DaoSerializer.getInteger(_d, "account_id")); + o.setMonth(DaoSerializer.getDate(_d, "month")); + o.setProgress(DaoSerializer.getFloat(_d, "progress")); + return o; + } +} \ No newline at end of file diff --git a/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/BreakerEnergyArchiveSerializer.java b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/BreakerEnergyArchiveSerializer.java new file mode 100644 index 0000000..269b374 --- /dev/null +++ b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/BreakerEnergyArchiveSerializer.java @@ -0,0 +1,43 @@ +package com.lanternsoftware.datamodel.currentmonitor.archive.dao; + +import com.lanternsoftware.datamodel.currentmonitor.archive.BreakerEnergyArchive; +import com.lanternsoftware.util.dao.AbstractDaoSerializer; +import com.lanternsoftware.util.dao.DaoEntity; +import com.lanternsoftware.util.dao.DaoProxyType; +import com.lanternsoftware.util.dao.DaoSerializer; +import java.util.Collections; +import java.util.List; + +public class BreakerEnergyArchiveSerializer extends AbstractDaoSerializer +{ + @Override + public Class getSupportedClass() + { + return BreakerEnergyArchive.class; + } + + @Override + public List getSupportedProxies() { + return Collections.singletonList(DaoProxyType.MONGO); + } + + @Override + public DaoEntity toDaoEntity(BreakerEnergyArchive _o) + { + DaoEntity d = new DaoEntity(); + d.put("panel", _o.getPanel()); + d.put("space", _o.getSpace()); + d.put("readings", _o.getReadings()); + return d; + } + + @Override + public BreakerEnergyArchive fromDaoEntity(DaoEntity _d) + { + BreakerEnergyArchive o = new BreakerEnergyArchive(); + o.setPanel(DaoSerializer.getInteger(_d, "panel")); + o.setSpace(DaoSerializer.getInteger(_d, "space")); + o.setReadings(DaoSerializer.getByteArray(_d, "readings")); + return o; + } +} \ No newline at end of file diff --git a/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/DailyEnergyArchiveSerializer.java b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/DailyEnergyArchiveSerializer.java new file mode 100644 index 0000000..e5c34e8 --- /dev/null +++ b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/DailyEnergyArchiveSerializer.java @@ -0,0 +1,40 @@ +package com.lanternsoftware.datamodel.currentmonitor.archive.dao; + +import com.lanternsoftware.datamodel.currentmonitor.archive.BreakerEnergyArchive; +import com.lanternsoftware.datamodel.currentmonitor.archive.DailyEnergyArchive; +import com.lanternsoftware.util.dao.AbstractDaoSerializer; +import com.lanternsoftware.util.dao.DaoEntity; +import com.lanternsoftware.util.dao.DaoProxyType; +import com.lanternsoftware.util.dao.DaoSerializer; +import java.util.Collections; +import java.util.List; + +public class DailyEnergyArchiveSerializer extends AbstractDaoSerializer +{ + @Override + public Class getSupportedClass() + { + return DailyEnergyArchive.class; + } + + @Override + public List getSupportedProxies() { + return Collections.singletonList(DaoProxyType.MONGO); + } + + @Override + public DaoEntity toDaoEntity(DailyEnergyArchive _o) + { + DaoEntity d = new DaoEntity(); + d.put("breakers", DaoSerializer.toDaoEntities(_o.getBreakers(), DaoProxyType.MONGO)); + return d; + } + + @Override + public DailyEnergyArchive fromDaoEntity(DaoEntity _d) + { + DailyEnergyArchive o = new DailyEnergyArchive(); + o.setBreakers(DaoSerializer.getList(_d, "breakers", BreakerEnergyArchive.class)); + return o; + } +} \ No newline at end of file diff --git a/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/MonthlyEnergyArchiveSerializer.java b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/MonthlyEnergyArchiveSerializer.java new file mode 100644 index 0000000..c484af2 --- /dev/null +++ b/currentmonitor/lantern-datamodel-currentmonitor/src/main/java/com/lanternsoftware/datamodel/currentmonitor/archive/dao/MonthlyEnergyArchiveSerializer.java @@ -0,0 +1,45 @@ +package com.lanternsoftware.datamodel.currentmonitor.archive.dao; + +import com.lanternsoftware.datamodel.currentmonitor.archive.DailyEnergyArchive; +import com.lanternsoftware.datamodel.currentmonitor.archive.MonthlyEnergyArchive; +import com.lanternsoftware.util.dao.AbstractDaoSerializer; +import com.lanternsoftware.util.dao.DaoEntity; +import com.lanternsoftware.util.dao.DaoProxyType; +import com.lanternsoftware.util.dao.DaoSerializer; +import java.util.Collections; +import java.util.List; + +public class MonthlyEnergyArchiveSerializer extends AbstractDaoSerializer +{ + @Override + public Class getSupportedClass() + { + return MonthlyEnergyArchive.class; + } + + @Override + public List getSupportedProxies() { + return Collections.singletonList(DaoProxyType.MONGO); + } + + @Override + public DaoEntity toDaoEntity(MonthlyEnergyArchive _o) + { + DaoEntity d = new DaoEntity(); + d.put("_id", _o.getId()); + d.put("account_id", _o.getAccountId()); + d.put("month", DaoSerializer.toLong(_o.getMonth())); + d.put("days", DaoSerializer.toDaoEntities(_o.getDays(), DaoProxyType.MONGO)); + return d; + } + + @Override + public MonthlyEnergyArchive fromDaoEntity(DaoEntity _d) + { + MonthlyEnergyArchive o = new MonthlyEnergyArchive(); + o.setAccountId(DaoSerializer.getInteger(_d, "account_id")); + o.setMonth(DaoSerializer.getDate(_d, "month")); + o.setDays(DaoSerializer.getList(_d, "days", DailyEnergyArchive.class)); + return o; + } +} \ No newline at end of file diff --git a/currentmonitor/lantern-datamodel-currentmonitor/src/main/resources/META-INF/services/com.lanternsoftware.util.dao.IDaoSerializer b/currentmonitor/lantern-datamodel-currentmonitor/src/main/resources/META-INF/services/com.lanternsoftware.util.dao.IDaoSerializer index 3a3f460..c3d0dd6 100644 --- a/currentmonitor/lantern-datamodel-currentmonitor/src/main/resources/META-INF/services/com.lanternsoftware.util.dao.IDaoSerializer +++ b/currentmonitor/lantern-datamodel-currentmonitor/src/main/resources/META-INF/services/com.lanternsoftware.util.dao.IDaoSerializer @@ -1,3 +1,7 @@ +com.lanternsoftware.datamodel.currentmonitor.archive.dao.ArchiveStatusSerializer +com.lanternsoftware.datamodel.currentmonitor.archive.dao.BreakerEnergyArchiveSerializer +com.lanternsoftware.datamodel.currentmonitor.archive.dao.DailyEnergyArchiveSerializer +com.lanternsoftware.datamodel.currentmonitor.archive.dao.MonthlyEnergyArchiveSerializer com.lanternsoftware.datamodel.currentmonitor.dao.AccountSerializer com.lanternsoftware.datamodel.currentmonitor.dao.BillingPlanSerializer com.lanternsoftware.datamodel.currentmonitor.dao.BillingRateSerializer diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/context/Globals.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/context/Globals.java index e4f387c..e2d041e 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/context/Globals.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/context/Globals.java @@ -6,7 +6,7 @@ import com.lanternsoftware.datamodel.currentmonitor.HubCommand; import com.lanternsoftware.datamodel.currentmonitor.HubCommands; import com.lanternsoftware.rules.RulesEngine; import com.lanternsoftware.util.DateUtils; -import com.lanternsoftware.util.LanternFiles; +import com.lanternsoftware.util.external.LanternFiles; import com.lanternsoftware.util.dao.mongo.MongoConfig; import javax.servlet.ServletContextEvent; @@ -24,7 +24,7 @@ public class Globals implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { - dao = new MongoCurrentMonitorDao(MongoConfig.fromDisk(LanternFiles.OPS_PATH + "mongo.cfg")); + dao = new MongoCurrentMonitorDao(MongoConfig.fromDisk(LanternFiles.CONFIG_PATH + "mongo.cfg")); RulesEngine.instance().start(); RulesEngine.instance().schedule(new CommandTask(), 0); } diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/AuthServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/AuthServlet.java index ccab743..c943482 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/AuthServlet.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/AuthServlet.java @@ -1,17 +1,10 @@ package com.lanternsoftware.currentmonitor.servlet; -import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest; -import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; -import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse; -import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.json.gson.GsonFactory; import com.lanternsoftware.currentmonitor.context.Globals; +import com.lanternsoftware.currentmonitor.util.GoogleAuthHelper; import com.lanternsoftware.util.DateUtils; -import com.lanternsoftware.util.LanternFiles; import com.lanternsoftware.util.NullUtils; -import com.lanternsoftware.util.ResourceLoader; import com.lanternsoftware.util.dao.DaoEntity; -import com.lanternsoftware.util.dao.DaoSerializer; import com.lanternsoftware.util.servlet.BasicAuth; import com.lanternsoftware.util.servlet.LanternServlet; import org.slf4j.Logger; @@ -23,15 +16,7 @@ import javax.servlet.http.HttpServletResponse; @WebServlet("/auth/*") public class AuthServlet extends LanternServlet { - private static final NetHttpTransport transport = new NetHttpTransport(); private static final Logger logger = LoggerFactory.getLogger(AuthServlet.class); - private static final String googleClientId; - private static final String googleClientSecret; - static { - DaoEntity google = DaoSerializer.parse(ResourceLoader.loadFileAsString(LanternFiles.OPS_PATH + "google_sso.txt")); - googleClientId = DaoSerializer.getString(google, "id"); - googleClientSecret = DaoSerializer.getString(google, "secret"); - } @Override protected void doGet(HttpServletRequest _req, HttpServletResponse _rep) { @@ -40,16 +25,7 @@ public class AuthServlet extends LanternServlet { BasicAuth auth = new BasicAuth(_req); if (NullUtils.isEqual(auth.getUsername(), "googlesso")) { logger.info("Attempting google SSO"); - try { - GoogleTokenResponse tokenResponse = new GoogleAuthorizationCodeTokenRequest(transport, new GsonFactory(), "https://oauth2.googleapis.com/token", googleClientId, googleClientSecret, auth.getPassword(), "").execute(); - if (tokenResponse != null) { - GoogleIdToken idToken = tokenResponse.parseIdToken(); - if (idToken != null) - authCode = Globals.dao.getAuthCodeForEmail(idToken.getPayload().getEmail(), DateUtils.fromTimeZoneId(_req.getHeader("timezone"))); - } - } catch (Exception _e) { - logger.error("Failed to validate google auth code", _e); - } + authCode = GoogleAuthHelper.signin(auth.getPassword(), DateUtils.fromTimeZoneId(_req.getHeader("timezone"))); } else authCode = Globals.dao.authenticateAccount(auth.getUsername(), auth.getPassword()); } diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/ChargeServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/ChargeServlet.java index 4effc31..ec9832b 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/ChargeServlet.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/ChargeServlet.java @@ -14,7 +14,7 @@ import javax.ws.rs.core.MediaType; import java.util.Date; @WebServlet("/charge/*") -public class ChargeServlet extends SecureServlet { +public class ChargeServlet extends SecureServiceServlet { @Override protected void get(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) { String[] path = path(_req); diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/CommandServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/CommandServlet.java index a58aad0..2097337 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/CommandServlet.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/CommandServlet.java @@ -7,7 +7,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/command") -public class CommandServlet extends SecureServlet { +public class CommandServlet extends SecureServiceServlet { @Override protected void get(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) { diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/ConfigServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/ConfigServlet.java index d746628..b6c7702 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/ConfigServlet.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/ConfigServlet.java @@ -13,7 +13,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/config/*") -public class ConfigServlet extends SecureServlet { +public class ConfigServlet extends SecureServiceServlet { private static final Logger logger = LoggerFactory.getLogger(ConfigServlet.class); @Override diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/EnergyServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/EnergyServlet.java index 1652317..4f1d556 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/EnergyServlet.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/EnergyServlet.java @@ -14,7 +14,7 @@ import javax.ws.rs.core.MediaType; import java.util.Date; @WebServlet("/energy/*") -public class EnergyServlet extends SecureServlet { +public class EnergyServlet extends SecureServiceServlet { @Override protected void get(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) { String[] path = path(_req); diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/FreemarkerCMServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/FreemarkerCMServlet.java new file mode 100644 index 0000000..f5f9304 --- /dev/null +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/FreemarkerCMServlet.java @@ -0,0 +1,23 @@ +package com.lanternsoftware.currentmonitor.servlet; + +import com.lanternsoftware.util.servlet.FreemarkerConfigUtil; +import com.lanternsoftware.util.servlet.FreemarkerServlet; +import com.lanternsoftware.util.servlet.FreemarkerUtil; +import freemarker.template.Configuration; + +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +public abstract class FreemarkerCMServlet extends FreemarkerServlet { + protected static final Configuration CONFIG = FreemarkerConfigUtil.createConfig(FreemarkerCMServlet.class, "/templates", 100); + + @Override + protected Configuration getFreemarkerConfig() { + return CONFIG; + } + + public void renderBody(HttpServletResponse _rep, String _sHtmlResourceKey, Map _mapModel) { + _mapModel.put("body", FreemarkerUtil.render(getFreemarkerConfig(), _sHtmlResourceKey, _mapModel)); + render(_rep, "frame.ftl", _mapModel); + } +} diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/GenerateBomServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/GenerateBomServlet.java index 52c0893..6578c64 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/GenerateBomServlet.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/GenerateBomServlet.java @@ -9,7 +9,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/generateBom") -public class GenerateBomServlet extends SecureServlet { +public class GenerateBomServlet extends SecureServiceServlet { @Override protected void get(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) { AuthCode authCode = Globals.dao.decryptAuthCode(_req.getHeader("auth_code")); diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/GroupEnergyServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/GroupEnergyServlet.java index 7db3044..031da97 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/GroupEnergyServlet.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/GroupEnergyServlet.java @@ -15,7 +15,7 @@ import javax.ws.rs.core.MediaType; import java.util.*; @WebServlet("/energy/group/*") -public class GroupEnergyServlet extends SecureServlet { +public class GroupEnergyServlet extends SecureServiceServlet { @Override protected void get(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) { String[] path = path(_req); diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/GroupPowerServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/GroupPowerServlet.java index 29b4ed8..c5326c8 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/GroupPowerServlet.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/GroupPowerServlet.java @@ -10,7 +10,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/power/group/*") -public class GroupPowerServlet extends SecureServlet { +public class GroupPowerServlet extends SecureServiceServlet { @Override protected void get(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) { String[] path = path(_req); diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/PowerServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/PowerServlet.java index f159626..3864de8 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/PowerServlet.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/PowerServlet.java @@ -19,7 +19,7 @@ import javax.servlet.http.HttpServletResponse; import java.util.List; @WebServlet("/power/*") -public class PowerServlet extends SecureServlet { +public class PowerServlet extends SecureServiceServlet { private static final Logger logger = LoggerFactory.getLogger(MongoCurrentMonitorDao.class); @Override diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/RebuildSummariesServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/RebuildSummariesServlet.java index 2719412..9befc30 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/RebuildSummariesServlet.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/RebuildSummariesServlet.java @@ -3,7 +3,6 @@ package com.lanternsoftware.currentmonitor.servlet; import com.lanternsoftware.currentmonitor.context.Globals; import com.lanternsoftware.datamodel.currentmonitor.Account; import com.lanternsoftware.util.CollectionUtils; -import com.lanternsoftware.util.NullUtils; import com.lanternsoftware.util.dao.DaoSerializer; import com.lanternsoftware.util.dao.auth.AuthCode; @@ -12,7 +11,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/rebuildSummaries/*") -public class RebuildSummariesServlet extends SecureServlet { +public class RebuildSummariesServlet extends SecureServiceServlet { @Override protected void get(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) { if (_authCode.getAccountId() == 100) { diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/ResetPasswordServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/ResetPasswordServlet.java index 32f521b..6004e0c 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/ResetPasswordServlet.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/ResetPasswordServlet.java @@ -1,7 +1,8 @@ package com.lanternsoftware.currentmonitor.servlet; import com.lanternsoftware.currentmonitor.context.Globals; -import com.lanternsoftware.util.LanternFiles; +import com.lanternsoftware.util.CollectionUtils; +import com.lanternsoftware.util.external.LanternFiles; import com.lanternsoftware.util.NullUtils; import com.lanternsoftware.util.ResourceLoader; import com.lanternsoftware.util.dao.DaoEntity; @@ -30,7 +31,7 @@ import java.io.IOException; public class ResetPasswordServlet extends FreemarkerServlet { protected static final Logger LOG = LoggerFactory.getLogger(ResetPasswordServlet.class); protected static final Configuration CONFIG = FreemarkerConfigUtil.createConfig(ResetPasswordServlet.class, "/templates", 100); - protected static final String api_key = ResourceLoader.loadFileAsString(LanternFiles.OPS_PATH + "sendgrid.txt"); + protected static final String api_key = ResourceLoader.loadFileAsString(LanternFiles.CONFIG_PATH + "sendgrid.txt"); @Override protected Configuration getFreemarkerConfig() { @@ -40,7 +41,7 @@ public class ResetPasswordServlet extends FreemarkerServlet { @Override protected void doGet(HttpServletRequest _req, HttpServletResponse _resp) { String[] path = getPath(_req); - String email = Globals.dao.getEmailForResetKey(path[1]); + String email = Globals.dao.getEmailForResetKey(CollectionUtils.get(path, 1)); if (EmailValidator.getInstance().isValid(email)) { render(_resp, "passwordReset.ftl", model(_req, "key", path[1])); } else { diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/SecureServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/SecureServiceServlet.java similarity index 93% rename from currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/SecureServlet.java rename to currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/SecureServiceServlet.java index 8dec803..5ad5340 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/SecureServlet.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/SecureServiceServlet.java @@ -7,7 +7,7 @@ import com.lanternsoftware.util.servlet.LanternServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -public abstract class SecureServlet extends LanternServlet { +public abstract class SecureServiceServlet extends LanternServlet { @Override protected void doGet(HttpServletRequest _req, HttpServletResponse _rep) { AuthCode authCode = Globals.dao.decryptAuthCode(_req.getHeader("auth_code")); diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/UpdateServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/UpdateServlet.java index 6383c7b..24b945f 100644 --- a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/UpdateServlet.java +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/UpdateServlet.java @@ -1,6 +1,6 @@ package com.lanternsoftware.currentmonitor.servlet; -import com.lanternsoftware.util.LanternFiles; +import com.lanternsoftware.util.external.LanternFiles; import com.lanternsoftware.util.ResourceLoader; import com.lanternsoftware.util.dao.DaoSerializer; import com.lanternsoftware.util.servlet.LanternServlet; @@ -16,8 +16,8 @@ public class UpdateServlet extends LanternServlet { @Override protected void doGet(HttpServletRequest _req, HttpServletResponse _rep) { if (isPath(_req, 0, "version")) - setResponseEntity(_rep, MediaType.APPLICATION_OCTET_STREAM, DaoSerializer.toZipBson(DaoSerializer.parse(ResourceLoader.loadFileAsString(LanternFiles.OPS_PATH + "release" + File.separator + "version.json")))); + setResponseEntity(_rep, MediaType.APPLICATION_OCTET_STREAM, DaoSerializer.toZipBson(DaoSerializer.parse(ResourceLoader.loadFileAsString(LanternFiles.CONFIG_PATH + "release" + File.separator + "version.json")))); else - setResponseEntity(_rep, MediaType.APPLICATION_OCTET_STREAM, ResourceLoader.loadFile(LanternFiles.OPS_PATH + "release" + File.separator + "lantern-currentmonitor.jar")); + setResponseEntity(_rep, MediaType.APPLICATION_OCTET_STREAM, ResourceLoader.loadFile(LanternFiles.CONFIG_PATH + "release" + File.separator + "lantern-currentmonitor.jar")); } } diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/ConsoleServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/ConsoleServlet.java new file mode 100644 index 0000000..6ccdbe1 --- /dev/null +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/ConsoleServlet.java @@ -0,0 +1,19 @@ +package com.lanternsoftware.currentmonitor.servlet.console; + +import com.lanternsoftware.util.dao.auth.AuthCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet("") +public class ConsoleServlet extends SecureConsoleServlet { + private static final Logger logger = LoggerFactory.getLogger(ConsoleServlet.class); + + @Override + protected void get(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) { + redirect(_rep, "export"); + } +} diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/ExportServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/ExportServlet.java new file mode 100644 index 0000000..deec364 --- /dev/null +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/ExportServlet.java @@ -0,0 +1,140 @@ +package com.lanternsoftware.currentmonitor.servlet.console; + +import com.lanternsoftware.currentmonitor.context.Globals; +import com.lanternsoftware.datamodel.currentmonitor.Breaker; +import com.lanternsoftware.datamodel.currentmonitor.BreakerConfig; +import com.lanternsoftware.datamodel.currentmonitor.archive.ArchiveStatus; +import com.lanternsoftware.datamodel.currentmonitor.archive.BreakerEnergyArchive; +import com.lanternsoftware.datamodel.currentmonitor.archive.DailyEnergyArchive; +import com.lanternsoftware.datamodel.currentmonitor.archive.MonthlyEnergyArchive; +import com.lanternsoftware.util.CollectionUtils; +import com.lanternsoftware.util.DateUtils; +import com.lanternsoftware.util.NullUtils; +import com.lanternsoftware.util.dao.DaoEntity; +import com.lanternsoftware.util.dao.DaoSerializer; +import com.lanternsoftware.util.dao.auth.AuthCode; +import org.apache.commons.io.IOUtils; +import org.bson.codecs.DocumentCodec; +import org.bson.codecs.EncoderContext; +import org.bson.json.JsonWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; +import java.util.zip.Deflater; +import java.util.zip.GZIPOutputStream; + +@WebServlet("/export/*") +public class ExportServlet extends SecureConsoleServlet { + private static final Logger logger = LoggerFactory.getLogger(ExportServlet.class); + + @Override + protected void get(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) { + TimeZone tz = Globals.dao.getTimeZoneForAccount(_authCode.getAccountId()); + String[] path = path(_req); + if (path.length > 1) { + synchronized (this) { + InputStream is = Globals.dao.streamArchive(_authCode.getAccountId(), new Date(DaoSerializer.toLong(path[0]))); + if (is == null) { + redirect(_rep, _req.getContextPath() + "/export"); + return; + } + OutputStream os = null; + GZIPOutputStream gout = null; + JsonWriter jsonWriter = null; + try { + os = _rep.getOutputStream(); + if (NullUtils.makeNotNull(path[1]).contains("csv")) { + BreakerConfig config = Globals.dao.getConfig(_authCode.getAccountId()); //TODO: get historical config for this month in case it's changed since then. + Map breakers = CollectionUtils.transformToMap(config.getAllBreakers(), Breaker::getIntKey); + os = new GZIPOutputStream(os) {{def.setLevel(Deflater.BEST_SPEED);}}; + MonthlyEnergyArchive archive = DaoSerializer.fromZipBson(IOUtils.toByteArray(is), MonthlyEnergyArchive.class); + DailyEnergyArchive fday = CollectionUtils.getFirst(archive.getDays()); + if (fday == null) { + redirect(_rep, _req.getContextPath() + "/export"); + return; + } + StringBuilder header = new StringBuilder("Timestamp"); + for (BreakerEnergyArchive ba : CollectionUtils.makeNotNull(fday.getBreakers())) { + Breaker b = breakers.get(Breaker.intKey(ba.getPanel(), ba.getSpace())); + header.append(","); + if (b != null) { + header.append(b.getKey()); + header.append("-"); + header.append(b.getName()); + } + } + header.append("\n"); + DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + os.write(NullUtils.toByteArray(header.toString())); + Date dayStart = archive.getMonth(); + for (DailyEnergyArchive day : CollectionUtils.makeNotNull(archive.getDays())) { + Date dayEnd = DateUtils.addDays(dayStart, 1, tz); + int secondsInDay = (int) ((dayEnd.getTime() - dayStart.getTime()) / 1000); + for (int sec = 0; sec < secondsInDay; sec++) { + StringBuilder line = new StringBuilder(); + line.append(df.format(new Date(dayStart.getTime() + ((long) sec * 1000)))); + for (BreakerEnergyArchive b : CollectionUtils.makeNotNull(day.getBreakers())) { + line.append(","); + if ((b.getReadings() == null) || (sec * 4 >= b.getReadings().length)) + line.append("NaN"); + else { + ByteBuffer readings = ByteBuffer.wrap(b.getReadings()); + line.append(readings.getFloat(sec * 4)); + } + } + line.append("\n"); + os.write(NullUtils.toByteArray(line.toString())); + } + } + return; + } + if (NullUtils.makeNotNull(path[1]).contains("json")) { + DaoEntity archive = DaoSerializer.fromZipBson(IOUtils.toByteArray(is)); + gout = new GZIPOutputStream(os) {{def.setLevel(Deflater.BEST_SPEED);}}; + jsonWriter = new JsonWriter(new OutputStreamWriter(gout, StandardCharsets.UTF_8), DaoSerializer.JSON_COMPACT_SETTINGS); + new DocumentCodec().encode(jsonWriter, archive.toDocument(), EncoderContext.builder().build()); + return; + } + IOUtils.copy(is, os); + return; + } catch (Exception _e) { + logger.error("Failed to send archive to browser", _e); + redirect(_rep, _req.getContextPath() + "/export"); + return; + } finally { + IOUtils.closeQuietly(is); + IOUtils.closeQuietly(jsonWriter); + IOUtils.closeQuietly(gout); + IOUtils.closeQuietly(os); + } + } + } + List status = Globals.dao.getArchiveStatus(_authCode.getAccountId()); + List months = CollectionUtils.transform(status, _s->new MonthDisplay(DateUtils.format("MMMM yyyy", tz, _s.getMonth()), _s.getMonth().getTime(), (int)_s.getProgress())); + Map model = model(_req, "months", months); + model.put("inprogress", CollectionUtils.anyQualify(months, _m->_m.getProgress() > 0 && _m.getProgress() < 100)); + renderBody(_rep, "export.ftl", model); + } + + @Override + protected void post(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) { + Date month = new Date(DaoSerializer.toLong(_req.getParameter("month"))); + Globals.dao.archiveMonth(_authCode.getAccountId(), month); + redirect(_rep, "."); + } +} diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/GsoServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/GsoServlet.java new file mode 100644 index 0000000..4aeef81 --- /dev/null +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/GsoServlet.java @@ -0,0 +1,33 @@ +package com.lanternsoftware.currentmonitor.servlet.console; + +import com.lanternsoftware.currentmonitor.servlet.FreemarkerCMServlet; +import com.lanternsoftware.currentmonitor.util.GoogleAuthHelper; +import com.lanternsoftware.util.NullUtils; + +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet("/gso") +public class GsoServlet extends FreemarkerCMServlet { + @Override + protected void doGet(HttpServletRequest _req, HttpServletResponse _rep) { + render(_rep, "login.ftl", model(_req)); + } + + @Override + protected void doPost(HttpServletRequest _req, HttpServletResponse _rep) { + String code = getRequestPayloadAsString(_req); + if (NullUtils.isNotEmpty(code)) { + String authCode = GoogleAuthHelper.signin(code, null); + if (NullUtils.isNotEmpty(authCode)) { + Cookie authCookie = new Cookie("auth_code", authCode); + authCookie.setMaxAge(157680000); + authCookie.setSecure(true); + _rep.addCookie(authCookie); + _req.getSession().setAttribute("auth_code", authCode); + } + } + } +} diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/LoginServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/LoginServlet.java new file mode 100644 index 0000000..6b5035c --- /dev/null +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/LoginServlet.java @@ -0,0 +1,42 @@ +package com.lanternsoftware.currentmonitor.servlet.console; + +import com.lanternsoftware.currentmonitor.context.Globals; +import com.lanternsoftware.currentmonitor.servlet.FreemarkerCMServlet; +import com.lanternsoftware.currentmonitor.util.GoogleAuthHelper; +import com.lanternsoftware.util.DateUtils; +import com.lanternsoftware.util.NullUtils; +import com.lanternsoftware.util.dao.DaoEntity; +import com.lanternsoftware.util.dao.DaoSerializer; +import com.lanternsoftware.util.dao.auth.AuthCode; +import com.lanternsoftware.util.servlet.LanternServlet; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@WebServlet("/login") +public class LoginServlet extends FreemarkerCMServlet { + @Override + protected void doGet(HttpServletRequest _req, HttpServletResponse _rep) { + render(_rep, "login.ftl", model(_req)); + } + + @Override + protected void doPost(HttpServletRequest _req, HttpServletResponse _rep) { + String username = _req.getParameter("username"); + String password = _req.getParameter("password"); + String authCode = Globals.dao.authenticateAccount(username, password); + if (NullUtils.isNotEmpty(authCode)) { + Cookie authCookie = new Cookie("auth_code", authCode); + authCookie.setMaxAge(157680000); + authCookie.setSecure(true); + _rep.addCookie(authCookie); + _req.getSession().setAttribute("auth_code", authCode); + redirect(_rep, _req.getContextPath()); + } + render(_rep, "login.ftl", model(_req, "error", "Invalid Credentials")); + } +} diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/LogoutServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/LogoutServlet.java new file mode 100644 index 0000000..0daa954 --- /dev/null +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/LogoutServlet.java @@ -0,0 +1,27 @@ +package com.lanternsoftware.currentmonitor.servlet.console; + +import com.lanternsoftware.currentmonitor.servlet.FreemarkerCMServlet; +import com.lanternsoftware.currentmonitor.util.GoogleAuthHelper; +import com.lanternsoftware.util.NullUtils; + +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet("/logout") +public class LogoutServlet extends FreemarkerCMServlet { + @Override + protected void doGet(HttpServletRequest _req, HttpServletResponse _rep) { + _req.getSession().removeAttribute("auth_code"); + Cookie authCookie = new Cookie("auth_code", ""); + authCookie.setMaxAge(0); + authCookie.setSecure(true); + _rep.addCookie(authCookie); + redirect(_rep, _req.getContextPath()); + } + + @Override + protected void doPost(HttpServletRequest _req, HttpServletResponse _rep) { + } +} diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/MonthDisplay.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/MonthDisplay.java new file mode 100644 index 0000000..c1779e3 --- /dev/null +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/MonthDisplay.java @@ -0,0 +1,29 @@ +package com.lanternsoftware.currentmonitor.servlet.console; + +public class MonthDisplay { + public final String name; + public final long date; + public final int progress; + + public MonthDisplay(String _name, long _date, int _progress) { + name = _name; + date = _date; + progress = _progress; + } + + public String getName() { + return name; + } + + public String getFileName() { + return name.replace(" ", "-"); + } + + public String getDate() { + return String.valueOf(date); + } + + public int getProgress() { + return progress; + } +} diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/SecureConsoleServlet.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/SecureConsoleServlet.java new file mode 100644 index 0000000..56c3e52 --- /dev/null +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/servlet/console/SecureConsoleServlet.java @@ -0,0 +1,48 @@ +package com.lanternsoftware.currentmonitor.servlet.console; + +import com.lanternsoftware.currentmonitor.context.Globals; +import com.lanternsoftware.currentmonitor.servlet.FreemarkerCMServlet; +import com.lanternsoftware.util.CollectionUtils; +import com.lanternsoftware.util.NullUtils; +import com.lanternsoftware.util.dao.DaoSerializer; +import com.lanternsoftware.util.dao.auth.AuthCode; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public abstract class SecureConsoleServlet extends FreemarkerCMServlet { + @Override + protected void doGet(HttpServletRequest _req, HttpServletResponse _rep) { + AuthCode code = getAuthCode(_req, _rep); + if (code != null) + get(code, _req, _rep); + } + + protected void get(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) { + } + + @Override + protected void doPost(HttpServletRequest _req, HttpServletResponse _rep) { + AuthCode code = getAuthCode(_req, _rep); + if (code != null) + post(code, _req, _rep); + } + + private AuthCode getAuthCode(HttpServletRequest _req, HttpServletResponse _rep) { + AuthCode authCode = Globals.dao.decryptAuthCode(DaoSerializer.toString(_req.getSession().getAttribute("auth_code"))); + if (authCode == null) { + Cookie authCookie = CollectionUtils.filterOne(CollectionUtils.asArrayList(_req.getCookies()), _c-> NullUtils.isEqual(_c.getName(), "auth_code")); + if (authCookie != null) + authCode = Globals.dao.decryptAuthCode(authCookie.getValue()); + } + if (authCode == null) { + redirect(_rep, _req.getContextPath() + "/login"); + return null; + } + return authCode; + } + + protected void post(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) { + } +} diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/util/GoogleAuthHelper.java b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/util/GoogleAuthHelper.java new file mode 100644 index 0000000..662ed30 --- /dev/null +++ b/currentmonitor/lantern-service-currentmonitor/src/main/java/com/lanternsoftware/currentmonitor/util/GoogleAuthHelper.java @@ -0,0 +1,42 @@ +package com.lanternsoftware.currentmonitor.util; + +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import com.lanternsoftware.currentmonitor.context.Globals; +import com.lanternsoftware.util.external.LanternFiles; +import com.lanternsoftware.util.ResourceLoader; +import com.lanternsoftware.util.dao.DaoEntity; +import com.lanternsoftware.util.dao.DaoSerializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.TimeZone; + +public class GoogleAuthHelper { + private static final Logger logger = LoggerFactory.getLogger(GoogleAuthHelper.class); + private static final NetHttpTransport transport = new NetHttpTransport(); + private static final String googleClientId; + private static final String googleClientSecret; + static { + DaoEntity google = DaoSerializer.parse(ResourceLoader.loadFileAsString(LanternFiles.CONFIG_PATH + "google_sso.txt")); + googleClientId = DaoSerializer.getString(google, "id"); + googleClientSecret = DaoSerializer.getString(google, "secret"); + } + + public static String signin(String _code, TimeZone _tz) { + try { + GoogleTokenResponse tokenResponse = new GoogleAuthorizationCodeTokenRequest(transport, new GsonFactory(), "https://oauth2.googleapis.com/token", googleClientId, googleClientSecret, _code, "postmessage").execute(); + if (tokenResponse != null) { + GoogleIdToken idToken = tokenResponse.parseIdToken(); + if (idToken != null) + return Globals.dao.getAuthCodeForEmail(idToken.getPayload().getEmail(), _tz); + } + } catch (Exception _e) { + logger.error("Failed to validate google auth code", _e); + } + return null; + } +} diff --git a/currentmonitor/lantern-service-currentmonitor/src/main/resources/templates/export.ftl b/currentmonitor/lantern-service-currentmonitor/src/main/resources/templates/export.ftl new file mode 100644 index 0000000..0aaadf2 --- /dev/null +++ b/currentmonitor/lantern-service-currentmonitor/src/main/resources/templates/export.ftl @@ -0,0 +1,50 @@ + + + + + + + <#if inprogress> + Lantern Console + + + + +